tensor4all_core/defaults/index.rs
1//! Index types for tensor network operations.
2//!
3//! This module provides the default index types:
4//!
5//! - [`DynId`]: Runtime identity (UUID-based unique identifier)
6//! - [`TagSet`]: Tag set for metadata (Arc-wrapped for cheap cloning)
7//! - [`Index`]: Generic index type parameterized by Id and Tags
8//! - [`DynIndex`]: Default index type (`Index<DynId, TagSet>`)
9//!
10//! The `DynIndex` type implements the [`IndexLike`] trait.
11//!
12//! **Note**: Symmetry (quantum numbers) is not included in the default implementation.
13//! For QSpace-compatible indices with non-Abelian symmetries, use a separate concrete type
14//! that implements `IndexLike` directly.
15
16use crate::index_like::IndexLike;
17use crate::tagset::{DefaultTagSet as InlineTagSet, TagSetError, TagSetIterator, TagSetLike};
18use anyhow::Result;
19use rand::Rng;
20use std::cell::RefCell;
21use std::sync::Arc;
22
23/// Runtime ID for ITensors-like dynamic identity.
24///
25/// Uses UInt64 for compatibility with ITensors.jl's `IDType = UInt64`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
27pub struct DynId(pub u64);
28
29impl DynId {
30 /// Return the numeric ID value used for ITensors-compatible serialization.
31 ///
32 /// Prefer full [`Index`] equality for tensor index identity.
33 /// This accessor exists for stable serialization and FFI query paths that
34 /// must expose the raw numeric identifier.
35 ///
36 /// # Examples
37 ///
38 /// ```
39 /// use tensor4all_core::DynId;
40 ///
41 /// let id = DynId(42);
42 /// assert_eq!(id.value(), 42);
43 /// ```
44 pub fn value(&self) -> u64 {
45 self.0
46 }
47}
48
49/// Tag set wrapper using `Arc` for efficient cloning.
50///
51/// This wraps the underlying default tag storage in an `Arc` for cheap cloning
52/// (reference count increment only). Tags are immutable and shared across
53/// indices with the same tag set. The default storage is string-backed so
54/// descriptive tags from language bindings are preserved losslessly.
55///
56/// # Example
57/// ```
58/// use tensor4all_core::index::TagSet;
59///
60/// let tags = TagSet::from_str("Site,Link").unwrap();
61/// assert!(tags.has_tag("Site"));
62/// assert!(tags.has_tag("Link"));
63/// ```
64#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
65pub struct TagSet(Arc<InlineTagSet>);
66
67impl TagSet {
68 /// Create an empty tag set.
69 pub fn new() -> Self {
70 Self(Arc::new(InlineTagSet::new()))
71 }
72
73 /// Create a tag set from a comma-separated string.
74 #[allow(clippy::should_implement_trait)]
75 /// # Errors
76 ///
77 /// Returns an error when the string cannot be parsed as an index (an
78 /// /// invalid-input failure).
79 ///
80 pub fn from_str(s: &str) -> Result<Self, TagSetError> {
81 Ok(Self(Arc::new(InlineTagSet::from_str(s)?)))
82 }
83
84 /// Create a tag set from a slice of tag strings.
85 ///
86 /// Returns an error if any tag contains a comma (reserved as separator in `from_str`).
87 ///
88 /// # Errors
89 ///
90 /// Returns an error when the tag string is invalid (an invalid-input
91 /// /// failure).
92 ///
93 /// # Example
94 /// ```
95 /// use tensor4all_core::index::TagSet;
96 ///
97 /// let tags = TagSet::from_tags(&["Site", "Link"]).unwrap();
98 /// assert!(tags.has_tag("Site"));
99 /// assert!(tags.has_tag("Link"));
100 /// assert_eq!(tags.len(), 2);
101 ///
102 /// // Comma in tag is an error
103 /// assert!(TagSet::from_tags(&["Site,Link"]).is_err());
104 /// ```
105 pub fn from_tags(tags: &[&str]) -> Result<Self, TagSetError> {
106 let mut inner = InlineTagSet::new();
107 for tag in tags {
108 if tag.contains(',') {
109 return Err(TagSetError::TagContainsComma {
110 tag: (*tag).to_string(),
111 });
112 }
113 inner.add_tag(tag)?;
114 }
115 Ok(Self(Arc::new(inner)))
116 }
117
118 /// Check if a tag is present.
119 pub fn has_tag(&self, tag: &str) -> bool {
120 self.0.has_tag(tag)
121 }
122
123 /// Get the number of tags.
124 pub fn len(&self) -> usize {
125 self.0.len()
126 }
127
128 /// Check if the tag set is empty.
129 pub fn is_empty(&self) -> bool {
130 self.0.is_empty()
131 }
132
133 /// Get the inner Arc for advanced use.
134 pub fn inner(&self) -> &Arc<InlineTagSet> {
135 &self.0
136 }
137}
138
139impl std::ops::Deref for TagSet {
140 type Target = InlineTagSet;
141
142 fn deref(&self) -> &Self::Target {
143 &self.0
144 }
145}
146
147impl TagSetLike for TagSet {
148 fn len(&self) -> usize {
149 self.0.len()
150 }
151
152 fn capacity(&self) -> usize {
153 self.0.capacity()
154 }
155
156 fn get(&self, index: usize) -> Option<String> {
157 TagSetLike::get(&*self.0, index)
158 }
159
160 fn iter(&self) -> TagSetIterator<'_> {
161 TagSetLike::iter(&*self.0)
162 }
163
164 fn has_tag(&self, tag: &str) -> bool {
165 self.0.has_tag(tag)
166 }
167
168 fn add_tag(&mut self, tag: &str) -> Result<(), TagSetError> {
169 // Arc is immutable, so we need to clone and replace
170 let mut inner = self.0.as_ref().clone();
171 inner.add_tag(tag)?;
172 self.0 = Arc::new(inner);
173 Ok(())
174 }
175
176 fn remove_tag(&mut self, tag: &str) -> bool {
177 // Arc is immutable, so we need to clone and replace
178 let mut inner = self.0.as_ref().clone();
179 let removed = inner.remove_tag(tag);
180 if removed {
181 self.0 = Arc::new(inner);
182 }
183 removed
184 }
185}
186
187/// Index with generic identity type `Id` and tag type `Tags`.
188///
189/// - `Id = DynId` for ITensors-like runtime identity
190/// - `Id = ZST marker type` for compile-time-known identity
191/// - `Tags = TagSet` for tags (default, Arc-wrapped for cheap cloning)
192///
193/// **Note**: This default implementation does not include symmetry (quantum numbers).
194/// For QSpace-compatible indices with non-Abelian symmetries, use a separate concrete type.
195///
196/// # Memory Layout
197/// With default types (`DynId`, `TagSet`):
198/// - Size: 32 bytes (8 + 8 + 8 + 8)
199/// - Tags are shared via `Arc`, so cloning is cheap (reference count increment only)
200///
201/// **Equality**: Two `Index` values are considered equal if and only if their `id` and `tags`
202/// fields and `plev` match (matching ITensors.jl semantics where equality = id + plev + tags).
203///
204/// # Example
205/// ```
206/// use tensor4all_core::index::{Index, DynId, TagSet};
207///
208/// // Create shared tags once
209/// let site_tags = TagSet::from_str("Site").unwrap();
210///
211/// // Share the same tags across many indices (cheap clone)
212/// let i1 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
213/// let i2 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
214/// // i1.tags and i2.tags point to the same Arc
215/// ```
216#[derive(Debug, Clone)]
217pub struct Index<Id, Tags = TagSet> {
218 /// The unique identifier for this index.
219 pub id: Id,
220 /// The dimension (size) of this index.
221 pub dim: usize,
222 /// The prime level of this index.
223 pub plev: i64,
224 /// The tag set associated with this index.
225 pub tags: Tags,
226}
227
228impl<Id, Tags> Index<Id, Tags>
229where
230 Tags: Default,
231{
232 /// Create a new index with the given identity and dimension.
233 pub fn new(id: Id, dim: usize) -> Self {
234 Self {
235 id,
236 dim,
237 plev: 0,
238 tags: Tags::default(),
239 }
240 }
241
242 /// Create a new index with the given identity, dimension, and tags.
243 pub fn new_with_tags(id: Id, dim: usize, tags: Tags) -> Self {
244 Self {
245 id,
246 dim,
247 plev: 0,
248 tags,
249 }
250 }
251
252 /// Get the dimension (size) of the index.
253 pub fn size(&self) -> usize {
254 self.dim
255 }
256
257 /// Get a reference to the tags.
258 pub fn tags(&self) -> &Tags {
259 &self.tags
260 }
261}
262
263impl<Id, Tags> Index<Id, Tags>
264where
265 Tags: Default,
266{
267 /// Create a new index from dimension (convenience constructor).
268 pub fn new_with_size(id: Id, size: usize) -> Self {
269 Self {
270 id,
271 dim: size,
272 plev: 0,
273 tags: Tags::default(),
274 }
275 }
276
277 /// Create a new index from dimension and tags.
278 pub fn new_with_size_and_tags(id: Id, size: usize, tags: Tags) -> Self {
279 Self {
280 id,
281 dim: size,
282 plev: 0,
283 tags,
284 }
285 }
286}
287
288// Constructors for Index with TagSet (default)
289impl Index<DynId, TagSet> {
290 /// Create a new index with a generated dynamic ID and no tags.
291 ///
292 /// This is the most common way to create indices. Each call generates
293 /// a unique random ID, so two calls to `new_dyn` with the same dimension
294 /// produce non-equal indices.
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use tensor4all_core::{DynIndex, IndexLike};
300 ///
301 /// let i = DynIndex::new_dyn(4);
302 /// let j = DynIndex::new_dyn(4);
303 ///
304 /// assert_eq!(i.dim(), 4);
305 /// assert_ne!(i, j); // different IDs
306 /// assert!(i.is_contractable(&i)); // same index is contractable with itself
307 /// assert!(!i.is_contractable(&j)); // different IDs
308 /// ```
309 pub fn new_dyn(size: usize) -> Self {
310 Self {
311 id: DynId(generate_id()),
312 dim: size,
313 plev: 0,
314 tags: TagSet::new(),
315 }
316 }
317
318 /// Create a new index with a generated dynamic ID and shared tags.
319 ///
320 /// This is the most efficient way to create many indices with the same tags.
321 /// The `Arc` is cloned (reference count increment only), not the underlying data.
322 ///
323 /// # Example
324 /// ```
325 /// use tensor4all_core::index::{Index, DynId, TagSet};
326 ///
327 /// let site_tags = TagSet::from_str("Site").unwrap();
328 /// let i1 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
329 /// let i2 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
330 /// ```
331 pub fn new_dyn_with_tags(size: usize, tags: TagSet) -> Self {
332 Self {
333 id: DynId(generate_id()),
334 dim: size,
335 plev: 0,
336 tags,
337 }
338 }
339
340 /// Create a new index with a generated dynamic ID and a single tag.
341 ///
342 /// This creates a new `TagSet` with the given tag.
343 /// For sharing the same tag across many indices, create the `TagSet`
344 /// once and use `new_dyn_with_tags` instead.
345 ///
346 /// # Errors
347 ///
348 /// Returns an error when the tag is invalid (an invalid-input failure).
349 ///
350 /// # Examples
351 ///
352 /// ```
353 /// use tensor4all_core::DynIndex;
354 /// use tensor4all_core::TagSetLike;
355 ///
356 /// let site = DynIndex::new_dyn_with_tag(2, "Site").unwrap();
357 /// assert!(site.tags().has_tag("Site"));
358 /// ```
359 pub fn new_dyn_with_tag(size: usize, tag: &str) -> Result<Self, TagSetError> {
360 Ok(Self {
361 id: DynId(generate_id()),
362 dim: size,
363 plev: 0,
364 tags: TagSet::from_str(tag)?,
365 })
366 }
367
368 /// Create a new bond index with "Link" tag (for SVD, QR, etc.).
369 ///
370 /// This is a convenience method for creating bond indices commonly used in tensor
371 /// decompositions like SVD and QR factorization.
372 ///
373 /// # Errors
374 ///
375 /// Returns an error when the tag construction fails (an invalid-input
376 /// failure). No dimension validation is performed.
377 ///
378 /// # Examples
379 ///
380 /// ```
381 /// use tensor4all_core::DynIndex;
382 /// use tensor4all_core::TagSetLike;
383 ///
384 /// let link = DynIndex::new_link(4).unwrap();
385 /// assert!(link.tags().has_tag("Link"));
386 /// ```
387 pub fn new_link(size: usize) -> Result<Self, TagSetError> {
388 Self::new_dyn_with_tag(size, "Link")
389 }
390}
391
392// Equality and Hash implementations: compare by `id`, `tags`, and `plev`
393// (matching ITensors.jl semantics where equality = id + plev + tags)
394impl<Id: PartialEq, Tags: PartialEq> PartialEq for Index<Id, Tags> {
395 fn eq(&self, other: &Self) -> bool {
396 self.id == other.id && self.tags == other.tags && self.plev == other.plev
397 }
398}
399
400impl<Id: Eq, Tags: Eq> Eq for Index<Id, Tags> {}
401
402impl<Id: std::hash::Hash, Tags: std::hash::Hash> std::hash::Hash for Index<Id, Tags> {
403 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
404 self.id.hash(state);
405 self.tags.hash(state);
406 self.plev.hash(state);
407 }
408}
409
410// Copy implementation: Index is Copy when Id and Tags are both Copy
411impl<Id: Copy, Tags: Copy> Copy for Index<Id, Tags> {}
412
413thread_local! {
414 /// Thread-local random number generator for ID generation.
415 ///
416 /// Each thread has its own RNG, similar to ITensors.jl's task-local RNG.
417 /// This provides thread-safe ID generation without global synchronization.
418 static ID_RNG: RefCell<rand::rngs::ThreadRng> = RefCell::new(rand::rng());
419}
420
421/// Generate a unique random ID for dynamic indices (thread-safe).
422///
423/// Uses thread-local random number generator to generate UInt64 IDs,
424/// compatible with ITensors.jl's `IDType = UInt64`.
425pub(crate) fn generate_id() -> u64 {
426 ID_RNG.with(|rng| rng.borrow_mut().random())
427}
428
429/// Default Index type alias (same as `Index<Id>` with default tags).
430///
431/// This is provided for convenience and compatibility.
432pub type DefaultIndex<Id> = Index<Id, TagSet>;
433
434/// Type alias for backwards compatibility.
435pub type DefaultTagSet = TagSet;
436
437// ============================================================================
438// DynIndex: Default index type with IndexLike implementation
439// ============================================================================
440
441/// Type alias for the default index type with IndexLike bound.
442///
443/// `DynIndex` uses:
444/// - `DynId`: Dynamic identity (UUID-based unique identifier)
445/// - `TagSet`: Default tag set for metadata
446///
447/// This is the recommended index type for most tensor network applications.
448/// It does not include symmetry (quantum numbers); for QSpace-compatible indices,
449/// use a separate concrete type that implements `IndexLike` directly.
450///
451/// # Examples
452///
453/// ```
454/// use tensor4all_core::DynIndex;
455/// use tensor4all_core::index_like::IndexLike;
456///
457/// // Create a dynamic index with dimension 4
458/// let idx = DynIndex::new_dyn(4);
459/// assert_eq!(idx.dim(), 4);
460/// assert_eq!(idx.plev(), 0);
461///
462/// // Prime level manipulation
463/// let primed = idx.prime();
464/// assert_eq!(primed.plev(), 1);
465///
466/// let noprime = primed.noprime();
467/// assert_eq!(noprime.plev(), 0);
468///
469/// // Bond index creation (for SVD/QR)
470/// let bond = DynIndex::new_bond(8).unwrap();
471/// assert_eq!(bond.dim(), 8);
472/// ```
473pub type DynIndex = Index<DynId, TagSet>;
474
475impl IndexLike for DynIndex {
476 type Id = DynId;
477
478 fn id(&self) -> &Self::Id {
479 &self.id
480 }
481
482 fn dim(&self) -> usize {
483 self.dim
484 }
485
486 fn plev(&self) -> i64 {
487 self.plev
488 }
489
490 fn conj_state(&self) -> crate::ConjState {
491 // Default indices are undirected (ITensors.jl-like behavior)
492 crate::ConjState::Undirected
493 }
494
495 fn conj(&self) -> Self {
496 // For undirected indices, conj() is a no-op
497 self.clone()
498 }
499
500 fn sim(&self) -> Self {
501 Index {
502 id: DynId(generate_id()),
503 dim: self.dim,
504 plev: self.plev,
505 tags: self.tags.clone(),
506 }
507 }
508
509 fn create_dummy_link_pair() -> (Self, Self) {
510 let id = DynId(generate_id());
511 let idx1 = Index {
512 id,
513 dim: 1,
514 plev: 0,
515 tags: TagSet::default(),
516 };
517 let idx2 = Index {
518 id,
519 dim: 1,
520 plev: 0,
521 tags: TagSet::default(),
522 };
523 (idx1, idx2)
524 }
525
526 fn product_link(indices: &[Self]) -> Result<Self> {
527 anyhow::ensure!(
528 !indices.is_empty(),
529 "product_link requires at least one index"
530 );
531 let dim = indices.iter().try_fold(1usize, |acc, idx| {
532 acc.checked_mul(idx.dim())
533 .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
534 })?;
535 DynIndex::new_bond(dim).map_err(|e| anyhow::anyhow!("failed to create bond index: {e}"))
536 }
537}
538
539impl DynIndex {
540 /// Create a new bond index with a fresh identity and the specified dimension.
541 ///
542 /// This is used by factorization operations (SVD, QR) to create new internal
543 /// bond indices connecting the factors.
544 ///
545 /// # Arguments
546 /// * `dim` - The dimension of the new index
547 ///
548 /// # Returns
549 /// A new index with a unique identity and the specified dimension.
550 ///
551 /// # Errors
552 ///
553 /// Returns an error when the bond dimension is invalid (a shape mismatch).
554 ///
555 /// # Examples
556 ///
557 /// ```
558 /// use tensor4all_core::{DynIndex, IndexLike};
559 ///
560 /// let bond = DynIndex::new_bond(8).unwrap();
561 /// assert_eq!(bond.dim(), 8);
562 /// ```
563 pub fn new_bond(dim: usize) -> std::result::Result<Self, TagSetError> {
564 Index::new_link(dim)
565 }
566
567 /// Return a copy of this index with its prime level incremented by one.
568 ///
569 /// Primed indices are commonly used to distinguish bra and ket indices
570 /// in MPO operations (e.g., `i` for input, `i'` for output).
571 ///
572 /// # Examples
573 ///
574 /// ```
575 /// use tensor4all_core::{DynIndex, IndexLike};
576 ///
577 /// let i = DynIndex::new_dyn(4);
578 /// assert_eq!(i.plev(), 0);
579 ///
580 /// let i_prime = i.prime();
581 /// assert_eq!(i_prime.plev(), 1);
582 ///
583 /// // Primed and unprimed indices have the same ID but are not equal
584 /// assert!(i.same_id(&i_prime));
585 /// assert_ne!(i, i_prime);
586 ///
587 /// // Primed indices are not contractable with unprimed ones
588 /// assert!(!i.is_contractable(&i_prime));
589 /// ```
590 pub fn prime(&self) -> Self {
591 let mut idx = self.clone();
592 idx.plev += 1;
593 idx
594 }
595
596 /// Return a copy of this index with its prime level reset to zero.
597 ///
598 /// # Examples
599 ///
600 /// ```
601 /// use tensor4all_core::{DynIndex, IndexLike};
602 ///
603 /// let i = DynIndex::new_dyn(4);
604 /// let i2 = i.prime().prime();
605 /// assert_eq!(i2.plev(), 2);
606 ///
607 /// let i0 = i2.noprime();
608 /// assert_eq!(i0.plev(), 0);
609 /// assert_eq!(i0, i);
610 /// ```
611 pub fn noprime(&self) -> Self {
612 let mut idx = self.clone();
613 idx.plev = 0;
614 idx
615 }
616
617 /// Return a copy of this index with its prime level set explicitly.
618 ///
619 /// # Examples
620 ///
621 /// ```
622 /// use tensor4all_core::{DynIndex, IndexLike};
623 ///
624 /// let i = DynIndex::new_dyn(4);
625 /// let i3 = i.set_plev(3);
626 /// assert_eq!(i3.plev(), 3);
627 /// ```
628 pub fn set_plev(&self, plev: i64) -> Self {
629 let mut idx = self.clone();
630 idx.plev = plev;
631 idx
632 }
633}
634
635#[cfg(test)]
636mod tests;