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    /// # Errors
76    ///
77    /// Returns [`TagSetError::InvalidTag`] when a parsed tag cannot be stored, or
78    /// [`TagSetError::TooManyTags`] when the parsed list exceeds the capacity.
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 [`TagSetError::TagContainsComma`] when a tag contains a comma, or
91    /// another [`TagSetError`] when adding a tag fails.
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 a [`TagSetError`] when `tag` is rejected by [`TagSet::from_str`]
349    /// (an invalid tag or an over-capacity set).
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// use tensor4all_core::DynIndex;
355    /// use tensor4all_core::TagSetLike;
356    ///
357    /// let site = DynIndex::new_dyn_with_tag(2, "Site").unwrap();
358    /// assert!(site.tags().has_tag("Site"));
359    /// ```
360    pub fn new_dyn_with_tag(size: usize, tag: &str) -> Result<Self, TagSetError> {
361        Ok(Self {
362            id: DynId(generate_id()),
363            dim: size,
364            plev: 0,
365            tags: TagSet::from_str(tag)?,
366        })
367    }
368
369    /// Create a new bond index with "Link" tag (for SVD, QR, etc.).
370    ///
371    /// This is a convenience method for creating bond indices commonly used in tensor
372    /// decompositions like SVD and QR factorization.
373    ///
374    /// # Errors
375    ///
376    /// Returns a [`TagSetError`] when the literal `"Link"` tag is rejected by
377    /// [`TagSet::from_str`]; no dimension validation is performed.
378    ///
379    /// # Examples
380    ///
381    /// ```
382    /// use tensor4all_core::DynIndex;
383    /// use tensor4all_core::TagSetLike;
384    ///
385    /// let link = DynIndex::new_link(4).unwrap();
386    /// assert!(link.tags().has_tag("Link"));
387    /// ```
388    pub fn new_link(size: usize) -> Result<Self, TagSetError> {
389        Self::new_dyn_with_tag(size, "Link")
390    }
391}
392
393// Equality and Hash implementations: compare by `id`, `tags`, and `plev`
394// (matching ITensors.jl semantics where equality = id + plev + tags)
395impl<Id: PartialEq, Tags: PartialEq> PartialEq for Index<Id, Tags> {
396    fn eq(&self, other: &Self) -> bool {
397        self.id == other.id && self.tags == other.tags && self.plev == other.plev
398    }
399}
400
401impl<Id: Eq, Tags: Eq> Eq for Index<Id, Tags> {}
402
403impl<Id: std::hash::Hash, Tags: std::hash::Hash> std::hash::Hash for Index<Id, Tags> {
404    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
405        self.id.hash(state);
406        self.tags.hash(state);
407        self.plev.hash(state);
408    }
409}
410
411// Copy implementation: Index is Copy when Id and Tags are both Copy
412impl<Id: Copy, Tags: Copy> Copy for Index<Id, Tags> {}
413
414thread_local! {
415    /// Thread-local random number generator for ID generation.
416    ///
417    /// Each thread has its own RNG, similar to ITensors.jl's task-local RNG.
418    /// This provides thread-safe ID generation without global synchronization.
419    static ID_RNG: RefCell<rand::rngs::ThreadRng> = RefCell::new(rand::rng());
420}
421
422/// Generate a unique random ID for dynamic indices (thread-safe).
423///
424/// Uses thread-local random number generator to generate UInt64 IDs,
425/// compatible with ITensors.jl's `IDType = UInt64`.
426pub(crate) fn generate_id() -> u64 {
427    ID_RNG.with(|rng| rng.borrow_mut().random())
428}
429
430/// Default Index type alias (same as `Index<Id>` with default tags).
431///
432/// This is provided for convenience and compatibility.
433pub type DefaultIndex<Id> = Index<Id, TagSet>;
434
435/// Type alias for backwards compatibility.
436pub type DefaultTagSet = TagSet;
437
438// ============================================================================
439// DynIndex: Default index type with IndexLike implementation
440// ============================================================================
441
442/// Type alias for the default index type with IndexLike bound.
443///
444/// `DynIndex` uses:
445/// - `DynId`: Dynamic identity (UUID-based unique identifier)
446/// - `TagSet`: Default tag set for metadata
447///
448/// This is the recommended index type for most tensor network applications.
449/// It does not include symmetry (quantum numbers); for QSpace-compatible indices,
450/// use a separate concrete type that implements `IndexLike` directly.
451///
452/// # Examples
453///
454/// ```
455/// use tensor4all_core::DynIndex;
456/// use tensor4all_core::index_like::IndexLike;
457///
458/// // Create a dynamic index with dimension 4
459/// let idx = DynIndex::new_dyn(4);
460/// assert_eq!(idx.dim(), 4);
461/// assert_eq!(idx.plev(), 0);
462///
463/// // Prime level manipulation
464/// let primed = idx.prime();
465/// assert_eq!(primed.plev(), 1);
466///
467/// let noprime = primed.noprime();
468/// assert_eq!(noprime.plev(), 0);
469///
470/// // Bond index creation (for SVD/QR)
471/// let bond = DynIndex::new_bond(8).unwrap();
472/// assert_eq!(bond.dim(), 8);
473/// ```
474pub type DynIndex = Index<DynId, TagSet>;
475
476impl IndexLike for DynIndex {
477    type Id = DynId;
478
479    fn id(&self) -> &Self::Id {
480        &self.id
481    }
482
483    fn dim(&self) -> usize {
484        self.dim
485    }
486
487    fn plev(&self) -> i64 {
488        self.plev
489    }
490
491    fn conj_state(&self) -> crate::ConjState {
492        // Default indices are undirected (ITensors.jl-like behavior)
493        crate::ConjState::Undirected
494    }
495
496    fn conj(&self) -> Self {
497        // For undirected indices, conj() is a no-op
498        self.clone()
499    }
500
501    fn sim(&self) -> Self {
502        Index {
503            id: DynId(generate_id()),
504            dim: self.dim,
505            plev: self.plev,
506            tags: self.tags.clone(),
507        }
508    }
509
510    fn create_dummy_link_pair() -> (Self, Self) {
511        let id = DynId(generate_id());
512        let idx1 = Index {
513            id,
514            dim: 1,
515            plev: 0,
516            tags: TagSet::default(),
517        };
518        let idx2 = Index {
519            id,
520            dim: 1,
521            plev: 0,
522            tags: TagSet::default(),
523        };
524        (idx1, idx2)
525    }
526
527    fn new_link(dim: usize) -> anyhow::Result<Self> {
528        Index::new_link(dim).map_err(anyhow::Error::new)
529    }
530
531    fn product_link(indices: &[Self]) -> Result<Self> {
532        anyhow::ensure!(
533            !indices.is_empty(),
534            "product_link requires at least one index"
535        );
536        let dim = indices.iter().try_fold(1usize, |acc, idx| {
537            acc.checked_mul(idx.dim())
538                .ok_or_else(|| anyhow::anyhow!("product link dimension overflow"))
539        })?;
540        DynIndex::new_bond(dim).map_err(|e| anyhow::anyhow!("failed to create bond index: {e}"))
541    }
542}
543
544impl DynIndex {
545    /// Create a new bond index with a fresh identity and the specified dimension.
546    ///
547    /// This is used by factorization operations (SVD, QR) to create new internal
548    /// bond indices connecting the factors.
549    ///
550    /// # Arguments
551    /// * `dim` - The dimension of the new index
552    ///
553    /// # Returns
554    /// A new index with a unique identity and the specified dimension.
555    ///
556    /// # Errors
557    ///
558    /// Returns a [`TagSetError`] when the literal `"Link"` tag is rejected by
559    /// [`TagSet::from_str`]; no dimension validation is performed.
560    ///
561    /// # Examples
562    ///
563    /// ```
564    /// use tensor4all_core::{DynIndex, IndexLike};
565    ///
566    /// let bond = DynIndex::new_bond(8).unwrap();
567    /// assert_eq!(bond.dim(), 8);
568    /// ```
569    pub fn new_bond(dim: usize) -> std::result::Result<Self, TagSetError> {
570        Index::new_link(dim)
571    }
572
573    /// Return a copy of this index with its prime level incremented by one.
574    ///
575    /// Primed indices are commonly used to distinguish bra and ket indices
576    /// in MPO operations (e.g., `i` for input, `i'` for output).
577    ///
578    /// # Examples
579    ///
580    /// ```
581    /// use tensor4all_core::{DynIndex, IndexLike};
582    ///
583    /// let i = DynIndex::new_dyn(4);
584    /// assert_eq!(i.plev(), 0);
585    ///
586    /// let i_prime = i.prime();
587    /// assert_eq!(i_prime.plev(), 1);
588    ///
589    /// // Primed and unprimed indices have the same ID but are not equal
590    /// assert!(i.same_id(&i_prime));
591    /// assert_ne!(i, i_prime);
592    ///
593    /// // Primed indices are not contractable with unprimed ones
594    /// assert!(!i.is_contractable(&i_prime));
595    /// ```
596    pub fn prime(&self) -> Self {
597        let mut idx = self.clone();
598        idx.plev += 1;
599        idx
600    }
601
602    /// Return a copy of this index with its prime level reset to zero.
603    ///
604    /// # Examples
605    ///
606    /// ```
607    /// use tensor4all_core::{DynIndex, IndexLike};
608    ///
609    /// let i = DynIndex::new_dyn(4);
610    /// let i2 = i.prime().prime();
611    /// assert_eq!(i2.plev(), 2);
612    ///
613    /// let i0 = i2.noprime();
614    /// assert_eq!(i0.plev(), 0);
615    /// assert_eq!(i0, i);
616    /// ```
617    pub fn noprime(&self) -> Self {
618        let mut idx = self.clone();
619        idx.plev = 0;
620        idx
621    }
622
623    /// Return a copy of this index with its prime level set explicitly.
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// use tensor4all_core::{DynIndex, IndexLike};
629    ///
630    /// let i = DynIndex::new_dyn(4);
631    /// let i3 = i.set_plev(3);
632    /// assert_eq!(i3.plev(), 3);
633    /// ```
634    pub fn set_plev(&self, plev: i64) -> Self {
635        let mut idx = self.clone();
636        idx.plev = plev;
637        idx
638    }
639}
640
641#[cfg(test)]
642mod tests;