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;