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