Skip to main content

tensor4all_core/
tensor_like.rs

1//! TensorLike trait for unifying tensor types.
2//!
3//! This module provides a fully generic trait for tensor-like objects that expose
4//! external indices and support contraction operations.
5//!
6//! # Design
7//!
8//! The trait is **fully generic** (monomorphic), meaning:
9//! - No trait objects (`dyn TensorLike`)
10//! - Uses associated type for `Index`
11//! - All methods return `Self` instead of concrete types
12//!
13//! For heterogeneous tensor collections, use an enum wrapper.
14
15use crate::any_scalar::AnyScalar;
16use crate::index_like::IndexLike;
17use crate::tensor_index::TensorIndex;
18use crate::truncation::{
19    validate_svd_truncation_options, SvdTruncationOptionsError, SvdTruncationPolicy,
20};
21use std::collections::HashSet;
22use std::fmt::Debug;
23use std::sync::Arc;
24
25// ============================================================================
26// Factorization types (non-generic, algorithm-specific)
27// ============================================================================
28
29use thiserror::Error;
30
31/// Error type for factorize operations.
32///
33/// # Examples
34///
35/// ```
36/// use tensor4all_core::{
37///     factorize, Canonical, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
38/// };
39///
40/// let i = DynIndex::new_dyn(3);
41/// let j = DynIndex::new_dyn(3);
42/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
43/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
44///
45/// // QR with Canonical::Right is not supported
46/// let result = factorize(
47///     &tensor,
48///     &[i],
49///     &FactorizeOptions::qr().with_canonical(Canonical::Right),
50/// );
51/// assert!(result.is_err());
52/// ```
53#[derive(Debug, Error)]
54pub enum FactorizeError {
55    /// Factorization computation failed.
56    #[error("Factorization failed: {0}")]
57    ComputationError(
58        /// The underlying error
59        #[from]
60        anyhow::Error,
61    ),
62    /// Invalid relative tolerance value (must be finite and non-negative).
63    #[error("Invalid rtol value: {0}. rtol must be finite and non-negative.")]
64    InvalidRtol(
65        /// The invalid rtol value
66        f64,
67    ),
68    /// Invalid algorithm-specific option combination.
69    #[error("Invalid factorize options: {0}")]
70    InvalidOptions(
71        /// Description of the invalid option combination
72        &'static str,
73    ),
74    /// The storage type is not supported for this operation.
75    #[error("Unsupported storage type: {0}")]
76    UnsupportedStorage(
77        /// Description of the unsupported storage type
78        &'static str,
79    ),
80    /// The canonical direction is not supported for this algorithm.
81    #[error("Unsupported canonical direction for this algorithm: {0}")]
82    UnsupportedCanonical(
83        /// Description of the unsupported canonical direction
84        &'static str,
85    ),
86    /// Error from SVD operation.
87    #[error("SVD error: {0}")]
88    SvdError(
89        /// The underlying SVD error
90        #[from]
91        crate::svd::SvdError,
92    ),
93    /// Error from QR operation.
94    #[error("QR error: {0}")]
95    QrError(
96        /// The underlying QR error
97        #[from]
98        crate::qr::QrError,
99    ),
100    /// Error from matrix CI operation.
101    #[error("Matrix CI error: {0}")]
102    MatrixCIError(
103        /// The underlying matrix CI error
104        #[from]
105        crate::MatrixCIError,
106    ),
107}
108
109/// Factorization algorithm.
110///
111/// Determines which matrix decomposition is used by [`TensorFactorizationLike::factorize`].
112///
113/// # Examples
114///
115/// ```
116/// use tensor4all_core::FactorizeAlg;
117///
118/// // Default is SVD
119/// assert_eq!(FactorizeAlg::default(), FactorizeAlg::SVD);
120/// ```
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub enum FactorizeAlg {
123    /// Singular Value Decomposition.
124    #[default]
125    SVD,
126    /// QR decomposition.
127    QR,
128    /// Rank-revealing LU decomposition.
129    LU,
130    /// Cross Interpolation (LU-based).
131    CI,
132}
133
134/// Canonical direction for factorization.
135///
136/// This determines which factor is "canonical" (orthogonal for SVD/QR,
137/// or unit-diagonal for LU/CI).
138///
139/// # Examples
140///
141/// ```
142/// use tensor4all_core::{
143///     factorize, Canonical, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
144/// };
145///
146/// let i = DynIndex::new_dyn(3);
147/// let j = DynIndex::new_dyn(3);
148/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
149/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
150///
151/// // Left canonical: left factor has orthonormal columns
152/// let left_result = factorize(
153///     &tensor,
154///     &[i.clone()],
155///     &FactorizeOptions::svd().with_canonical(Canonical::Left),
156/// ).unwrap();
157///
158/// // Right canonical: right factor has orthonormal rows
159/// let right_result = factorize(
160///     &tensor,
161///     &[i.clone()],
162///     &FactorizeOptions::svd().with_canonical(Canonical::Right),
163/// ).unwrap();
164///
165/// // Both recover the same tensor
166/// let recovered_left = left_result.left.contract_pair(&left_result.right).unwrap();
167/// let recovered_right = right_result.left.contract_pair(&right_result.right).unwrap();
168/// assert!(recovered_left.distance(&recovered_right).unwrap() < 1e-12);
169/// ```
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171pub enum Canonical {
172    /// Left factor is canonical.
173    /// - SVD: L=U (orthogonal), R=S*V
174    /// - QR: L=Q (orthogonal), R=R
175    /// - LU/CI: L has unit diagonal
176    #[default]
177    Left,
178    /// Right factor is canonical.
179    /// - SVD: L=U*S, R=V (orthogonal)
180    /// - QR: Not supported (would need LQ)
181    /// - LU/CI: U has unit diagonal
182    Right,
183}
184
185/// Options for tensor factorization.
186///
187/// Controls the algorithm, canonical direction, and truncation parameters
188/// for [`TensorFactorizationLike::factorize`].
189///
190/// # Defaults
191///
192/// - Algorithm: SVD
193/// - Canonical: Left (left factor is orthogonal)
194/// - max_bond_dim: `None` (no rank limit)
195/// - svd_policy: `None` (uses the SVD global default policy)
196/// - qr_rtol: `None` (uses the QR global default tolerance)
197///
198/// # Field Interactions
199///
200/// - `svd_policy` is only valid for `FactorizeAlg::SVD`.
201/// - `qr_rtol` is only valid for `FactorizeAlg::QR`.
202/// - `max_bond_dim` is independent of the algorithm-specific tolerance settings.
203///
204/// # Examples
205///
206/// ```
207/// use tensor4all_core::{
208///     factorize, Canonical, DynIndex, FactorizeOptions, IdxTensor,
209/// };
210///
211/// let i = DynIndex::new_dyn(4);
212/// let j = DynIndex::new_dyn(4);
213/// let mut data = vec![0.0_f64; 16];
214/// data[0] = 1.0;  // rank-1 matrix
215/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
216///
217/// // SVD with an explicit policy
218/// let opts = FactorizeOptions::svd()
219///     .with_svd_policy(tensor4all_core::SvdTruncationPolicy::new(1e-10));
220/// let result = factorize(&tensor, &[i.clone()], &opts).unwrap();
221/// assert_eq!(result.rank, 1);
222///
223/// // QR with max-rank truncation
224/// let opts = FactorizeOptions::qr().with_qr_rtol(1e-8).with_max_bond_dim(2);
225/// let result = factorize(&tensor, &[i.clone()], &opts).unwrap();
226/// assert!(result.rank <= 2);
227/// ```
228#[derive(Debug, Clone)]
229pub struct FactorizeOptions {
230    /// Factorization algorithm to use.
231    pub alg: FactorizeAlg,
232    /// Canonical direction.
233    pub canonical: Canonical,
234    /// Maximum rank for truncation.
235    /// If `None`, no rank limit is applied.
236    pub max_bond_dim: Option<usize>,
237    /// SVD truncation policy.
238    /// If `None`, uses the SVD global default.
239    pub svd_policy: Option<SvdTruncationPolicy>,
240    /// QR-specific relative tolerance.
241    /// If `None`, uses the QR global default.
242    pub qr_rtol: Option<f64>,
243}
244
245impl Default for FactorizeOptions {
246    fn default() -> Self {
247        Self {
248            alg: FactorizeAlg::SVD,
249            canonical: Canonical::Left,
250            max_bond_dim: None,
251            svd_policy: None,
252            qr_rtol: None,
253        }
254    }
255}
256
257impl FactorizeOptions {
258    /// Create options for SVD factorization.
259    ///
260    /// # Examples
261    ///
262    /// ```
263    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
264    ///
265    /// let opts = FactorizeOptions::svd();
266    /// assert_eq!(opts.alg, FactorizeAlg::SVD);
267    /// ```
268    pub fn svd() -> Self {
269        Self {
270            alg: FactorizeAlg::SVD,
271            ..Default::default()
272        }
273    }
274
275    /// Create options for QR factorization.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
281    ///
282    /// let opts = FactorizeOptions::qr();
283    /// assert_eq!(opts.alg, FactorizeAlg::QR);
284    /// ```
285    pub fn qr() -> Self {
286        Self {
287            alg: FactorizeAlg::QR,
288            ..Default::default()
289        }
290    }
291
292    /// Create options for LU factorization.
293    ///
294    /// # Examples
295    ///
296    /// ```
297    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
298    ///
299    /// let opts = FactorizeOptions::lu();
300    /// assert_eq!(opts.alg, FactorizeAlg::LU);
301    /// ```
302    pub fn lu() -> Self {
303        Self {
304            alg: FactorizeAlg::LU,
305            ..Default::default()
306        }
307    }
308
309    /// Create options for CI factorization.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
315    ///
316    /// let opts = FactorizeOptions::ci();
317    /// assert_eq!(opts.alg, FactorizeAlg::CI);
318    /// ```
319    pub fn ci() -> Self {
320        Self {
321            alg: FactorizeAlg::CI,
322            ..Default::default()
323        }
324    }
325
326    /// Set canonical direction.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use tensor4all_core::{Canonical, FactorizeOptions};
332    ///
333    /// let opts = FactorizeOptions::svd().with_canonical(Canonical::Right);
334    /// assert_eq!(opts.canonical, Canonical::Right);
335    /// ```
336    pub fn with_canonical(mut self, canonical: Canonical) -> Self {
337        self.canonical = canonical;
338        self
339    }
340
341    /// Set the SVD truncation policy.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use tensor4all_core::{FactorizeOptions, SvdTruncationPolicy};
347    ///
348    /// let opts = FactorizeOptions::svd().with_svd_policy(SvdTruncationPolicy::new(1e-8));
349    /// assert_eq!(opts.svd_policy, Some(SvdTruncationPolicy::new(1e-8)));
350    /// ```
351    pub fn with_svd_policy(mut self, policy: SvdTruncationPolicy) -> Self {
352        self.svd_policy = Some(policy);
353        self
354    }
355
356    /// Set the QR-specific relative tolerance.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use tensor4all_core::FactorizeOptions;
362    ///
363    /// let opts = FactorizeOptions::qr().with_qr_rtol(1e-8);
364    /// assert_eq!(opts.qr_rtol, Some(1e-8));
365    /// ```
366    pub fn with_qr_rtol(mut self, rtol: f64) -> Self {
367        self.qr_rtol = Some(rtol);
368        self
369    }
370
371    /// Set maximum rank.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use tensor4all_core::FactorizeOptions;
377    ///
378    /// let opts = FactorizeOptions::svd().with_max_bond_dim(10);
379    /// assert_eq!(opts.max_bond_dim, Some(10));
380    /// ```
381    pub fn with_max_bond_dim(mut self, max_bond_dim: usize) -> Self {
382        self.max_bond_dim = Some(max_bond_dim);
383        self
384    }
385
386    /// Validate that the selected fields make sense for the chosen algorithm.
387    ///
388    /// Validates max bond dimension and explicit SVD policy thresholds through
389    /// the shared [`validate_svd_truncation_options`] seam, then checks the
390    /// algorithm/option compatibility rules.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`FactorizeError::InvalidOptions`] if an algorithm is paired with
395    /// unsupported algorithm-specific truncation settings, or if `max_bond_dim`
396    /// is zero or an SVD policy threshold is non-finite/negative.
397    pub fn validate(&self) -> std::result::Result<(), FactorizeError> {
398        validate_svd_truncation_options(self.max_bond_dim, self.svd_policy).map_err(|error| {
399            FactorizeError::InvalidOptions(match error {
400                SvdTruncationOptionsError::ZeroMaxBondDim => "max_bond_dim must be at least 1",
401                SvdTruncationOptionsError::InvalidThreshold(_) => {
402                    "SVD truncation threshold must be finite and non-negative"
403                }
404            })
405        })?;
406
407        match self.alg {
408            FactorizeAlg::SVD => {
409                if self.qr_rtol.is_some() {
410                    return Err(FactorizeError::InvalidOptions(
411                        "SVD factorization does not accept qr_rtol",
412                    ));
413                }
414            }
415            FactorizeAlg::QR => {
416                if self.svd_policy.is_some() {
417                    return Err(FactorizeError::InvalidOptions(
418                        "QR factorization does not accept svd_policy",
419                    ));
420                }
421            }
422            FactorizeAlg::LU | FactorizeAlg::CI => {
423                if self.svd_policy.is_some() {
424                    return Err(FactorizeError::InvalidOptions(
425                        "LU/CI factorization does not accept svd_policy",
426                    ));
427                }
428                if self.qr_rtol.is_some() {
429                    return Err(FactorizeError::InvalidOptions(
430                        "LU/CI factorization does not accept qr_rtol",
431                    ));
432                }
433            }
434        }
435
436        Ok(())
437    }
438}
439
440/// Result of tensor factorization.
441///
442/// Contains the two factors, the bond index connecting them, and metadata
443/// about the decomposition. The original tensor can be recovered (up to
444/// truncation error) by contracting `left` and `right` along `bond_index`.
445///
446/// # Examples
447///
448/// ```
449/// use tensor4all_core::{
450///     factorize, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
451/// };
452///
453/// let i = DynIndex::new_dyn(3);
454/// let j = DynIndex::new_dyn(4);
455/// let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
456/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
457///
458/// let result = factorize(&tensor, &[i.clone()], &FactorizeOptions::svd()).unwrap();
459///
460/// // Contracting left * right recovers the original tensor
461/// let recovered = result.left.contract_pair(&result.right).unwrap();
462/// assert!(tensor.distance(&recovered).unwrap() < 1e-12);
463///
464/// // SVD provides singular values
465/// assert!(result.singular_values.is_some());
466/// assert_eq!(result.singular_values.as_ref().unwrap().len(), result.rank);
467/// ```
468#[derive(Debug, Clone)]
469pub struct FactorizeResult<T: TensorIndex> {
470    /// Left factor tensor.
471    pub left: T,
472    /// Right factor tensor.
473    pub right: T,
474    /// Bond index connecting left and right factors.
475    pub bond_index: T::Index,
476    /// Singular values (only for SVD).
477    pub singular_values: Option<Vec<f64>>,
478    /// Rank of the factorization.
479    pub rank: usize,
480}
481
482// ============================================================================
483// Contraction types
484// ============================================================================
485
486/// Linearization order used when fusing or unfusing multiple logical indices
487/// into one physical index.
488///
489/// This matters for exact reshape-style operations such as replacing one fused
490/// index with several unfused indices.
491///
492/// # Examples
493///
494/// ```
495/// use tensor4all_core::LinearizationOrder;
496///
497/// assert_eq!(LinearizationOrder::ColumnMajor.as_str(), "column-major");
498/// assert_eq!(LinearizationOrder::RowMajor.as_str(), "row-major");
499/// ```
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501pub enum LinearizationOrder {
502    /// First index changes fastest.
503    ColumnMajor,
504    /// Last index changes fastest.
505    RowMajor,
506}
507
508impl LinearizationOrder {
509    /// Return a short human-readable description.
510    ///
511    /// # Examples
512    ///
513    /// ```
514    /// use tensor4all_core::LinearizationOrder;
515    ///
516    /// assert_eq!(LinearizationOrder::ColumnMajor.as_str(), "column-major");
517    /// ```
518    pub fn as_str(self) -> &'static str {
519        match self {
520            Self::ColumnMajor => "column-major",
521            Self::RowMajor => "row-major",
522        }
523    }
524}
525
526// ============================================================================
527// Capability traits (fully generic)
528// ============================================================================
529
530/// Generic adapter error used by vector-space implementations whose concrete
531/// backend error is not part of their public API.
532///
533/// # Examples
534///
535/// ```
536/// use tensor4all_core::TensorVectorSpaceError;
537///
538/// let error = TensorVectorSpaceError::from(anyhow::anyhow!("backend failed"));
539/// assert!(error.to_string().contains("backend failed"));
540/// ```
541#[derive(Debug, thiserror::Error)]
542#[error("tensor vector-space operation failed: {source}")]
543pub struct TensorVectorSpaceError {
544    #[source]
545    source: Arc<dyn std::error::Error + Send + Sync + 'static>,
546}
547
548impl From<anyhow::Error> for TensorVectorSpaceError {
549    fn from(source: anyhow::Error) -> Self {
550        Self {
551            source: Arc::from(source.into_boxed_dyn_error()),
552        }
553    }
554}
555
556/// Vector-space operations for iterative linear algebra over tensor-like values.
557///
558/// This trait intentionally does not require tensor contraction/einsum,
559/// factorization, or tensor-network construction. Krylov solvers should depend
560/// on this trait instead of [`TensorLike`] so block vectors and other abstract
561/// state types do not have to provide unrelated tensor-network operations.
562pub trait TensorVectorSpace: TensorIndex {
563    /// Compute the squared Frobenius norm of the tensor.
564    ///
565    /// # Errors
566    ///
567    /// Returns `Self::Error` when the norm cannot be evaluated (a materialization
568    /// /// or backend failure).
569    ///
570    /// # Examples
571    /// ```
572    /// # fn main() -> anyhow::Result<()> {
573    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
574    ///
575    /// let index = DynIndex::new_dyn(2);
576    /// let tensor = IdxTensor::from_dense(vec![index], vec![3.0_f64, 4.0])?;
577    /// assert!((TensorVectorSpace::norm_squared(&tensor)? - 25.0).abs() < 1e-12);
578    /// # Ok(())
579    /// # }
580    /// ```
581    fn norm_squared(&self) -> std::result::Result<f64, Self::Error>;
582
583    /// Compute a linear combination: `a * self + b * other`.
584    ///
585    /// # Errors
586    ///
587    /// Returns `Self::Error` when the operands have incompatible index spaces
588    /// (an index-space mismatch) or the underlying arithmetic or backend
589    /// computation reports a failure.
590    fn axpby(
591        &self,
592        a: AnyScalar,
593        other: &Self,
594        b: AnyScalar,
595    ) -> std::result::Result<Self, Self::Error>;
596
597    /// Scalar multiplication.
598    ///
599    /// # Errors
600    ///
601    /// Returns `Self::Error` when the scalar coefficient is invalid for the
602    /// tensor's scalar type or the underlying backend computation reports a
603    /// failure.
604    fn scale(&self, scalar: AnyScalar) -> std::result::Result<Self, Self::Error>;
605
606    /// Inner product (dot product) of two tensors.
607    ///
608    /// Computes `⟨self, other⟩ = Σ conj(self)_i * other_i`.
609    ///
610    /// # Errors
611    ///
612    /// Returns `Self::Error` when the operands have incompatible index spaces
613    /// (an index-space mismatch) or the underlying contraction or backend
614    /// computation reports a failure.
615    fn inner_product(&self, other: &Self) -> std::result::Result<AnyScalar, Self::Error>;
616
617    /// Compute the Frobenius norm of the tensor.
618    ///
619    /// # Errors
620    ///
621    /// Returns `Self::Error` when the norm cannot be evaluated (a materialization
622    /// /// or backend failure).
623    ///
624    /// # Examples
625    /// ```
626    /// # fn main() -> anyhow::Result<()> {
627    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
628    ///
629    /// let index = DynIndex::new_dyn(2);
630    /// let tensor = IdxTensor::from_dense(vec![index], vec![3.0_f64, 4.0])?;
631    /// assert!((TensorVectorSpace::norm(&tensor)? - 5.0).abs() < 1e-12);
632    /// # Ok(())
633    /// # }
634    /// ```
635    fn norm(&self) -> std::result::Result<f64, Self::Error> {
636        Ok(self.norm_squared()?.sqrt())
637    }
638
639    /// Compute the maximum absolute value of all tensor elements.
640    ///
641    /// # Errors
642    ///
643    /// Returns `Self::Error` when the maximum cannot be evaluated (a
644    /// /// materialization or backend failure).
645    ///
646    /// # Examples
647    /// ```
648    /// # fn main() -> anyhow::Result<()> {
649    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
650    ///
651    /// let index = DynIndex::new_dyn(2);
652    /// let tensor = IdxTensor::from_dense(vec![index], vec![-3.0_f64, 2.0])?;
653    /// assert!((TensorVectorSpace::maxabs(&tensor)? - 3.0).abs() < 1e-12);
654    /// # Ok(())
655    /// # }
656    /// ```
657    fn maxabs(&self) -> std::result::Result<f64, Self::Error>;
658
659    /// Element-wise subtraction: `self - other`.
660    ///
661    /// # Errors
662    ///
663    /// Returns `Self::Error` when the operand index spaces are incompatible
664    /// (an index-space mismatch) or the underlying arithmetic or backend
665    /// computation reports a failure; propagates failures from [`Self::axpby`].
666    fn sub(&self, other: &Self) -> std::result::Result<Self, Self::Error> {
667        self.axpby(AnyScalar::new_real(1.0), other, AnyScalar::new_real(-1.0))
668    }
669
670    /// Negate all elements: `-self`.
671    ///
672    /// # Errors
673    ///
674    /// Returns `Self::Error` when scaling reports a failure (an invalid
675    /// scalar coefficient or a backend computation failure); propagates
676    /// failures from [`Self::scale`].
677    fn neg(&self) -> std::result::Result<Self, Self::Error> {
678        self.scale(AnyScalar::new_real(-1.0))
679    }
680
681    /// Approximate equality check (Julia `isapprox` semantics).
682    ///
683    /// # Errors
684    /// Returns `Self::Error` when the operands have incompatible index spaces
685    /// (an index-space mismatch) or when norm evaluation reports a failure;
686    /// propagates failures from subtraction and norm evaluation.
687    fn isapprox(
688        &self,
689        other: &Self,
690        atol: f64,
691        rtol: f64,
692    ) -> std::result::Result<bool, Self::Error> {
693        let diff = self.sub(other)?;
694        let diff_norm = diff.norm()?;
695        let self_norm = self.norm()?;
696        let other_norm = other.norm()?;
697        Ok(diff_norm <= atol.max(rtol * self_norm.max(other_norm)))
698    }
699
700    /// Validate structural consistency of this tensor-like vector.
701    ///
702    /// # Errors
703    ///
704    /// Returns `Self::Error` when the tensor's index space or shape is
705    /// structurally inconsistent (for example index-space or dimension
706    /// mismatches, or an invalid internal state). The default implementation
707    /// always returns `Ok(())`.
708    fn validate(&self) -> std::result::Result<(), Self::Error> {
709        Ok(())
710    }
711}
712
713/// Contraction/einsum-style operations for tensor-like values.
714///
715/// Types that only need vector-space algebra should not implement or require
716/// this trait. Tree tensor-network algorithms should use this trait when they
717/// truly need index-based contraction.
718pub trait TensorContractionLike: TensorIndex {
719    /// Tensor conjugate operation.
720    fn conj(&self) -> Self;
721
722    /// Direct sum of two tensors along specified index pairs.
723    ///
724    /// # Errors
725    ///
726    /// Returns `Self::Error` when the summed index spaces are incompatible
727    /// (an index-space mismatch) or the underlying construction reports a
728    /// failure.
729    fn direct_sum(
730        &self,
731        other: &Self,
732        pairs: &[(<Self as TensorIndex>::Index, <Self as TensorIndex>::Index)],
733    ) -> std::result::Result<DirectSumResult<Self>, Self::Error>;
734
735    /// Outer product (tensor product) of two tensors.
736    ///
737    /// # Errors
738    ///
739    /// Returns `Self::Error` when the two tensors share contractable indices
740    /// (a shared-index mismatch) or the underlying construction reports a
741    /// failure.
742    fn outer_product(&self, other: &Self) -> std::result::Result<Self, Self::Error>;
743
744    /// Permute tensor indices to match the specified order.
745    ///
746    /// # Errors
747    ///
748    /// Returns `Self::Error` when `new_order` does not contain exactly the
749    /// tensor's external indices (an index-set mismatch or a missing-index
750    /// failure).
751    fn permuteinds(
752        &self,
753        new_order: &[<Self as TensorIndex>::Index],
754    ) -> std::result::Result<Self, Self::Error>;
755
756    /// Fuse local tensor indices into one replacement index.
757    ///
758    /// # Errors
759    ///
760    /// Returns `Self::Error` when `old_indices` is not a subset of the
761    /// tensor's external indices (a missing-index failure) or the fused
762    /// dimension product overflows (an overflow failure).
763    fn fuse_indices(
764        &self,
765        old_indices: &[<Self as TensorIndex>::Index],
766        new_index: <Self as TensorIndex>::Index,
767        order: LinearizationOrder,
768    ) -> std::result::Result<Self, Self::Error>;
769
770    /// Contract a connected tensor network over its contractable indices.
771    ///
772    /// # Errors
773    ///
774    /// Returns `Self::Error` when the network is disconnected (a
775    /// disconnected-network failure), when indices are incompatible (an
776    /// index-space mismatch), or when the underlying contraction reports a
777    /// failure.
778    fn contract(tensors: &[&Self]) -> std::result::Result<Self, Self::Error>;
779
780    /// Contract this tensor with one other tensor using default pairwise semantics.
781    ///
782    /// # Errors
783    ///
784    /// Returns `Self::Error` when the pair is disconnected or has
785    /// incompatible indices (an index-space mismatch); propagates failures
786    /// from [`Self::contract`].
787    fn contract_pair(&self, other: &Self) -> std::result::Result<Self, Self::Error> {
788        Self::contract(&[self, other])
789    }
790
791    /// Validate structural consistency of this tensor.
792    ///
793    /// # Errors
794    ///
795    /// Returns `Self::Error` when the tensor's index space or shape is
796    /// structurally inconsistent (an index-space mismatch or an invalid
797    /// internal state). The default implementation always returns `Ok(())`.
798    fn validate(&self) -> std::result::Result<(), Self::Error> {
799        Ok(())
800    }
801}
802
803/// Factorization operations for tensor-like values.
804pub trait TensorFactorizationLike: TensorIndex {
805    /// Factorize this tensor into left and right factors.
806    /// # Errors
807    ///
808    /// Returns `FactorizeError` when the factorization fails (a non-convergence,
809    /// /// singular, or unsupported-storage failure).
810    ///
811    fn factorize(
812        &self,
813        left_inds: &[<Self as TensorIndex>::Index],
814        options: &FactorizeOptions,
815    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>;
816
817    /// Factorize this tensor using policy-aware automatic SVD/eigen selection.
818    ///
819    /// Implementations may use Hermitian Gram eigendecomposition when the
820    /// requested SVD policy is numerically suitable. The default delegates to
821    /// [`Self::factorize`], preserving the existing behavior for tensor types
822    /// without an automatic eigendecomposition path.
823    ///
824    /// # Arguments
825    /// * `left_inds` - Indices to place on the left side of the split.
826    /// * `options` - Ordinary SVD factorization options, including canonical
827    ///   direction, truncation policy, and maximum bond dimension.
828    ///
829    /// # Returns
830    /// The same factorization result as [`Self::factorize`], with singular
831    /// values populated for SVD-compatible implementations.
832    ///
833    /// # Errors
834    /// Returns [`FactorizeError::InvalidOptions`] when `options.alg` is not
835    /// [`FactorizeAlg::SVD`], or another [`FactorizeError`] when factorization
836    /// fails.
837    ///
838    /// # Examples
839    ///
840    /// ```
841    /// use tensor4all_core::{
842    ///     DynIndex, FactorizeOptions, IdxTensor, SvdTruncationPolicy,
843    ///     TensorFactorizationLike,
844    /// };
845    ///
846    /// let left = DynIndex::new_dyn(2);
847    /// let right = DynIndex::new_dyn(2);
848    /// let tensor = IdxTensor::from_dense(
849    ///     vec![left.clone(), right],
850    ///     vec![1.0_f64, 0.0, 0.0, 1.0e-3],
851    /// )?;
852    /// let options = FactorizeOptions::svd()
853    ///     .with_svd_policy(SvdTruncationPolicy::new(1.0e-2));
854    /// let result = tensor.factorize_auto(&[left], &options)?;
855    /// assert_eq!(result.rank, 1);
856    /// # Ok::<(), Box<dyn std::error::Error>>(())
857    /// ```
858    fn factorize_auto(
859        &self,
860        left_inds: &[<Self as TensorIndex>::Index],
861        options: &FactorizeOptions,
862    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
863        if options.alg != FactorizeAlg::SVD {
864            return Err(FactorizeError::InvalidOptions(
865                "automatic factorization only supports SVD options",
866            ));
867        }
868        self.factorize(left_inds, options)
869    }
870
871    /// Factorize this tensor without applying truncation controls.
872    /// # Errors
873    ///
874    /// Returns `FactorizeError` when the factorization fails (a non-convergence,
875    /// /// singular, or unsupported-storage failure).
876    ///
877    fn factorize_full_rank(
878        &self,
879        left_inds: &[<Self as TensorIndex>::Index],
880        alg: FactorizeAlg,
881        canonical: Canonical,
882    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>;
883}
884
885/// Constructors and selection helpers for index-labelled tensors.
886pub trait TensorConstructionLike: TensorContractionLike {
887    /// Create a diagonal (Kronecker delta) tensor for a single index pair.
888    ///
889    /// # Errors
890    ///
891    /// Returns `Self::Error` when the input and output indices have unequal
892    /// dimensions (a shape mismatch) or the underlying construction
893    /// reports a failure.
894    fn diagonal(
895        input_index: &<Self as TensorIndex>::Index,
896        output_index: &<Self as TensorIndex>::Index,
897    ) -> std::result::Result<Self, Self::Error>;
898
899    /// Create a delta (identity) tensor as outer product of diagonals.
900    ///
901    /// # Errors
902    ///
903    /// Returns `Self::Error` when the input and output index lists differ in
904    /// length (a length mismatch) or when a constituent diagonal or outer
905    /// product reports a failure; propagates failures from [`Self::diagonal`],
906    /// [`Self::scalar_one`], and [`Self::outer_product`].
907    fn delta(
908        input_indices: &[<Self as TensorIndex>::Index],
909        output_indices: &[<Self as TensorIndex>::Index],
910    ) -> std::result::Result<Self, Self::Error> {
911        if input_indices.len() != output_indices.len() {
912            return Err(anyhow::anyhow!(
913                "Number of input indices ({}) must match output indices ({})",
914                input_indices.len(),
915                output_indices.len()
916            )
917            .into());
918        }
919
920        if input_indices.is_empty() {
921            return Self::scalar_one();
922        }
923
924        let mut result = Self::diagonal(&input_indices[0], &output_indices[0])?;
925        for (inp, out) in input_indices[1..].iter().zip(output_indices[1..].iter()) {
926            let diag = Self::diagonal(inp, out)?;
927            result = result.outer_product(&diag)?;
928        }
929        Ok(result)
930    }
931
932    /// Create a scalar tensor with value 1.0.
933    ///
934    /// # Errors
935    ///
936    /// Returns `Self::Error` when the scalar type does not support the
937    /// required construction (an invalid scalar dtype or a backend
938    /// construction failure).
939    fn scalar_one() -> std::result::Result<Self, Self::Error>;
940
941    /// Create a tensor filled with 1.0 for the given indices.
942    ///
943    /// # Errors
944    ///
945    /// Returns `Self::Error` when an index dimension product overflows (an
946    /// overflow failure) or the underlying construction reports a failure.
947    fn ones(indices: &[<Self as TensorIndex>::Index]) -> std::result::Result<Self, Self::Error>;
948
949    /// Select fixed coordinates for a subset of this tensor's external indices.
950    ///
951    /// # Errors
952    ///
953    /// Returns `Self::Error` when `selected_indices` and `positions` differ in
954    /// length (a length mismatch), when an index is selected more than once
955    /// (a duplicate-index failure), when a coordinate is out of range (an
956    /// out of bounds failure), or when the underlying one-hot construction or
957    /// contraction reports a failure; propagates failures from
958    /// [`Self::onehot`] and [`Self::contract`].
959    fn select_indices(
960        &self,
961        selected_indices: &[<Self as TensorIndex>::Index],
962        positions: &[usize],
963    ) -> std::result::Result<Self, Self::Error> {
964        if selected_indices.len() != positions.len() {
965            return Err(anyhow::anyhow!(
966                "selected_indices length {} does not match positions length {}",
967                selected_indices.len(),
968                positions.len()
969            )
970            .into());
971        }
972        if selected_indices.is_empty() {
973            return Ok(self.clone());
974        }
975
976        let mut seen = HashSet::with_capacity(selected_indices.len());
977        for (index, &position) in selected_indices.iter().zip(positions.iter()) {
978            if !seen.insert(index.clone()) {
979                return Err(anyhow::anyhow!("selected index appears more than once").into());
980            }
981            if position >= index.dim() {
982                return Err(anyhow::anyhow!(
983                    "selected coordinate {} is out of range for index {:?} with dim {}",
984                    position,
985                    index,
986                    index.dim()
987                )
988                .into());
989            }
990        }
991
992        let index_vals = selected_indices
993            .iter()
994            .cloned()
995            .zip(positions.iter().copied())
996            .collect::<Vec<_>>();
997        let onehot = Self::onehot(&index_vals)?;
998        Self::contract(&[self, &onehot])
999    }
1000
1001    /// Create a one-hot tensor with value 1.0 at the specified index positions.
1002    ///
1003    /// # Errors
1004    ///
1005    /// Returns `Self::Error` when a position is out of range for its index
1006    /// (an out of bounds failure) or the underlying construction reports a
1007    /// failure.
1008    fn onehot(
1009        index_vals: &[(<Self as TensorIndex>::Index, usize)],
1010    ) -> std::result::Result<Self, Self::Error>;
1011}
1012
1013// ============================================================================
1014// TensorLike trait (fully generic composite)
1015// ============================================================================
1016
1017/// Trait for tensor-like objects that expose external indices and support contraction.
1018///
1019/// This trait is **fully generic** (monomorphic), meaning it does not support
1020/// trait objects (`dyn TensorLike`). For heterogeneous tensor collections,
1021/// use an enum wrapper instead.
1022///
1023/// # Design Principles
1024///
1025/// - **Capability composition**: combines vector-space, factorization, construction, and contraction traits
1026/// - **Fully generic**: Uses associated type for `Index`, returns `Self`
1027/// - **Stable ordering**: `external_indices()` returns indices in deterministic order
1028/// - **No trait objects**: Requires `Sized`, cannot use `dyn TensorLike`
1029///
1030/// # Example
1031///
1032/// ```
1033/// use tensor4all_core::{DynIndex, TensorContractionLike, IdxTensor};
1034///
1035/// fn contract_pair(a: &IdxTensor, b: &IdxTensor) -> anyhow::Result<IdxTensor> {
1036///     Ok(<IdxTensor as TensorContractionLike>::contract(&[a, b])?)
1037/// }
1038///
1039/// # fn main() -> anyhow::Result<()> {
1040/// let i = DynIndex::new_dyn(2);
1041/// let j = DynIndex::new_dyn(2);
1042/// let a = IdxTensor::from_dense(
1043///     vec![i.clone(), j.clone()],
1044///     vec![1.0, 0.0, 0.0, 1.0],
1045/// )?;
1046/// let b = IdxTensor::from_dense(vec![j.clone()], vec![2.0, 3.0])?;
1047///
1048/// let result = contract_pair(&a, &b)?;
1049/// assert_eq!(result.to_vec::<f64>()?, vec![2.0, 3.0]);
1050/// # Ok(())
1051/// # }
1052/// ```
1053///
1054/// # Heterogeneous Collections
1055///
1056/// For mixing different tensor types, define an enum:
1057///
1058/// ```
1059/// use tensor4all_core::{block_tensor::BlockTensor, DynIndex, IdxTensor};
1060///
1061/// let i = DynIndex::new_dyn(2);
1062/// let dense = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
1063/// let block = BlockTensor::new(vec![dense.clone()], (1, 1)).unwrap();
1064///
1065/// enum TensorNetwork {
1066///     Dense(IdxTensor),
1067///     Block(BlockTensor<IdxTensor>),
1068/// }
1069///
1070/// let network = TensorNetwork::Block(block);
1071/// assert!(matches!(network, TensorNetwork::Block(_)));
1072/// ```
1073///
1074/// # Supertrait
1075///
1076/// `TensorLike` extends several capability traits. Through those traits it provides:
1077/// - `external_indices()` - Get all external indices
1078/// - `num_external_indices()` - Count external indices
1079/// - `replaceind()` / `replace_indices()` - Replace indices
1080/// - vector-space operations such as `axpby`, `inner_product`, and `norm`
1081/// - tensor-network operations such as contraction, construction, and factorization
1082///
1083/// Use narrower traits such as [`TensorVectorSpace`] or
1084/// [`TensorContractionLike`] when an algorithm does not need the full surface.
1085pub trait TensorLike: TensorVectorSpace + TensorFactorizationLike + TensorConstructionLike {}
1086
1087impl<T> TensorLike for T where
1088    T: TensorVectorSpace + TensorFactorizationLike + TensorConstructionLike
1089{
1090}
1091
1092/// Result of direct sum operation.
1093///
1094/// Contains the resulting tensor and the new indices created for the summed
1095/// dimensions (one new index per pair in the input).
1096///
1097/// # Examples
1098///
1099/// ```
1100/// use tensor4all_core::{DynIndex, IndexLike, TensorContractionLike, IdxTensor};
1101///
1102/// let i = DynIndex::new_dyn(2);
1103/// let j = DynIndex::new_dyn(3);
1104///
1105/// let a = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
1106/// let b = IdxTensor::from_dense(vec![j.clone()], vec![3.0, 4.0, 5.0]).unwrap();
1107///
1108/// let result = a.direct_sum(&b, &[(i.clone(), j.clone())]).unwrap();
1109///
1110/// // New index has dimension = 2 + 3 = 5
1111/// assert_eq!(result.new_indices.len(), 1);
1112/// assert_eq!(result.new_indices[0].dim(), 5);
1113/// assert_eq!(result.tensor.to_vec::<f64>().unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
1114/// ```
1115#[derive(Debug, Clone)]
1116pub struct DirectSumResult<T: TensorIndex> {
1117    /// The resulting tensor from direct sum.
1118    pub tensor: T,
1119    /// New indices created for the summed dimensions (one per pair).
1120    pub new_indices: Vec<T::Index>,
1121}
1122
1123#[cfg(test)]
1124mod tests;