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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum ConjState {
26    /// Directionless index (ITensors.jl-like default).
27    ///
28    /// Undirected indices can contract with other undirected indices
29    /// if they have the same ID and dimension.
30    Undirected,
31    /// Ket (ingoing) index.
32    ///
33    /// In QSpace terminology, this corresponds to an index without a trailing `*` in its itag.
34    /// Ket indices can only contract with Bra indices (and vice versa).
35    Ket,
36    /// Bra (outgoing) index.
37    ///
38    /// In QSpace terminology, this corresponds to an index with a trailing `*` in its itag.
39    /// Bra indices can only contract with Ket indices (and vice versa).
40    Bra,
41}
42
43/// Trait for index-like types that can be used in tensor operations.
44///
45/// This trait abstracts away the identity mechanism of indices, allowing algorithms
46/// to work with any index type that provides equality, hashing, and dimension access.
47///
48/// # Design Principles
49///
50/// - **`Id` as associated type**: Lightweight identifier (conjugate-independent)
51/// - **`Eq` by object equality**: Two indices are equal iff they represent the same object
52///   (including ID, dimension, and conjugate state if applicable)
53/// - **`dim()`**: Returns the dimension of the index
54/// - **`conj_state()`**: Returns the conjugate state (direction) of the index
55///
56/// # Key Properties
57///
58/// - **`Eq`**: Defines object equality (includes ID, dimension, and conjugate state)
59/// - **`Hash`**: Enables efficient lookup in `HashMap<I, ...>` / `HashSet<I>`
60/// - **`Clone`**: Indices are small value types, freely copyable
61/// - **`is_contractable()`**: Determines if two indices can be contracted
62///
63/// # Conjugate State and Contractability
64///
65/// The `conj_state()` method returns the direction of an index:
66/// - `Undirected`: Directionless index (ITensors.jl-like default)
67/// - `Ket`: Ingoing index (QSpace: no trailing `*` in itag)
68/// - `Bra`: Outgoing index (QSpace: trailing `*` in itag)
69///
70/// Two indices are contractable if:
71/// - They have the same `id()` and `dim()`
72/// - Their conjugate states are compatible:
73///   - `(Ket, Bra)` or `(Bra, Ket)` → contractable
74///   - `(Undirected, Undirected)` → contractable
75///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
76///
77/// # Example
78///
79/// ```ignore
80/// fn contract_common<I: IndexLike>(a: &Tensor<I>, b: &Tensor<I>) -> Tensor<I> {
81///     // Algorithm doesn't need to know about Id, Symm, Tags
82///     // It only needs indices to be comparable and have dimensions
83///     let common: Vec<_> = a.indices().iter()
84///         .filter(|idx| b.indices().contains(idx))
85///         .cloned()
86///         .collect();
87///     // ...
88/// }
89/// ```
90pub trait IndexLike: Clone + Eq + Hash + Debug + Send + Sync + 'static {
91    /// Lightweight identifier type (conjugate-independent).
92    ///
93    /// **Rule**: Contractable indices must have the same ID.
94    ///
95    /// The ID serves as a "pairing key" to identify which legs are intended to contract.
96    /// In large tensor networks, IDs enable efficient graph-based lookups (O(1) with HashSet/HashMap)
97    /// to find matching legs across many tensors.
98    ///
99    /// This is separate from dimension/direction checks:
100    /// - **ID**: "intent to pair" (which specific legs should connect)
101    /// - **dim/ConjState**: "mathematical compatibility" (can they actually contract)
102    type Id: Clone + Eq + Hash + Debug + Send + Sync;
103
104    /// Get the identifier of this index.
105    ///
106    /// The ID is used as the pairing key during contraction.
107    /// **Contractable indices must have the same ID** — this is enforced by `is_contractable()`.
108    ///
109    /// Two indices with the same ID represent the same logical leg (though they may differ
110    /// in conjugate state for directed indices).
111    fn id(&self) -> &Self::Id;
112
113    /// Get the total dimension (state-space dimension) of the index.
114    fn dim(&self) -> usize;
115
116    /// Get the prime level of this index.
117    /// Default: 0 (unprimed).
118    fn plev(&self) -> i64 {
119        0
120    }
121
122    /// Get the conjugate state (direction) of this index.
123    ///
124    /// Returns `ConjState::Undirected` for directionless indices (ITensors.jl-like default),
125    /// or `ConjState::Ket`/`ConjState::Bra` for directed indices (QSpace-compatible).
126    fn conj_state(&self) -> ConjState;
127
128    /// Create the conjugate of this index.
129    ///
130    /// For directed indices, this toggles between `Ket` and `Bra`.
131    /// For `Undirected` indices, this returns `self` unchanged (no-op).
132    ///
133    /// # Returns
134    /// A new index with the conjugate state toggled (if directed) or unchanged (if undirected).
135    fn conj(&self) -> Self;
136
137    /// Check if this index can be contracted with another index.
138    ///
139    /// Two indices are contractable if:
140    /// - They have the same `id()` and `dim()`
141    /// - Their conjugate states are compatible:
142    ///   - `(Ket, Bra)` or `(Bra, Ket)` → contractable
143    ///   - `(Undirected, Undirected)` → contractable
144    ///   - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
145    ///
146    /// # Default Implementation
147    ///
148    /// The default implementation checks:
149    /// 1. Same ID: `self.id() == other.id()`
150    /// 2. Same dimension: `self.dim() == other.dim()`
151    /// 3. Same prime level: `self.plev() == other.plev()`
152    /// 4. Compatible conjugate states (see rules above)
153    fn is_contractable(&self, other: &Self) -> bool {
154        if self.id() != other.id() || self.dim() != other.dim() || self.plev() != other.plev() {
155            return false;
156        }
157        match (self.conj_state(), other.conj_state()) {
158            (ConjState::Ket, ConjState::Bra) | (ConjState::Bra, ConjState::Ket) => true,
159            (ConjState::Undirected, ConjState::Undirected) => true,
160            _ => false, // Mixed directed/undirected is forbidden
161        }
162    }
163
164    /// Check if this index has the same ID as another.
165    ///
166    /// Default implementation compares IDs directly.
167    /// This is a convenience method for pure ID comparison (does not check contractability).
168    fn same_id(&self, other: &Self) -> bool {
169        self.id() == other.id()
170    }
171
172    /// Check if this index has the given ID.
173    ///
174    /// Default implementation compares with the given ID.
175    fn has_id(&self, id: &Self::Id) -> bool {
176        self.id() == id
177    }
178
179    /// Create a similar index with a new identity but the same structure (dimension, tags, etc.).
180    ///
181    /// This is used to create "equivalent" indices that have the same properties
182    /// but different identities, commonly needed in index replacement operations.
183    ///
184    /// # Returns
185    /// A new index with a fresh identity and the same structure as `self`.
186    fn sim(&self) -> Self
187    where
188        Self: Sized;
189
190    /// Create a pair of contractable dummy indices with dimension 1.
191    ///
192    /// These are used for structural connections that don't carry quantum numbers,
193    /// such as connecting components in a tree tensor network.
194    ///
195    /// Both indices will be `Undirected` and have the same ID, making them contractable.
196    ///
197    /// # Returns
198    /// A pair `(idx1, idx2)` where `idx1.is_contractable(&idx2)` is true.
199    fn create_dummy_link_pair() -> (Self, Self)
200    where
201        Self: Sized;
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    /// Minimal IndexLike implementation that uses the default plev() method.
209    /// Used to test coverage of the default trait implementations.
210    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
211    struct TestIndex {
212        id: u64,
213        dim: usize,
214    }
215
216    impl IndexLike for TestIndex {
217        type Id = u64;
218
219        fn id(&self) -> &u64 {
220            &self.id
221        }
222
223        fn dim(&self) -> usize {
224            self.dim
225        }
226
227        fn conj_state(&self) -> ConjState {
228            ConjState::Undirected
229        }
230
231        fn conj(&self) -> Self {
232            self.clone()
233        }
234
235        fn sim(&self) -> Self {
236            TestIndex {
237                id: self.id + 1000,
238                dim: self.dim,
239            }
240        }
241
242        fn create_dummy_link_pair() -> (Self, Self) {
243            (TestIndex { id: 0, dim: 1 }, TestIndex { id: 0, dim: 1 })
244        }
245    }
246
247    #[test]
248    fn test_default_plev_is_zero() {
249        let idx = TestIndex { id: 1, dim: 3 };
250        assert_eq!(idx.plev(), 0);
251    }
252
253    #[test]
254    fn test_default_is_contractable_with_plev() {
255        let a = TestIndex { id: 1, dim: 3 };
256        let b = TestIndex { id: 1, dim: 3 };
257        // Same id, dim, and default plev=0: contractable
258        assert!(a.is_contractable(&b));
259    }
260
261    #[test]
262    fn test_default_same_id() {
263        let a = TestIndex { id: 1, dim: 3 };
264        let b = TestIndex { id: 1, dim: 5 };
265        let c = TestIndex { id: 2, dim: 3 };
266        assert!(a.same_id(&b));
267        assert!(!a.same_id(&c));
268    }
269
270    #[test]
271    fn test_default_has_id() {
272        let a = TestIndex { id: 42, dim: 3 };
273        assert!(a.has_id(&42));
274        assert!(!a.has_id(&99));
275    }
276}