Skip to main content

tensor4all_core/defaults/
factorize.rs

1//! Unified tensor factorization module.
2//!
3//! This module provides a unified `factorize()` function that dispatches to
4//! SVD, QR, LU, or CI (Cross Interpolation) algorithms based on options.
5//!
6//! # Note
7//!
8//! This module works with concrete types (`DynIndex`, `IdxTensor`) only.
9//! Generic tensor types are not supported.
10//!
11//! # Example
12//!
13//! ```
14//! use tensor4all_core::{factorize, Canonical, DynIndex, FactorizeOptions, IdxTensor, TensorLike};
15//!
16//! # fn main() -> anyhow::Result<()> {
17//! let i = DynIndex::new_dyn(2);
18//! let j = DynIndex::new_dyn(2);
19//! let tensor = IdxTensor::from_dense(
20//!     vec![i.clone(), j.clone()],
21//!     vec![1.0, 0.0, 0.0, 1.0],
22//! )?;
23//! let result = factorize(
24//!     &tensor,
25//!     std::slice::from_ref(&i),
26//!     &FactorizeOptions::svd().with_canonical(Canonical::Left),
27//! )?;
28//!
29//! assert_eq!(result.rank, 2);
30//! assert_eq!(result.left.dims(), vec![2, 2]);
31//! # Ok(())
32//! # }
33//! ```
34
35use super::eager_session::with_eager_session;
36use crate::defaults::idx_tensor::unfold_split_inner;
37use crate::defaults::DynIndex;
38use crate::{contract_pair, unfold_split, AnyScalar, IdxTensor};
39use crate::{
40    matrix_luci_factors_from_matrix_owned, rrlu_mut, MatrixLuciFactors, RrLUOptions,
41    Scalar as MatrixScalar,
42};
43use num_complex::{Complex32, Complex64, ComplexFloat};
44use tenferro_ad::EagerTensor;
45use tensor4all_tensorbackend::{Matrix, TensorElement};
46
47use crate::defaults::svd::{
48    compute_retained_rank, svd_for_factorize, svd_for_factorize_in, svd_for_factorize_on_session,
49    SvdFactorizeResult,
50};
51use crate::qr::{qr_with, qr_with_in, QrOptions};
52use crate::svd::SvdOptions;
53use crate::truncation::{
54    validate_svd_truncation_policy, SingularValueMeasure, SvdTruncationPolicy, ThresholdScale,
55    TruncationRule,
56};
57use crate::ExecutionContext;
58
59// Re-export types from tensor_like for backwards compatibility
60pub use crate::tensor_like::{
61    Canonical, FactorizeAlg, FactorizeError, FactorizeOptions, FactorizeResult,
62};
63
64/// Factorize a tensor into left and right factors.
65///
66/// This function dispatches to the appropriate algorithm based on `options.alg`:
67/// - `SVD`: Singular Value Decomposition
68/// - `QR`: QR decomposition
69/// - `LU`: Rank-revealing LU decomposition
70/// - `CI`: Cross Interpolation
71///
72/// The `canonical` option controls which factor is "canonical":
73/// - `Canonical::Left`: Left factor is orthogonal (SVD/QR) or unit-diagonal (LU/CI)
74/// - `Canonical::Right`: Right factor is orthogonal (SVD) or unit-diagonal (LU/CI)
75///
76/// # Arguments
77/// * `t` - Input tensor
78/// * `left_inds` - Indices to place on the left side
79/// * `options` - Factorization options
80///
81/// # Returns
82/// A `FactorizeResult` containing the left and right factors, bond index,
83/// singular values (for SVD), and rank.
84///
85/// # Errors
86/// Returns `FactorizeError` if:
87/// - The storage type is not supported (f64/Complex64 for all host methods,
88///   with f32/Complex32 additionally supported by CI)
89/// - QR is used with `Canonical::Right`
90/// - LU or CI is requested for a tracked tensor (those paths do not yet
91///   preserve reverse-mode AD metadata)
92/// - The underlying algorithm fails
93pub fn factorize(
94    t: &IdxTensor,
95    left_inds: &[DynIndex],
96    options: &FactorizeOptions,
97) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
98    options.validate()?;
99
100    if t.is_diag() {
101        return Err(FactorizeError::UnsupportedStorage(
102            "Diagonal storage not supported for factorize",
103        ));
104    }
105    if t.tracks_grad() && matches!(options.alg, FactorizeAlg::LU | FactorizeAlg::CI) {
106        return Err(FactorizeError::UnsupportedStorage(
107            "LU and CI factorization do not support tracked tensors yet",
108        ));
109    }
110
111    if options.alg == FactorizeAlg::CI && t.is_f32() {
112        factorize_ci::<f32>(t, left_inds, options)
113    } else if options.alg == FactorizeAlg::CI && t.is_c32() {
114        factorize_ci::<Complex32>(t, left_inds, options)
115    } else if t.is_f64() {
116        factorize_impl::<f64>(t, left_inds, options)
117    } else if t.is_c64() {
118        factorize_impl::<Complex64>(t, left_inds, options)
119    } else {
120        Err(FactorizeError::UnsupportedStorage(
121            "factorize requires f64/Complex64, or f32/Complex32 with CI",
122        ))
123    }
124}
125
126/// Select Gram eigendecomposition for safe untracked SVD truncations.
127///
128/// This is intentionally crate-visible: the public entry point is the
129/// [`TensorFactorizationLike::factorize_auto`] method.
130pub(crate) fn factorize_auto(
131    t: &IdxTensor,
132    left_inds: &[DynIndex],
133    options: &FactorizeOptions,
134) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
135    if options.alg != FactorizeAlg::SVD {
136        return Err(FactorizeError::InvalidOptions(
137            "automatic factorization only supports SVD options",
138        ));
139    }
140    options.validate()?;
141
142    let Some(policy) = options.svd_policy else {
143        return factorize(t, left_inds, options);
144    };
145    validate_svd_truncation_policy(policy).map_err(|error| FactorizeError::InvalidRtol(error.0))?;
146
147    let effective_cutoff = match (policy.scale, policy.measure, policy.rule) {
148        (ThresholdScale::Relative, SingularValueMeasure::SquaredValue, _) => policy.threshold,
149        (ThresholdScale::Relative, SingularValueMeasure::Value, TruncationRule::PerValue) => {
150            policy.threshold * policy.threshold
151        }
152        _ => 0.0,
153    };
154    if effective_cutoff <= 1.0e-12 || t.tracks_grad() || t.is_diag() {
155        return factorize(t, left_inds, options);
156    }
157    if !(t.is_f64() || t.is_c64()) {
158        return factorize(t, left_inds, options);
159    }
160
161    factorize_gram(t, left_inds, options, policy).or_else(|_| factorize(t, left_inds, options))
162}
163
164fn factorize_gram(
165    t: &IdxTensor,
166    left_inds: &[DynIndex],
167    options: &FactorizeOptions,
168    policy: SvdTruncationPolicy,
169) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
170    let (matrix_inner, _, m, n, left_indices, right_indices) = unfold_split_inner(t, left_inds)
171        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
172    if m == 0 || n == 0 {
173        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
174            "cannot factorize a matrix with an empty dimension"
175        )));
176    }
177
178    let row = DynIndex::new_dyn(m);
179    let column = DynIndex::new_dyn(n);
180    let matrix = IdxTensor::from_inner(vec![row.clone(), column.clone()], matrix_inner)
181        .map_err(FactorizeError::ComputationError)?;
182
183    // Form the smaller Gram tensor directly through the native contraction
184    // path. No rectangular host Matrix or Matrix↔backend round-trip is needed.
185    let eigenvectors_left = m <= n;
186    let gram = if eigenvectors_left {
187        let sim_row = DynIndex::new_dyn(m);
188        let adjoint = matrix
189            .conj()
190            .replaceind(&row, &sim_row)
191            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
192        contract_pair(&matrix, &adjoint)
193    } else {
194        let sim_column = DynIndex::new_dyn(n);
195        let adjoint = matrix
196            .conj()
197            .replaceind(&column, &sim_column)
198            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
199        contract_pair(&adjoint, &matrix)
200    }
201    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
202    let decomposition = gram
203        .hermitian_eigendecomposition(1.0e-12)
204        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
205
206    let lambda_scale = decomposition
207        .eigenvalues
208        .iter()
209        .map(|lambda| lambda.abs())
210        .fold(0.0_f64, f64::max);
211    if lambda_scale == 0.0 {
212        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
213            "zero Gram matrix uses the SVD fallback"
214        )));
215    }
216    let negative_tolerance = 1.0e-12 * lambda_scale;
217    let mut eigenpairs: Vec<(f64, usize)> = decomposition
218        .eigenvalues
219        .iter()
220        .copied()
221        .enumerate()
222        .map(|(column, mut lambda)| {
223            if !lambda.is_finite() {
224                return Err(FactorizeError::ComputationError(anyhow::anyhow!(
225                    "Gram eigendecomposition returned a non-finite eigenvalue"
226                )));
227            }
228            if lambda < -negative_tolerance {
229                return Err(FactorizeError::ComputationError(anyhow::anyhow!(
230                    "Gram eigendecomposition returned a materially negative eigenvalue"
231                )));
232            }
233            if lambda < 0.0 {
234                lambda = 0.0;
235            }
236            Ok((lambda, column))
237        })
238        .collect::<Result<_, _>>()?;
239    eigenpairs.sort_by(|(left, _), (right, _)| right.total_cmp(left));
240
241    let all_singular_values: Vec<f64> =
242        eigenpairs.iter().map(|(lambda, _)| lambda.sqrt()).collect();
243    let mut rank = compute_retained_rank(&all_singular_values, &policy);
244    if let Some(max_bond_dim) = options.max_bond_dim {
245        rank = rank.min(max_bond_dim);
246    }
247    rank = rank.max(1).min(m.min(n));
248    let singular_values = all_singular_values[..rank].to_vec();
249    if singular_values.contains(&0.0) {
250        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
251            "zero retained singular value uses the SVD fallback"
252        )));
253    }
254
255    let retained_columns: Vec<usize> = eigenpairs
256        .iter()
257        .take(rank)
258        .map(|(_, column)| *column)
259        .collect();
260    let eigenvectors_inner = decomposition
261        .eigenvectors
262        .as_inner()
263        .map_err(FactorizeError::ComputationError)?;
264    let basis_inner = with_eager_session(eigenvectors_inner.runtime(), None, |session| {
265        session.take_cols(eigenvectors_inner, &retained_columns)
266    })
267    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
268    let bond_index = DynIndex::new_bond(rank)
269        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
270    let basis_index = if eigenvectors_left {
271        row.clone()
272    } else {
273        column.clone()
274    };
275    let basis = IdxTensor::from_inner(vec![basis_index, bond_index.clone()], basis_inner)
276        .map_err(FactorizeError::ComputationError)?;
277
278    let (left_matrix, right_matrix) = if eigenvectors_left {
279        let sigma_vh = contract_pair(&basis.conj(), &matrix)
280            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
281            .permute_indices(&[bond_index.clone(), column.clone()])
282            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
283        match options.canonical {
284            Canonical::Left => (basis, sigma_vh),
285            Canonical::Right => {
286                let inverse: Vec<f64> = singular_values.iter().map(|sigma| 1.0 / sigma).collect();
287                (
288                    scale_bond(&basis, &bond_index, &singular_values)?,
289                    scale_bond(&sigma_vh, &bond_index, &inverse)?,
290                )
291            }
292        }
293    } else {
294        let u_sigma = contract_pair(&matrix, &basis)
295            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
296            .permute_indices(&[row.clone(), bond_index.clone()])
297            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
298        let vh = basis
299            .conj()
300            .permute_indices(&[bond_index.clone(), column.clone()])
301            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
302        match options.canonical {
303            Canonical::Right => (u_sigma, vh),
304            Canonical::Left => {
305                let inverse: Vec<f64> = singular_values.iter().map(|sigma| 1.0 / sigma).collect();
306                (
307                    scale_bond(&u_sigma, &bond_index, &inverse)?,
308                    scale_bond(&vh, &bond_index, &singular_values)?,
309                )
310            }
311        }
312    };
313
314    let mut output_left_indices = left_indices;
315    output_left_indices.push(bond_index.clone());
316    let left = reshape_factor(left_matrix, output_left_indices)?;
317    let mut output_right_indices = vec![bond_index.clone()];
318    output_right_indices.extend(right_indices);
319    let right = reshape_factor(right_matrix, output_right_indices)?;
320
321    Ok(FactorizeResult::new(
322        left,
323        right,
324        bond_index,
325        Some(singular_values),
326        rank,
327    ))
328}
329
330fn scale_bond(
331    tensor: &IdxTensor,
332    bond: &DynIndex,
333    values: &[f64],
334) -> Result<IdxTensor, FactorizeError> {
335    let temporary = DynIndex::new_bond(values.len())
336        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
337    let diagonal_values = values
338        .iter()
339        .map(|value| {
340            if tensor.is_complex() {
341                AnyScalar::new_complex(*value, 0.0)
342            } else {
343                AnyScalar::new_real(*value)
344            }
345        })
346        .collect();
347    let diagonal = IdxTensor::from_diag_any(vec![bond.clone(), temporary.clone()], diagonal_values)
348        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
349    contract_pair(tensor, &diagonal)
350        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
351        .replaceind(&temporary, bond)
352        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
353        .permute_indices(tensor.indices())
354        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))
355}
356
357fn reshape_factor(tensor: IdxTensor, indices: Vec<DynIndex>) -> Result<IdxTensor, FactorizeError> {
358    let dims: Vec<usize> = indices.iter().map(|index| index.dim).collect();
359    let inner = tensor
360        .as_inner()
361        .map_err(FactorizeError::ComputationError)?;
362    let inner = with_eager_session(inner.runtime(), None, |session| {
363        session.reshape(inner, &dims)
364    })
365    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
366    IdxTensor::from_inner(indices, inner).map_err(FactorizeError::ComputationError)
367}
368
369/// Factorize a tensor without applying algorithm-specific truncation options.
370///
371/// This path is intended for canonicalization and other exact tensor-network
372/// rewrites where the decomposition must preserve the represented tensor rather
373/// than obey global SVD/QR/LU rank-dropping defaults.
374///
375/// # Arguments
376/// * `t` - Input tensor.
377/// * `left_inds` - Indices to place on the left side.
378/// * `alg` - Decomposition algorithm to use.
379/// * `canonical` - Which factor should carry the canonical form.
380///
381/// # Returns
382/// A factorization whose contracted factors reconstruct `t` up to numerical
383/// roundoff, with no tolerance-based or maximum-rank truncation applied.
384///
385/// # Errors
386/// Returns [`FactorizeError`] if the storage type is unsupported, LU or CI is
387/// requested for a tracked tensor, the canonical direction is unsupported for
388/// the selected algorithm, or the underlying decomposition fails.
389///
390/// # Examples
391///
392/// ```
393/// use tensor4all_core::{
394///     factorize_full_rank, Canonical, DynIndex, FactorizeAlg, TensorContractionLike, IdxTensor,
395/// };
396///
397/// let i = DynIndex::new_dyn(2);
398/// let j = DynIndex::new_dyn(2);
399/// let tensor = IdxTensor::from_dense(
400///     vec![i.clone(), j.clone()],
401///     vec![1.0_f64, 0.0, 0.0, 1.0e-16],
402/// )?;
403///
404/// let result = factorize_full_rank(
405///     &tensor,
406///     std::slice::from_ref(&i),
407///     FactorizeAlg::QR,
408///     Canonical::Left,
409/// )?;
410/// let reconstructed = result.left.contract_pair(&result.right).unwrap();
411/// assert!(tensor.sub(&reconstructed)?.maxabs()? < 1.0e-18);
412/// # Ok::<(), Box<dyn std::error::Error>>(())
413/// ```
414pub fn factorize_full_rank(
415    t: &IdxTensor,
416    left_inds: &[DynIndex],
417    alg: FactorizeAlg,
418    canonical: Canonical,
419) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
420    if t.is_diag() {
421        return Err(FactorizeError::UnsupportedStorage(
422            "Diagonal storage not supported for factorize",
423        ));
424    }
425    if t.tracks_grad() && matches!(alg, FactorizeAlg::LU | FactorizeAlg::CI) {
426        return Err(FactorizeError::UnsupportedStorage(
427            "LU and CI factorization do not support tracked tensors yet",
428        ));
429    }
430
431    if alg == FactorizeAlg::CI && t.is_f32() {
432        factorize_ci_full_rank::<f32>(t, left_inds, canonical)
433    } else if alg == FactorizeAlg::CI && t.is_c32() {
434        factorize_ci_full_rank::<Complex32>(t, left_inds, canonical)
435    } else if t.is_f64() {
436        factorize_impl_full_rank::<f64>(t, left_inds, alg, canonical)
437    } else if t.is_c64() {
438        factorize_impl_full_rank::<Complex64>(t, left_inds, alg, canonical)
439    } else {
440        Err(FactorizeError::UnsupportedStorage(
441            "factorize requires f64/Complex64, or f32/Complex32 with CI",
442        ))
443    }
444}
445
446/// Factorize a tensor in a caller-owned execution context.
447///
448/// Context-scoped counterpart of [`factorize`]: the input must belong to
449/// `context`, and QR/SVD factors, truncation, and results stay in `context`
450/// with only bounded decision payloads crossing the explicit readback
451/// boundary on CUDA. LU and CI have no context-scoped path and return a typed
452/// error; run them through [`factorize`] on host inputs instead.
453///
454/// # Arguments
455/// * `t` - Input tensor, which must belong to `context`.
456/// * `left_inds` - Indices to place on the left side.
457/// * `options` - Factorization options.
458/// * `context` - Caller-owned execution context owning the input and results.
459///
460/// # Examples
461///
462/// ```
463/// use std::sync::Arc;
464/// use tensor4all_core::{
465///     DynIndex, ExecutionContext, FactorizeOptions, TensorContractionLike,
466///     factorize_in, Canonical, FactorizeAlg,
467/// };
468/// use tensor4all_tensorbackend::CpuExecutionContext;
469/// use tensor4all_tensorbackend::CpuBackend;
470///
471/// let context = ExecutionContext::Cpu(Arc::new(
472///     CpuExecutionContext::from_backend(CpuBackend::new()),
473/// ));
474/// let i = DynIndex::new_dyn(4);
475/// let j = DynIndex::new_dyn(3);
476/// let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
477/// let tensor =
478///     tensor4all_core::IdxTensor::from_dense_in(&context, vec![i.clone(), j.clone()], data)?;
479/// let options = FactorizeOptions::qr();
480/// let result = factorize_in(&tensor, &[i.clone()], &options, &context)?;
481/// let recovered = result.left.contract_pair(&result.right)?;
482/// assert_eq!(recovered.dims(), vec![4, 3]);
483/// # Ok::<(), Box<dyn std::error::Error>>(())
484/// ```
485///
486/// # Errors
487/// Returns `FactorizeError` when the tensor does not belong to `context`,
488/// when the storage, algorithm, or options are unsupported, or when the
489/// factorization or explicit decision readback fails. When this build cannot
490/// execute in `context`, the rejection carries
491/// [`IdxTensorError::UnsupportedExecutionContext`](crate::IdxTensorError::UnsupportedExecutionContext)
492/// as its source.
493pub fn factorize_in(
494    t: &IdxTensor,
495    left_inds: &[DynIndex],
496    options: &FactorizeOptions,
497    context: &ExecutionContext,
498) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
499    options.validate()?;
500    t.validate_context(context)
501        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
502
503    if t.is_diag() {
504        return Err(FactorizeError::UnsupportedStorage(
505            "Diagonal storage not supported for factorize",
506        ));
507    }
508
509    if !(t.is_f64() || t.is_complex()) {
510        return Err(FactorizeError::UnsupportedStorage(
511            "factorize currently supports only f64 and Complex64 tensors",
512        ));
513    }
514
515    match options.alg {
516        FactorizeAlg::SVD => factorize_svd_with_options_in(t, left_inds, options, context),
517        FactorizeAlg::QR => factorize_qr_with_options_in(t, left_inds, options, context),
518        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
519            "LU and CI factorization have no context-scoped path; use factorize() on host inputs",
520        )),
521    }
522}
523
524/// Full-rank factorization in a caller-owned execution context.
525///
526/// Context-scoped counterpart of [`factorize_full_rank`] with the same
527/// algorithm coverage: QR and SVD execute in `context`; LU and CI return a
528/// typed error.
529///
530/// # Errors
531/// Returns `FactorizeError` when the tensor does not belong to `context`,
532/// when the storage or algorithm is unsupported, or when the factorization
533/// fails. When this build cannot execute in `context`, the rejection carries
534/// [`IdxTensorError::UnsupportedExecutionContext`](crate::IdxTensorError::UnsupportedExecutionContext)
535/// as its source.
536pub fn factorize_full_rank_in(
537    t: &IdxTensor,
538    left_inds: &[DynIndex],
539    alg: FactorizeAlg,
540    canonical: Canonical,
541    context: &ExecutionContext,
542) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
543    t.validate_context(context)
544        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
545
546    if t.is_diag() {
547        return Err(FactorizeError::UnsupportedStorage(
548            "Diagonal storage not supported for factorize",
549        ));
550    }
551
552    if !(t.is_f64() || t.is_complex()) {
553        return Err(FactorizeError::UnsupportedStorage(
554            "factorize currently supports only f64 and Complex64 tensors",
555        ));
556    }
557
558    match alg {
559        FactorizeAlg::SVD => {
560            factorize_svd_with_options_in_full_rank(t, left_inds, canonical, context)
561        }
562        FactorizeAlg::QR => factorize_qr_full_rank_in(t, left_inds, canonical, context),
563        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
564            "LU and CI factorization have no context-scoped path; use factorize_full_rank() on host inputs",
565        )),
566    }
567}
568
569fn factorize_svd_with_options_in(
570    t: &IdxTensor,
571    left_inds: &[DynIndex],
572    options: &FactorizeOptions,
573    context: &ExecutionContext,
574) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
575    let mut svd_options = SvdOptions::new();
576    if let Some(policy) = options.svd_policy {
577        svd_options = svd_options.with_policy(policy);
578    }
579    if let Some(max_bond_dim) = options.max_bond_dim {
580        svd_options = svd_options.with_max_bond_dim(max_bond_dim);
581    }
582
583    factorize_svd_with_eager_options_in(t, left_inds, options.canonical, &svd_options, context)
584}
585
586fn factorize_svd_with_options_in_full_rank(
587    t: &IdxTensor,
588    left_inds: &[DynIndex],
589    canonical: Canonical,
590    context: &ExecutionContext,
591) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
592    let svd_options = SvdOptions::full_rank();
593    factorize_svd_with_eager_options_in(t, left_inds, canonical, &svd_options, context)
594}
595
596fn factorize_svd_with_eager_options_in(
597    t: &IdxTensor,
598    left_inds: &[DynIndex],
599    canonical: Canonical,
600    svd_options: &SvdOptions,
601    context: &ExecutionContext,
602) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
603    let result = svd_for_factorize_in(t, left_inds, svd_options, context)
604        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
605    let mut contract =
606        |lhs: &IdxTensor, rhs: &IdxTensor| context_svd_factor_contraction(lhs, rhs, context);
607    assemble_svd_factors(result, canonical, &mut contract)
608}
609
610/// Contract two SVD factors through the caller-owned context's engine.
611///
612/// The context-scoped factorization assembles `S * V^H` (or `U * S`) after the
613/// decomposition returns, so the assembly must run on the same context engine
614/// instead of the process-global default one. `S` stays a compact diagonal, so
615/// the contraction is the structured diagonal scaling of the other factor, run
616/// as one payload einsum on `context`'s engine. The result keeps the factors'
617/// own eager runtime; only the executing engine is selected by `context`.
618fn context_svd_factor_contraction(
619    lhs: &IdxTensor,
620    rhs: &IdxTensor,
621    context: &ExecutionContext,
622) -> Result<IdxTensor, FactorizeError> {
623    lhs.try_contract_pairwise_default_with_options_in(
624        rhs,
625        crate::PairwiseContractionOptions::new(),
626        context,
627    )
628    .map_err(FactorizeError::ComputationError)
629}
630
631fn factorize_qr_with_options_in(
632    t: &IdxTensor,
633    left_inds: &[DynIndex],
634    options: &FactorizeOptions,
635    context: &ExecutionContext,
636) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
637    if options.canonical == Canonical::Right {
638        return Err(FactorizeError::UnsupportedCanonical(
639            "QR only supports Canonical::Left (would need LQ for right)",
640        ));
641    }
642
643    let qr_options = if let Some(rtol) = options.qr_rtol {
644        QrOptions::new().with_rtol(rtol)
645    } else {
646        QrOptions::new()
647    };
648
649    factorize_qr_with_eager_options_in(t, left_inds, &qr_options, context)
650}
651
652fn factorize_qr_full_rank_in(
653    t: &IdxTensor,
654    left_inds: &[DynIndex],
655    canonical: Canonical,
656    context: &ExecutionContext,
657) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
658    if canonical == Canonical::Right {
659        return Err(FactorizeError::UnsupportedCanonical(
660            "QR only supports Canonical::Left (would need LQ for right)",
661        ));
662    }
663
664    factorize_qr_with_eager_options_in(t, left_inds, &QrOptions::full_rank(), context)
665}
666
667fn factorize_qr_with_eager_options_in(
668    t: &IdxTensor,
669    left_inds: &[DynIndex],
670    qr_options: &QrOptions,
671    context: &ExecutionContext,
672) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
673    let (q, r) = qr_with_in::<f64>(t, left_inds, qr_options, context)
674        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
675
676    let bond_index = q
677        .indices
678        .last()
679        .cloned()
680        .ok_or_else(|| anyhow::anyhow!("QR factorization returned a rank-0 Q tensor"))?;
681    let q_dims = q.dims();
682    let rank = *q_dims
683        .last()
684        .ok_or_else(|| anyhow::anyhow!("QR factorization returned Q with no dimensions"))?;
685
686    Ok(FactorizeResult::new(q, r, bond_index, None, rank))
687}
688
689/// The eager/global contraction the SVD factor assembly uses.
690fn global_svd_factor_contraction(
691    lhs: &IdxTensor,
692    rhs: &IdxTensor,
693) -> Result<IdxTensor, FactorizeError> {
694    contract_pair(lhs, rhs)
695        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))
696}
697
698/// Factorize a tensor on a caller-held explicit session.
699///
700/// This is the runtime-free counterpart of [`factorize`] for the concrete (non-AD) frontend: the
701/// index bookkeeping, the truncation/retention policies and the factor assembly are the same code
702/// paths as the eager route, while every decomposition runs on `session` and the factors are
703/// published as plain host-storage [`IdxTensor`]s. Nothing here enters another session or the eager
704/// runtime, so a scheduler-owned lane can call it directly.
705///
706/// QR and SVD are served. LU and CI have no session route (the same restriction as
707/// [`factorize_in`]).
708///
709/// # Errors
710///
711/// Returns [`FactorizeError`] for invalid options, an unsupported storage representation or dtype
712/// (diagonal/structured payloads and tracked or device-resident values are refused), a `Right`
713/// canonical QR request, an LU/CI request, or a rejection by the supplied session.
714///
715/// # Examples
716///
717/// ```
718/// use tensor4all_core::{
719///     factorize_on_session, DynIndex, FactorizeOptions, IdxTensor, TensorContractionLike,
720/// };
721/// use tensor4all_tensorbackend::{explicit::Session, CpuBackend};
722///
723/// let i = DynIndex::new_dyn(2);
724/// let j = DynIndex::new_dyn(2);
725/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], vec![1.0_f64, 0.0, 0.0, 1.0])?;
726///
727/// let mut backend = CpuBackend::with_threads(1)?;
728/// let mut session = Session::new(backend.engine_mut());
729/// let result =
730///     factorize_on_session(&mut session, &tensor, std::slice::from_ref(&i), &FactorizeOptions::svd())?;
731/// assert_eq!(result.rank, 2);
732/// let reconstructed = result.left.contract_pair(&result.right)?;
733/// assert!(tensor.sub(&reconstructed)?.maxabs()? < 1.0e-12);
734/// # Ok::<(), Box<dyn std::error::Error>>(())
735/// ```
736pub fn factorize_on_session(
737    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
738    t: &IdxTensor,
739    left_inds: &[DynIndex],
740    options: &FactorizeOptions,
741) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
742    options.validate()?;
743    if t.is_diag() {
744        return Err(FactorizeError::UnsupportedStorage(
745            "Diagonal storage not supported for factorize",
746        ));
747    }
748    if t.tracks_grad() {
749        return Err(FactorizeError::UnsupportedStorage(
750            "a tracked value cannot enter the plain explicit-session route; detach it explicitly before the region",
751        ));
752    }
753    match options.alg {
754        FactorizeAlg::SVD => factorize_svd_with_options_on_session(session, t, left_inds, options),
755        FactorizeAlg::QR => {
756            if options.canonical == Canonical::Right {
757                return Err(FactorizeError::UnsupportedCanonical(
758                    "QR only supports Canonical::Left (would need LQ for right)",
759                ));
760            }
761            let qr_options = if let Some(rtol) = options.qr_rtol {
762                QrOptions::new().with_rtol(rtol)
763            } else {
764                QrOptions::new()
765            };
766            let (q, r) = qr_factors_on_session(session, t, left_inds, &qr_options)
767                .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
768            let bond_index = q.indices.last().cloned().ok_or_else(|| {
769                FactorizeError::ComputationError(anyhow::anyhow!(
770                    "QR factorization returned a rank-0 Q tensor"
771                ))
772            })?;
773            let rank = *q.dims().last().ok_or_else(|| {
774                FactorizeError::ComputationError(anyhow::anyhow!(
775                    "QR factorization returned Q with no dimensions"
776                ))
777            })?;
778            Ok(FactorizeResult::new(q, r, bond_index, None, rank))
779        }
780        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
781            "LU and CI factorization have no explicit-session path",
782        )),
783    }
784}
785
786/// Full-rank factorization on a caller-held explicit session.
787///
788/// The runtime-free counterpart of [`factorize_full_rank`]: the split
789/// bookkeeping and factor assembly are shared with the eager route, while the
790/// decomposition runs on `session` and the factors are published as plain
791/// host-storage [`IdxTensor`]s. Nothing here enters another session or the
792/// eager runtime, so a caller that already owns a held session (TreeTN
793/// canonicalization, for example) can call it directly.
794///
795/// QR and SVD are served. LU and CI have no session route.
796///
797/// # Errors
798///
799/// Returns [`FactorizeError`] for a diagonal, structured or tracked operand, a
800/// `Right` canonical QR request, an LU/CI request, or a rejection by the
801/// supplied session.
802///
803/// # Examples
804///
805/// ```
806/// use tensor4all_core::{
807///     factorize_full_rank_on_session, Canonical, DynIndex, FactorizeAlg, IdxTensor,
808///     TensorContractionLike,
809/// };
810/// use tensor4all_tensorbackend::{explicit::Session, CpuBackend};
811///
812/// let i = DynIndex::new_dyn(2);
813/// let j = DynIndex::new_dyn(2);
814/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], vec![1.0_f64, 0.0, 0.0, 1.0])?;
815///
816/// let mut backend = CpuBackend::with_threads(1)?;
817/// let mut session = Session::new(backend.engine_mut());
818/// let result = factorize_full_rank_on_session(
819///     &mut session,
820///     &tensor,
821///     std::slice::from_ref(&i),
822///     FactorizeAlg::QR,
823///     Canonical::Left,
824/// )?;
825/// assert_eq!(result.rank, 2);
826/// let reconstructed = result.left.contract_pair(&result.right)?;
827/// assert!(tensor.sub(&reconstructed)?.maxabs()? < 1e-12);
828/// # Ok::<(), Box<dyn std::error::Error>>(())
829/// ```
830pub fn factorize_full_rank_on_session(
831    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
832    t: &IdxTensor,
833    left_inds: &[DynIndex],
834    alg: FactorizeAlg,
835    canonical: Canonical,
836) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
837    if t.is_diag() {
838        return Err(FactorizeError::UnsupportedStorage(
839            "Diagonal storage not supported for factorize",
840        ));
841    }
842    if t.tracks_grad() {
843        return Err(FactorizeError::UnsupportedStorage(
844            "a tracked value cannot enter the plain explicit-session route; detach it explicitly before the region",
845        ));
846    }
847    match alg {
848        FactorizeAlg::SVD => {
849            let options = SvdOptions::full_rank();
850            let result = if t.is_f32() {
851                svd_for_factorize_on_session::<f32>(session, t, left_inds, &options)
852            } else if t.is_f64() {
853                svd_for_factorize_on_session::<f64>(session, t, left_inds, &options)
854            } else if t.is_c32() {
855                svd_for_factorize_on_session::<Complex32>(session, t, left_inds, &options)
856            } else if t.is_c64() {
857                svd_for_factorize_on_session::<Complex64>(session, t, left_inds, &options)
858            } else {
859                Err(crate::svd::SvdError::ComputationError(anyhow::anyhow!(
860                    "the explicit-session SVD route serves floating-point tensors"
861                )))
862            }
863            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
864            let mut contract = |lhs: &IdxTensor, rhs: &IdxTensor| {
865                crate::contract_pair_on_session(
866                    session,
867                    lhs,
868                    rhs,
869                    crate::PairwiseContractionOptions::new(),
870                )
871                .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))
872            };
873            assemble_svd_factors(result, canonical, &mut contract)
874        }
875        FactorizeAlg::QR => {
876            if canonical == Canonical::Right {
877                return Err(FactorizeError::UnsupportedCanonical(
878                    "QR only supports Canonical::Left (would need LQ for right)",
879                ));
880            }
881            factorize_qr_full_rank_on_session(session, t, left_inds)
882        }
883        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
884            "LU and CI factorization have no explicit-session path",
885        )),
886    }
887}
888
889fn factorize_qr_full_rank_on_session(
890    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
891    t: &IdxTensor,
892    left_inds: &[DynIndex],
893) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
894    let options = QrOptions::full_rank();
895    let (q, r) = if t.is_f32() {
896        crate::qr::qr_with_on_session::<f32>(session, t, left_inds, &options)
897    } else if t.is_f64() {
898        crate::qr::qr_with_on_session::<f64>(session, t, left_inds, &options)
899    } else if t.is_c32() {
900        crate::qr::qr_with_on_session::<Complex32>(session, t, left_inds, &options)
901    } else if t.is_c64() {
902        crate::qr::qr_with_on_session::<Complex64>(session, t, left_inds, &options)
903    } else {
904        Err(crate::qr::QrError::ComputationError(anyhow::anyhow!(
905            "the explicit-session QR route serves floating-point tensors"
906        )))
907    }
908    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
909
910    let bond_index = q
911        .indices
912        .last()
913        .cloned()
914        .ok_or_else(|| anyhow::anyhow!("QR factorization returned a rank-0 Q tensor"))?;
915    let rank = *q
916        .dims()
917        .last()
918        .ok_or_else(|| anyhow::anyhow!("QR factorization returned Q with no dimensions"))?;
919    Ok(FactorizeResult::new(q, r, bond_index, None, rank))
920}
921
922/// Thin QR factors on the caller's session, in the tensor's own scalar type.
923///
924/// The session route keeps the operand dtype instead of narrowing the
925/// factorization to `f64`, so `f32`/`c32` operands factorize without a widening
926/// round trip.
927fn qr_factors_on_session(
928    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
929    t: &IdxTensor,
930    left_inds: &[DynIndex],
931    options: &QrOptions,
932) -> Result<(IdxTensor, IdxTensor), crate::qr::QrError> {
933    if t.is_f32() {
934        crate::qr::qr_with_on_session::<f32>(session, t, left_inds, options)
935    } else if t.is_f64() {
936        crate::qr::qr_with_on_session::<f64>(session, t, left_inds, options)
937    } else if t.is_c32() {
938        crate::qr::qr_with_on_session::<Complex32>(session, t, left_inds, options)
939    } else if t.is_c64() {
940        crate::qr::qr_with_on_session::<Complex64>(session, t, left_inds, options)
941    } else {
942        Err(crate::qr::QrError::ComputationError(anyhow::anyhow!(
943            "the explicit-session QR route serves floating-point tensors"
944        )))
945    }
946}
947
948fn factorize_svd_with_options_on_session(
949    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
950    t: &IdxTensor,
951    left_inds: &[DynIndex],
952    options: &FactorizeOptions,
953) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
954    let mut svd_options = SvdOptions::new();
955    if let Some(policy) = options.svd_policy {
956        svd_options = svd_options.with_policy(policy);
957    }
958    if let Some(max_bond_dim) = options.max_bond_dim {
959        svd_options = svd_options.with_max_bond_dim(max_bond_dim);
960    }
961    let result = if t.is_f32() {
962        svd_for_factorize_on_session::<f32>(session, t, left_inds, &svd_options)
963    } else if t.is_f64() {
964        svd_for_factorize_on_session::<f64>(session, t, left_inds, &svd_options)
965    } else if t.is_c32() {
966        svd_for_factorize_on_session::<Complex32>(session, t, left_inds, &svd_options)
967    } else if t.is_c64() {
968        svd_for_factorize_on_session::<Complex64>(session, t, left_inds, &svd_options)
969    } else {
970        Err(crate::svd::SvdError::ComputationError(anyhow::anyhow!(
971            "the explicit-session SVD route serves floating-point tensors"
972        )))
973    }
974    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
975    let mut contract = |lhs: &IdxTensor, rhs: &IdxTensor| -> Result<IdxTensor, FactorizeError> {
976        crate::contract_pair_on_session(session, lhs, rhs, crate::PairwiseContractionOptions::new())
977            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))
978    };
979    assemble_svd_factors(result, options.canonical, &mut contract)
980}
981
982fn factorize_impl<T>(
983    t: &IdxTensor,
984    left_inds: &[DynIndex],
985    options: &FactorizeOptions,
986) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
987where
988    T: TensorElement
989        + ComplexFloat
990        + Default
991        + From<<T as ComplexFloat>::Real>
992        + MatrixScalar
993        + crate::MatrixLuciScalar
994        + 'static,
995    <T as ComplexFloat>::Real: Into<f64> + 'static,
996{
997    match options.alg {
998        FactorizeAlg::SVD => factorize_svd(t, left_inds, options),
999        FactorizeAlg::QR => factorize_qr(t, left_inds, options),
1000        FactorizeAlg::LU => factorize_lu::<T>(t, left_inds, options),
1001        FactorizeAlg::CI => factorize_ci::<T>(t, left_inds, options),
1002    }
1003}
1004
1005fn factorize_impl_full_rank<T>(
1006    t: &IdxTensor,
1007    left_inds: &[DynIndex],
1008    alg: FactorizeAlg,
1009    canonical: Canonical,
1010) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1011where
1012    T: TensorElement
1013        + ComplexFloat
1014        + Default
1015        + From<<T as ComplexFloat>::Real>
1016        + MatrixScalar
1017        + crate::MatrixLuciScalar
1018        + 'static,
1019    <T as ComplexFloat>::Real: Into<f64> + 'static,
1020{
1021    match alg {
1022        FactorizeAlg::SVD => factorize_svd_full_rank(t, left_inds, canonical),
1023        FactorizeAlg::QR => factorize_qr_full_rank(t, left_inds, canonical),
1024        FactorizeAlg::LU => factorize_lu_full_rank::<T>(t, left_inds, canonical),
1025        FactorizeAlg::CI => factorize_ci_full_rank::<T>(t, left_inds, canonical),
1026    }
1027}
1028
1029/// SVD factorization implementation.
1030fn factorize_svd(
1031    t: &IdxTensor,
1032    left_inds: &[DynIndex],
1033    options: &FactorizeOptions,
1034) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1035    let mut svd_options = SvdOptions::new();
1036    if let Some(policy) = options.svd_policy {
1037        svd_options = svd_options.with_policy(policy);
1038    }
1039    if let Some(max_bond_dim) = options.max_bond_dim {
1040        svd_options = svd_options.with_max_bond_dim(max_bond_dim);
1041    }
1042
1043    factorize_svd_with_options(t, left_inds, options.canonical, &svd_options)
1044}
1045
1046fn factorize_svd_full_rank(
1047    t: &IdxTensor,
1048    left_inds: &[DynIndex],
1049    canonical: Canonical,
1050) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1051    let svd_options = SvdOptions::full_rank();
1052    factorize_svd_with_options(t, left_inds, canonical, &svd_options)
1053}
1054
1055fn factorize_svd_with_options(
1056    t: &IdxTensor,
1057    left_inds: &[DynIndex],
1058    canonical: Canonical,
1059    svd_options: &SvdOptions,
1060) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1061    let result = svd_for_factorize(t, left_inds, svd_options)?;
1062    assemble_svd_factors(result, canonical, &mut global_svd_factor_contraction)
1063}
1064
1065/// Absorb the SVD factors into left/right canonical form.
1066///
1067/// Shared by the host and context-scoped SVD factorization paths: contracts
1068/// `S` into one side and renames the leftover `sim` leg back to `bond`.
1069fn assemble_svd_factors(
1070    result: SvdFactorizeResult,
1071    canonical: Canonical,
1072    contract: &mut dyn FnMut(&IdxTensor, &IdxTensor) -> Result<IdxTensor, FactorizeError>,
1073) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1074    let u = result.u;
1075    let s = result.s;
1076    let vh = result.vh;
1077    let bond_index = result.bond_index;
1078    let singular_values = result.singular_values;
1079    let rank = result.rank;
1080    // Internal SVD plumbing: svd_for_factorize keeps the legacy convention
1081    // (vh shares the original `bond` leg), so S's leftover `sim` leg is
1082    // renamed back to `bond` after each contraction. The public svd_with API
1083    // instead returns V with the `sim` leg and needs no compensation.
1084    let sim_bond_index = s.indices[1].clone();
1085
1086    match canonical {
1087        Canonical::Left => {
1088            // L = U (orthogonal), R = S * V^H
1089            let right_contracted = contract(&s, &vh)?;
1090            let right = right_contracted
1091                .replaceind(&sim_bond_index, &bond_index)
1092                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1093            Ok(FactorizeResult::new(
1094                u,
1095                right,
1096                bond_index,
1097                Some(singular_values),
1098                rank,
1099            ))
1100        }
1101        Canonical::Right => {
1102            // L = U * S, R = V^H
1103            let left_contracted = contract(&u, &s)?;
1104            let left = left_contracted
1105                .replaceind(&sim_bond_index, &bond_index)
1106                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1107            Ok(FactorizeResult::new(
1108                left,
1109                vh,
1110                bond_index,
1111                Some(singular_values),
1112                rank,
1113            ))
1114        }
1115    }
1116}
1117
1118/// QR factorization implementation.
1119fn factorize_qr(
1120    t: &IdxTensor,
1121    left_inds: &[DynIndex],
1122    options: &FactorizeOptions,
1123) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1124    if options.canonical == Canonical::Right {
1125        return Err(FactorizeError::UnsupportedCanonical(
1126            "QR only supports Canonical::Left (would need LQ for right)",
1127        ));
1128    }
1129
1130    let qr_options = if let Some(rtol) = options.qr_rtol {
1131        QrOptions::new().with_rtol(rtol)
1132    } else {
1133        QrOptions::new()
1134    };
1135
1136    factorize_qr_with_options(t, left_inds, &qr_options)
1137}
1138
1139fn factorize_qr_full_rank(
1140    t: &IdxTensor,
1141    left_inds: &[DynIndex],
1142    canonical: Canonical,
1143) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1144    if canonical == Canonical::Right {
1145        return Err(FactorizeError::UnsupportedCanonical(
1146            "QR only supports Canonical::Left (would need LQ for right)",
1147        ));
1148    }
1149
1150    factorize_qr_with_options(t, left_inds, &QrOptions::full_rank())
1151}
1152
1153fn factorize_qr_with_options(
1154    t: &IdxTensor,
1155    left_inds: &[DynIndex],
1156    qr_options: &QrOptions,
1157) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
1158    let (q, r) = qr_with::<f64>(t, left_inds, qr_options)?;
1159
1160    // Get bond index from Q tensor (last index)
1161    let bond_index = q
1162        .indices
1163        .last()
1164        .cloned()
1165        .ok_or_else(|| anyhow::anyhow!("QR factorization returned a rank-0 Q tensor"))?;
1166    // Rank is the last dimension of Q
1167    let q_dims = q.dims();
1168    let rank = *q_dims
1169        .last()
1170        .ok_or_else(|| anyhow::anyhow!("QR factorization returned Q with no dimensions"))?;
1171
1172    Ok(FactorizeResult::new(q, r, bond_index, None, rank))
1173}
1174
1175/// LU factorization implementation.
1176fn factorize_lu<T>(
1177    t: &IdxTensor,
1178    left_inds: &[DynIndex],
1179    options: &FactorizeOptions,
1180) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1181where
1182    T: TensorElement
1183        + ComplexFloat
1184        + Default
1185        + From<<T as ComplexFloat>::Real>
1186        + MatrixScalar
1187        + crate::MatrixLuciScalar
1188        + 'static,
1189    <T as ComplexFloat>::Real: Into<f64> + 'static,
1190{
1191    factorize_lu_with_options::<T>(
1192        t,
1193        left_inds,
1194        options.canonical,
1195        options.max_bond_dim.unwrap_or(usize::MAX),
1196        1e-14,
1197    )
1198}
1199
1200fn factorize_lu_full_rank<T>(
1201    t: &IdxTensor,
1202    left_inds: &[DynIndex],
1203    canonical: Canonical,
1204) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1205where
1206    T: TensorElement
1207        + ComplexFloat
1208        + Default
1209        + From<<T as ComplexFloat>::Real>
1210        + MatrixScalar
1211        + crate::MatrixLuciScalar
1212        + 'static,
1213    <T as ComplexFloat>::Real: Into<f64> + 'static,
1214{
1215    factorize_lu_with_options::<T>(t, left_inds, canonical, usize::MAX, 0.0)
1216}
1217
1218fn factorize_lu_with_options<T>(
1219    t: &IdxTensor,
1220    left_inds: &[DynIndex],
1221    canonical: Canonical,
1222    max_bond_dim: usize,
1223    rel_tol: f64,
1224) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1225where
1226    T: TensorElement
1227        + ComplexFloat
1228        + Default
1229        + From<<T as ComplexFloat>::Real>
1230        + MatrixScalar
1231        + crate::MatrixLuciScalar
1232        + 'static,
1233    <T as ComplexFloat>::Real: Into<f64> + 'static,
1234{
1235    // Unfold tensor into matrix
1236    let (a_tensor, _, m, n, left_indices, right_indices) = unfold_split(t, left_inds)
1237        .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))?;
1238
1239    // Convert to Matrix type for rrLU without rebuilding a zero-filled matrix.
1240    let a_matrix = native_tensor_to_matrix::<T>(&a_tensor, m, n)?;
1241
1242    // Set up LU options
1243    let left_orthogonal = canonical == Canonical::Left;
1244    let lu_options = RrLUOptions {
1245        max_bond_dim,
1246        rel_tol,
1247        abs_tol: 0.0,
1248        left_orthogonal,
1249    };
1250
1251    // Perform LU decomposition
1252    let mut a_matrix = a_matrix;
1253    let lu = rrlu_mut(&mut a_matrix, Some(lu_options))?;
1254    let rank = lu.npivots();
1255
1256    // Tensor factorization keeps a valid rank-one zero representation even
1257    // when low-level LU reports a rank-zero selection.
1258    let (l_matrix, u_matrix, bond_dim) = if rank == 0 {
1259        let (left, right) = rank_one_zero_factors(m, n, canonical);
1260        (left, right, 1)
1261    } else {
1262        (lu.left(true), lu.right(true), rank)
1263    };
1264
1265    let bond_index = DynIndex::new_bond(bond_dim)
1266        .map_err(|e| anyhow::anyhow!("Failed to create bond index: {:?}", e))?;
1267
1268    // Convert L matrix back to tensor
1269    let l_vec = l_matrix.into_col_major_vec();
1270    let mut l_indices = left_indices.clone();
1271    l_indices.push(bond_index.clone());
1272    let left = IdxTensor::from_dense(l_indices, l_vec)
1273        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1274
1275    // Convert U matrix back to tensor
1276    let u_vec = u_matrix.into_col_major_vec();
1277    let mut r_indices = vec![bond_index.clone()];
1278    r_indices.extend_from_slice(&right_indices);
1279    let right = IdxTensor::from_dense(r_indices, u_vec)
1280        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1281
1282    Ok(FactorizeResult::new(
1283        left, right, bond_index, None, bond_dim,
1284    ))
1285}
1286
1287/// CI (Cross Interpolation) factorization implementation.
1288fn factorize_ci<T>(
1289    t: &IdxTensor,
1290    left_inds: &[DynIndex],
1291    options: &FactorizeOptions,
1292) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1293where
1294    T: TensorElement
1295        + ComplexFloat
1296        + Default
1297        + From<<T as ComplexFloat>::Real>
1298        + MatrixScalar
1299        + crate::MatrixLuciScalar
1300        + 'static,
1301    <T as ComplexFloat>::Real: Into<f64> + 'static,
1302{
1303    factorize_ci_with_options::<T>(
1304        t,
1305        left_inds,
1306        options.canonical,
1307        options.max_bond_dim.unwrap_or(usize::MAX),
1308        1e-14,
1309    )
1310}
1311
1312fn factorize_ci_full_rank<T>(
1313    t: &IdxTensor,
1314    left_inds: &[DynIndex],
1315    canonical: Canonical,
1316) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1317where
1318    T: TensorElement
1319        + ComplexFloat
1320        + Default
1321        + From<<T as ComplexFloat>::Real>
1322        + MatrixScalar
1323        + crate::MatrixLuciScalar
1324        + 'static,
1325    <T as ComplexFloat>::Real: Into<f64> + 'static,
1326{
1327    factorize_ci_with_options::<T>(t, left_inds, canonical, usize::MAX, 0.0)
1328}
1329
1330// CI-factorization default seam.
1331//
1332// The default LU/CI factorization implementations in this module consume the
1333// TCI substrate (rrlu, MatrixLUCI, MatrixLuciScalar) that was absorbed from
1334// tensor4all-tcicore when that crate was dissolved into tensor4all-core
1335// (#639); the substrate now lives in this crate and the inverted
1336// core -> tcicore dependency no longer exists. IdxTensor data is unfolded
1337// into a column-major eager matrix at this boundary, and fixed-pivot CI
1338// factors are rebuilt from
1339// that primal value.
1340fn factorize_ci_with_options<T>(
1341    t: &IdxTensor,
1342    left_inds: &[DynIndex],
1343    canonical: Canonical,
1344    max_bond_dim: usize,
1345    rel_tol: f64,
1346) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1347where
1348    T: TensorElement
1349        + ComplexFloat
1350        + Default
1351        + From<<T as ComplexFloat>::Real>
1352        + MatrixScalar
1353        + crate::MatrixLuciScalar
1354        + 'static,
1355    <T as ComplexFloat>::Real: Into<f64> + 'static,
1356{
1357    // Unfold tensor into an eager matrix. Pivot selection is primal-only, but
1358    // fixed-pivot CI factors are rebuilt from this eager value below.
1359    let (matrix_inner, _, m, n, left_indices, right_indices) = unfold_split_inner(t, left_inds)
1360        .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))?;
1361
1362    // Convert to Matrix type for MatrixLUCI.
1363    let a_matrix = eager_tensor_to_matrix::<T>(&matrix_inner, m, n)?;
1364
1365    // Set up LU options for CI
1366    let left_orthogonal = canonical == Canonical::Left;
1367    let lu_options = RrLUOptions {
1368        max_bond_dim,
1369        rel_tol,
1370        abs_tol: 0.0,
1371        left_orthogonal,
1372    };
1373
1374    // Perform CI decomposition and reuse its backend factors directly. The
1375    // previous path gathered pivot blocks into eager tensors and solved them
1376    // element-wise at this boundary, duplicating work already done by the
1377    // backend factorization.
1378    let factors = matrix_luci_factors_from_matrix_owned(a_matrix, Some(lu_options))?;
1379    let rank = factors.rank;
1380    let (left, right, bond_index) =
1381        matrix_luci_factors_to_idx_tensors(factors, &left_indices, &right_indices, canonical)?;
1382
1383    Ok(FactorizeResult::new(
1384        left,
1385        right,
1386        bond_index,
1387        None,
1388        rank.max(1),
1389    ))
1390}
1391
1392// The selected side still has unit diagonal; only the opposite side carries
1393// the zero value. Both factors keep a valid dimension-one bond.
1394fn rank_one_zero_factors<T: MatrixScalar>(
1395    m: usize,
1396    n: usize,
1397    canonical: Canonical,
1398) -> (Matrix<T>, Matrix<T>) {
1399    let mut left = Matrix::zeros(m, 1);
1400    let mut right = Matrix::zeros(1, n);
1401    match canonical {
1402        Canonical::Left => left[[0, 0]] = T::one(),
1403        Canonical::Right => right[[0, 0]] = T::one(),
1404    }
1405    (left, right)
1406}
1407
1408fn matrix_luci_factors_to_idx_tensors<T>(
1409    factors: MatrixLuciFactors<T>,
1410    left_indices: &[DynIndex],
1411    right_indices: &[DynIndex],
1412    canonical: Canonical,
1413) -> Result<(IdxTensor, IdxTensor, DynIndex), FactorizeError>
1414where
1415    T: TensorElement + MatrixScalar,
1416{
1417    let bond_dim = factors.rank.max(1);
1418    let (left_matrix, right_matrix) = if factors.rank == 0 {
1419        rank_one_zero_factors(factors.left.nrows(), factors.right.ncols(), canonical)
1420    } else {
1421        (factors.left, factors.right)
1422    };
1423    let bond_index = DynIndex::new_bond(bond_dim)
1424        .map_err(|e| anyhow::anyhow!("Failed to create bond index: {:?}", e))?;
1425
1426    let mut l_indices = left_indices.to_vec();
1427    l_indices.push(bond_index.clone());
1428    let left = IdxTensor::from_dense(l_indices, left_matrix.into_col_major_vec())
1429        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1430
1431    let mut r_indices = vec![bond_index.clone()];
1432    r_indices.extend_from_slice(right_indices);
1433    let right = IdxTensor::from_dense(r_indices, right_matrix.into_col_major_vec())
1434        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1435
1436    Ok((left, right, bond_index))
1437}
1438
1439/// Convert a native rank-2 tensor into a backend [`Matrix`].
1440fn native_tensor_to_matrix<T>(
1441    tensor: &tenferro::Tensor,
1442    m: usize,
1443    n: usize,
1444) -> Result<Matrix<T>, FactorizeError>
1445where
1446    T: TensorElement + MatrixScalar + Copy,
1447{
1448    let data = T::dense_values_from_native_col_major(tensor).map_err(|e| {
1449        FactorizeError::ComputationError(anyhow::anyhow!(
1450            "failed to extract dense matrix entries from native tensor: {e}"
1451        ))
1452    })?;
1453    matrix_from_col_major_values(data, m, n, "native")
1454}
1455
1456fn eager_tensor_to_matrix<T>(
1457    tensor: &EagerTensor,
1458    m: usize,
1459    n: usize,
1460) -> Result<Matrix<T>, FactorizeError>
1461where
1462    T: TensorElement + MatrixScalar + Copy,
1463{
1464    let values = tensor
1465        .value()
1466        .map_err(|source| FactorizeError::ComputationError(anyhow::Error::new(source)))?
1467        .as_slice::<T>()
1468        .map_err(|source| FactorizeError::ComputationError(anyhow::Error::new(source)))?
1469        .to_vec();
1470    matrix_from_col_major_values(values, m, n, "eager")
1471}
1472
1473fn matrix_from_col_major_values<T>(
1474    data: Vec<T>,
1475    m: usize,
1476    n: usize,
1477    source: &str,
1478) -> Result<Matrix<T>, FactorizeError>
1479where
1480    T: MatrixScalar + Copy,
1481{
1482    Matrix::try_from_col_major_vec(m, n, data).map_err(|error| {
1483        FactorizeError::ComputationError(anyhow::anyhow!(
1484            "{source} matrix conversion failed for shape ({m}, {n}): {error}"
1485        ))
1486    })
1487}
1488
1489#[cfg(test)]
1490fn checked_matrix_len(m: usize, n: usize, source: &str) -> Result<usize, FactorizeError> {
1491    m.checked_mul(n).ok_or_else(|| {
1492        FactorizeError::ComputationError(anyhow::anyhow!(
1493            "{source} matrix shape ({m}, {n}) overflows usize"
1494        ))
1495    })
1496}
1497
1498#[cfg(test)]
1499mod tests;