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}