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///
63/// (including ID, dimension, and conjugate state if applicable)
64/// - **`dim()`**: Returns the dimension of the index
65/// - **`conj_state()`**: Returns the conjugate state (direction) of the index
66///
67/// # Key Properties
68///
69/// - **`Eq`**: Defines object equality (includes ID, dimension, and conjugate state)
70/// - **`Hash`**: Enables efficient lookup in `HashMap<I, ...>` / `HashSet<I>`
71/// - **`Clone`**: Indices are small value types, freely copyable
72/// - **`is_contractable()`**: Determines if two indices can be contracted
73///
74/// # Conjugate State and Contractability
75///
76/// The `conj_state()` method returns the direction of an index:
77/// - `Undirected`: Directionless index (ITensors.jl-like default)
78/// - `Ket`: Ingoing index (QSpace: no trailing `*` in itag)
79/// - `Bra`: Outgoing index (QSpace: trailing `*` in itag)
80///
81/// Two indices are contractable if:
82/// - Their dimensions match
83/// - Their full index identity is compatible:
84///
85/// - `(Undirected, Undirected)` → equal indices
86/// - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
87/// - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
88///
89/// # Example
90///
91/// ```
92/// use tensor4all_core::{DynIndex, IndexLike};
93///
94/// let i = DynIndex::new_dyn(2);
95/// let j = DynIndex::new_dyn(3);
96/// let k = DynIndex::new_dyn(4);
97///
98/// let a = vec![i.clone(), j.clone()];
99/// let b = vec![j.clone(), k.clone()];
100/// let common: Vec<_> = a.iter().filter(|idx| b.contains(idx)).cloned().collect();
101///
102/// assert_eq!(common, vec![j]);
103/// ```
104pub trait IndexLike: Clone + Eq + Hash + Debug + Send + Sync + 'static {
105 /// Lightweight identifier type (conjugate-independent).
106 ///
107 /// **Rule**: Contractable indices must have compatible full index identity.
108 ///
109 /// The ID serves as a lightweight component of full index identity.
110 /// In large tensor networks, IDs enable efficient graph-based lookups (O(1) with HashSet/HashMap)
111 /// to find matching legs across many tensors.
112 ///
113 /// This is separate from the full `Eq`/`Hash` identity:
114 /// - **ID**: raw logical identifier, useful for serialization and diagnostics
115 /// - **Eq/Hash**: concrete tensor leg identity used by maps, sets, and contraction
116 /// - **dim/ConjState**: mathematical compatibility checks
117 type Id: Clone + Eq + Hash + Debug + Send + Sync;
118
119 /// Get the identifier of this index.
120 ///
121 /// The ID is a lightweight raw identifier.
122 /// **Do not use ID-only equality as concrete tensor leg identity**; use full index
123 /// equality, or `is_contractable()` when deciding contraction compatibility.
124 ///
125 /// Two indices with the same ID may still be distinct concrete legs, for example when
126 /// they differ by prime level, tags, or direction.
127 fn id(&self) -> &Self::Id;
128
129 /// Get the total dimension (state-space dimension) of the index.
130 fn dim(&self) -> usize;
131
132 /// Get the prime level of this index.
133 /// Default: 0 (unprimed).
134 fn plev(&self) -> i64 {
135 0
136 }
137
138 /// Get the conjugate state (direction) of this index.
139 ///
140 /// Returns `ConjState::Undirected` for directionless indices (ITensors.jl-like default),
141 /// or `ConjState::Ket`/`ConjState::Bra` for directed indices (QSpace-compatible).
142 fn conj_state(&self) -> ConjState;
143
144 /// Create the conjugate of this index.
145 ///
146 /// For directed indices, this toggles between `Ket` and `Bra`.
147 /// For `Undirected` indices, this returns `self` unchanged (no-op).
148 ///
149 /// # Returns
150 /// A new index with the conjugate state toggled (if directed) or unchanged (if undirected).
151 fn conj(&self) -> Self;
152
153 /// Check if this index can be contracted with another index.
154 ///
155 /// Two indices are contractable if:
156 /// - They have the same dimension
157 /// - Their full identity is compatible:
158 ///
159 /// - `(Undirected, Undirected)` → equal indices
160 /// - `(Ket, Bra)` or `(Bra, Ket)` → one index is the other's conjugate
161 /// - Mixed `(Undirected, Ket/Bra)` → **not contractable** (mixing forbidden)
162 ///
163 /// # Default Implementation
164 ///
165 /// The default implementation checks:
166 /// 1. Same dimension: `self.dim() == other.dim()`
167 /// 2. Compatible conjugate states (see rules above)
168 /// 3. Full index equality for undirected indices, or conjugate full equality for
169 /// directed pairs.
170 fn is_contractable(&self, other: &Self) -> bool {
171 if self.dim() != other.dim() {
172 return false;
173 }
174 match (self.conj_state(), other.conj_state()) {
175 (ConjState::Ket, ConjState::Bra) | (ConjState::Bra, ConjState::Ket) => {
176 self.conj() == other.clone()
177 }
178 (ConjState::Undirected, ConjState::Undirected) => self == other,
179 _ => false, // Mixed directed/undirected is forbidden
180 }
181 }
182
183 /// Check if this index has the same ID as another.
184 ///
185 /// Default implementation compares IDs directly.
186 /// This is a convenience method for pure ID comparison (does not check contractability).
187 fn same_id(&self, other: &Self) -> bool {
188 self.id() == other.id()
189 }
190
191 /// Check if this index has the given ID.
192 ///
193 /// Default implementation compares with the given ID.
194 fn has_id(&self, id: &Self::Id) -> bool {
195 self.id() == id
196 }
197
198 /// Create a similar index with a new identity but the same structure (dimension, tags, etc.).
199 ///
200 /// This is used to create "equivalent" indices that have the same properties
201 /// but different identities, commonly needed in index replacement operations.
202 ///
203 /// # Returns
204 /// A new index with a fresh identity and the same structure as `self`.
205 fn sim(&self) -> Self
206 where
207 Self: Sized;
208
209 /// Create a pair of contractable dummy indices with dimension 1.
210 ///
211 /// These are used for structural connections that don't carry quantum numbers,
212 /// such as connecting components in a tree tensor network.
213 ///
214 /// Both indices will be `Undirected` and have the same ID, making them contractable.
215 ///
216 /// # Returns
217 /// A pair `(idx1, idx2)` where `idx1.is_contractable(&idx2)` is true.
218 fn create_dummy_link_pair() -> (Self, Self)
219 where
220 Self: Sized;
221
222 /// Create a fresh link index representing the tensor-product space of input indices.
223 ///
224 /// Generic algorithms may use this to replace multiple local bond legs by one fused
225 /// leg without depending on a concrete index implementation. The returned link must
226 /// have a fresh identity and represent the exact tensor-product basis of `indices`.
227 ///
228 /// Implementations with symmetry or sector metadata should preserve the tensor-product
229 /// basis and charge structure when possible. They should return `Err` if the fused
230 /// product link cannot be represented exactly.
231 ///
232 /// # Arguments
233 /// * `indices` - Non-empty input indices whose tensor-product space is represented by
234 ///
235 /// the output. Typical inputs are link or bond indices being fused into one link.
236 ///
237 /// # Returns
238 /// A new index with fresh identity and dimension equal to the checked product of all input
239 /// dimensions.
240 ///
241 /// # Errors
242 ///
243 /// Returns an error when the operation fails (a shape or index mismatch, or
244 /// /// a backend failure).
245 ///
246 /// # Examples
247 ///
248 /// ```
249 /// use tensor4all_core::{DynIndex, IndexLike, TagSetLike};
250 ///
251 /// let a = DynIndex::new_link(2).unwrap();
252 /// let b = DynIndex::new_link(3).unwrap();
253 /// let product = DynIndex::product_link(&[a.clone(), b.clone()]).unwrap();
254 ///
255 /// assert_eq!(product.dim(), 6);
256 /// assert!(product.tags().has_tag("Link"));
257 /// assert_ne!(product.id(), a.id());
258 /// assert_ne!(product.id(), b.id());
259 /// ```
260 fn product_link(indices: &[Self]) -> anyhow::Result<Self>
261 where
262 Self: Sized;
263}
264
265/// Sort indices by a deterministic comparator over `dim`, `plev`, `id`, then
266/// `Debug` representation.
267///
268/// The `plev` and `Debug` terms keep distinct indices that share the same ID —
269/// for example a same-ID pair differing by prime level or tags — in a pinned,
270/// insertion-order-independent relative order instead of leaving them to the
271/// stable sort's input order.
272///
273/// The comparator is a pure function of the index values, so the result never
274/// depends on insertion order as long as `Debug` is deterministic and
275/// distinguishes all identity-relevant fields. The concrete
276/// [`DynIndex`](crate::defaults::DynIndex) satisfies the determinism part: its
277/// derived `Debug` covers `id`, `dim`, `plev`, and `tags`, so two `DynIndex`
278/// values compare `Equal` only when every field matches. Note that `dim`
279/// participates in the sort even though `DynIndex`'s `Eq` deliberately ignores
280/// it (ITensors semantics: equality = id + plev + tags). Implementors whose
281/// `Debug` omits identity-relevant fields may still see `Equal` comparisons
282/// between distinct indices, in which case the stable sort preserves the input
283/// order for those ties.
284///
285/// # Arguments
286/// * `indices` - The slice of indices to reorder in place.
287///
288/// # Returns
289/// Nothing; `indices` is reordered in place.
290///
291/// # Panics
292/// Never panics; the comparator relies only on `IndexLike` accessors and
293/// `Debug` implementations.
294///
295/// # Examples
296///
297/// ```
298/// use tensor4all_core::{sort_indices_deterministic, DynIndex, IndexLike};
299///
300/// let s = DynIndex::new_dyn(2);
301/// let s_prime = s.prime(); // same ID, plev 1
302/// assert!(s.same_id(&s_prime));
303///
304/// // Insertion order (primed first) must not survive the sort.
305/// let mut indices = vec![s_prime.clone(), s.clone()];
306/// sort_indices_deterministic(&mut indices);
307/// assert_eq!(indices, vec![s, s_prime]);
308/// ```
309pub fn sort_indices_deterministic<I: IndexLike>(indices: &mut [I])
310where
311 I::Id: Ord,
312{
313 indices.sort_by(|left, right| {
314 left.dim()
315 .cmp(&right.dim())
316 .then_with(|| left.plev().cmp(&right.plev()))
317 .then_with(|| left.id().cmp(right.id()))
318 .then_with(|| format!("{left:?}").cmp(&format!("{right:?}")))
319 });
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 /// Minimal IndexLike implementation that uses the default plev() method.
327 /// Used to test coverage of the default trait implementations.
328 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
329 struct TestIndex {
330 id: u64,
331 dim: usize,
332 state: ConjState,
333 }
334
335 fn test_index(id: u64, dim: usize) -> TestIndex {
336 TestIndex {
337 id,
338 dim,
339 state: ConjState::Undirected,
340 }
341 }
342
343 fn directed_test_index(id: u64, dim: usize, state: ConjState) -> TestIndex {
344 TestIndex { id, dim, state }
345 }
346
347 impl IndexLike for TestIndex {
348 type Id = u64;
349
350 fn id(&self) -> &u64 {
351 &self.id
352 }
353
354 fn dim(&self) -> usize {
355 self.dim
356 }
357
358 fn conj_state(&self) -> ConjState {
359 self.state
360 }
361
362 fn conj(&self) -> Self {
363 let state = match self.state {
364 ConjState::Undirected => ConjState::Undirected,
365 ConjState::Ket => ConjState::Bra,
366 ConjState::Bra => ConjState::Ket,
367 };
368 TestIndex {
369 state,
370 ..self.clone()
371 }
372 }
373
374 fn sim(&self) -> Self {
375 TestIndex {
376 id: self.id + 1000,
377 ..self.clone()
378 }
379 }
380
381 fn create_dummy_link_pair() -> (Self, Self) {
382 (test_index(0, 1), test_index(0, 1))
383 }
384
385 /// # Errors
386 ///
387 /// Returns an error when the operation fails (a shape or index mismatch, or
388 /// /// a backend failure).
389 ///
390 fn product_link(indices: &[Self]) -> anyhow::Result<Self> {
391 anyhow::ensure!(
392 !indices.is_empty(),
393 "product_link requires at least one index"
394 );
395 let dim = indices.iter().try_fold(1usize, |acc, idx| {
396 acc.checked_mul(idx.dim)
397 .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
398 })?;
399 Ok(test_index(9999, dim))
400 }
401 }
402
403 #[test]
404 fn test_default_plev_is_zero() {
405 let idx = test_index(1, 3);
406 assert_eq!(idx.plev(), 0);
407 }
408
409 #[test]
410 fn test_default_is_contractable_with_plev() {
411 let a = test_index(1, 3);
412 let b = test_index(1, 3);
413 // Same id, dim, and default plev=0: contractable
414 assert!(a.is_contractable(&b));
415 }
416
417 #[test]
418 fn test_default_is_contractable_rejects_id_and_dim_mismatch() {
419 let a = test_index(1, 3);
420 let different_id = test_index(2, 3);
421 let different_dim = test_index(1, 4);
422
423 assert!(!a.is_contractable(&different_id));
424 assert!(!a.is_contractable(&different_dim));
425 }
426
427 #[test]
428 fn test_default_is_contractable_supports_directed_pairs_only() {
429 let ket = directed_test_index(1, 3, ConjState::Ket);
430 let bra = directed_test_index(1, 3, ConjState::Bra);
431 let undirected = test_index(1, 3);
432
433 assert!(ket.is_contractable(&bra));
434 assert!(bra.is_contractable(&ket));
435 assert!(!ket.is_contractable(&undirected));
436 assert!(!undirected.is_contractable(&bra));
437 }
438
439 #[test]
440 fn test_default_link_helpers_preserve_expected_structure() {
441 let idx = directed_test_index(7, 5, ConjState::Ket);
442 let conjugated = idx.conj();
443 assert_eq!(conjugated.conj_state(), ConjState::Bra);
444 assert_eq!(conjugated.id(), idx.id());
445 assert_eq!(conjugated.dim(), idx.dim());
446
447 let similar = idx.sim();
448 assert_eq!(similar.dim(), idx.dim());
449 assert_eq!(similar.conj_state(), idx.conj_state());
450 assert_ne!(similar.id(), idx.id());
451
452 let (left, right) = TestIndex::create_dummy_link_pair();
453 assert_eq!(left.dim(), 1);
454 assert!(left.is_contractable(&right));
455 }
456
457 #[test]
458 fn test_product_link_helper_checks_empty_and_overflow_inputs() {
459 let a = test_index(1, 2);
460 let b = test_index(2, 3);
461 let product = TestIndex::product_link(&[a, b]).unwrap();
462 assert_eq!(product.dim(), 6);
463 assert_eq!(product.conj_state(), ConjState::Undirected);
464
465 assert!(TestIndex::product_link(&[]).is_err());
466 assert!(TestIndex::product_link(&[test_index(1, usize::MAX), test_index(2, 2)]).is_err());
467 }
468
469 #[test]
470 fn test_default_same_id() {
471 let a = test_index(1, 3);
472 let b = test_index(1, 5);
473 let c = test_index(2, 3);
474 assert!(a.same_id(&b));
475 assert!(!a.same_id(&c));
476 }
477
478 #[test]
479 fn test_default_has_id() {
480 let a = test_index(42, 3);
481 assert!(a.has_id(&42));
482 assert!(!a.has_id(&99));
483 }
484
485 #[test]
486 fn test_sort_indices_deterministic_pins_same_id_prime_pair() {
487 use crate::defaults::DynIndex;
488
489 let s = DynIndex::new_dyn(2);
490 let s_prime = s.prime();
491 assert!(s.same_id(&s_prime));
492 assert_ne!(s, s_prime);
493
494 // Primed index first (worst-case insertion order): the sort must pin
495 // unprimed before primed by full-index order (plev 0 < plev 1), not
496 // preserve insertion order.
497 let mut indices = vec![s_prime.clone(), s.clone()];
498 sort_indices_deterministic(&mut indices);
499 assert_eq!(indices, vec![s.clone(), s_prime.clone()]);
500
501 // Same canonical order regardless of input permutation.
502 let mut indices = vec![s.clone(), s_prime.clone()];
503 sort_indices_deterministic(&mut indices);
504 assert_eq!(indices, vec![s, s_prime]);
505 }
506
507 #[test]
508 fn test_sort_indices_deterministic_distinguishes_same_id_tag_pair() {
509 use crate::defaults::{DynId, DynIndex, TagSet};
510
511 let site = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Site"]).unwrap());
512 let link = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Link"]).unwrap());
513 assert!(site.same_id(&link));
514 assert_ne!(site, link);
515
516 // The Debug tiebreak must give one deterministic order, not leave the
517 // pair to insertion order.
518 let mut a = vec![link.clone(), site.clone()];
519 let mut b = vec![site.clone(), link.clone()];
520 sort_indices_deterministic(&mut a);
521 sort_indices_deterministic(&mut b);
522 assert_eq!(a, b);
523 assert!(a.contains(&site));
524 assert!(a.contains(&link));
525 }
526}