Skip to main content

tensor4all_core/
index_like.rs

1//! IndexLike trait for abstracting index types.
2//!
3//! This trait allows algorithms to be generic over different index types
4//! without needing to know about the internal ID representation.
5
6use std::fmt::Debug;
7use std::hash::Hash;
8
9/// Conjugate state (direction) of an index.
10///
11/// This enum represents whether an index has a direction (bra/ket) or is directionless.
12/// The direction is used to determine contractability between indices.
13///
14/// # QSpace Compatibility
15///
16/// In QSpace (extern/qspace-v4-pub), index direction is encoded via trailing `*` in `itags`:
17/// - **Ket** = ingoing index (QSpace: itag **without** trailing `*`)
18/// - **Bra** = outgoing index (QSpace: itag **with** trailing `*`)
19///
20/// # ITensors.jl Compatibility
21///
22/// ITensors.jl uses directionless indices by default (convenient for general tensor operations).
23/// The `Undirected` variant provides this behavior.
24///
25/// # Examples
26///
27/// ```
28/// use tensor4all_core::{DynIndex, IndexLike, ConjState};
29///
30/// let i = DynIndex::new_dyn(4);
31/// // Default DynIndex is undirected
32/// assert_eq!(i.conj_state(), ConjState::Undirected);
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum ConjState {
36    /// Directionless index (ITensors.jl-like default).
37    ///
38    /// Undirected indices can contract with other undirected indices
39    /// only when their full index identity matches and their dimensions match.
40    Undirected,
41    /// Ket (ingoing) index.
42    ///
43    /// In QSpace terminology, this corresponds to an index without a trailing `*` in its itag.
44    /// Ket indices can only contract with Bra indices (and vice versa).
45    Ket,
46    /// Bra (outgoing) index.
47    ///
48    /// In QSpace terminology, this corresponds to an index with a trailing `*` in its itag.
49    /// Bra indices can only contract with Ket indices (and vice versa).
50    Bra,
51}
52
53/// Trait for index-like types that can be used in tensor operations.
54///
55/// This trait abstracts away the identity mechanism of indices, allowing algorithms
56/// to work with any index type that provides equality, hashing, and dimension access.
57///
58/// # Design Principles
59///
60/// - **`Id` as associated type**: Lightweight identifier (conjugate-independent)
61/// - **`Eq` by object equality**: Two indices are equal iff they represent the same object
62///
63///   (including ID, dimension, and conjugate state if applicable)
64/// - **`dim()`**: Returns the dimension of the index
65/// - **`conj_state()`**: Returns the conjugate state (direction) of the index
66///
67/// # Key Properties
68///
69/// - **`Eq`**: Defines object equality (includes ID, dimension, and conjugate state)
70/// - **`Hash`**: Enables efficient lookup in `HashMap<I, ...>` / `HashSet<I>`
71/// - **`Clone`**: Indices are small value types, freely copyable
72/// - **`is_contractable()`**: Determines if two indices can be contracted
73///
74/// # Conjugate State and Contractability
75///
76/// The `conj_state()` method returns the direction of an index:
77/// - `Undirected`: Directionless index (ITensors.jl-like default)
78/// - `Ket`: Ingoing index (QSpace: no trailing `*` in itag)
79/// - `Bra`: Outgoing index (QSpace: trailing `*` in itag)
80///
81/// Two indices are contractable if:
82/// - Their dimensions match
83/// - Their full index identity is compatible:
84///
85///   - `(Undirected, Undirected)` → equal indices
86///   - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
87///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
88///
89/// # Example
90///
91/// ```
92/// use tensor4all_core::{DynIndex, IndexLike};
93///
94/// let i = DynIndex::new_dyn(2);
95/// let j = DynIndex::new_dyn(3);
96/// let k = DynIndex::new_dyn(4);
97///
98/// let a = vec![i.clone(), j.clone()];
99/// let b = vec![j.clone(), k.clone()];
100/// let common: Vec<_> = a.iter().filter(|idx| b.contains(idx)).cloned().collect();
101///
102/// assert_eq!(common, vec![j]);
103/// ```
104pub trait IndexLike: Clone + Eq + Hash + Debug + Send + Sync + 'static {
105    /// Lightweight identifier type (conjugate-independent).
106    ///
107    /// **Rule**: Contractable indices must have compatible full index identity.
108    ///
109    /// The ID serves as a lightweight component of full index identity.
110    /// In large tensor networks, IDs enable efficient graph-based lookups (O(1) with HashSet/HashMap)
111    /// to find matching legs across many tensors.
112    ///
113    /// This is separate from the full `Eq`/`Hash` identity:
114    /// - **ID**: raw logical identifier, useful for serialization and diagnostics
115    /// - **Eq/Hash**: concrete tensor leg identity used by maps, sets, and contraction
116    /// - **dim/ConjState**: mathematical compatibility checks
117    type Id: Clone + Eq + Hash + Debug + Send + Sync;
118
119    /// Get the identifier of this index.
120    ///
121    /// The ID is a lightweight raw identifier.
122    /// **Do not use ID-only equality as concrete tensor leg identity**; use full index
123    /// equality, or `is_contractable()` when deciding contraction compatibility.
124    ///
125    /// Two indices with the same ID may still be distinct concrete legs, for example when
126    /// they differ by prime level, tags, or direction.
127    fn id(&self) -> &Self::Id;
128
129    /// Get the total dimension (state-space dimension) of the index.
130    fn dim(&self) -> usize;
131
132    /// Get the prime level of this index.
133    /// Default: 0 (unprimed).
134    fn plev(&self) -> i64 {
135        0
136    }
137
138    /// Get the conjugate state (direction) of this index.
139    ///
140    /// Returns `ConjState::Undirected` for directionless indices (ITensors.jl-like default),
141    /// or `ConjState::Ket`/`ConjState::Bra` for directed indices (QSpace-compatible).
142    fn conj_state(&self) -> ConjState;
143
144    /// Create the conjugate of this index.
145    ///
146    /// For directed indices, this toggles between `Ket` and `Bra`.
147    /// For `Undirected` indices, this returns `self` unchanged (no-op).
148    ///
149    /// # Returns
150    /// A new index with the conjugate state toggled (if directed) or unchanged (if undirected).
151    fn conj(&self) -> Self;
152
153    /// Check if this index can be contracted with another index.
154    ///
155    /// Two indices are contractable if:
156    /// - They have the same dimension
157    /// - Their full identity is compatible:
158    ///
159    ///   - `(Undirected, Undirected)` → equal indices
160    ///   - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
161    ///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
162    ///
163    /// # Default Implementation
164    ///
165    /// The default implementation checks:
166    /// 1. Same dimension: `self.dim() == other.dim()`
167    /// 2. Compatible conjugate states (see rules above)
168    /// 3. Full index equality for undirected indices, or conjugate full equality for
169    ///    directed pairs.
170    fn is_contractable(&self, other: &Self) -> bool {
171        if self.dim() != other.dim() {
172            return false;
173        }
174        match (self.conj_state(), other.conj_state()) {
175            (ConjState::Ket, ConjState::Bra) | (ConjState::Bra, ConjState::Ket) => {
176                self.conj() == other.clone()
177            }
178            (ConjState::Undirected, ConjState::Undirected) => self == other,
179            _ => false, // Mixed directed/undirected is forbidden
180        }
181    }
182
183    /// Check if this index has the same ID as another.
184    ///
185    /// Default implementation compares IDs directly.
186    /// This is a convenience method for pure ID comparison (does not check contractability).
187    fn same_id(&self, other: &Self) -> bool {
188        self.id() == other.id()
189    }
190
191    /// Check if this index has the given ID.
192    ///
193    /// Default implementation compares with the given ID.
194    fn has_id(&self, id: &Self::Id) -> bool {
195        self.id() == id
196    }
197
198    /// Create a similar index with a new identity but the same structure (dimension, tags, etc.).
199    ///
200    /// This is used to create "equivalent" indices that have the same properties
201    /// but different identities, commonly needed in index replacement operations.
202    ///
203    /// # Returns
204    /// A new index with a fresh identity and the same structure as `self`.
205    fn sim(&self) -> Self
206    where
207        Self: Sized;
208
209    /// Create a pair of contractable dummy indices with dimension 1.
210    ///
211    /// These are used for structural connections that don't carry quantum numbers,
212    /// such as connecting components in a tree tensor network.
213    ///
214    /// Both indices will be `Undirected` and have the same ID, making them contractable.
215    ///
216    /// # Returns
217    /// A pair `(idx1, idx2)` where `idx1.is_contractable(&idx2)` is true.
218    fn create_dummy_link_pair() -> (Self, Self)
219    where
220        Self: Sized;
221
222    /// Create a fresh undirected link-like index with the requested dimension.
223    ///
224    /// Algorithms that assemble a batch or randomized sketch need an
225    /// auxiliary column index whose dimension is not known from an input leg.
226    /// Implementations should give the index a fresh identity and link
227    /// metadata where their index model has such metadata.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error when `dim` causes a dimension overflow or cannot be
232    /// represented by the index type.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use tensor4all_core::{DynIndex, IndexLike};
238    ///
239    /// let probe = DynIndex::new_link(5).unwrap();
240    /// assert_eq!(probe.dim(), 5);
241    /// ```
242    fn new_link(dim: usize) -> anyhow::Result<Self>
243    where
244        Self: Sized;
245
246    /// Create a fresh link index representing the tensor-product space of input indices.
247    ///
248    /// Generic algorithms may use this to replace multiple local bond legs by one fused
249    /// leg without depending on a concrete index implementation. The returned link must
250    /// have a fresh identity and represent the exact tensor-product basis of `indices`.
251    ///
252    /// Implementations with symmetry or sector metadata should preserve the tensor-product
253    /// basis and charge structure when possible. They should return `Err` if the fused
254    /// product link cannot be represented exactly.
255    ///
256    /// # Arguments
257    /// * `indices` - Non-empty input indices whose tensor-product space is represented by
258    ///
259    ///   the output. Typical inputs are link or bond indices being fused into one link.
260    ///
261    /// # Returns
262    /// A new index with fresh identity and dimension equal to the checked product of all input
263    /// dimensions.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error when `indices` is empty or when the product of their
268    /// dimensions would overflow `usize`.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use tensor4all_core::{DynIndex, IndexLike, TagSetLike};
274    ///
275    /// let a = DynIndex::new_link(2).unwrap();
276    /// let b = DynIndex::new_link(3).unwrap();
277    /// let product = DynIndex::product_link(&[a.clone(), b.clone()]).unwrap();
278    ///
279    /// assert_eq!(product.dim(), 6);
280    /// assert!(product.tags().has_tag("Link"));
281    /// assert_ne!(product.id(), a.id());
282    /// assert_ne!(product.id(), b.id());
283    /// ```
284    fn product_link(indices: &[Self]) -> anyhow::Result<Self>
285    where
286        Self: Sized;
287}
288
289/// Sort indices by a deterministic comparator over `dim`, `plev`, `id`, then
290/// `Debug` representation.
291///
292/// The `plev` and `Debug` terms keep distinct indices that share the same ID —
293/// for example a same-ID pair differing by prime level or tags — in a pinned,
294/// insertion-order-independent relative order instead of leaving them to the
295/// stable sort's input order.
296///
297/// The comparator is a pure function of the index values, so the result never
298/// depends on insertion order as long as `Debug` is deterministic and
299/// distinguishes all identity-relevant fields. The concrete
300/// [`DynIndex`](crate::defaults::DynIndex) satisfies the determinism part: its
301/// derived `Debug` covers `id`, `dim`, `plev`, and `tags`, so two `DynIndex`
302/// values compare `Equal` only when every field matches. Note that `dim`
303/// participates in the sort even though `DynIndex`'s `Eq` deliberately ignores
304/// it (ITensors semantics: equality = id + plev + tags). Implementors whose
305/// `Debug` omits identity-relevant fields may still see `Equal` comparisons
306/// between distinct indices, in which case the stable sort preserves the input
307/// order for those ties.
308///
309/// # Arguments
310/// * `indices` - The slice of indices to reorder in place.
311///
312/// # Returns
313/// Nothing; `indices` is reordered in place.
314///
315/// # Panics
316/// Never panics; the comparator relies only on `IndexLike` accessors and
317/// `Debug` implementations.
318///
319/// # Examples
320///
321/// ```
322/// use tensor4all_core::{sort_indices_deterministic, DynIndex, IndexLike};
323///
324/// let s = DynIndex::new_dyn(2);
325/// let s_prime = s.prime(); // same ID, plev 1
326/// assert!(s.same_id(&s_prime));
327///
328/// // Insertion order (primed first) must not survive the sort.
329/// let mut indices = vec![s_prime.clone(), s.clone()];
330/// sort_indices_deterministic(&mut indices);
331/// assert_eq!(indices, vec![s, s_prime]);
332/// ```
333pub fn sort_indices_deterministic<I: IndexLike>(indices: &mut [I])
334where
335    I::Id: Ord,
336{
337    indices.sort_by(|left, right| {
338        left.dim()
339            .cmp(&right.dim())
340            .then_with(|| left.plev().cmp(&right.plev()))
341            .then_with(|| left.id().cmp(right.id()))
342            .then_with(|| format!("{left:?}").cmp(&format!("{right:?}")))
343    });
344}
345
346#[cfg(test)]
347mod tests;