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///   (including ID, dimension, and conjugate state if applicable)
63/// - **`dim()`**: Returns the dimension of the index
64/// - **`conj_state()`**: Returns the conjugate state (direction) of the index
65///
66/// # Key Properties
67///
68/// - **`Eq`**: Defines object equality (includes ID, dimension, and conjugate state)
69/// - **`Hash`**: Enables efficient lookup in `HashMap<I, ...>` / `HashSet<I>`
70/// - **`Clone`**: Indices are small value types, freely copyable
71/// - **`is_contractable()`**: Determines if two indices can be contracted
72///
73/// # Conjugate State and Contractability
74///
75/// The `conj_state()` method returns the direction of an index:
76/// - `Undirected`: Directionless index (ITensors.jl-like default)
77/// - `Ket`: Ingoing index (QSpace: no trailing `*` in itag)
78/// - `Bra`: Outgoing index (QSpace: trailing `*` in itag)
79///
80/// Two indices are contractable if:
81/// - Their dimensions match
82/// - Their full index identity is compatible:
83///   - `(Undirected, Undirected)` → equal indices
84///   - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
85///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
86///
87/// # Example
88///
89/// ```
90/// use tensor4all_core::{DynIndex, IndexLike};
91///
92/// let i = DynIndex::new_dyn(2);
93/// let j = DynIndex::new_dyn(3);
94/// let k = DynIndex::new_dyn(4);
95///
96/// let a = vec![i.clone(), j.clone()];
97/// let b = vec![j.clone(), k.clone()];
98/// let common: Vec<_> = a.iter().filter(|idx| b.contains(idx)).cloned().collect();
99///
100/// assert_eq!(common, vec![j]);
101/// ```
102pub trait IndexLike: Clone + Eq + Hash + Debug + Send + Sync + 'static {
103    /// Lightweight identifier type (conjugate-independent).
104    ///
105    /// **Rule**: Contractable indices must have compatible full index identity.
106    ///
107    /// The ID serves as a lightweight component of full index identity.
108    /// In large tensor networks, IDs enable efficient graph-based lookups (O(1) with HashSet/HashMap)
109    /// to find matching legs across many tensors.
110    ///
111    /// This is separate from the full `Eq`/`Hash` identity:
112    /// - **ID**: raw logical identifier, useful for serialization and diagnostics
113    /// - **Eq/Hash**: concrete tensor leg identity used by maps, sets, and contraction
114    /// - **dim/ConjState**: mathematical compatibility checks
115    type Id: Clone + Eq + Hash + Debug + Send + Sync;
116
117    /// Get the identifier of this index.
118    ///
119    /// The ID is a lightweight raw identifier.
120    /// **Do not use ID-only equality as concrete tensor leg identity**; use full index
121    /// equality, or `is_contractable()` when deciding contraction compatibility.
122    ///
123    /// Two indices with the same ID may still be distinct concrete legs, for example when
124    /// they differ by prime level, tags, or direction.
125    fn id(&self) -> &Self::Id;
126
127    /// Get the total dimension (state-space dimension) of the index.
128    fn dim(&self) -> usize;
129
130    /// Get the prime level of this index.
131    /// Default: 0 (unprimed).
132    fn plev(&self) -> i64 {
133        0
134    }
135
136    /// Get the conjugate state (direction) of this index.
137    ///
138    /// Returns `ConjState::Undirected` for directionless indices (ITensors.jl-like default),
139    /// or `ConjState::Ket`/`ConjState::Bra` for directed indices (QSpace-compatible).
140    fn conj_state(&self) -> ConjState;
141
142    /// Create the conjugate of this index.
143    ///
144    /// For directed indices, this toggles between `Ket` and `Bra`.
145    /// For `Undirected` indices, this returns `self` unchanged (no-op).
146    ///
147    /// # Returns
148    /// A new index with the conjugate state toggled (if directed) or unchanged (if undirected).
149    fn conj(&self) -> Self;
150
151    /// Check if this index can be contracted with another index.
152    ///
153    /// Two indices are contractable if:
154    /// - They have the same dimension
155    /// - Their full identity is compatible:
156    ///   - `(Undirected, Undirected)` → equal indices
157    ///   - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
158    ///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
159    ///
160    /// # Default Implementation
161    ///
162    /// The default implementation checks:
163    /// 1. Same dimension: `self.dim() == other.dim()`
164    /// 2. Compatible conjugate states (see rules above)
165    /// 3. Full index equality for undirected indices, or conjugate full equality for
166    ///    directed pairs.
167    fn is_contractable(&self, other: &Self) -> bool {
168        if self.dim() != other.dim() {
169            return false;
170        }
171        match (self.conj_state(), other.conj_state()) {
172            (ConjState::Ket, ConjState::Bra) | (ConjState::Bra, ConjState::Ket) => {
173                self.conj() == other.clone()
174            }
175            (ConjState::Undirected, ConjState::Undirected) => self == other,
176            _ => false, // Mixed directed/undirected is forbidden
177        }
178    }
179
180    /// Check if this index has the same ID as another.
181    ///
182    /// Default implementation compares IDs directly.
183    /// This is a convenience method for pure ID comparison (does not check contractability).
184    fn same_id(&self, other: &Self) -> bool {
185        self.id() == other.id()
186    }
187
188    /// Check if this index has the given ID.
189    ///
190    /// Default implementation compares with the given ID.
191    fn has_id(&self, id: &Self::Id) -> bool {
192        self.id() == id
193    }
194
195    /// Create a similar index with a new identity but the same structure (dimension, tags, etc.).
196    ///
197    /// This is used to create "equivalent" indices that have the same properties
198    /// but different identities, commonly needed in index replacement operations.
199    ///
200    /// # Returns
201    /// A new index with a fresh identity and the same structure as `self`.
202    fn sim(&self) -> Self
203    where
204        Self: Sized;
205
206    /// Create a pair of contractable dummy indices with dimension 1.
207    ///
208    /// These are used for structural connections that don't carry quantum numbers,
209    /// such as connecting components in a tree tensor network.
210    ///
211    /// Both indices will be `Undirected` and have the same ID, making them contractable.
212    ///
213    /// # Returns
214    /// A pair `(idx1, idx2)` where `idx1.is_contractable(&idx2)` is true.
215    fn create_dummy_link_pair() -> (Self, Self)
216    where
217        Self: Sized;
218
219    /// Create a fresh link index representing the tensor-product space of input indices.
220    ///
221    /// Generic algorithms may use this to replace multiple local bond legs by one fused
222    /// leg without depending on a concrete index implementation. The returned link must
223    /// have a fresh identity and represent the exact tensor-product basis of `indices`.
224    ///
225    /// Implementations with symmetry or sector metadata should preserve the tensor-product
226    /// basis and charge structure when possible. They should return `Err` if the fused
227    /// product link cannot be represented exactly.
228    ///
229    /// # Arguments
230    /// * `indices` - Non-empty input indices whose tensor-product space is represented by
231    ///   the output. Typical inputs are link or bond indices being fused into one link.
232    ///
233    /// # Returns
234    /// A new index with fresh identity and dimension equal to the checked product of all input
235    /// dimensions.
236    ///
237    /// # Errors
238    /// Returns an error when `indices` is empty, when the product dimension overflows `usize`,
239    /// or when the implementation cannot represent the exact product-link structure.
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// use tensor4all_core::{DynIndex, IndexLike, TagSetLike};
245    ///
246    /// let a = DynIndex::new_link(2).unwrap();
247    /// let b = DynIndex::new_link(3).unwrap();
248    /// let product = DynIndex::product_link(&[a.clone(), b.clone()]).unwrap();
249    ///
250    /// assert_eq!(product.dim(), 6);
251    /// assert!(product.tags().has_tag("Link"));
252    /// assert_ne!(product.id(), a.id());
253    /// assert_ne!(product.id(), b.id());
254    /// ```
255    fn product_link(indices: &[Self]) -> anyhow::Result<Self>
256    where
257        Self: Sized;
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    /// Minimal IndexLike implementation that uses the default plev() method.
265    /// Used to test coverage of the default trait implementations.
266    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
267    struct TestIndex {
268        id: u64,
269        dim: usize,
270        state: ConjState,
271    }
272
273    fn test_index(id: u64, dim: usize) -> TestIndex {
274        TestIndex {
275            id,
276            dim,
277            state: ConjState::Undirected,
278        }
279    }
280
281    fn directed_test_index(id: u64, dim: usize, state: ConjState) -> TestIndex {
282        TestIndex { id, dim, state }
283    }
284
285    impl IndexLike for TestIndex {
286        type Id = u64;
287
288        fn id(&self) -> &u64 {
289            &self.id
290        }
291
292        fn dim(&self) -> usize {
293            self.dim
294        }
295
296        fn conj_state(&self) -> ConjState {
297            self.state
298        }
299
300        fn conj(&self) -> Self {
301            let state = match self.state {
302                ConjState::Undirected => ConjState::Undirected,
303                ConjState::Ket => ConjState::Bra,
304                ConjState::Bra => ConjState::Ket,
305            };
306            TestIndex {
307                state,
308                ..self.clone()
309            }
310        }
311
312        fn sim(&self) -> Self {
313            TestIndex {
314                id: self.id + 1000,
315                ..self.clone()
316            }
317        }
318
319        fn create_dummy_link_pair() -> (Self, Self) {
320            (test_index(0, 1), test_index(0, 1))
321        }
322
323        fn product_link(indices: &[Self]) -> anyhow::Result<Self> {
324            anyhow::ensure!(
325                !indices.is_empty(),
326                "product_link requires at least one index"
327            );
328            let dim = indices.iter().try_fold(1usize, |acc, idx| {
329                acc.checked_mul(idx.dim)
330                    .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
331            })?;
332            Ok(test_index(9999, dim))
333        }
334    }
335
336    #[test]
337    fn test_default_plev_is_zero() {
338        let idx = test_index(1, 3);
339        assert_eq!(idx.plev(), 0);
340    }
341
342    #[test]
343    fn test_default_is_contractable_with_plev() {
344        let a = test_index(1, 3);
345        let b = test_index(1, 3);
346        // Same id, dim, and default plev=0: contractable
347        assert!(a.is_contractable(&b));
348    }
349
350    #[test]
351    fn test_default_is_contractable_rejects_id_and_dim_mismatch() {
352        let a = test_index(1, 3);
353        let different_id = test_index(2, 3);
354        let different_dim = test_index(1, 4);
355
356        assert!(!a.is_contractable(&different_id));
357        assert!(!a.is_contractable(&different_dim));
358    }
359
360    #[test]
361    fn test_default_is_contractable_supports_directed_pairs_only() {
362        let ket = directed_test_index(1, 3, ConjState::Ket);
363        let bra = directed_test_index(1, 3, ConjState::Bra);
364        let undirected = test_index(1, 3);
365
366        assert!(ket.is_contractable(&bra));
367        assert!(bra.is_contractable(&ket));
368        assert!(!ket.is_contractable(&undirected));
369        assert!(!undirected.is_contractable(&bra));
370    }
371
372    #[test]
373    fn test_default_link_helpers_preserve_expected_structure() {
374        let idx = directed_test_index(7, 5, ConjState::Ket);
375        let conjugated = idx.conj();
376        assert_eq!(conjugated.conj_state(), ConjState::Bra);
377        assert_eq!(conjugated.id(), idx.id());
378        assert_eq!(conjugated.dim(), idx.dim());
379
380        let similar = idx.sim();
381        assert_eq!(similar.dim(), idx.dim());
382        assert_eq!(similar.conj_state(), idx.conj_state());
383        assert_ne!(similar.id(), idx.id());
384
385        let (left, right) = TestIndex::create_dummy_link_pair();
386        assert_eq!(left.dim(), 1);
387        assert!(left.is_contractable(&right));
388    }
389
390    #[test]
391    fn test_product_link_helper_checks_empty_and_overflow_inputs() {
392        let a = test_index(1, 2);
393        let b = test_index(2, 3);
394        let product = TestIndex::product_link(&[a, b]).unwrap();
395        assert_eq!(product.dim(), 6);
396        assert_eq!(product.conj_state(), ConjState::Undirected);
397
398        assert!(TestIndex::product_link(&[]).is_err());
399        assert!(TestIndex::product_link(&[test_index(1, usize::MAX), test_index(2, 2)]).is_err());
400    }
401
402    #[test]
403    fn test_default_same_id() {
404        let a = test_index(1, 3);
405        let b = test_index(1, 5);
406        let c = test_index(2, 3);
407        assert!(a.same_id(&b));
408        assert!(!a.same_id(&c));
409    }
410
411    #[test]
412    fn test_default_has_id() {
413        let a = test_index(42, 3);
414        assert!(a.has_id(&42));
415        assert!(!a.has_id(&99));
416    }
417}