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}