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