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 undirected link-like index with the requested dimension.
223 ///
224 /// Algorithms that assemble a batch or randomized sketch need an
225 /// auxiliary column index whose dimension is not known from an input leg.
226 /// Implementations should give the index a fresh identity and link
227 /// metadata where their index model has such metadata.
228 ///
229 /// # Errors
230 ///
231 /// Returns an error when `dim` causes a dimension overflow or cannot be
232 /// represented by the index type.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use tensor4all_core::{DynIndex, IndexLike};
238 ///
239 /// let probe = DynIndex::new_link(5).unwrap();
240 /// assert_eq!(probe.dim(), 5);
241 /// ```
242 fn new_link(dim: usize) -> anyhow::Result<Self>
243 where
244 Self: Sized;
245
246 /// Create a fresh link index representing the tensor-product space of input indices.
247 ///
248 /// Generic algorithms may use this to replace multiple local bond legs by one fused
249 /// leg without depending on a concrete index implementation. The returned link must
250 /// have a fresh identity and represent the exact tensor-product basis of `indices`.
251 ///
252 /// Implementations with symmetry or sector metadata should preserve the tensor-product
253 /// basis and charge structure when possible. They should return `Err` if the fused
254 /// product link cannot be represented exactly.
255 ///
256 /// # Arguments
257 /// * `indices` - Non-empty input indices whose tensor-product space is represented by
258 ///
259 /// the output. Typical inputs are link or bond indices being fused into one link.
260 ///
261 /// # Returns
262 /// A new index with fresh identity and dimension equal to the checked product of all input
263 /// dimensions.
264 ///
265 /// # Errors
266 ///
267 /// Returns an error when the operation fails (a shape or index mismatch, or
268 /// /// a backend failure).
269 ///
270 /// # Examples
271 ///
272 /// ```
273 /// use tensor4all_core::{DynIndex, IndexLike, TagSetLike};
274 ///
275 /// let a = DynIndex::new_link(2).unwrap();
276 /// let b = DynIndex::new_link(3).unwrap();
277 /// let product = DynIndex::product_link(&[a.clone(), b.clone()]).unwrap();
278 ///
279 /// assert_eq!(product.dim(), 6);
280 /// assert!(product.tags().has_tag("Link"));
281 /// assert_ne!(product.id(), a.id());
282 /// assert_ne!(product.id(), b.id());
283 /// ```
284 fn product_link(indices: &[Self]) -> anyhow::Result<Self>
285 where
286 Self: Sized;
287}
288
289/// Sort indices by a deterministic comparator over `dim`, `plev`, `id`, then
290/// `Debug` representation.
291///
292/// The `plev` and `Debug` terms keep distinct indices that share the same ID —
293/// for example a same-ID pair differing by prime level or tags — in a pinned,
294/// insertion-order-independent relative order instead of leaving them to the
295/// stable sort's input order.
296///
297/// The comparator is a pure function of the index values, so the result never
298/// depends on insertion order as long as `Debug` is deterministic and
299/// distinguishes all identity-relevant fields. The concrete
300/// [`DynIndex`](crate::defaults::DynIndex) satisfies the determinism part: its
301/// derived `Debug` covers `id`, `dim`, `plev`, and `tags`, so two `DynIndex`
302/// values compare `Equal` only when every field matches. Note that `dim`
303/// participates in the sort even though `DynIndex`'s `Eq` deliberately ignores
304/// it (ITensors semantics: equality = id + plev + tags). Implementors whose
305/// `Debug` omits identity-relevant fields may still see `Equal` comparisons
306/// between distinct indices, in which case the stable sort preserves the input
307/// order for those ties.
308///
309/// # Arguments
310/// * `indices` - The slice of indices to reorder in place.
311///
312/// # Returns
313/// Nothing; `indices` is reordered in place.
314///
315/// # Panics
316/// Never panics; the comparator relies only on `IndexLike` accessors and
317/// `Debug` implementations.
318///
319/// # Examples
320///
321/// ```
322/// use tensor4all_core::{sort_indices_deterministic, DynIndex, IndexLike};
323///
324/// let s = DynIndex::new_dyn(2);
325/// let s_prime = s.prime(); // same ID, plev 1
326/// assert!(s.same_id(&s_prime));
327///
328/// // Insertion order (primed first) must not survive the sort.
329/// let mut indices = vec![s_prime.clone(), s.clone()];
330/// sort_indices_deterministic(&mut indices);
331/// assert_eq!(indices, vec![s, s_prime]);
332/// ```
333pub fn sort_indices_deterministic<I: IndexLike>(indices: &mut [I])
334where
335 I::Id: Ord,
336{
337 indices.sort_by(|left, right| {
338 left.dim()
339 .cmp(&right.dim())
340 .then_with(|| left.plev().cmp(&right.plev()))
341 .then_with(|| left.id().cmp(right.id()))
342 .then_with(|| format!("{left:?}").cmp(&format!("{right:?}")))
343 });
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 /// Minimal IndexLike implementation that uses the default plev() method.
351 /// Used to test coverage of the default trait implementations.
352 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
353 struct TestIndex {
354 id: u64,
355 dim: usize,
356 state: ConjState,
357 }
358
359 fn test_index(id: u64, dim: usize) -> TestIndex {
360 TestIndex {
361 id,
362 dim,
363 state: ConjState::Undirected,
364 }
365 }
366
367 fn directed_test_index(id: u64, dim: usize, state: ConjState) -> TestIndex {
368 TestIndex { id, dim, state }
369 }
370
371 impl IndexLike for TestIndex {
372 type Id = u64;
373
374 fn id(&self) -> &u64 {
375 &self.id
376 }
377
378 fn dim(&self) -> usize {
379 self.dim
380 }
381
382 fn conj_state(&self) -> ConjState {
383 self.state
384 }
385
386 fn conj(&self) -> Self {
387 let state = match self.state {
388 ConjState::Undirected => ConjState::Undirected,
389 ConjState::Ket => ConjState::Bra,
390 ConjState::Bra => ConjState::Ket,
391 };
392 TestIndex {
393 state,
394 ..self.clone()
395 }
396 }
397
398 fn sim(&self) -> Self {
399 TestIndex {
400 id: self.id + 1000,
401 ..self.clone()
402 }
403 }
404
405 fn create_dummy_link_pair() -> (Self, Self) {
406 (test_index(0, 1), test_index(0, 1))
407 }
408
409 fn new_link(dim: usize) -> anyhow::Result<Self> {
410 Ok(test_index(10000, dim))
411 }
412
413 /// # Errors
414 ///
415 /// Returns an error when the operation fails (a shape or index mismatch, or
416 /// /// a backend failure).
417 ///
418 fn product_link(indices: &[Self]) -> anyhow::Result<Self> {
419 anyhow::ensure!(
420 !indices.is_empty(),
421 "product_link requires at least one index"
422 );
423 let dim = indices.iter().try_fold(1usize, |acc, idx| {
424 acc.checked_mul(idx.dim)
425 .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
426 })?;
427 Ok(test_index(9999, dim))
428 }
429 }
430
431 #[test]
432 fn test_default_plev_is_zero() {
433 let idx = test_index(1, 3);
434 assert_eq!(idx.plev(), 0);
435 }
436
437 #[test]
438 fn test_default_is_contractable_with_plev() {
439 let a = test_index(1, 3);
440 let b = test_index(1, 3);
441 // Same id, dim, and default plev=0: contractable
442 assert!(a.is_contractable(&b));
443 }
444
445 #[test]
446 fn test_default_is_contractable_rejects_id_and_dim_mismatch() {
447 let a = test_index(1, 3);
448 let different_id = test_index(2, 3);
449 let different_dim = test_index(1, 4);
450
451 assert!(!a.is_contractable(&different_id));
452 assert!(!a.is_contractable(&different_dim));
453 }
454
455 #[test]
456 fn test_default_is_contractable_supports_directed_pairs_only() {
457 let ket = directed_test_index(1, 3, ConjState::Ket);
458 let bra = directed_test_index(1, 3, ConjState::Bra);
459 let undirected = test_index(1, 3);
460
461 assert!(ket.is_contractable(&bra));
462 assert!(bra.is_contractable(&ket));
463 assert!(!ket.is_contractable(&undirected));
464 assert!(!undirected.is_contractable(&bra));
465 }
466
467 #[test]
468 fn test_default_link_helpers_preserve_expected_structure() {
469 let idx = directed_test_index(7, 5, ConjState::Ket);
470 let conjugated = idx.conj();
471 assert_eq!(conjugated.conj_state(), ConjState::Bra);
472 assert_eq!(conjugated.id(), idx.id());
473 assert_eq!(conjugated.dim(), idx.dim());
474
475 let similar = idx.sim();
476 assert_eq!(similar.dim(), idx.dim());
477 assert_eq!(similar.conj_state(), idx.conj_state());
478 assert_ne!(similar.id(), idx.id());
479
480 let (left, right) = TestIndex::create_dummy_link_pair();
481 assert_eq!(left.dim(), 1);
482 assert!(left.is_contractable(&right));
483 }
484
485 #[test]
486 fn test_product_link_helper_checks_empty_and_overflow_inputs() {
487 let a = test_index(1, 2);
488 let b = test_index(2, 3);
489 let product = TestIndex::product_link(&[a, b]).unwrap();
490 assert_eq!(product.dim(), 6);
491 assert_eq!(product.conj_state(), ConjState::Undirected);
492
493 assert!(TestIndex::product_link(&[]).is_err());
494 assert!(TestIndex::product_link(&[test_index(1, usize::MAX), test_index(2, 2)]).is_err());
495 }
496
497 #[test]
498 fn test_default_same_id() {
499 let a = test_index(1, 3);
500 let b = test_index(1, 5);
501 let c = test_index(2, 3);
502 assert!(a.same_id(&b));
503 assert!(!a.same_id(&c));
504 }
505
506 #[test]
507 fn test_default_has_id() {
508 let a = test_index(42, 3);
509 assert!(a.has_id(&42));
510 assert!(!a.has_id(&99));
511 }
512
513 #[test]
514 fn test_sort_indices_deterministic_pins_same_id_prime_pair() {
515 use crate::defaults::DynIndex;
516
517 let s = DynIndex::new_dyn(2);
518 let s_prime = s.prime();
519 assert!(s.same_id(&s_prime));
520 assert_ne!(s, s_prime);
521
522 // Primed index first (worst-case insertion order): the sort must pin
523 // unprimed before primed by full-index order (plev 0 < plev 1), not
524 // preserve insertion order.
525 let mut indices = vec![s_prime.clone(), s.clone()];
526 sort_indices_deterministic(&mut indices);
527 assert_eq!(indices, vec![s.clone(), s_prime.clone()]);
528
529 // Same canonical order regardless of input permutation.
530 let mut indices = vec![s.clone(), s_prime.clone()];
531 sort_indices_deterministic(&mut indices);
532 assert_eq!(indices, vec![s, s_prime]);
533 }
534
535 #[test]
536 fn test_sort_indices_deterministic_distinguishes_same_id_tag_pair() {
537 use crate::defaults::{DynId, DynIndex, TagSet};
538
539 let site = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Site"]).unwrap());
540 let link = DynIndex::new_with_tags(DynId(42), 2, TagSet::from_tags(&["Link"]).unwrap());
541 assert!(site.same_id(&link));
542 assert_ne!(site, link);
543
544 // The Debug tiebreak must give one deterministic order, not leave the
545 // pair to insertion order.
546 let mut a = vec![link.clone(), site.clone()];
547 let mut b = vec![site.clone(), link.clone()];
548 sort_indices_deterministic(&mut a);
549 sort_indices_deterministic(&mut b);
550 assert_eq!(a, b);
551 assert!(a.contains(&site));
552 assert!(a.contains(&link));
553 }
554}