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
29/// Tag set wrapper using `Arc` for efficient cloning.
30///
31/// This wraps the underlying tag storage in an `Arc` for cheap cloning (reference count increment only).
32/// Tags are immutable and shared across indices with the same tag set.
33///
34/// # Size comparison
35/// - Inline storage: 168 bytes (Copy)
36/// - `TagSet` (Arc): 8 bytes (Clone only)
37///
38/// # Example
39/// ```
40/// use tensor4all_core::index::TagSet;
41///
42/// let tags = TagSet::from_str("Site,Link").unwrap();
43/// assert!(tags.has_tag("Site"));
44/// assert!(tags.has_tag("Link"));
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
47pub struct TagSet(Arc<InlineTagSet>);
48
49impl TagSet {
50    /// Create an empty tag set.
51    pub fn new() -> Self {
52        Self(Arc::new(InlineTagSet::new()))
53    }
54
55    /// Create a tag set from a comma-separated string.
56    #[allow(clippy::should_implement_trait)]
57    pub fn from_str(s: &str) -> Result<Self, TagSetError> {
58        Ok(Self(Arc::new(InlineTagSet::from_str(s)?)))
59    }
60
61    /// Create a tag set from a slice of tag strings.
62    ///
63    /// Returns an error if any tag contains a comma (reserved as separator in `from_str`).
64    ///
65    /// # Example
66    /// ```
67    /// use tensor4all_core::index::TagSet;
68    ///
69    /// let tags = TagSet::from_tags(&["Site", "Link"]).unwrap();
70    /// assert!(tags.has_tag("Site"));
71    /// assert!(tags.has_tag("Link"));
72    /// assert_eq!(tags.len(), 2);
73    ///
74    /// // Comma in tag is an error
75    /// assert!(TagSet::from_tags(&["Site,Link"]).is_err());
76    /// ```
77    pub fn from_tags(tags: &[&str]) -> Result<Self, TagSetError> {
78        let mut inner = InlineTagSet::new();
79        for tag in tags {
80            if tag.contains(',') {
81                return Err(TagSetError::TagContainsComma {
82                    tag: (*tag).to_string(),
83                });
84            }
85            inner.add_tag(tag)?;
86        }
87        Ok(Self(Arc::new(inner)))
88    }
89
90    /// Check if a tag is present.
91    pub fn has_tag(&self, tag: &str) -> bool {
92        self.0.has_tag(tag)
93    }
94
95    /// Get the number of tags.
96    pub fn len(&self) -> usize {
97        self.0.len()
98    }
99
100    /// Check if the tag set is empty.
101    pub fn is_empty(&self) -> bool {
102        self.0.is_empty()
103    }
104
105    /// Get the inner Arc for advanced use.
106    pub fn inner(&self) -> &Arc<InlineTagSet> {
107        &self.0
108    }
109}
110
111impl std::ops::Deref for TagSet {
112    type Target = InlineTagSet;
113
114    fn deref(&self) -> &Self::Target {
115        &self.0
116    }
117}
118
119impl TagSetLike for TagSet {
120    fn len(&self) -> usize {
121        self.0.len()
122    }
123
124    fn capacity(&self) -> usize {
125        self.0.capacity()
126    }
127
128    fn get(&self, index: usize) -> Option<String> {
129        TagSetLike::get(&*self.0, index)
130    }
131
132    fn iter(&self) -> TagSetIterator<'_> {
133        TagSetLike::iter(&*self.0)
134    }
135
136    fn has_tag(&self, tag: &str) -> bool {
137        self.0.has_tag(tag)
138    }
139
140    fn add_tag(&mut self, tag: &str) -> Result<(), TagSetError> {
141        // Arc is immutable, so we need to clone and replace
142        let mut inner = *self.0;
143        inner.add_tag(tag)?;
144        self.0 = Arc::new(inner);
145        Ok(())
146    }
147
148    fn remove_tag(&mut self, tag: &str) -> bool {
149        // Arc is immutable, so we need to clone and replace
150        let mut inner = *self.0;
151        let removed = inner.remove_tag(tag);
152        if removed {
153            self.0 = Arc::new(inner);
154        }
155        removed
156    }
157}
158
159/// Index with generic identity type `Id` and tag type `Tags`.
160///
161/// - `Id = DynId` for ITensors-like runtime identity
162/// - `Id = ZST marker type` for compile-time-known identity
163/// - `Tags = TagSet` for tags (default, Arc-wrapped for cheap cloning)
164///
165/// **Note**: This default implementation does not include symmetry (quantum numbers).
166/// For QSpace-compatible indices with non-Abelian symmetries, use a separate concrete type.
167///
168/// # Memory Layout
169/// With default types (`DynId`, `TagSet`):
170/// - Size: 32 bytes (8 + 8 + 8 + 8)
171/// - Tags are shared via `Arc`, so cloning is cheap (reference count increment only)
172///
173/// **Equality**: Two `Index` values are considered equal if and only if their `id` and `tags`
174/// fields and `plev` match (matching ITensors.jl semantics where equality = id + plev + tags).
175///
176/// # Example
177/// ```
178/// use tensor4all_core::index::{Index, DynId, TagSet};
179///
180/// // Create shared tags once
181/// let site_tags = TagSet::from_str("Site").unwrap();
182///
183/// // Share the same tags across many indices (cheap clone)
184/// let i1 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
185/// let i2 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
186/// // i1.tags and i2.tags point to the same Arc
187/// ```
188#[derive(Debug, Clone)]
189pub struct Index<Id, Tags = TagSet> {
190    /// The unique identifier for this index.
191    pub id: Id,
192    /// The dimension (size) of this index.
193    pub dim: usize,
194    /// The prime level of this index.
195    pub plev: i64,
196    /// The tag set associated with this index.
197    pub tags: Tags,
198}
199
200impl<Id, Tags> Index<Id, Tags>
201where
202    Tags: Default,
203{
204    /// Create a new index with the given identity and dimension.
205    pub fn new(id: Id, dim: usize) -> Self {
206        Self {
207            id,
208            dim,
209            plev: 0,
210            tags: Tags::default(),
211        }
212    }
213
214    /// Create a new index with the given identity, dimension, and tags.
215    pub fn new_with_tags(id: Id, dim: usize, tags: Tags) -> Self {
216        Self {
217            id,
218            dim,
219            plev: 0,
220            tags,
221        }
222    }
223
224    /// Get the dimension (size) of the index.
225    pub fn size(&self) -> usize {
226        self.dim
227    }
228
229    /// Get a reference to the tags.
230    pub fn tags(&self) -> &Tags {
231        &self.tags
232    }
233}
234
235impl<Id, Tags> Index<Id, Tags>
236where
237    Tags: Default,
238{
239    /// Create a new index from dimension (convenience constructor).
240    pub fn new_with_size(id: Id, size: usize) -> Self {
241        Self {
242            id,
243            dim: size,
244            plev: 0,
245            tags: Tags::default(),
246        }
247    }
248
249    /// Create a new index from dimension and tags.
250    pub fn new_with_size_and_tags(id: Id, size: usize, tags: Tags) -> Self {
251        Self {
252            id,
253            dim: size,
254            plev: 0,
255            tags,
256        }
257    }
258}
259
260// Constructors for Index with TagSet (default)
261impl Index<DynId, TagSet> {
262    /// Create a new index with a generated dynamic ID and no tags.
263    pub fn new_dyn(size: usize) -> Self {
264        Self {
265            id: DynId(generate_id()),
266            dim: size,
267            plev: 0,
268            tags: TagSet::new(),
269        }
270    }
271
272    /// Create a new index with a generated dynamic ID and shared tags.
273    ///
274    /// This is the most efficient way to create many indices with the same tags.
275    /// The `Arc` is cloned (reference count increment only), not the underlying data.
276    ///
277    /// # Example
278    /// ```
279    /// use tensor4all_core::index::{Index, DynId, TagSet};
280    ///
281    /// let site_tags = TagSet::from_str("Site").unwrap();
282    /// let i1 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
283    /// let i2 = Index::<DynId>::new_dyn_with_tags(2, site_tags.clone());
284    /// ```
285    pub fn new_dyn_with_tags(size: usize, tags: TagSet) -> Self {
286        Self {
287            id: DynId(generate_id()),
288            dim: size,
289            plev: 0,
290            tags,
291        }
292    }
293
294    /// Create a new index with a generated dynamic ID and a single tag.
295    ///
296    /// This creates a new `TagSet` with the given tag.
297    /// For sharing the same tag across many indices, create the `TagSet`
298    /// once and use `new_dyn_with_tags` instead.
299    pub fn new_dyn_with_tag(size: usize, tag: &str) -> Result<Self, TagSetError> {
300        Ok(Self {
301            id: DynId(generate_id()),
302            dim: size,
303            plev: 0,
304            tags: TagSet::from_str(tag)?,
305        })
306    }
307
308    /// Create a new bond index with "Link" tag (for SVD, QR, etc.).
309    ///
310    /// This is a convenience method for creating bond indices commonly used in tensor
311    /// decompositions like SVD and QR factorization.
312    pub fn new_link(size: usize) -> Result<Self, TagSetError> {
313        Self::new_dyn_with_tag(size, "Link")
314    }
315}
316
317// Equality and Hash implementations: compare by `id`, `tags`, and `plev`
318// (matching ITensors.jl semantics where equality = id + plev + tags)
319impl<Id: PartialEq, Tags: PartialEq> PartialEq for Index<Id, Tags> {
320    fn eq(&self, other: &Self) -> bool {
321        self.id == other.id && self.tags == other.tags && self.plev == other.plev
322    }
323}
324
325impl<Id: Eq, Tags: Eq> Eq for Index<Id, Tags> {}
326
327impl<Id: std::hash::Hash, Tags: std::hash::Hash> std::hash::Hash for Index<Id, Tags> {
328    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
329        self.id.hash(state);
330        self.tags.hash(state);
331        self.plev.hash(state);
332    }
333}
334
335// Copy implementation: Index is Copy when Id and Tags are both Copy
336impl<Id: Copy, Tags: Copy> Copy for Index<Id, Tags> {}
337
338thread_local! {
339    /// Thread-local random number generator for ID generation.
340    ///
341    /// Each thread has its own RNG, similar to ITensors.jl's task-local RNG.
342    /// This provides thread-safe ID generation without global synchronization.
343    static ID_RNG: RefCell<rand::rngs::ThreadRng> = RefCell::new(rand::rng());
344}
345
346/// Generate a unique random ID for dynamic indices (thread-safe).
347///
348/// Uses thread-local random number generator to generate UInt64 IDs,
349/// compatible with ITensors.jl's `IDType = UInt64`.
350pub(crate) fn generate_id() -> u64 {
351    ID_RNG.with(|rng| rng.borrow_mut().random())
352}
353
354/// Default Index type alias (same as `Index<Id>` with default tags).
355///
356/// This is provided for convenience and compatibility.
357pub type DefaultIndex<Id> = Index<Id, TagSet>;
358
359/// Type alias for backwards compatibility.
360pub type DefaultTagSet = TagSet;
361
362// ============================================================================
363// DynIndex: Default index type with IndexLike implementation
364// ============================================================================
365
366/// Type alias for the default index type with IndexLike bound.
367///
368/// `DynIndex` uses:
369/// - `DynId`: Dynamic identity (UUID-based unique identifier)
370/// - `TagSet`: Default tag set for metadata
371///
372/// This is the recommended index type for most tensor network applications.
373/// It does not include symmetry (quantum numbers); for QSpace-compatible indices,
374/// use a separate concrete type that implements `IndexLike` directly.
375///
376/// # Examples
377///
378/// ```
379/// use tensor4all_core::DynIndex;
380/// use tensor4all_core::index_like::IndexLike;
381///
382/// // Create a dynamic index with dimension 4
383/// let idx = DynIndex::new_dyn(4);
384/// assert_eq!(idx.dim(), 4);
385/// assert_eq!(idx.plev(), 0);
386///
387/// // Prime level manipulation
388/// let primed = idx.prime();
389/// assert_eq!(primed.plev(), 1);
390///
391/// let noprime = primed.noprime();
392/// assert_eq!(noprime.plev(), 0);
393///
394/// // Bond index creation (for SVD/QR)
395/// let bond = DynIndex::new_bond(8).unwrap();
396/// assert_eq!(bond.dim(), 8);
397/// ```
398pub type DynIndex = Index<DynId, TagSet>;
399
400impl IndexLike for DynIndex {
401    type Id = DynId;
402
403    fn id(&self) -> &Self::Id {
404        &self.id
405    }
406
407    fn dim(&self) -> usize {
408        self.dim
409    }
410
411    fn plev(&self) -> i64 {
412        self.plev
413    }
414
415    fn conj_state(&self) -> crate::ConjState {
416        // Default indices are undirected (ITensors.jl-like behavior)
417        crate::ConjState::Undirected
418    }
419
420    fn conj(&self) -> Self {
421        // For undirected indices, conj() is a no-op
422        self.clone()
423    }
424
425    fn sim(&self) -> Self {
426        Index {
427            id: DynId(generate_id()),
428            dim: self.dim,
429            plev: self.plev,
430            tags: self.tags.clone(),
431        }
432    }
433
434    fn create_dummy_link_pair() -> (Self, Self) {
435        let id = DynId(generate_id());
436        let idx1 = Index {
437            id,
438            dim: 1,
439            plev: 0,
440            tags: TagSet::default(),
441        };
442        let idx2 = Index {
443            id,
444            dim: 1,
445            plev: 0,
446            tags: TagSet::default(),
447        };
448        (idx1, idx2)
449    }
450}
451
452impl DynIndex {
453    /// Create a new bond index with a fresh identity and the specified dimension.
454    ///
455    /// This is used by factorization operations (SVD, QR) to create new internal
456    /// bond indices connecting the factors.
457    ///
458    /// # Arguments
459    /// * `dim` - The dimension of the new index
460    ///
461    /// # Returns
462    /// A new index with a unique identity and the specified dimension.
463    pub fn new_bond(dim: usize) -> Result<Self> {
464        Index::new_link(dim).map_err(|e| anyhow::anyhow!("Failed to create bond index: {:?}", e))
465    }
466
467    /// Return a copy of this index with its prime level incremented by one.
468    pub fn prime(&self) -> Self {
469        let mut idx = self.clone();
470        idx.plev += 1;
471        idx
472    }
473
474    /// Return a copy of this index with its prime level reset to zero.
475    pub fn noprime(&self) -> Self {
476        let mut idx = self.clone();
477        idx.plev = 0;
478        idx
479    }
480
481    /// Return a copy of this index with its prime level set explicitly.
482    pub fn set_plev(&self, plev: i64) -> Self {
483        let mut idx = self.clone();
484        idx.plev = plev;
485        idx
486    }
487}
488
489#[cfg(test)]
490mod tests;