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;