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 link index representing the tensor-product space of input indices.
223    ///
224    /// Generic algorithms may use this to replace multiple local bond legs by one fused
225    /// leg without depending on a concrete index implementation. The returned link must
226    /// have a fresh identity and represent the exact tensor-product basis of `indices`.
227    ///
228    /// Implementations with symmetry or sector metadata should preserve the tensor-product
229    /// basis and charge structure when possible. They should return `Err` if the fused
230    /// product link cannot be represented exactly.
231    ///
232    /// # Arguments
233    /// * `indices` - Non-empty input indices whose tensor-product space is represented by
234    ///
235    ///   the output. Typical inputs are link or bond indices being fused into one link.
236    ///
237    /// # Returns
238    /// A new index with fresh identity and dimension equal to the checked product of all input
239    /// dimensions.
240    ///
241    /// # Errors
242    ///
243    /// Returns an error when the operation fails (a shape or index mismatch, or
244    /// /// a backend failure).
245    ///
246    /// # Examples
247    ///
248    /// ```
249    /// use tensor4all_core::{DynIndex, IndexLike, TagSetLike};
250    ///
251    /// let a = DynIndex::new_link(2).unwrap();
252    /// let b = DynIndex::new_link(3).unwrap();
253    /// let product = DynIndex::product_link(&[a.clone(), b.clone()]).unwrap();
254    ///
255    /// assert_eq!(product.dim(), 6);
256    /// assert!(product.tags().has_tag("Link"));
257    /// assert_ne!(product.id(), a.id());
258    /// assert_ne!(product.id(), b.id());
259    /// ```
260    fn product_link(indices: &[Self]) -> anyhow::Result<Self>
261    where
262        Self: Sized;
263}
264
265/// Sort indices by a deterministic comparator over `dim`, `plev`, `id`, then
266/// `Debug` representation.
267///
268/// The `plev` and `Debug` terms keep distinct indices that share the same ID —
269/// for example a same-ID pair differing by prime level or tags — in a pinned,
270/// insertion-order-independent relative order instead of leaving them to the
271/// stable sort's input order.
272///
273/// The comparator is a pure function of the index values, so the result never
274/// depends on insertion order as long as `Debug` is deterministic and
275/// distinguishes all identity-relevant fields. The concrete
276/// [`DynIndex`](crate::defaults::DynIndex) satisfies the determinism part: its
277/// derived `Debug` covers `id`, `dim`, `plev`, and `tags`, so two `DynIndex`
278/// values compare `Equal` only when every field matches. Note that `dim`
279/// participates in the sort even though `DynIndex`'s `Eq` deliberately ignores
280/// it (ITensors semantics: equality = id + plev + tags). Implementors whose
281/// `Debug` omits identity-relevant fields may still see `Equal` comparisons
282/// between distinct indices, in which case the stable sort preserves the input
283/// order for those ties.
284///
285/// # Arguments
286/// * `indices` - The slice of indices to reorder in place.
287///
288/// # Returns
289/// Nothing; `indices` is reordered in place.
290///
291/// # Panics
292/// Never panics; the comparator relies only on `IndexLike` accessors and
293/// `Debug` implementations.
294///
295/// # Examples
296///
297/// ```
298/// use tensor4all_core::{sort_indices_deterministic, DynIndex, IndexLike};
299///
300/// let s = DynIndex::new_dyn(2);
301/// let s_prime = s.prime(); // same ID, plev 1
302/// assert!(s.same_id(&s_prime));
303///
304/// // Insertion order (primed first) must not survive the sort.
305/// let mut indices = vec![s_prime.clone(), s.clone()];
306/// sort_indices_deterministic(&mut indices);
307/// assert_eq!(indices, vec![s, s_prime]);
308/// ```
309pub fn sort_indices_deterministic<I: IndexLike>(indices: &mut [I])
310where
311    I::Id: Ord,
312{
313    indices.sort_by(|left, right| {
314        left.dim()
315            .cmp(&right.dim())
316            .then_with(|| left.plev().cmp(&right.plev()))
317            .then_with(|| left.id().cmp(right.id()))
318            .then_with(|| format!("{left:?}").cmp(&format!("{right:?}")))
319    });
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    /// Minimal IndexLike implementation that uses the default plev() method.
327    /// Used to test coverage of the default trait implementations.
328    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
329    struct TestIndex {
330        id: u64,
331        dim: usize,
332        state: ConjState,
333    }
334
335    fn test_index(id: u64, dim: usize) -> TestIndex {
336        TestIndex {
337            id,
338            dim,
339            state: ConjState::Undirected,
340        }
341    }
342
343    fn directed_test_index(id: u64, dim: usize, state: ConjState) -> TestIndex {
344        TestIndex { id, dim, state }
345    }
346
347    impl IndexLike for TestIndex {
348        type Id = u64;
349
350        fn id(&self) -> &u64 {
351            &self.id
352        }
353
354        fn dim(&self) -> usize {
355            self.dim
356        }
357
358        fn conj_state(&self) -> ConjState {
359            self.state
360        }
361
362        fn conj(&self) -> Self {
363            let state = match self.state {
364                ConjState::Undirected => ConjState::Undirected,
365                ConjState::Ket => ConjState::Bra,
366                ConjState::Bra => ConjState::Ket,
367            };
368            TestIndex {
369                state,
370                ..self.clone()
371            }
372        }
373
374        fn sim(&self) -> Self {
375            TestIndex {
376                id: self.id + 1000,
377                ..self.clone()
378            }
379        }
380
381        fn create_dummy_link_pair() -> (Self, Self) {
382            (test_index(0, 1), test_index(0, 1))
383        }
384
385        /// # Errors
386        ///
387        /// Returns an error when the operation fails (a shape or index mismatch, or
388        /// /// a backend failure).
389        ///
390        fn product_link(indices: &[Self]) -> anyhow::Result<Self> {
391            anyhow::ensure!(
392                !indices.is_empty(),
393                "product_link requires at least one index"
394            );
395            let dim = indices.iter().try_fold(1usize, |acc, idx| {
396                acc.checked_mul(idx.dim)
397                    .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
398            })?;
399            Ok(test_index(9999, dim))
400        }
401    }
402
403    #[test]
404    fn test_default_plev_is_zero() {
405        let idx = test_index(1, 3);
406        assert_eq!(idx.plev(), 0);
407    }
408
409    #[test]
410    fn test_default_is_contractable_with_plev() {
411        let a = test_index(1, 3);
412        let b = test_index(1, 3);
413        // Same id, dim, and default plev=0: contractable
414        assert!(a.is_contractable(&b));
415    }
416
417    #[test]
418    fn test_default_is_contractable_rejects_id_and_dim_mismatch() {
419        let a = test_index(1, 3);
420        let different_id = test_index(2, 3);
421        let different_dim = test_index(1, 4);
422
423        assert!(!a.is_contractable(&different_id));
424        assert!(!a.is_contractable(&different_dim));
425    }
426
427    #[test]
428    fn test_default_is_contractable_supports_directed_pairs_only() {
429        let ket = directed_test_index(1, 3, ConjState::Ket);
430        let bra = directed_test_index(1, 3, ConjState::Bra);
431        let undirected = test_index(1, 3);
432
433        assert!(ket.is_contractable(&bra));
434        assert!(bra.is_contractable(&ket));
435        assert!(!ket.is_contractable(&undirected));
436        assert!(!undirected.is_contractable(&bra));
437    }
438
439    #[test]
440    fn test_default_link_helpers_preserve_expected_structure() {
441        let idx = directed_test_index(7, 5, ConjState::Ket);
442        let conjugated = idx.conj();
443        assert_eq!(conjugated.conj_state(), ConjState::Bra);
444        assert_eq!(conjugated.id(), idx.id());
445        assert_eq!(conjugated.dim(), idx.dim());
446
447        let similar = idx.sim();
448        assert_eq!(similar.dim(), idx.dim());
449        assert_eq!(similar.conj_state(), idx.conj_state());
450        assert_ne!(similar.id(), idx.id());
451
452        let (left, right) = TestIndex::create_dummy_link_pair();
453        assert_eq!(left.dim(), 1);
454        assert!(left.is_contractable(&right));
455    }
456
457    #[test]
458    fn test_product_link_helper_checks_empty_and_overflow_inputs() {
459        let a = test_index(1, 2);
460        let b = test_index(2, 3);
461        let product = TestIndex::product_link(&[a, b]).unwrap();
462        assert_eq!(product.dim(), 6);
463        assert_eq!(product.conj_state(), ConjState::Undirected);
464
465        assert!(TestIndex::product_link(&[]).is_err());
466        assert!(TestIndex::product_link(&[test_index(1, usize::MAX), test_index(2, 2)]).is_err());
467    }
468
469    #[test]
470    fn test_default_same_id() {
471        let a = test_index(1, 3);
472        let b = test_index(1, 5);
473        let c = test_index(2, 3);
474        assert!(a.same_id(&b));
475        assert!(!a.same_id(&c));
476    }
477
478    #[test]
479    fn test_default_has_id() {
480        let a = test_index(42, 3);
481        assert!(a.has_id(&42));
482        assert!(!a.has_id(&99));
483    }
484
485    #[test]
486    fn test_sort_indices_deterministic_pins_same_id_prime_pair() {
487        use crate::defaults::DynIndex;
488
489        let s = DynIndex::new_dyn(2);
490        let s_prime = s.prime();
491        assert!(s.same_id(&s_prime));
492        assert_ne!(s, s_prime);
493
494        // Primed index first (worst-case insertion order): the sort must pin
495        // unprimed before primed by full-index order (plev 0 < plev 1), not
496        // preserve insertion order.
497        let mut indices = vec![s_prime.clone(), s.clone()];
498        sort_indices_deterministic(&mut indices);
499        assert_eq!(indices, vec![s.clone(), s_prime.clone()]);
500
501        // Same canonical order regardless of input permutation.
502        let mut indices = vec![s.clone(), s_prime.clone()];
503        sort_indices_deterministic(&mut indices);
504        assert_eq!(indices, vec![s, s_prime]);
505    }
506
507    #[test]
508    fn test_sort_indices_deterministic_distinguishes_same_id_tag_pair() {
509        use crate::defaults::{DynId, DynIndex, TagSet};
510
511        let site = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Site"]).unwrap());
512        let link = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Link"]).unwrap());
513        assert!(site.same_id(&link));
514        assert_ne!(site, link);
515
516        // The Debug tiebreak must give one deterministic order, not leave the
517        // pair to insertion order.
518        let mut a = vec![link.clone(), site.clone()];
519        let mut b = vec![site.clone(), link.clone()];
520        sort_indices_deterministic(&mut a);
521        sort_indices_deterministic(&mut b);
522        assert_eq!(a, b);
523        assert!(a.contains(&site));
524        assert!(a.contains(&link));
525    }
526}