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 crate::TensorElement;
22use num_complex::Complex64;
23use std::collections::HashSet;
24use std::fmt::Debug;
25use std::sync::Arc;
26
27// ============================================================================
28// Factorization types (non-generic, algorithm-specific)
29// ============================================================================
30
31use thiserror::Error;
32
33/// Error type for factorize operations.
34///
35/// # Examples
36///
37/// ```
38/// use tensor4all_core::{
39///     factorize, Canonical, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
40/// };
41///
42/// let i = DynIndex::new_dyn(3);
43/// let j = DynIndex::new_dyn(3);
44/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
45/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
46///
47/// // QR with Canonical::Right is not supported
48/// let result = factorize(
49///     &tensor,
50///     &[i],
51///     &FactorizeOptions::qr().with_canonical(Canonical::Right),
52/// );
53/// assert!(result.is_err());
54/// ```
55#[derive(Debug, Error)]
56pub enum FactorizeError {
57    /// Factorization computation failed.
58    ///
59    /// The underlying error is preserved as an `anyhow` source, so a typed
60    /// cause such as
61    /// [`IdxTensorError::UnsupportedExecutionContext`](crate::IdxTensorError::UnsupportedExecutionContext)
62    /// stays reachable through `downcast_ref`.
63    #[error("Factorization failed: {0}")]
64    ComputationError(
65        /// The underlying error
66        #[from]
67        anyhow::Error,
68    ),
69    /// Invalid relative tolerance value (must be finite and non-negative).
70    #[error("Invalid rtol value: {0}. rtol must be finite and non-negative.")]
71    InvalidRtol(
72        /// The invalid rtol value
73        f64,
74    ),
75    /// Invalid algorithm-specific option combination.
76    #[error("Invalid factorize options: {0}")]
77    InvalidOptions(
78        /// Description of the invalid option combination
79        &'static str,
80    ),
81    /// The storage type is not supported for this operation.
82    #[error("Unsupported storage type: {0}")]
83    UnsupportedStorage(
84        /// Description of the unsupported storage type
85        &'static str,
86    ),
87    /// The canonical direction is not supported for this algorithm.
88    #[error("Unsupported canonical direction for this algorithm: {0}")]
89    UnsupportedCanonical(
90        /// Description of the unsupported canonical direction
91        &'static str,
92    ),
93    /// Error from SVD operation.
94    #[error("SVD error: {0}")]
95    SvdError(
96        /// The underlying SVD error
97        #[from]
98        crate::svd::SvdError,
99    ),
100    /// Error from QR operation.
101    #[error("QR error: {0}")]
102    QrError(
103        /// The underlying QR error
104        #[from]
105        crate::qr::QrError,
106    ),
107    /// Error from matrix CI operation.
108    #[error("Matrix CI error: {0}")]
109    MatrixCIError(
110        /// The underlying matrix CI error
111        #[from]
112        crate::MatrixCIError,
113    ),
114}
115
116/// Factorization algorithm.
117///
118/// Determines which matrix decomposition is used by [`TensorFactorizationLike::factorize`].
119///
120/// # Examples
121///
122/// ```
123/// use tensor4all_core::FactorizeAlg;
124///
125/// // Default is SVD
126/// assert_eq!(FactorizeAlg::default(), FactorizeAlg::SVD);
127/// ```
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub enum FactorizeAlg {
130    /// Singular Value Decomposition.
131    #[default]
132    SVD,
133    /// QR decomposition.
134    QR,
135    /// Rank-revealing LU decomposition.
136    LU,
137    /// Cross Interpolation (LU-based).
138    CI,
139}
140
141/// Canonical direction for factorization.
142///
143/// This determines which factor is "canonical" (orthogonal for SVD/QR,
144/// or unit-diagonal for LU/CI).
145///
146/// # Examples
147///
148/// ```
149/// use tensor4all_core::{
150///     factorize, Canonical, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
151/// };
152///
153/// let i = DynIndex::new_dyn(3);
154/// let j = DynIndex::new_dyn(3);
155/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
156/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
157///
158/// // Left canonical: left factor has orthonormal columns
159/// let left_result = factorize(
160///     &tensor,
161///     &[i.clone()],
162///     &FactorizeOptions::svd().with_canonical(Canonical::Left),
163/// ).unwrap();
164///
165/// // Right canonical: right factor has orthonormal rows
166/// let right_result = factorize(
167///     &tensor,
168///     &[i.clone()],
169///     &FactorizeOptions::svd().with_canonical(Canonical::Right),
170/// ).unwrap();
171///
172/// // Both recover the same tensor
173/// let recovered_left = left_result.left.contract_pair(&left_result.right).unwrap();
174/// let recovered_right = right_result.left.contract_pair(&right_result.right).unwrap();
175/// assert!(recovered_left.distance(&recovered_right).unwrap() < 1e-12);
176/// ```
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub enum Canonical {
179    /// Left factor is canonical.
180    /// - SVD: L=U (orthogonal), R=S*V
181    /// - QR: L=Q (orthogonal), R=R
182    /// - LU/CI: L has unit diagonal
183    #[default]
184    Left,
185    /// Right factor is canonical.
186    /// - SVD: L=U*S, R=V (orthogonal)
187    /// - QR: Not supported (would need LQ)
188    /// - LU/CI: U has unit diagonal
189    Right,
190}
191
192/// Options for tensor factorization.
193///
194/// Controls the algorithm, canonical direction, and truncation parameters
195/// for [`TensorFactorizationLike::factorize`].
196///
197/// # Defaults
198///
199/// - Algorithm: SVD
200/// - Canonical: Left (left factor is orthogonal)
201/// - max_bond_dim: `None` (no rank limit)
202/// - svd_policy: `None` (uses the SVD global default policy)
203/// - qr_rtol: `None` (uses the QR global default tolerance)
204///
205/// # Field Interactions
206///
207/// - `svd_policy` is only valid for `FactorizeAlg::SVD`.
208/// - `qr_rtol` is only valid for `FactorizeAlg::QR`.
209/// - `max_bond_dim` is independent of the algorithm-specific tolerance settings.
210///
211/// # Examples
212///
213/// ```
214/// use tensor4all_core::{
215///     factorize, Canonical, DynIndex, FactorizeOptions, IdxTensor,
216/// };
217///
218/// let i = DynIndex::new_dyn(4);
219/// let j = DynIndex::new_dyn(4);
220/// let mut data = vec![0.0_f64; 16];
221/// data[0] = 1.0;  // rank-1 matrix
222/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
223///
224/// // SVD with an explicit policy
225/// let opts = FactorizeOptions::svd()
226///     .with_svd_policy(tensor4all_core::SvdTruncationPolicy::new(1e-10));
227/// let result = factorize(&tensor, &[i.clone()], &opts).unwrap();
228/// assert_eq!(result.rank, 1);
229///
230/// // QR with max-rank truncation
231/// let opts = FactorizeOptions::qr().with_qr_rtol(1e-8).with_max_bond_dim(2);
232/// let result = factorize(&tensor, &[i.clone()], &opts).unwrap();
233/// assert!(result.rank <= 2);
234/// ```
235#[derive(Debug, Clone)]
236pub struct FactorizeOptions {
237    /// Factorization algorithm to use.
238    pub alg: FactorizeAlg,
239    /// Canonical direction.
240    pub canonical: Canonical,
241    /// Maximum rank for truncation.
242    /// If `None`, no rank limit is applied.
243    pub max_bond_dim: Option<usize>,
244    /// SVD truncation policy.
245    /// If `None`, uses the SVD global default.
246    pub svd_policy: Option<SvdTruncationPolicy>,
247    /// QR-specific relative tolerance.
248    /// If `None`, uses the QR global default.
249    pub qr_rtol: Option<f64>,
250}
251
252impl Default for FactorizeOptions {
253    fn default() -> Self {
254        Self {
255            alg: FactorizeAlg::SVD,
256            canonical: Canonical::Left,
257            max_bond_dim: None,
258            svd_policy: None,
259            qr_rtol: None,
260        }
261    }
262}
263
264impl FactorizeOptions {
265    /// Create options for SVD factorization.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
271    ///
272    /// let opts = FactorizeOptions::svd();
273    /// assert_eq!(opts.alg, FactorizeAlg::SVD);
274    /// ```
275    pub fn svd() -> Self {
276        Self {
277            alg: FactorizeAlg::SVD,
278            ..Default::default()
279        }
280    }
281
282    /// Create options for QR factorization.
283    ///
284    /// # Examples
285    ///
286    /// ```
287    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
288    ///
289    /// let opts = FactorizeOptions::qr();
290    /// assert_eq!(opts.alg, FactorizeAlg::QR);
291    /// ```
292    pub fn qr() -> Self {
293        Self {
294            alg: FactorizeAlg::QR,
295            ..Default::default()
296        }
297    }
298
299    /// Create options for LU factorization.
300    ///
301    /// # Examples
302    ///
303    /// ```
304    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
305    ///
306    /// let opts = FactorizeOptions::lu();
307    /// assert_eq!(opts.alg, FactorizeAlg::LU);
308    /// ```
309    pub fn lu() -> Self {
310        Self {
311            alg: FactorizeAlg::LU,
312            ..Default::default()
313        }
314    }
315
316    /// Create options for CI factorization.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use tensor4all_core::{FactorizeAlg, FactorizeOptions};
322    ///
323    /// let opts = FactorizeOptions::ci();
324    /// assert_eq!(opts.alg, FactorizeAlg::CI);
325    /// ```
326    pub fn ci() -> Self {
327        Self {
328            alg: FactorizeAlg::CI,
329            ..Default::default()
330        }
331    }
332
333    /// Set canonical direction.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use tensor4all_core::{Canonical, FactorizeOptions};
339    ///
340    /// let opts = FactorizeOptions::svd().with_canonical(Canonical::Right);
341    /// assert_eq!(opts.canonical, Canonical::Right);
342    /// ```
343    pub fn with_canonical(mut self, canonical: Canonical) -> Self {
344        self.canonical = canonical;
345        self
346    }
347
348    /// Set the SVD truncation policy.
349    ///
350    /// # Examples
351    ///
352    /// ```
353    /// use tensor4all_core::{FactorizeOptions, SvdTruncationPolicy};
354    ///
355    /// let opts = FactorizeOptions::svd().with_svd_policy(SvdTruncationPolicy::new(1e-8));
356    /// assert_eq!(opts.svd_policy, Some(SvdTruncationPolicy::new(1e-8)));
357    /// ```
358    pub fn with_svd_policy(mut self, policy: SvdTruncationPolicy) -> Self {
359        self.svd_policy = Some(policy);
360        self
361    }
362
363    /// Set the QR-specific relative tolerance.
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use tensor4all_core::FactorizeOptions;
369    ///
370    /// let opts = FactorizeOptions::qr().with_qr_rtol(1e-8);
371    /// assert_eq!(opts.qr_rtol, Some(1e-8));
372    /// ```
373    pub fn with_qr_rtol(mut self, rtol: f64) -> Self {
374        self.qr_rtol = Some(rtol);
375        self
376    }
377
378    /// Set maximum rank.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use tensor4all_core::FactorizeOptions;
384    ///
385    /// let opts = FactorizeOptions::svd().with_max_bond_dim(10);
386    /// assert_eq!(opts.max_bond_dim, Some(10));
387    /// ```
388    pub fn with_max_bond_dim(mut self, max_bond_dim: usize) -> Self {
389        self.max_bond_dim = Some(max_bond_dim);
390        self
391    }
392
393    /// Validate that the selected fields make sense for the chosen algorithm.
394    ///
395    /// Validates max bond dimension and explicit SVD policy thresholds through
396    /// the shared [`validate_svd_truncation_options`] seam, then checks the
397    /// algorithm/option compatibility rules.
398    ///
399    /// # Errors
400    ///
401    /// Returns [`FactorizeError::InvalidOptions`] if an algorithm is paired with
402    /// unsupported algorithm-specific truncation settings, or if `max_bond_dim`
403    /// is zero or an SVD policy threshold is non-finite/negative.
404    pub fn validate(&self) -> std::result::Result<(), FactorizeError> {
405        validate_svd_truncation_options(self.max_bond_dim, self.svd_policy).map_err(|error| {
406            FactorizeError::InvalidOptions(match error {
407                SvdTruncationOptionsError::ZeroMaxBondDim => "max_bond_dim must be at least 1",
408                SvdTruncationOptionsError::InvalidThreshold(_) => {
409                    "SVD truncation threshold must be finite and non-negative"
410                }
411            })
412        })?;
413
414        match self.alg {
415            FactorizeAlg::SVD => {
416                if self.qr_rtol.is_some() {
417                    return Err(FactorizeError::InvalidOptions(
418                        "SVD factorization does not accept qr_rtol",
419                    ));
420                }
421            }
422            FactorizeAlg::QR => {
423                if self.svd_policy.is_some() {
424                    return Err(FactorizeError::InvalidOptions(
425                        "QR factorization does not accept svd_policy",
426                    ));
427                }
428            }
429            FactorizeAlg::LU | FactorizeAlg::CI => {
430                if self.svd_policy.is_some() {
431                    return Err(FactorizeError::InvalidOptions(
432                        "LU/CI factorization does not accept svd_policy",
433                    ));
434                }
435                if self.qr_rtol.is_some() {
436                    return Err(FactorizeError::InvalidOptions(
437                        "LU/CI factorization does not accept qr_rtol",
438                    ));
439                }
440            }
441        }
442
443        Ok(())
444    }
445}
446
447/// Result of tensor factorization.
448///
449/// Contains the two factors, the bond index connecting them, and metadata
450/// about the decomposition. The original tensor can be recovered (up to
451/// truncation error) by contracting `left` and `right` along `bond_index`.
452///
453/// # Examples
454///
455/// ```
456/// use tensor4all_core::{
457///     factorize, DynIndex, FactorizeOptions, TensorContractionLike, IdxTensor,
458/// };
459///
460/// let i = DynIndex::new_dyn(3);
461/// let j = DynIndex::new_dyn(4);
462/// let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
463/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
464///
465/// let result = factorize(&tensor, &[i.clone()], &FactorizeOptions::svd()).unwrap();
466///
467/// // Contracting left * right recovers the original tensor
468/// let recovered = result.left.contract_pair(&result.right).unwrap();
469/// assert!(tensor.distance(&recovered).unwrap() < 1e-12);
470///
471/// // SVD provides singular values
472/// assert!(result.singular_values.is_some());
473/// assert_eq!(result.singular_values.as_ref().unwrap().len(), result.rank);
474/// ```
475///
476/// This struct is `#[non_exhaustive]`: it carries a private
477/// algorithm-internal field (`incremental_qr_state`) that downstream crates
478/// cannot see or set, so external construction always goes through
479/// [`FactorizeResult::new`] rather than struct-literal syntax.
480#[derive(Debug, Clone)]
481#[non_exhaustive]
482pub struct FactorizeResult<T: TensorIndex> {
483    /// Left factor tensor.
484    pub left: T,
485    /// Right factor tensor.
486    pub right: T,
487    /// Bond index connecting left and right factors.
488    pub bond_index: T::Index,
489    /// Singular values (only for SVD).
490    pub singular_values: Option<Vec<f64>>,
491    /// Retained bond dimension of the factorization.
492    ///
493    /// Tensor LU/CI factorization represents a rank-zero matrix selection
494    /// with a dimension-one bond and an exactly zero product. The selected
495    /// canonical factor keeps its unit diagonal; the other factor is zero.
496    pub rank: usize,
497    incremental_qr_state: Option<IncrementalQrState>,
498}
499
500#[derive(Debug, Clone)]
501pub(crate) enum IncrementalQrState {
502    F64(tensor4all_tensorbackend::IncrementalQr<f64>),
503    C64(tensor4all_tensorbackend::IncrementalQr<Complex64>),
504}
505
506impl<T: TensorIndex> FactorizeResult<T> {
507    /// Construct a factorization result without an algorithm-private update
508    /// state.
509    ///
510    /// # Arguments
511    /// * `left` - Left factor tensor.
512    /// * `right` - Right factor tensor.
513    /// * `bond_index` - Index connecting the two factors.
514    /// * `singular_values` - Singular values when the result came from SVD.
515    /// * `rank` - Number of columns in the factorization.
516    ///
517    /// # Returns
518    /// A factorization result suitable for public factorization APIs. Native
519    /// incremental QR callers attach their private update state afterward.
520    ///
521    /// # Examples
522    ///
523    /// ```
524    /// use tensor4all_core::{DynIndex, FactorizeResult, IdxTensor};
525    ///
526    /// let left_index = DynIndex::new_dyn(1);
527    /// let bond = DynIndex::new_bond(1).unwrap();
528    /// let left = IdxTensor::from_dense(vec![left_index.clone(), bond.clone()], vec![1.0]).unwrap();
529    /// let right = IdxTensor::from_dense(vec![bond.clone()], vec![2.0]).unwrap();
530    /// let result = FactorizeResult::new(left, right, bond, None, 1);
531    /// assert_eq!(result.rank, 1);
532    /// ```
533    pub fn new(
534        left: T,
535        right: T,
536        bond_index: T::Index,
537        singular_values: Option<Vec<f64>>,
538        rank: usize,
539    ) -> Self {
540        Self {
541            left,
542            right,
543            bond_index,
544            singular_values,
545            rank,
546            incremental_qr_state: None,
547        }
548    }
549
550    pub(crate) fn with_incremental_qr_state(mut self, state: IncrementalQrState) -> Self {
551        self.incremental_qr_state = Some(state);
552        self
553    }
554
555    pub(crate) fn incremental_qr_state(&self) -> Option<&IncrementalQrState> {
556        self.incremental_qr_state.as_ref()
557    }
558}
559
560// ============================================================================
561// Contraction types
562// ============================================================================
563
564/// Linearization order used when fusing or unfusing multiple logical indices
565/// into one physical index.
566///
567/// This matters for exact reshape-style operations such as replacing one fused
568/// index with several unfused indices.
569///
570/// # Examples
571///
572/// ```
573/// use tensor4all_core::LinearizationOrder;
574///
575/// assert_eq!(LinearizationOrder::ColumnMajor.as_str(), "column-major");
576/// assert_eq!(LinearizationOrder::RowMajor.as_str(), "row-major");
577/// ```
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum LinearizationOrder {
580    /// First index changes fastest.
581    ColumnMajor,
582    /// Last index changes fastest.
583    RowMajor,
584}
585
586impl LinearizationOrder {
587    /// Return a short human-readable description.
588    ///
589    /// # Examples
590    ///
591    /// ```
592    /// use tensor4all_core::LinearizationOrder;
593    ///
594    /// assert_eq!(LinearizationOrder::ColumnMajor.as_str(), "column-major");
595    /// ```
596    pub fn as_str(self) -> &'static str {
597        match self {
598            Self::ColumnMajor => "column-major",
599            Self::RowMajor => "row-major",
600        }
601    }
602}
603
604// ============================================================================
605// Capability traits (fully generic)
606// ============================================================================
607
608/// Generic adapter error used by vector-space implementations whose concrete
609/// backend error is not part of their public API.
610///
611/// # Examples
612///
613/// ```
614/// use tensor4all_core::TensorVectorSpaceError;
615///
616/// let error = TensorVectorSpaceError::from(anyhow::anyhow!("backend failed"));
617/// assert!(error.to_string().contains("backend failed"));
618/// ```
619#[derive(Debug, thiserror::Error)]
620#[error("tensor vector-space operation failed: {source}")]
621pub struct TensorVectorSpaceError {
622    #[source]
623    source: Arc<dyn std::error::Error + Send + Sync + 'static>,
624}
625
626impl From<anyhow::Error> for TensorVectorSpaceError {
627    fn from(source: anyhow::Error) -> Self {
628        Self {
629            source: Arc::from(source.into_boxed_dyn_error()),
630        }
631    }
632}
633
634/// Vector-space operations for iterative linear algebra over tensor-like values.
635///
636/// This trait intentionally does not require tensor contraction/einsum,
637/// factorization, or tensor-network construction. Krylov solvers should depend
638/// on this trait instead of [`TensorLike`] so block vectors and other abstract
639/// state types do not have to provide unrelated tensor-network operations.
640pub trait TensorVectorSpace: TensorIndex {
641    /// Compute the squared Frobenius norm of the tensor.
642    ///
643    /// # Errors
644    ///
645    /// Returns `Self::Error` when the norm cannot be evaluated (a materialization
646    /// or backend failure).
647    ///
648    /// # Examples
649    /// ```
650    /// # fn main() -> anyhow::Result<()> {
651    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
652    ///
653    /// let index = DynIndex::new_dyn(2);
654    /// let tensor = IdxTensor::from_dense(vec![index], vec![3.0_f64, 4.0])?;
655    /// assert!((TensorVectorSpace::norm_squared(&tensor)? - 25.0).abs() < 1e-12);
656    /// # Ok(())
657    /// # }
658    /// ```
659    fn norm_squared(&self) -> std::result::Result<f64, Self::Error>;
660
661    /// Compute a linear combination: `a * self + b * other`.
662    ///
663    /// # Errors
664    ///
665    /// Returns `Self::Error` when the operands have incompatible index spaces
666    /// (an index-space mismatch) or the underlying arithmetic or backend
667    /// computation reports a failure.
668    fn axpby(
669        &self,
670        a: AnyScalar,
671        other: &Self,
672        b: AnyScalar,
673    ) -> std::result::Result<Self, Self::Error>;
674
675    /// Scalar multiplication.
676    ///
677    /// # Errors
678    ///
679    /// Returns `Self::Error` when the scalar coefficient is invalid for the
680    /// tensor's scalar type or the underlying backend computation reports a
681    /// failure.
682    fn scale(&self, scalar: AnyScalar) -> std::result::Result<Self, Self::Error>;
683
684    /// Real scalar multiplication in a caller-owned execution context.
685    ///
686    /// Context-scoped counterpart of [`Self::scale`] for real factors: the
687    /// scalar is constructed in the tensor's owning runtime (explicit upload
688    /// for CUDA), so explicit-context tensors never enter the process-global
689    /// default context.
690    ///
691    /// # Errors
692    ///
693    /// Returns `Self::Error` with an `UnsupportedStorage` failure when scoped
694    /// scaling is unsupported for this tensor type; other failures report a
695    /// missing tensor context, an invalid payload, or a backend failure.
696    fn scale_in(
697        &self,
698        _factor: f64,
699        _context: &tensor4all_tensorbackend::ExecutionContext,
700    ) -> std::result::Result<Self, Self::Error> {
701        Err(anyhow::anyhow!("context-scoped scaling is not supported for this tensor type").into())
702    }
703
704    /// Inner product (dot product) of two tensors.
705    ///
706    /// Computes `⟨self, other⟩ = Σ conj(self)_i * other_i`.
707    ///
708    /// # Errors
709    ///
710    /// Returns `Self::Error` when the operands have incompatible index spaces
711    /// (an index-space mismatch) or the underlying contraction or backend
712    /// computation reports a failure.
713    fn inner_product(&self, other: &Self) -> std::result::Result<AnyScalar, Self::Error>;
714
715    /// Compute the Frobenius norm of the tensor.
716    ///
717    /// # Errors
718    ///
719    /// Returns `Self::Error` when the norm cannot be evaluated (a materialization
720    /// or backend failure).
721    ///
722    /// # Examples
723    /// ```
724    /// # fn main() -> anyhow::Result<()> {
725    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
726    ///
727    /// let index = DynIndex::new_dyn(2);
728    /// let tensor = IdxTensor::from_dense(vec![index], vec![3.0_f64, 4.0])?;
729    /// assert!((TensorVectorSpace::norm(&tensor)? - 5.0).abs() < 1e-12);
730    /// # Ok(())
731    /// # }
732    /// ```
733    fn norm(&self) -> std::result::Result<f64, Self::Error> {
734        Ok(self.norm_squared()?.sqrt())
735    }
736
737    /// Frobenius norm in a caller-owned execution context.
738    ///
739    /// Context-scoped counterpart of [`Self::norm`]: reductions run in the
740    /// tensor's owning runtime and only the scalar result crosses the
741    /// explicit readback boundary on CUDA. CPU contexts delegate to
742    /// [`Self::norm`] unchanged.
743    ///
744    /// # Errors
745    ///
746    /// Returns `Self::Error` with an `UnsupportedStorage` failure when scoped
747    /// norm evaluation is unsupported for this tensor type; other failures
748    /// report a missing tensor context or a backend failure.
749    fn norm_in(
750        &self,
751        _context: &tensor4all_tensorbackend::ExecutionContext,
752    ) -> std::result::Result<f64, Self::Error> {
753        Err(anyhow::anyhow!("context-scoped norm is not supported for this tensor type").into())
754    }
755
756    /// Compute the maximum absolute value of all tensor elements.
757    ///
758    /// # Errors
759    ///
760    /// Returns `Self::Error` when the maximum cannot be evaluated (a
761    /// materialization or backend failure).
762    ///
763    /// # Examples
764    /// ```
765    /// # fn main() -> anyhow::Result<()> {
766    /// use tensor4all_core::{DynIndex, IdxTensor, TensorVectorSpace};
767    ///
768    /// let index = DynIndex::new_dyn(2);
769    /// let tensor = IdxTensor::from_dense(vec![index], vec![-3.0_f64, 2.0])?;
770    /// assert!((TensorVectorSpace::maxabs(&tensor)? - 3.0).abs() < 1e-12);
771    /// # Ok(())
772    /// # }
773    /// ```
774    fn maxabs(&self) -> std::result::Result<f64, Self::Error>;
775
776    /// Element-wise subtraction: `self - other`.
777    ///
778    /// # Errors
779    ///
780    /// Returns `Self::Error` when the operand index spaces are incompatible
781    /// (an index-space mismatch) or the underlying arithmetic or backend
782    /// computation reports a failure; propagates failures from [`Self::axpby`].
783    fn sub(&self, other: &Self) -> std::result::Result<Self, Self::Error> {
784        self.axpby(AnyScalar::new_real(1.0), other, AnyScalar::new_real(-1.0))
785    }
786
787    /// Negate all elements: `-self`.
788    ///
789    /// # Errors
790    ///
791    /// Returns `Self::Error` when scaling reports a failure (an invalid
792    /// scalar coefficient or a backend computation failure); propagates
793    /// failures from [`Self::scale`].
794    fn neg(&self) -> std::result::Result<Self, Self::Error> {
795        self.scale(AnyScalar::new_real(-1.0))
796    }
797
798    /// Approximate equality check (Julia `isapprox` semantics).
799    ///
800    /// # Errors
801    /// Returns `Self::Error` when the operands have incompatible index spaces
802    /// (an index-space mismatch) or when norm evaluation reports a failure;
803    /// propagates failures from subtraction and norm evaluation.
804    fn isapprox(
805        &self,
806        other: &Self,
807        atol: f64,
808        rtol: f64,
809    ) -> std::result::Result<bool, Self::Error> {
810        let diff = self.sub(other)?;
811        let diff_norm = diff.norm()?;
812        let self_norm = self.norm()?;
813        let other_norm = other.norm()?;
814        Ok(diff_norm <= atol.max(rtol * self_norm.max(other_norm)))
815    }
816
817    /// Validate structural consistency of this tensor-like vector.
818    ///
819    /// # Errors
820    ///
821    /// Returns `Self::Error` when the tensor's index space or shape is
822    /// structurally inconsistent (for example index-space or dimension
823    /// mismatches, or an invalid internal state). The default implementation
824    /// always returns `Ok(())`.
825    fn validate(&self) -> std::result::Result<(), Self::Error> {
826        Ok(())
827    }
828}
829
830/// Contraction/einsum-style operations for tensor-like values.
831///
832/// Types that only need vector-space algebra should not implement or require
833/// this trait. Tree tensor-network algorithms should use this trait when they
834/// truly need index-based contraction.
835pub trait TensorContractionLike: TensorIndex {
836    /// Tensor conjugate operation.
837    fn conj(&self) -> Self;
838
839    /// Direct sum of two tensors along specified index pairs.
840    ///
841    /// # Errors
842    ///
843    /// Returns `Self::Error` when the summed index spaces are incompatible
844    /// (an index-space mismatch) or the underlying construction reports a
845    /// failure.
846    fn direct_sum(
847        &self,
848        other: &Self,
849        pairs: &[(<Self as TensorIndex>::Index, <Self as TensorIndex>::Index)],
850    ) -> std::result::Result<DirectSumResult<Self>, Self::Error>;
851
852    /// Outer product (tensor product) of two tensors.
853    ///
854    /// # Errors
855    ///
856    /// Returns `Self::Error` when the two tensors share contractable indices
857    /// (a shared-index mismatch) or the underlying construction reports a
858    /// failure.
859    fn outer_product(&self, other: &Self) -> std::result::Result<Self, Self::Error>;
860
861    /// Permute tensor indices to match the specified order.
862    ///
863    /// # Errors
864    ///
865    /// Returns `Self::Error` when `new_order` does not contain exactly the
866    /// tensor's external indices (an index-set mismatch or a missing-index
867    /// failure).
868    fn permuteinds(
869        &self,
870        new_order: &[<Self as TensorIndex>::Index],
871    ) -> std::result::Result<Self, Self::Error>;
872
873    /// Fuse local tensor indices into one replacement index.
874    ///
875    /// # Errors
876    ///
877    /// Returns `Self::Error` when `old_indices` is not a subset of the
878    /// tensor's external indices (a missing-index failure) or the fused
879    /// dimension product overflows (an overflow failure).
880    fn fuse_indices(
881        &self,
882        old_indices: &[<Self as TensorIndex>::Index],
883        new_index: <Self as TensorIndex>::Index,
884        order: LinearizationOrder,
885    ) -> std::result::Result<Self, Self::Error>;
886
887    /// Contract a connected tensor network over its contractable indices.
888    ///
889    /// # Errors
890    ///
891    /// Returns `Self::Error` when the network is disconnected (a
892    /// disconnected-network failure), when indices are incompatible (an
893    /// index-space mismatch), or when the underlying contraction reports a
894    /// failure.
895    fn contract(tensors: &[&Self]) -> std::result::Result<Self, Self::Error>;
896
897    /// Contract tensors while preserving selected shared indices as batch axes.
898    ///
899    /// This seam was introduced for the batched SRC sketch. The paper basis is
900    /// Algorithm 1's independent probe columns; the retained-index execution
901    /// and fallback contract are tensor4all-specific `[AI-Supplied]` plumbing.
902    ///
903    /// A retained index is matched elementwise across operands instead of being
904    /// summed. This is useful for evaluating several independent contractions
905    /// in one backend call, such as a batch of SRC probe columns. Implementations
906    /// that do not support retained batch axes may return an operation error.
907    ///
908    /// # Arguments
909    /// * `tensors` - Connected tensor operands to contract.
910    /// * `retained_indices` - Shared indices to preserve as output batch axes.
911    ///
912    /// # Returns
913    /// The contracted tensor with each retained index present once in its output
914    /// index list. An empty `retained_indices` list has the same meaning as
915    /// [`Self::contract`].
916    ///
917    /// # Errors
918    ///
919    /// Returns `Self::Error` when the operands are disconnected, an operand
920    /// index is missing or incompatible, or the retained-index operation is
921    /// unsupported by the tensor type.
922    ///
923    /// # Examples
924    ///
925    /// ```
926    /// use tensor4all_core::{DynIndex, IdxTensor, TensorContractionLike};
927    ///
928    /// let batch = DynIndex::new_dyn(2);
929    /// let contracted = DynIndex::new_dyn(2);
930    /// let left = IdxTensor::from_dense(
931    ///     vec![batch.clone(), contracted.clone()],
932    ///     vec![1.0_f64, 2.0, 3.0, 4.0],
933    /// )
934    /// .unwrap();
935    /// let right = IdxTensor::from_dense(
936    ///     vec![batch.clone(), contracted],
937    ///     vec![2.0_f64, 3.0, 4.0, 5.0],
938    /// )
939    /// .unwrap();
940    /// let result = IdxTensor::contract_retaining_indices(&[&left, &right], &[batch]).unwrap();
941    /// assert_eq!(result.dims(), vec![2]);
942    /// assert_eq!(result.to_vec::<f64>().unwrap(), vec![14.0, 26.0]);
943    /// ```
944    fn contract_retaining_indices(
945        tensors: &[&Self],
946        retained_indices: &[<Self as TensorIndex>::Index],
947    ) -> std::result::Result<Self, Self::Error> {
948        if retained_indices.is_empty() {
949            return Self::contract(tensors);
950        }
951        Err(anyhow::anyhow!(
952            "{} does not support retained contraction indices",
953            std::any::type_name::<Self>()
954        )
955        .into())
956    }
957
958    /// Contract this tensor with one other tensor using default pairwise semantics.
959    ///
960    /// # Errors
961    ///
962    /// Returns `Self::Error` when the pair is disconnected or has
963    /// incompatible indices (an index-space mismatch); propagates failures
964    /// from [`Self::contract`].
965    fn contract_pair(&self, other: &Self) -> std::result::Result<Self, Self::Error> {
966        Self::contract(&[self, other])
967    }
968
969    /// Validate structural consistency of this tensor.
970    ///
971    /// # Errors
972    ///
973    /// Returns `Self::Error` when the tensor's index space or shape is
974    /// structurally inconsistent (an index-space mismatch or an invalid
975    /// internal state). The default implementation always returns `Ok(())`.
976    fn validate(&self) -> std::result::Result<(), Self::Error> {
977        Ok(())
978    }
979}
980
981/// Factorization operations for tensor-like values.
982pub trait TensorFactorizationLike: TensorIndex {
983    /// Factorize this tensor into left and right factors.
984    /// # Errors
985    ///
986    /// Returns `FactorizeError` when the factorization fails (a non-convergence,
987    /// singular, or unsupported-storage failure).
988    ///
989    fn factorize(
990        &self,
991        left_inds: &[<Self as TensorIndex>::Index],
992        options: &FactorizeOptions,
993    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>;
994
995    /// Factorize this tensor in a caller-owned execution context.
996    ///
997    /// Context-scoped counterpart of [`Self::factorize`]: the input must
998    /// belong to `context`, and factors, truncation, and results stay in
999    /// `context` with only bounded decision payloads crossing the explicit
1000    /// readback boundary on device-resident inputs.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Returns `FactorizeError` when scoped factorization is unsupported for
1005    /// this tensor type, when the tensor does not belong to `context`, or
1006    /// when the factorization fails.
1007    fn factorize_in(
1008        &self,
1009        _left_inds: &[<Self as TensorIndex>::Index],
1010        _options: &FactorizeOptions,
1011        _context: &tensor4all_tensorbackend::ExecutionContext,
1012    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
1013        Err(FactorizeError::UnsupportedStorage(
1014            "context-scoped factorization is not supported for this tensor type",
1015        ))
1016    }
1017
1018    /// Factorize this tensor using policy-aware automatic SVD/eigen selection.
1019    ///
1020    /// Implementations may use Hermitian Gram eigendecomposition when the
1021    /// requested SVD policy is numerically suitable. The default delegates to
1022    /// [`Self::factorize`], preserving the existing behavior for tensor types
1023    /// without an automatic eigendecomposition path.
1024    ///
1025    /// # Arguments
1026    /// * `left_inds` - Indices to place on the left side of the split.
1027    /// * `options` - Ordinary SVD factorization options, including canonical
1028    ///   direction, truncation policy, and maximum bond dimension.
1029    ///
1030    /// # Returns
1031    /// The same factorization result as [`Self::factorize`], with singular
1032    /// values populated for SVD-compatible implementations.
1033    ///
1034    /// # Errors
1035    /// Returns [`FactorizeError::InvalidOptions`] when `options.alg` is not
1036    /// [`FactorizeAlg::SVD`], or another [`FactorizeError`] when factorization
1037    /// fails.
1038    ///
1039    /// # Examples
1040    ///
1041    /// ```
1042    /// use tensor4all_core::{
1043    ///     DynIndex, FactorizeOptions, IdxTensor, SvdTruncationPolicy,
1044    ///     TensorFactorizationLike,
1045    /// };
1046    ///
1047    /// let left = DynIndex::new_dyn(2);
1048    /// let right = DynIndex::new_dyn(2);
1049    /// let tensor = IdxTensor::from_dense(
1050    ///     vec![left.clone(), right],
1051    ///     vec![1.0_f64, 0.0, 0.0, 1.0e-3],
1052    /// )?;
1053    /// let options = FactorizeOptions::svd()
1054    ///     .with_svd_policy(SvdTruncationPolicy::new(1.0e-2));
1055    /// let result = tensor.factorize_auto(&[left], &options)?;
1056    /// assert_eq!(result.rank, 1);
1057    /// # Ok::<(), Box<dyn std::error::Error>>(())
1058    /// ```
1059    fn factorize_auto(
1060        &self,
1061        left_inds: &[<Self as TensorIndex>::Index],
1062        options: &FactorizeOptions,
1063    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
1064        if options.alg != FactorizeAlg::SVD {
1065            return Err(FactorizeError::InvalidOptions(
1066                "automatic factorization only supports SVD options",
1067            ));
1068        }
1069        self.factorize(left_inds, options)
1070    }
1071
1072    /// Factorize this tensor without applying truncation controls.
1073    /// # Errors
1074    ///
1075    /// Returns `FactorizeError` when the factorization fails (a non-convergence,
1076    /// singular, or unsupported-storage failure).
1077    ///
1078    fn factorize_full_rank(
1079        &self,
1080        left_inds: &[<Self as TensorIndex>::Index],
1081        alg: FactorizeAlg,
1082        canonical: Canonical,
1083    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>;
1084
1085    /// Full-rank factorization in a caller-owned execution context.
1086    ///
1087    /// Context-scoped counterpart of [`Self::factorize_full_rank`] with the
1088    /// same algorithm coverage as the scoped free functions: QR and SVD
1089    /// execute in `context`; other algorithms return a typed error.
1090    ///
1091    /// # Errors
1092    ///
1093    /// Returns `FactorizeError` when scoped factorization is unsupported for
1094    /// this tensor type, when the tensor does not belong to `context`, or
1095    /// when the factorization fails.
1096    fn factorize_full_rank_in(
1097        &self,
1098        _left_inds: &[<Self as TensorIndex>::Index],
1099        _alg: FactorizeAlg,
1100        _canonical: Canonical,
1101        _context: &tensor4all_tensorbackend::ExecutionContext,
1102    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError> {
1103        Err(FactorizeError::UnsupportedStorage(
1104            "context-scoped full-rank factorization is not supported for this tensor type",
1105        ))
1106    }
1107
1108    /// Factorize a probe prefix, optionally extending an existing QR prefix.
1109    ///
1110    /// The append semantics are based on Appendix C of
1111    /// Camaño--Epperly--Tropp and the author's `IncrementalQR.append`; the
1112    /// generic trait/default implementation is repository plumbing and is
1113    /// labelled `[AI-Supplied]` in the audit.
1114    ///
1115    /// `all_columns` contains the complete prefix requested by the caller;
1116    /// `appended_columns` contains only the newly added columns when
1117    /// `previous` is present. Native dense implementations may use the
1118    /// previous factors and append only the new block. The default is a
1119    /// correctness fallback that factorizes the complete prefix from scratch.
1120    ///
1121    /// # Arguments
1122    /// * `previous` - Factors for the preceding prefix, or `None` for the first
1123    ///   prefix.
1124    /// * `all_columns` - All probe-column tensors in the new prefix.
1125    /// * `appended_columns` - Only the columns appended since `previous`.
1126    /// * `left_inds` - Tensor indices that form the matrix rows.
1127    ///
1128    /// # Returns
1129    /// A full-rank left-canonical QR factorization of the requested prefix.
1130    ///
1131    /// # Errors
1132    /// Returns [`FactorizeError`] when the prefix cannot be stacked or QR
1133    /// factorized.
1134    ///
1135    /// # Examples
1136    ///
1137    /// ```
1138    /// use tensor4all_core::{DynIndex, IdxTensor, TensorFactorizationLike};
1139    ///
1140    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1141    /// let row = DynIndex::new_dyn(2);
1142    /// let first = IdxTensor::from_dense(
1143    ///     vec![row.clone()],
1144    ///     vec![1.0, 0.0],
1145    /// )?;
1146    /// let second = IdxTensor::from_dense(
1147    ///     vec![row.clone()],
1148    ///     vec![0.0, 1.0],
1149    /// )?;
1150    /// let result = <IdxTensor as TensorFactorizationLike>::factorize_probe_columns_incremental(
1151    ///     None,
1152    ///     &[&first, &second],
1153    ///     &[&first, &second],
1154    ///     &[row],
1155    /// )?;
1156    /// assert_eq!(result.rank, 2);
1157    /// assert_eq!(result.left.indices().len(), 2);
1158    /// # Ok(())
1159    /// # }
1160    /// ```
1161    fn factorize_probe_columns_incremental(
1162        _previous: Option<&FactorizeResult<Self>>,
1163        all_columns: &[&Self],
1164        _appended_columns: &[&Self],
1165        left_inds: &[<Self as TensorIndex>::Index],
1166    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>
1167    where
1168        Self: TensorVectorSpace + TensorConstructionLike,
1169    {
1170        let batch = <Self as TensorIndex>::Index::new_link(all_columns.len())
1171            .map_err(FactorizeError::ComputationError)?;
1172        let sketch = Self::stack_along_new_index(all_columns, batch, -1)
1173            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
1174        sketch.factorize_full_rank(left_inds, FactorizeAlg::QR, Canonical::Left)
1175    }
1176
1177    /// Batch-native variant of [`Self::factorize_probe_columns_incremental`]:
1178    /// `batch_tensor` carries a batch axis (`batch_index`) instead of being
1179    /// split into separate column tensors, letting an implementation avoid
1180    /// splitting an already-computed batch into columns and re-stacking them
1181    /// only to feed this call.
1182    ///
1183    /// The default implementation only supports the from-scratch case
1184    /// (`previous.is_none()`); it returns
1185    /// [`FactorizeError::UnsupportedStorage`] when asked to extend a
1186    /// previous factorization, exactly like this trait's
1187    /// [`Self::src_error_estimate`] default does for a capability a generic
1188    /// implementation cannot provide. `IdxTensor` overrides this with a true
1189    /// incremental implementation.
1190    ///
1191    /// Unlike [`Self::factorize_probe_columns_incremental`], whose default
1192    /// can always fall back to re-stacking `all_columns` and factorizing
1193    /// from scratch, this method's default has no such fallback once growth
1194    /// is needed: `tensor4all-treetn`'s adaptive SRC path
1195    /// (`factorize_probe_batches` in
1196    /// `crates/tensor4all-treetn/src/treetn/contraction/src_probe.rs`) calls
1197    /// this method with `previous.is_some()` on every growth step after the
1198    /// first, and always passes only the newly appended batch slice, never
1199    /// the cumulative range from the start. A backend that does not override
1200    /// this method therefore cannot use adaptive SRC at all once a second
1201    /// growth step is needed, even though the column-based path would have
1202    /// kept working (just less efficiently). This is a deliberate trade-off
1203    /// of the batch interface, not an oversight: a correct from-scratch
1204    /// re-derivation is not possible from an appended-only slice, so no
1205    /// generic default can close this gap.
1206    ///
1207    /// # Errors
1208    /// Returns [`FactorizeError`] when the batch cannot be factorized, or
1209    /// (default implementation only) when asked to extend a previous
1210    /// factorization.
1211    fn factorize_probe_batch_incremental(
1212        previous: Option<&FactorizeResult<Self>>,
1213        batch_tensor: &Self,
1214        batch_index: &<Self as TensorIndex>::Index,
1215        left_inds: &[<Self as TensorIndex>::Index],
1216    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>
1217    where
1218        Self: TensorVectorSpace + TensorConstructionLike,
1219    {
1220        if previous.is_some() {
1221            return Err(FactorizeError::UnsupportedStorage(
1222                "incremental probe-batch growth is not supported for this tensor type",
1223            ));
1224        }
1225        let _ = batch_index;
1226        batch_tensor.factorize_full_rank(left_inds, FactorizeAlg::QR, Canonical::Left)
1227    }
1228
1229    /// Evaluate the SRC adaptive stopping estimator for a small QR factor.
1230    ///
1231    /// The tensor must represent a square upper-triangular `R` matrix in
1232    /// column-major order. Implementations that do not expose their small
1233    /// matrix storage return an unsupported-storage error.
1234    ///
1235    /// # Errors
1236    ///
1237    /// Returns [`FactorizeError`] when the tensor is not a supported QR factor,
1238    /// when its storage cannot be read as a dense matrix, or when the backend
1239    /// estimator rejects the matrix.
1240    ///
1241    /// # Examples
1242    ///
1243    /// ```
1244    /// use tensor4all_core::{DynIndex, IdxTensor, TensorFactorizationLike};
1245    ///
1246    /// let row = DynIndex::new_dyn(2);
1247    /// let column = DynIndex::new_dyn(2);
1248    /// let r = IdxTensor::from_dense(
1249    ///     vec![row, column],
1250    ///     vec![2.0_f64, 0.0, 1.0, 3.0],
1251    /// ).unwrap();
1252    /// let estimate = r.src_error_estimate().unwrap();
1253    /// assert!(estimate.error.is_finite());
1254    /// assert!(estimate.norm.is_finite());
1255    /// ```
1256    fn src_error_estimate(
1257        &self,
1258    ) -> std::result::Result<tensor4all_tensorbackend::SrcErrorEstimate, FactorizeError> {
1259        Err(FactorizeError::UnsupportedStorage(
1260            "SRC adaptive estimation is not supported for this tensor type",
1261        ))
1262    }
1263
1264    /// Evaluate the Appendix-C adaptive SRC error estimate in a caller-owned
1265    /// execution context.
1266    ///
1267    /// Context-scoped counterpart of [`Self::src_error_estimate`]: all
1268    /// arithmetic executes in `context`, with only bounded decision scalars
1269    /// crossing the explicit readback boundary on device-resident inputs.
1270    ///
1271    /// # Examples
1272    ///
1273    /// ```
1274    /// use std::sync::Arc;
1275    /// use tensor4all_core::{DynIndex, ExecutionContext, IdxTensor, TensorFactorizationLike};
1276    /// use tensor4all_tensorbackend::CpuExecutionContext;
1277    /// use tensor4all_tensorbackend::CpuBackend;
1278    ///
1279    /// let context = ExecutionContext::Cpu(Arc::new(
1280    ///     CpuExecutionContext::from_backend(CpuBackend::new()),
1281    /// ));
1282    /// let cap = DynIndex::new_dyn(2);
1283    /// let batch = DynIndex::new_dyn(2);
1284    /// let r = IdxTensor::from_dense_in(
1285    ///     &context,
1286    ///     vec![cap, batch],
1287    ///     vec![2.0_f64, 0.0, 0.0, 3.0],
1288    /// )?;
1289    /// let estimate = r.src_error_estimate_in(&context)?;
1290    /// // error = sqrt(mean(1/||x_col||^2)) with x = R^{-T}: (4 + 9)/2 -> sqrt.
1291    /// let expected = ((4.0_f64 + 9.0) / 2.0).sqrt();
1292    /// assert!((estimate.error - expected).abs() < 1e-12, "got {}", estimate.error);
1293    /// assert!((estimate.norm - (13.0_f64 / 2.0).sqrt()).abs() < 1e-12);
1294    /// # Ok::<(), Box<dyn std::error::Error>>(())
1295    /// ```
1296    ///
1297    /// # Errors
1298    ///
1299    /// Returns `FactorizeError` when the estimate is unsupported for this
1300    /// tensor type, when the factor does not belong to `context`, or when the
1301    /// solve, reductions, or explicit decision readback fail.
1302    fn src_error_estimate_in(
1303        &self,
1304        _context: &tensor4all_tensorbackend::ExecutionContext,
1305    ) -> std::result::Result<tensor4all_tensorbackend::SrcErrorEstimate, FactorizeError> {
1306        Err(FactorizeError::UnsupportedStorage(
1307            "context-scoped SRC adaptive estimation is not supported for this tensor type",
1308        ))
1309    }
1310
1311    /// Batch-native incremental probe factorization in a caller-owned context.
1312    ///
1313    /// Context-scoped counterpart of
1314    /// [`Self::factorize_probe_batch_incremental`]: sketch columns, QR factors,
1315    /// and results all stay in `context` with no host round-trip.
1316    ///
1317    /// # Errors
1318    ///
1319    /// Returns `FactorizeError` when the factorization is unsupported for this
1320    /// tensor type, when an input does not belong to `context`, or when the
1321    /// resident factorization fails.
1322    fn factorize_probe_batch_incremental_in(
1323        _previous: Option<&FactorizeResult<Self>>,
1324        _batch_tensor: &Self,
1325        _batch_index: &<Self as TensorIndex>::Index,
1326        _left_inds: &[<Self as TensorIndex>::Index],
1327        _context: &tensor4all_tensorbackend::ExecutionContext,
1328    ) -> std::result::Result<FactorizeResult<Self>, FactorizeError>
1329    where
1330        Self: TensorVectorSpace + TensorConstructionLike,
1331    {
1332        Err(FactorizeError::UnsupportedStorage(
1333            "context-scoped incremental probe factorization is not supported for this tensor type",
1334        ))
1335    }
1336}
1337
1338/// Constructors and selection helpers for index-labelled tensors.
1339pub trait TensorConstructionLike: TensorContractionLike {
1340    /// Create a diagonal (Kronecker delta) tensor for a single index pair.
1341    ///
1342    /// # Errors
1343    ///
1344    /// Returns `Self::Error` when the input and output indices have unequal
1345    /// dimensions (a shape mismatch) or the underlying construction
1346    /// reports a failure.
1347    fn diagonal(
1348        input_index: &<Self as TensorIndex>::Index,
1349        output_index: &<Self as TensorIndex>::Index,
1350    ) -> std::result::Result<Self, Self::Error>;
1351
1352    /// Create a delta (identity) tensor as outer product of diagonals.
1353    ///
1354    /// # Errors
1355    ///
1356    /// Returns `Self::Error` when the input and output index lists differ in
1357    /// length (a length mismatch) or when a constituent diagonal or outer
1358    /// product reports a failure; propagates failures from [`Self::diagonal`],
1359    /// [`Self::scalar_one`], and [`TensorContractionLike::outer_product`].
1360    fn delta(
1361        input_indices: &[<Self as TensorIndex>::Index],
1362        output_indices: &[<Self as TensorIndex>::Index],
1363    ) -> std::result::Result<Self, Self::Error> {
1364        if input_indices.len() != output_indices.len() {
1365            return Err(anyhow::anyhow!(
1366                "Number of input indices ({}) must match output indices ({})",
1367                input_indices.len(),
1368                output_indices.len()
1369            )
1370            .into());
1371        }
1372
1373        if input_indices.is_empty() {
1374            return Self::scalar_one();
1375        }
1376
1377        let mut result = Self::diagonal(&input_indices[0], &output_indices[0])?;
1378        for (inp, out) in input_indices[1..].iter().zip(output_indices[1..].iter()) {
1379            let diag = Self::diagonal(inp, out)?;
1380            result = result.outer_product(&diag)?;
1381        }
1382        Ok(result)
1383    }
1384
1385    /// Create a scalar tensor with value 1.0.
1386    ///
1387    /// # Errors
1388    ///
1389    /// Returns `Self::Error` when the scalar type does not support the
1390    /// required construction (an invalid scalar dtype or a backend
1391    /// construction failure).
1392    fn scalar_one() -> std::result::Result<Self, Self::Error>;
1393
1394    /// Create a tensor filled with 1.0 for the given indices.
1395    ///
1396    /// # Errors
1397    ///
1398    /// Returns `Self::Error` when an index dimension product overflows (an
1399    /// overflow failure) or the underlying construction reports a failure.
1400    fn ones(indices: &[<Self as TensorIndex>::Index]) -> std::result::Result<Self, Self::Error>;
1401
1402    /// Create an all-ones tensor in a caller-owned execution context.
1403    ///
1404    /// Context-scoped counterpart of [`Self::ones`]: the result belongs to
1405    /// `context`, with an explicit host-to-device transfer for CUDA contexts.
1406    ///
1407    /// # Examples
1408    ///
1409    /// ```
1410    /// use std::sync::Arc;
1411    /// use tensor4all_core::{DynIndex, ExecutionContext, TensorConstructionLike};
1412    /// use tensor4all_core::IdxTensor;
1413    /// use tensor4all_tensorbackend::CpuExecutionContext;
1414    /// use tensor4all_tensorbackend::CpuBackend;
1415    ///
1416    /// let context = ExecutionContext::Cpu(Arc::new(
1417    ///     CpuExecutionContext::from_backend(CpuBackend::new()),
1418    /// ));
1419    /// let tensor = <IdxTensor as TensorConstructionLike>::ones_in(
1420    ///     &context,
1421    ///     &[DynIndex::new_dyn(2)],
1422    /// )?;
1423    /// assert_eq!(tensor.to_vec::<f64>()?, vec![1.0, 1.0]);
1424    /// # Ok::<(), Box<dyn std::error::Error>>(())
1425    /// ```
1426    ///
1427    /// # Errors
1428    ///
1429    /// Returns `Self::Error` with an `UnsupportedStorage` failure when
1430    /// construction is unsupported for this tensor type; other failures
1431    /// report an invalid payload or a backend transfer failure.
1432    fn ones_in(
1433        _context: &tensor4all_tensorbackend::ExecutionContext,
1434        _indices: &[<Self as TensorIndex>::Index],
1435    ) -> std::result::Result<Self, Self::Error> {
1436        Err(anyhow::anyhow!(
1437            "context-scoped ones construction is not supported for this tensor type"
1438        )
1439        .into())
1440    }
1441
1442    /// Validate that this tensor belongs to the supplied execution context.
1443    ///
1444    /// Generic SRC entries call this on every input tensor before RNG
1445    /// advancement or contraction, so mixed host/CUDA inputs and foreign CUDA
1446    /// contexts fail at the boundary with typed errors.
1447    ///
1448    /// # Errors
1449    ///
1450    /// Returns `Self::Error` with an `UnsupportedStorage` failure when
1451    /// validation is unsupported for this tensor type, or a backend failure
1452    /// when tensor placement does not belong to `context`.
1453    fn validate_context(
1454        &self,
1455        _context: &tensor4all_tensorbackend::ExecutionContext,
1456    ) -> std::result::Result<(), Self::Error> {
1457        Err(anyhow::anyhow!("context validation is not supported for this tensor type").into())
1458    }
1459
1460    /// Construct a tensor from a column-major dense payload.
1461    ///
1462    /// Implementations with a native dense storage path should override this
1463    /// method. The default preserves compatibility for tensor types that only
1464    /// expose one-hot construction, at the cost of constructing a sparse sum
1465    /// of one-hot tensors.
1466    ///
1467    /// # Arguments
1468    /// * `indices` - External indices in the intended column-major axis order.
1469    /// * `data` - Dense values in column-major order; its length must equal the
1470    ///   product of the index dimensions.
1471    ///
1472    /// # Errors
1473    ///
1474    /// Returns `Self::Error` when the input payload length does not match the
1475    /// product of the index dimensions, that index-dimension product would
1476    /// overflow `usize`, or an underlying tensor construction operation fails.
1477    ///
1478    /// # Examples
1479    ///
1480    /// ```
1481    /// use tensor4all_core::{AnyScalar, DynIndex, IdxTensor, TensorConstructionLike};
1482    ///
1483    /// let index = DynIndex::new_dyn(2);
1484    /// let tensor = <IdxTensor as TensorConstructionLike>::from_dense_any(
1485    ///     vec![index],
1486    ///     vec![AnyScalar::new_real(2.0), AnyScalar::new_real(3.0)],
1487    /// )
1488    /// .unwrap();
1489    /// assert_eq!(tensor.to_vec::<f64>().unwrap(), vec![2.0, 3.0]);
1490    /// ```
1491    fn from_dense_any(
1492        indices: Vec<<Self as TensorIndex>::Index>,
1493        data: Vec<AnyScalar>,
1494    ) -> std::result::Result<Self, Self::Error>
1495    where
1496        Self: TensorVectorSpace,
1497    {
1498        let expected_len = indices.iter().try_fold(1usize, |size, index| {
1499            size.checked_mul(index.dim())
1500                .ok_or_else(|| anyhow::anyhow!("dense tensor shape product overflows usize"))
1501        })?;
1502        if data.len() != expected_len {
1503            return Err(anyhow::anyhow!(
1504                "dense tensor payload has length {}, expected {}",
1505                data.len(),
1506                expected_len
1507            )
1508            .into());
1509        }
1510
1511        let mut result = Self::ones(&indices)?.scale(AnyScalar::new_real(0.0))?;
1512        let one = AnyScalar::new_real(1.0);
1513        for (linear, value) in data.into_iter().enumerate() {
1514            if value.is_zero() {
1515                continue;
1516            }
1517            let mut remainder = linear;
1518            let mut index_vals = Vec::with_capacity(indices.len());
1519            for index in &indices {
1520                let dim = index.dim();
1521                let position = remainder % dim;
1522                remainder /= dim;
1523                index_vals.push((index.clone(), position));
1524            }
1525            let basis = Self::onehot(&index_vals)?;
1526            let term = basis.scale(value)?;
1527            result = result.axpby(one.clone(), &term, one.clone())?;
1528        }
1529        Ok(result)
1530    }
1531
1532    /// Construct a tensor directly from a typed column-major dense payload.
1533    ///
1534    /// Implementations with native typed storage should override this method to
1535    /// avoid converting every element through [`AnyScalar`].
1536    ///
1537    /// # Arguments
1538    ///
1539    /// * `indices` - External indices in the intended column-major axis order.
1540    /// * `data` - Typed dense values in column-major order; its length must equal
1541    ///   the product of the index dimensions.
1542    ///
1543    /// # Returns
1544    ///
1545    /// A tensor whose dtype is selected from `T` by the implementation.
1546    ///
1547    /// # Errors
1548    ///
1549    /// Returns `Self::Error` when the index-dimension product overflows, the
1550    /// data length does not match that product, the scalar dtype is unsupported,
1551    /// or [`Self::from_dense_any`] otherwise rejects construction.
1552    ///
1553    /// # Examples
1554    ///
1555    /// ```
1556    /// use tensor4all_core::{DynIndex, IdxTensor, TensorConstructionLike};
1557    ///
1558    /// let index = DynIndex::new_dyn(2);
1559    /// let tensor = <IdxTensor as TensorConstructionLike>::from_dense(
1560    ///     vec![index],
1561    ///     vec![2.0_f64, 3.0],
1562    /// )
1563    /// .unwrap();
1564    /// assert_eq!(tensor.to_vec::<f64>().unwrap(), vec![2.0, 3.0]);
1565    /// ```
1566    fn from_dense<T>(
1567        indices: Vec<<Self as TensorIndex>::Index>,
1568        data: Vec<T>,
1569    ) -> std::result::Result<Self, Self::Error>
1570    where
1571        Self: TensorVectorSpace,
1572        T: TensorElement + Into<AnyScalar>,
1573    {
1574        Self::from_dense_any(indices, data.into_iter().map(Into::into).collect())
1575    }
1576
1577    /// Construct a tensor from a column-major dense payload in a caller-owned
1578    /// execution context.
1579    ///
1580    /// Context-scoped counterpart of [`Self::from_dense`]: host-originated
1581    /// data takes one explicit construction transfer for CUDA contexts, and
1582    /// the result belongs to `context`.
1583    ///
1584    /// # Examples
1585    ///
1586    /// ```
1587    /// use std::sync::Arc;
1588    /// use tensor4all_core::{DynIndex, ExecutionContext, TensorConstructionLike};
1589    /// use tensor4all_core::IdxTensor;
1590    /// use tensor4all_tensorbackend::CpuExecutionContext;
1591    /// use tensor4all_tensorbackend::CpuBackend;
1592    ///
1593    /// let context = ExecutionContext::Cpu(Arc::new(
1594    ///     CpuExecutionContext::from_backend(CpuBackend::new()),
1595    /// ));
1596    /// let tensor = <IdxTensor as TensorConstructionLike>::from_dense_in(
1597    ///     &context,
1598    ///     vec![DynIndex::new_dyn(2)],
1599    ///     vec![2.0_f64, 3.0],
1600    /// )?;
1601    /// assert_eq!(tensor.to_vec::<f64>()?, vec![2.0, 3.0]);
1602    /// # Ok::<(), Box<dyn std::error::Error>>(())
1603    /// ```
1604    ///
1605    /// # Errors
1606    ///
1607    /// Returns `Self::Error` with an `UnsupportedStorage` failure when
1608    /// construction is unsupported for this tensor type; other failures
1609    /// report an invalid payload or a backend transfer failure.
1610    fn from_dense_in<T>(
1611        _context: &tensor4all_tensorbackend::ExecutionContext,
1612        _indices: Vec<<Self as TensorIndex>::Index>,
1613        _data: Vec<T>,
1614    ) -> std::result::Result<Self, Self::Error>
1615    where
1616        Self: TensorVectorSpace,
1617        T: TensorElement + Into<AnyScalar>,
1618    {
1619        Err(anyhow::anyhow!(
1620            "context-scoped dense construction is not supported for this tensor type"
1621        )
1622        .into())
1623    }
1624
1625    /// Stack tensors along a newly created batch index.
1626    ///
1627    /// Implementations with a native batch stack should override this method.
1628    /// The default constructs the batch by outer products with one-hot batch
1629    /// vectors, which is correct but intended only as a compatibility path.
1630    ///
1631    /// # Arguments
1632    /// * `tensors` - Non-empty tensors with identical external index order.
1633    /// * `new_index` - Fresh index whose dimension equals `tensors.len()`.
1634    /// * `axis` - Insertion axis; negative axes count from the end, so `-1`
1635    ///   appends the batch axis.
1636    ///
1637    /// # Errors
1638    /// Returns `Self::Error` when tensors are empty, their index orders differ,
1639    /// the batch dimension is wrong, the axis is invalid, or construction
1640    /// fails.
1641    ///
1642    /// # Examples
1643    ///
1644    /// ```
1645    /// use tensor4all_core::{DynIndex, IdxTensor, TensorConstructionLike};
1646    ///
1647    /// let index = DynIndex::new_dyn(2);
1648    /// let batch = DynIndex::new_dyn(2);
1649    /// let first = IdxTensor::from_dense(vec![index.clone()], vec![1.0, 2.0]).unwrap();
1650    /// let second = IdxTensor::from_dense(vec![index.clone()], vec![3.0, 4.0]).unwrap();
1651    /// let stacked = <IdxTensor as TensorConstructionLike>::stack_along_new_index(
1652    ///     &[&first, &second],
1653    ///     batch.clone(),
1654    ///     -1,
1655    /// )
1656    /// .unwrap();
1657    /// assert_eq!(stacked.indices(), &[index, batch]);
1658    /// assert_eq!(stacked.to_vec::<f64>().unwrap(), vec![1.0, 2.0, 3.0, 4.0]);
1659    /// ```
1660    fn stack_along_new_index(
1661        tensors: &[&Self],
1662        new_index: <Self as TensorIndex>::Index,
1663        axis: isize,
1664    ) -> std::result::Result<Self, Self::Error>
1665    where
1666        Self: TensorVectorSpace,
1667    {
1668        let first = tensors
1669            .first()
1670            .ok_or_else(|| anyhow::anyhow!("stack_along_new_index requires at least one tensor"))?;
1671        if new_index.dim() != tensors.len() {
1672            return Err(anyhow::anyhow!(
1673                "stack_along_new_index batch dimension {} does not match tensor count {}",
1674                new_index.dim(),
1675                tensors.len()
1676            )
1677            .into());
1678        }
1679        let base_indices = first.external_indices();
1680        for tensor in tensors.iter().skip(1) {
1681            if tensor.external_indices() != base_indices {
1682                return Err(anyhow::anyhow!(
1683                    "stack_along_new_index tensors must have identical index order"
1684                )
1685                .into());
1686            }
1687        }
1688
1689        let result_rank = base_indices.len() + 1;
1690        let insertion_axis = if axis < 0 {
1691            result_rank as isize + axis
1692        } else {
1693            axis
1694        };
1695        if !(0..=base_indices.len() as isize).contains(&insertion_axis) {
1696            return Err(anyhow::anyhow!(
1697                "stack_along_new_index axis {} is invalid for rank {}",
1698                axis,
1699                base_indices.len()
1700            )
1701            .into());
1702        }
1703
1704        let batch_one = AnyScalar::new_real(1.0);
1705        let mut result: Option<Self> = None;
1706        for (position, tensor) in tensors.iter().enumerate() {
1707            let batch = Self::onehot(&[(new_index.clone(), position)])?;
1708            let term = tensor.outer_product(&batch)?;
1709            result = Some(match result {
1710                Some(current) => current.axpby(batch_one.clone(), &term, batch_one.clone())?,
1711                None => term,
1712            });
1713        }
1714        let mut desired = base_indices;
1715        desired.insert(insertion_axis as usize, new_index);
1716        let result = result
1717            .ok_or_else(|| anyhow::anyhow!("stack_along_new_index requires at least one tensor"))?;
1718        if result.external_indices() == desired {
1719            Ok(result)
1720        } else {
1721            result.permuteinds(&desired)
1722        }
1723    }
1724
1725    /// Concatenate tensors whose selected axes are replaced by one new index.
1726    ///
1727    /// The tensors must have the same index order away from the selected axis.
1728    /// Each tensor may use a distinct source index at that axis; the source
1729    /// axes are copied in tensor order into `new_index`. This is the batched
1730    /// counterpart to appending column blocks without recomputing the old
1731    /// columns.
1732    ///
1733    /// # Arguments
1734    /// * `tensors` - Non-empty tensors with matching non-concatenated axes.
1735    /// * `source_indices` - One axis to concatenate for each tensor.
1736    /// * `new_index` - Fresh output axis whose dimension is the sum of source
1737    ///   dimensions.
1738    ///
1739    /// # Errors
1740    ///
1741    /// Returns `Self::Error` when the input list is empty; the tensor and
1742    /// source-index counts do not match; a source index is missing; source axes
1743    /// occupy incompatible positions; non-concatenated indices are
1744    /// incompatible; the source-dimension sum overflows; or the new index
1745    /// dimension does not match that sum.
1746    ///
1747    /// # Examples
1748    ///
1749    /// ```
1750    /// use tensor4all_core::{DynIndex, IdxTensor, TensorConstructionLike};
1751    ///
1752    /// let row = DynIndex::new_dyn(2);
1753    /// let first_batch = DynIndex::new_link(1).unwrap();
1754    /// let second_batch = DynIndex::new_link(2).unwrap();
1755    /// let combined = DynIndex::new_link(3).unwrap();
1756    /// let first = IdxTensor::from_dense(
1757    ///     vec![row.clone(), first_batch.clone()],
1758    ///     vec![1.0_f64, 2.0],
1759    /// ).unwrap();
1760    /// let second = IdxTensor::from_dense(
1761    ///     vec![row.clone(), second_batch.clone()],
1762    ///     vec![3.0, 4.0, 5.0, 6.0],
1763    /// ).unwrap();
1764    /// let result = <IdxTensor as TensorConstructionLike>::concatenate_along_new_index(
1765    ///     &[&first, &second],
1766    ///     &[first_batch, second_batch],
1767    ///     combined.clone(),
1768    /// ).unwrap();
1769    /// assert_eq!(result.indices(), &[row, combined]);
1770    /// assert_eq!(result.to_vec::<f64>().unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1771    /// ```
1772    fn concatenate_along_new_index(
1773        tensors: &[&Self],
1774        source_indices: &[<Self as TensorIndex>::Index],
1775        new_index: <Self as TensorIndex>::Index,
1776    ) -> std::result::Result<Self, Self::Error>
1777    where
1778        Self: TensorVectorSpace,
1779    {
1780        let first = tensors
1781            .first()
1782            .ok_or_else(|| anyhow::anyhow!("concatenate requires at least one tensor"))?;
1783        if tensors.len() != source_indices.len() {
1784            return Err(anyhow::anyhow!(
1785                "concatenate tensor count {} does not match source-index count {}",
1786                tensors.len(),
1787                source_indices.len()
1788            )
1789            .into());
1790        }
1791        let first_axis = first
1792            .external_indices()
1793            .iter()
1794            .position(|index| index == &source_indices[0])
1795            .ok_or_else(|| anyhow::anyhow!("concatenate source index is not present"))?;
1796        let mut total_dim = 0usize;
1797        for (tensor, source) in tensors.iter().zip(source_indices) {
1798            let axis = tensor
1799                .external_indices()
1800                .iter()
1801                .position(|index| index == source)
1802                .ok_or_else(|| anyhow::anyhow!("concatenate source index is not present"))?;
1803            if axis != first_axis {
1804                return Err(
1805                    anyhow::anyhow!("concatenate source axes must have the same position").into(),
1806                );
1807            }
1808            let indices = tensor.external_indices();
1809            let first_indices = first.external_indices();
1810            if indices.len() != first_indices.len()
1811                || indices.iter().zip(first_indices).enumerate().any(
1812                    |(position, (actual, expected))| position != first_axis && *actual != expected,
1813                )
1814            {
1815                return Err(anyhow::anyhow!(
1816                    "concatenate tensors must match away from the source axis"
1817                )
1818                .into());
1819            }
1820            total_dim = total_dim
1821                .checked_add(source.dim())
1822                .ok_or_else(|| anyhow::anyhow!("concatenate dimension overflow"))?;
1823        }
1824        if new_index.dim() != total_dim {
1825            return Err(anyhow::anyhow!(
1826                "concatenate output dimension {} does not match source dimension sum {}",
1827                new_index.dim(),
1828                total_dim
1829            )
1830            .into());
1831        }
1832
1833        let mut slices = Vec::with_capacity(total_dim);
1834        for (tensor, source) in tensors.iter().zip(source_indices) {
1835            for position in 0..source.dim() {
1836                slices.push(tensor.select_indices(std::slice::from_ref(source), &[position])?);
1837            }
1838        }
1839        let slice_refs = slices.iter().collect::<Vec<_>>();
1840        Self::stack_along_new_index(&slice_refs, new_index, first_axis as isize)
1841    }
1842
1843    /// Select fixed coordinates for a subset of this tensor's external indices.
1844    ///
1845    /// # Errors
1846    ///
1847    /// Returns `Self::Error` when `selected_indices` and `positions` differ in
1848    /// length (a length mismatch), when an index is selected more than once
1849    /// (a duplicate-index failure), when a coordinate is out of range (an
1850    /// out of bounds failure), or when the underlying one-hot construction or
1851    /// contraction reports a failure; propagates failures from
1852    /// [`Self::onehot`] and [`TensorContractionLike::contract`].
1853    fn select_indices(
1854        &self,
1855        selected_indices: &[<Self as TensorIndex>::Index],
1856        positions: &[usize],
1857    ) -> std::result::Result<Self, Self::Error> {
1858        if selected_indices.len() != positions.len() {
1859            return Err(anyhow::anyhow!(
1860                "selected_indices length {} does not match positions length {}",
1861                selected_indices.len(),
1862                positions.len()
1863            )
1864            .into());
1865        }
1866        if selected_indices.is_empty() {
1867            return Ok(self.clone());
1868        }
1869
1870        let mut seen = HashSet::with_capacity(selected_indices.len());
1871        for (index, &position) in selected_indices.iter().zip(positions.iter()) {
1872            if !seen.insert(index.clone()) {
1873                return Err(anyhow::anyhow!("selected index appears more than once").into());
1874            }
1875            if position >= index.dim() {
1876                return Err(anyhow::anyhow!(
1877                    "selected coordinate {} is out of range for index {:?} with dim {}",
1878                    position,
1879                    index,
1880                    index.dim()
1881                )
1882                .into());
1883            }
1884        }
1885
1886        let index_vals = selected_indices
1887            .iter()
1888            .cloned()
1889            .zip(positions.iter().copied())
1890            .collect::<Vec<_>>();
1891        let onehot = Self::onehot(&index_vals)?;
1892        Self::contract(&[self, &onehot])
1893    }
1894
1895    /// Create a one-hot tensor with value 1.0 at the specified index positions.
1896    ///
1897    /// # Errors
1898    ///
1899    /// Returns `Self::Error` when a position is out of range for its index
1900    /// (an out of bounds failure) or the underlying construction reports a
1901    /// failure.
1902    fn onehot(
1903        index_vals: &[(<Self as TensorIndex>::Index, usize)],
1904    ) -> std::result::Result<Self, Self::Error>;
1905}
1906
1907// ============================================================================
1908// TensorLike trait (fully generic composite)
1909// ============================================================================
1910
1911/// Trait for tensor-like objects that expose external indices and support contraction.
1912///
1913/// This trait is **fully generic** (monomorphic), meaning it does not support
1914/// trait objects (`dyn TensorLike`). For heterogeneous tensor collections,
1915/// use an enum wrapper instead.
1916///
1917/// # Design Principles
1918///
1919/// - **Capability composition**: combines vector-space, factorization, construction, and contraction traits
1920/// - **Fully generic**: Uses associated type for `Index`, returns `Self`
1921/// - **Stable ordering**: `external_indices()` returns indices in deterministic order
1922/// - **No trait objects**: Requires `Sized`, cannot use `dyn TensorLike`
1923///
1924/// # Example
1925///
1926/// ```
1927/// use tensor4all_core::{DynIndex, TensorContractionLike, IdxTensor};
1928///
1929/// fn contract_pair(a: &IdxTensor, b: &IdxTensor) -> anyhow::Result<IdxTensor> {
1930///     Ok(<IdxTensor as TensorContractionLike>::contract(&[a, b])?)
1931/// }
1932///
1933/// # fn main() -> anyhow::Result<()> {
1934/// let i = DynIndex::new_dyn(2);
1935/// let j = DynIndex::new_dyn(2);
1936/// let a = IdxTensor::from_dense(
1937///     vec![i.clone(), j.clone()],
1938///     vec![1.0, 0.0, 0.0, 1.0],
1939/// )?;
1940/// let b = IdxTensor::from_dense(vec![j.clone()], vec![2.0, 3.0])?;
1941///
1942/// let result = contract_pair(&a, &b)?;
1943/// assert_eq!(result.to_vec::<f64>()?, vec![2.0, 3.0]);
1944/// # Ok(())
1945/// # }
1946/// ```
1947///
1948/// # Heterogeneous Collections
1949///
1950/// For mixing different tensor types, define an enum:
1951///
1952/// ```
1953/// use tensor4all_core::{block_tensor::BlockTensor, DynIndex, IdxTensor};
1954///
1955/// let i = DynIndex::new_dyn(2);
1956/// let dense = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
1957/// let block = BlockTensor::new(vec![dense.clone()], (1, 1)).unwrap();
1958///
1959/// enum TensorNetwork {
1960///     Dense(IdxTensor),
1961///     Block(BlockTensor<IdxTensor>),
1962/// }
1963///
1964/// let network = TensorNetwork::Block(block);
1965/// assert!(matches!(network, TensorNetwork::Block(_)));
1966/// ```
1967///
1968/// # Supertrait
1969///
1970/// `TensorLike` extends several capability traits. Through those traits it provides:
1971/// - `external_indices()` - Get all external indices
1972/// - `num_external_indices()` - Count external indices
1973/// - `replaceind()` / `replace_indices()` - Replace indices
1974/// - vector-space operations such as `axpby`, `inner_product`, and `norm`
1975/// - tensor-network operations such as contraction, construction, and factorization
1976///
1977/// Use narrower traits such as [`TensorVectorSpace`] or
1978/// [`TensorContractionLike`] when an algorithm does not need the full surface.
1979pub trait TensorLike: TensorVectorSpace + TensorFactorizationLike + TensorConstructionLike {}
1980
1981impl<T> TensorLike for T where
1982    T: TensorVectorSpace + TensorFactorizationLike + TensorConstructionLike
1983{
1984}
1985
1986/// Result of direct sum operation.
1987///
1988/// Contains the resulting tensor and the new indices created for the summed
1989/// dimensions (one new index per pair in the input).
1990///
1991/// # Examples
1992///
1993/// ```
1994/// use tensor4all_core::{DynIndex, IndexLike, TensorContractionLike, IdxTensor};
1995///
1996/// let i = DynIndex::new_dyn(2);
1997/// let j = DynIndex::new_dyn(3);
1998///
1999/// let a = IdxTensor::from_dense(vec![i.clone()], vec![1.0, 2.0]).unwrap();
2000/// let b = IdxTensor::from_dense(vec![j.clone()], vec![3.0, 4.0, 5.0]).unwrap();
2001///
2002/// let result = a.direct_sum(&b, &[(i.clone(), j.clone())]).unwrap();
2003///
2004/// // New index has dimension = 2 + 3 = 5
2005/// assert_eq!(result.new_indices.len(), 1);
2006/// assert_eq!(result.new_indices[0].dim(), 5);
2007/// assert_eq!(result.tensor.to_vec::<f64>().unwrap(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
2008/// ```
2009#[derive(Debug, Clone)]
2010pub struct DirectSumResult<T: TensorIndex> {
2011    /// The resulting tensor from direct sum.
2012    pub tensor: T,
2013    /// New indices created for the summed dimensions (one per pair).
2014    pub new_indices: Vec<T::Index>,
2015}
2016
2017#[cfg(test)]
2018mod tests;