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