Skip to main content

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