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 crate::defaults::idx_tensor::unfold_split_inner;
36use crate::defaults::DynIndex;
37use crate::{contract_pair, unfold_split, AnyScalar, IdxTensor};
38use crate::{
39    matrix_luci_factors_from_matrix_owned, rrlu_mut, MatrixLuciFactors, RrLUOptions,
40    Scalar as MatrixScalar,
41};
42use num_complex::{Complex64, ComplexFloat};
43use tenferro_ad::EagerTensor;
44use tensor4all_tensorbackend::{Matrix, TensorElement};
45
46use crate::defaults::svd::{
47    compute_retained_rank, svd_for_factorize, svd_for_factorize_in, SvdFactorizeResult,
48};
49use crate::qr::{qr_with, qr_with_in, QrOptions};
50use crate::svd::SvdOptions;
51use crate::truncation::{
52    validate_svd_truncation_policy, SingularValueMeasure, SvdTruncationPolicy, ThresholdScale,
53    TruncationRule,
54};
55use crate::ExecutionContext;
56
57// Re-export types from tensor_like for backwards compatibility
58pub use crate::tensor_like::{
59    Canonical, FactorizeAlg, FactorizeError, FactorizeOptions, FactorizeResult,
60};
61
62/// Factorize a tensor into left and right factors.
63///
64/// This function dispatches to the appropriate algorithm based on `options.alg`:
65/// - `SVD`: Singular Value Decomposition
66/// - `QR`: QR decomposition
67/// - `LU`: Rank-revealing LU decomposition
68/// - `CI`: Cross Interpolation
69///
70/// The `canonical` option controls which factor is "canonical":
71/// - `Canonical::Left`: Left factor is orthogonal (SVD/QR) or unit-diagonal (LU/CI)
72/// - `Canonical::Right`: Right factor is orthogonal (SVD) or unit-diagonal (LU/CI)
73///
74/// # Arguments
75/// * `t` - Input tensor
76/// * `left_inds` - Indices to place on the left side
77/// * `options` - Factorization options
78///
79/// # Returns
80/// A `FactorizeResult` containing the left and right factors, bond index,
81/// singular values (for SVD), and rank.
82///
83/// # Errors
84/// Returns `FactorizeError` if:
85/// - The storage type is not supported (only DenseF64 and DenseC64)
86/// - QR is used with `Canonical::Right`
87/// - LU or CI is requested for a tracked tensor (those paths do not yet
88///   preserve reverse-mode AD metadata)
89/// - The underlying algorithm fails
90pub fn factorize(
91    t: &IdxTensor,
92    left_inds: &[DynIndex],
93    options: &FactorizeOptions,
94) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
95    options.validate()?;
96
97    if t.is_diag() {
98        return Err(FactorizeError::UnsupportedStorage(
99            "Diagonal storage not supported for factorize",
100        ));
101    }
102    if t.tracks_grad() && matches!(options.alg, FactorizeAlg::LU | FactorizeAlg::CI) {
103        return Err(FactorizeError::UnsupportedStorage(
104            "LU and CI factorization do not support tracked tensors yet",
105        ));
106    }
107
108    if t.is_f64() {
109        factorize_impl_f64(t, left_inds, options)
110    } else if t.is_complex() {
111        factorize_impl_c64(t, left_inds, options)
112    } else {
113        Err(FactorizeError::UnsupportedStorage(
114            "factorize currently supports only f64 and Complex64 tensors",
115        ))
116    }
117}
118
119/// Select Gram eigendecomposition for safe untracked SVD truncations.
120///
121/// This is intentionally crate-visible: the public entry point is the
122/// [`TensorFactorizationLike::factorize_auto`] method.
123pub(crate) fn factorize_auto(
124    t: &IdxTensor,
125    left_inds: &[DynIndex],
126    options: &FactorizeOptions,
127) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
128    if options.alg != FactorizeAlg::SVD {
129        return Err(FactorizeError::InvalidOptions(
130            "automatic factorization only supports SVD options",
131        ));
132    }
133    options.validate()?;
134
135    let Some(policy) = options.svd_policy else {
136        return factorize(t, left_inds, options);
137    };
138    validate_svd_truncation_policy(policy).map_err(|error| FactorizeError::InvalidRtol(error.0))?;
139
140    let effective_cutoff = match (policy.scale, policy.measure, policy.rule) {
141        (ThresholdScale::Relative, SingularValueMeasure::SquaredValue, _) => policy.threshold,
142        (ThresholdScale::Relative, SingularValueMeasure::Value, TruncationRule::PerValue) => {
143            policy.threshold * policy.threshold
144        }
145        _ => 0.0,
146    };
147    if effective_cutoff <= 1.0e-12 || t.tracks_grad() || t.is_diag() {
148        return factorize(t, left_inds, options);
149    }
150    if !(t.is_f64() || t.is_c64()) {
151        return factorize(t, left_inds, options);
152    }
153
154    factorize_gram(t, left_inds, options, policy).or_else(|_| factorize(t, left_inds, options))
155}
156
157fn factorize_gram(
158    t: &IdxTensor,
159    left_inds: &[DynIndex],
160    options: &FactorizeOptions,
161    policy: SvdTruncationPolicy,
162) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
163    let (matrix_inner, _, m, n, left_indices, right_indices) = unfold_split_inner(t, left_inds)
164        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
165    if m == 0 || n == 0 {
166        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
167            "cannot factorize a matrix with an empty dimension"
168        )));
169    }
170
171    let row = DynIndex::new_dyn(m);
172    let column = DynIndex::new_dyn(n);
173    let matrix = IdxTensor::from_inner(vec![row.clone(), column.clone()], matrix_inner)
174        .map_err(FactorizeError::ComputationError)?;
175
176    // Form the smaller Gram tensor directly through the native contraction
177    // path. No rectangular host Matrix or Matrix↔backend round-trip is needed.
178    let eigenvectors_left = m <= n;
179    let gram = if eigenvectors_left {
180        let sim_row = DynIndex::new_dyn(m);
181        let adjoint = matrix
182            .conj()
183            .replaceind(&row, &sim_row)
184            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
185        contract_pair(&matrix, &adjoint)
186    } else {
187        let sim_column = DynIndex::new_dyn(n);
188        let adjoint = matrix
189            .conj()
190            .replaceind(&column, &sim_column)
191            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
192        contract_pair(&adjoint, &matrix)
193    }
194    .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
195    let decomposition = gram
196        .hermitian_eigendecomposition(1.0e-12)
197        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
198
199    let lambda_scale = decomposition
200        .eigenvalues
201        .iter()
202        .map(|lambda| lambda.abs())
203        .fold(0.0_f64, f64::max);
204    if lambda_scale == 0.0 {
205        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
206            "zero Gram matrix uses the SVD fallback"
207        )));
208    }
209    let negative_tolerance = 1.0e-12 * lambda_scale;
210    let mut eigenpairs: Vec<(f64, usize)> = decomposition
211        .eigenvalues
212        .iter()
213        .copied()
214        .enumerate()
215        .map(|(column, mut lambda)| {
216            if !lambda.is_finite() {
217                return Err(FactorizeError::ComputationError(anyhow::anyhow!(
218                    "Gram eigendecomposition returned a non-finite eigenvalue"
219                )));
220            }
221            if lambda < -negative_tolerance {
222                return Err(FactorizeError::ComputationError(anyhow::anyhow!(
223                    "Gram eigendecomposition returned a materially negative eigenvalue"
224                )));
225            }
226            if lambda < 0.0 {
227                lambda = 0.0;
228            }
229            Ok((lambda, column))
230        })
231        .collect::<Result<_, _>>()?;
232    eigenpairs.sort_by(|(left, _), (right, _)| right.total_cmp(left));
233
234    let all_singular_values: Vec<f64> =
235        eigenpairs.iter().map(|(lambda, _)| lambda.sqrt()).collect();
236    let mut rank = compute_retained_rank(&all_singular_values, &policy);
237    if let Some(max_bond_dim) = options.max_bond_dim {
238        rank = rank.min(max_bond_dim);
239    }
240    rank = rank.max(1).min(m.min(n));
241    let singular_values = all_singular_values[..rank].to_vec();
242    if singular_values.contains(&0.0) {
243        return Err(FactorizeError::ComputationError(anyhow::anyhow!(
244            "zero retained singular value uses the SVD fallback"
245        )));
246    }
247
248    let retained_columns: Vec<usize> = eigenpairs
249        .iter()
250        .take(rank)
251        .map(|(_, column)| *column)
252        .collect();
253    let basis_inner = decomposition
254        .eigenvectors
255        .as_inner()
256        .map_err(FactorizeError::ComputationError)?
257        .take_cols(&retained_columns)
258        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
259    let bond_index = DynIndex::new_bond(rank)
260        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
261    let basis_index = if eigenvectors_left {
262        row.clone()
263    } else {
264        column.clone()
265    };
266    let basis = IdxTensor::from_inner(vec![basis_index, bond_index.clone()], basis_inner)
267        .map_err(FactorizeError::ComputationError)?;
268
269    let (left_matrix, right_matrix) = if eigenvectors_left {
270        let sigma_vh = contract_pair(&basis.conj(), &matrix)
271            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
272            .permute_indices(&[bond_index.clone(), column.clone()])
273            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
274        match options.canonical {
275            Canonical::Left => (basis, sigma_vh),
276            Canonical::Right => {
277                let inverse: Vec<f64> = singular_values.iter().map(|sigma| 1.0 / sigma).collect();
278                (
279                    scale_bond(&basis, &bond_index, &singular_values)?,
280                    scale_bond(&sigma_vh, &bond_index, &inverse)?,
281                )
282            }
283        }
284    } else {
285        let u_sigma = contract_pair(&matrix, &basis)
286            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
287            .permute_indices(&[row.clone(), bond_index.clone()])
288            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
289        let vh = basis
290            .conj()
291            .permute_indices(&[bond_index.clone(), column.clone()])
292            .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
293        match options.canonical {
294            Canonical::Right => (u_sigma, vh),
295            Canonical::Left => {
296                let inverse: Vec<f64> = singular_values.iter().map(|sigma| 1.0 / sigma).collect();
297                (
298                    scale_bond(&u_sigma, &bond_index, &inverse)?,
299                    scale_bond(&vh, &bond_index, &singular_values)?,
300                )
301            }
302        }
303    };
304
305    let mut output_left_indices = left_indices;
306    output_left_indices.push(bond_index.clone());
307    let left = reshape_factor(left_matrix, output_left_indices)?;
308    let mut output_right_indices = vec![bond_index.clone()];
309    output_right_indices.extend(right_indices);
310    let right = reshape_factor(right_matrix, output_right_indices)?;
311
312    Ok(FactorizeResult::new(
313        left,
314        right,
315        bond_index,
316        Some(singular_values),
317        rank,
318    ))
319}
320
321fn scale_bond(
322    tensor: &IdxTensor,
323    bond: &DynIndex,
324    values: &[f64],
325) -> Result<IdxTensor, FactorizeError> {
326    let temporary = DynIndex::new_bond(values.len())
327        .map_err(|error| FactorizeError::ComputationError(anyhow::anyhow!(error)))?;
328    let diagonal_values = values
329        .iter()
330        .map(|value| {
331            if tensor.is_complex() {
332                AnyScalar::new_complex(*value, 0.0)
333            } else {
334                AnyScalar::new_real(*value)
335            }
336        })
337        .collect();
338    let diagonal = IdxTensor::from_diag_any(vec![bond.clone(), temporary.clone()], diagonal_values)
339        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
340    contract_pair(tensor, &diagonal)
341        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
342        .replaceind(&temporary, bond)
343        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?
344        .permute_indices(tensor.indices())
345        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))
346}
347
348fn reshape_factor(tensor: IdxTensor, indices: Vec<DynIndex>) -> Result<IdxTensor, FactorizeError> {
349    let dims: Vec<usize> = indices.iter().map(|index| index.dim).collect();
350    let inner = tensor
351        .as_inner()
352        .map_err(FactorizeError::ComputationError)?
353        .reshape(&dims)
354        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
355    IdxTensor::from_inner(indices, inner).map_err(FactorizeError::ComputationError)
356}
357
358/// Factorize a tensor without applying algorithm-specific truncation options.
359///
360/// This path is intended for canonicalization and other exact tensor-network
361/// rewrites where the decomposition must preserve the represented tensor rather
362/// than obey global SVD/QR/LU rank-dropping defaults.
363///
364/// # Arguments
365/// * `t` - Input tensor.
366/// * `left_inds` - Indices to place on the left side.
367/// * `alg` - Decomposition algorithm to use.
368/// * `canonical` - Which factor should carry the canonical form.
369///
370/// # Returns
371/// A factorization whose contracted factors reconstruct `t` up to numerical
372/// roundoff, with no tolerance-based or maximum-rank truncation applied.
373///
374/// # Errors
375/// Returns [`FactorizeError`] if the storage type is unsupported, LU or CI is
376/// requested for a tracked tensor, the canonical direction is unsupported for
377/// the selected algorithm, or the underlying decomposition fails.
378///
379/// # Examples
380///
381/// ```
382/// use tensor4all_core::{
383///     factorize_full_rank, Canonical, DynIndex, FactorizeAlg, TensorContractionLike, IdxTensor,
384/// };
385///
386/// let i = DynIndex::new_dyn(2);
387/// let j = DynIndex::new_dyn(2);
388/// let tensor = IdxTensor::from_dense(
389///     vec![i.clone(), j.clone()],
390///     vec![1.0_f64, 0.0, 0.0, 1.0e-16],
391/// )?;
392///
393/// let result = factorize_full_rank(
394///     &tensor,
395///     std::slice::from_ref(&i),
396///     FactorizeAlg::QR,
397///     Canonical::Left,
398/// )?;
399/// let reconstructed = result.left.contract_pair(&result.right).unwrap();
400/// assert!(tensor.sub(&reconstructed)?.maxabs()? < 1.0e-18);
401/// # Ok::<(), Box<dyn std::error::Error>>(())
402/// ```
403pub fn factorize_full_rank(
404    t: &IdxTensor,
405    left_inds: &[DynIndex],
406    alg: FactorizeAlg,
407    canonical: Canonical,
408) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
409    if t.is_diag() {
410        return Err(FactorizeError::UnsupportedStorage(
411            "Diagonal storage not supported for factorize",
412        ));
413    }
414    if t.tracks_grad() && matches!(alg, FactorizeAlg::LU | FactorizeAlg::CI) {
415        return Err(FactorizeError::UnsupportedStorage(
416            "LU and CI factorization do not support tracked tensors yet",
417        ));
418    }
419
420    if t.is_f64() {
421        factorize_impl_f64_full_rank(t, left_inds, alg, canonical)
422    } else if t.is_complex() {
423        factorize_impl_c64_full_rank(t, left_inds, alg, canonical)
424    } else {
425        Err(FactorizeError::UnsupportedStorage(
426            "factorize currently supports only f64 and Complex64 tensors",
427        ))
428    }
429}
430
431/// Factorize a tensor in a caller-owned execution context.
432///
433/// Context-scoped counterpart of [`factorize`]: the input must belong to
434/// `context`, and QR/SVD factors, truncation, and results stay in `context`
435/// with only bounded decision payloads crossing the explicit readback
436/// boundary on CUDA. LU and CI have no context-scoped path and return a typed
437/// error; run them through [`factorize`] on host inputs instead.
438///
439/// # Arguments
440/// * `t` - Input tensor, which must belong to `context`.
441/// * `left_inds` - Indices to place on the left side.
442/// * `options` - Factorization options.
443/// * `context` - Caller-owned execution context owning the input and results.
444///
445/// # Examples
446///
447/// ```
448/// use std::sync::Arc;
449/// use tensor4all_core::{
450///     DynIndex, ExecutionContext, FactorizeOptions, TensorContractionLike,
451///     factorize_in, Canonical, FactorizeAlg,
452/// };
453/// use tensor4all_tensorbackend::CpuExecutionContext;
454/// use tenferro_cpu::CpuBackend;
455///
456/// let context = ExecutionContext::Cpu(Arc::new(
457///     CpuExecutionContext::from_backend(CpuBackend::new()),
458/// ));
459/// let i = DynIndex::new_dyn(4);
460/// let j = DynIndex::new_dyn(3);
461/// let data: Vec<f64> = (0..12).map(|x| x as f64).collect();
462/// let tensor =
463///     tensor4all_core::IdxTensor::from_dense_in(&context, vec![i.clone(), j.clone()], data)?;
464/// let options = FactorizeOptions::qr();
465/// let result = factorize_in(&tensor, &[i.clone()], &options, &context)?;
466/// let recovered = result.left.contract_pair(&result.right)?;
467/// assert_eq!(recovered.dims(), vec![4, 3]);
468/// # Ok::<(), Box<dyn std::error::Error>>(())
469/// ```
470///
471/// # Errors
472/// Returns `FactorizeError` when the tensor does not belong to `context`,
473/// when the storage, algorithm, or options are unsupported, or when the
474/// factorization or explicit decision readback fails.
475pub fn factorize_in(
476    t: &IdxTensor,
477    left_inds: &[DynIndex],
478    options: &FactorizeOptions,
479    context: &ExecutionContext,
480) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
481    options.validate()?;
482    t.validate_context(context)
483        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
484
485    if t.is_diag() {
486        return Err(FactorizeError::UnsupportedStorage(
487            "Diagonal storage not supported for factorize",
488        ));
489    }
490
491    if !(t.is_f64() || t.is_complex()) {
492        return Err(FactorizeError::UnsupportedStorage(
493            "factorize currently supports only f64 and Complex64 tensors",
494        ));
495    }
496
497    match options.alg {
498        FactorizeAlg::SVD => factorize_svd_with_options_in(t, left_inds, options, context),
499        FactorizeAlg::QR => factorize_qr_with_options_in(t, left_inds, options, context),
500        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
501            "LU and CI factorization have no context-scoped path; use factorize() on host inputs",
502        )),
503    }
504}
505
506/// Full-rank factorization in a caller-owned execution context.
507///
508/// Context-scoped counterpart of [`factorize_full_rank`] with the same
509/// algorithm coverage: QR and SVD execute in `context`; LU and CI return a
510/// typed error.
511///
512/// # Errors
513/// Returns `FactorizeError` when the tensor does not belong to `context`,
514/// when the storage or algorithm is unsupported, or when the factorization
515/// fails.
516pub fn factorize_full_rank_in(
517    t: &IdxTensor,
518    left_inds: &[DynIndex],
519    alg: FactorizeAlg,
520    canonical: Canonical,
521    context: &ExecutionContext,
522) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
523    t.validate_context(context)
524        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
525
526    if t.is_diag() {
527        return Err(FactorizeError::UnsupportedStorage(
528            "Diagonal storage not supported for factorize",
529        ));
530    }
531
532    if !(t.is_f64() || t.is_complex()) {
533        return Err(FactorizeError::UnsupportedStorage(
534            "factorize currently supports only f64 and Complex64 tensors",
535        ));
536    }
537
538    match alg {
539        FactorizeAlg::SVD => {
540            factorize_svd_with_options_in_full_rank(t, left_inds, canonical, context)
541        }
542        FactorizeAlg::QR => factorize_qr_full_rank_in(t, left_inds, canonical, context),
543        FactorizeAlg::LU | FactorizeAlg::CI => Err(FactorizeError::UnsupportedStorage(
544            "LU and CI factorization have no context-scoped path; use factorize_full_rank() on host inputs",
545        )),
546    }
547}
548
549fn factorize_svd_with_options_in(
550    t: &IdxTensor,
551    left_inds: &[DynIndex],
552    options: &FactorizeOptions,
553    context: &ExecutionContext,
554) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
555    let mut svd_options = SvdOptions::new();
556    if let Some(policy) = options.svd_policy {
557        svd_options = svd_options.with_policy(policy);
558    }
559    if let Some(max_bond_dim) = options.max_bond_dim {
560        svd_options = svd_options.with_max_bond_dim(max_bond_dim);
561    }
562
563    factorize_svd_with_eager_options_in(t, left_inds, options.canonical, &svd_options, context)
564}
565
566fn factorize_svd_with_options_in_full_rank(
567    t: &IdxTensor,
568    left_inds: &[DynIndex],
569    canonical: Canonical,
570    context: &ExecutionContext,
571) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
572    let svd_options = SvdOptions::full_rank();
573    factorize_svd_with_eager_options_in(t, left_inds, canonical, &svd_options, context)
574}
575
576fn factorize_svd_with_eager_options_in(
577    t: &IdxTensor,
578    left_inds: &[DynIndex],
579    canonical: Canonical,
580    svd_options: &SvdOptions,
581    context: &ExecutionContext,
582) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
583    let result = svd_for_factorize_in(t, left_inds, svd_options, context)
584        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
585    assemble_svd_factors(result, canonical)
586}
587
588fn factorize_qr_with_options_in(
589    t: &IdxTensor,
590    left_inds: &[DynIndex],
591    options: &FactorizeOptions,
592    context: &ExecutionContext,
593) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
594    if options.canonical == Canonical::Right {
595        return Err(FactorizeError::UnsupportedCanonical(
596            "QR only supports Canonical::Left (would need LQ for right)",
597        ));
598    }
599
600    let qr_options = if let Some(rtol) = options.qr_rtol {
601        QrOptions::new().with_rtol(rtol)
602    } else {
603        QrOptions::new()
604    };
605
606    factorize_qr_with_eager_options_in(t, left_inds, &qr_options, context)
607}
608
609fn factorize_qr_full_rank_in(
610    t: &IdxTensor,
611    left_inds: &[DynIndex],
612    canonical: Canonical,
613    context: &ExecutionContext,
614) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
615    if canonical == Canonical::Right {
616        return Err(FactorizeError::UnsupportedCanonical(
617            "QR only supports Canonical::Left (would need LQ for right)",
618        ));
619    }
620
621    factorize_qr_with_eager_options_in(t, left_inds, &QrOptions::full_rank(), context)
622}
623
624fn factorize_qr_with_eager_options_in(
625    t: &IdxTensor,
626    left_inds: &[DynIndex],
627    qr_options: &QrOptions,
628    context: &ExecutionContext,
629) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
630    let (q, r) = qr_with_in::<f64>(t, left_inds, qr_options, context)
631        .map_err(|error| FactorizeError::ComputationError(anyhow::Error::new(error)))?;
632
633    let bond_index = q
634        .indices
635        .last()
636        .cloned()
637        .ok_or_else(|| anyhow::anyhow!("QR factorization returned a rank-0 Q tensor"))?;
638    let q_dims = q.dims();
639    let rank = *q_dims
640        .last()
641        .ok_or_else(|| anyhow::anyhow!("QR factorization returned Q with no dimensions"))?;
642
643    Ok(FactorizeResult::new(q, r, bond_index, None, rank))
644}
645
646fn factorize_impl_f64(
647    t: &IdxTensor,
648    left_inds: &[DynIndex],
649    options: &FactorizeOptions,
650) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
651    match options.alg {
652        FactorizeAlg::SVD => factorize_svd(t, left_inds, options),
653        FactorizeAlg::QR => factorize_qr(t, left_inds, options),
654        FactorizeAlg::LU => factorize_lu::<f64>(t, left_inds, options),
655        FactorizeAlg::CI => factorize_ci::<f64>(t, left_inds, options),
656    }
657}
658
659fn factorize_impl_f64_full_rank(
660    t: &IdxTensor,
661    left_inds: &[DynIndex],
662    alg: FactorizeAlg,
663    canonical: Canonical,
664) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
665    match alg {
666        FactorizeAlg::SVD => factorize_svd_full_rank(t, left_inds, canonical),
667        FactorizeAlg::QR => factorize_qr_full_rank(t, left_inds, canonical),
668        FactorizeAlg::LU => factorize_lu_full_rank::<f64>(t, left_inds, canonical),
669        FactorizeAlg::CI => factorize_ci_full_rank::<f64>(t, left_inds, canonical),
670    }
671}
672
673fn factorize_impl_c64(
674    t: &IdxTensor,
675    left_inds: &[DynIndex],
676    options: &FactorizeOptions,
677) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
678    match options.alg {
679        FactorizeAlg::SVD => factorize_svd(t, left_inds, options),
680        FactorizeAlg::QR => factorize_qr(t, left_inds, options),
681        FactorizeAlg::LU => factorize_lu::<Complex64>(t, left_inds, options),
682        FactorizeAlg::CI => factorize_ci::<Complex64>(t, left_inds, options),
683    }
684}
685
686fn factorize_impl_c64_full_rank(
687    t: &IdxTensor,
688    left_inds: &[DynIndex],
689    alg: FactorizeAlg,
690    canonical: Canonical,
691) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
692    match alg {
693        FactorizeAlg::SVD => factorize_svd_full_rank(t, left_inds, canonical),
694        FactorizeAlg::QR => factorize_qr_full_rank(t, left_inds, canonical),
695        FactorizeAlg::LU => factorize_lu_full_rank::<Complex64>(t, left_inds, canonical),
696        FactorizeAlg::CI => factorize_ci_full_rank::<Complex64>(t, left_inds, canonical),
697    }
698}
699
700/// SVD factorization implementation.
701fn factorize_svd(
702    t: &IdxTensor,
703    left_inds: &[DynIndex],
704    options: &FactorizeOptions,
705) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
706    let mut svd_options = SvdOptions::new();
707    if let Some(policy) = options.svd_policy {
708        svd_options = svd_options.with_policy(policy);
709    }
710    if let Some(max_bond_dim) = options.max_bond_dim {
711        svd_options = svd_options.with_max_bond_dim(max_bond_dim);
712    }
713
714    factorize_svd_with_options(t, left_inds, options.canonical, &svd_options)
715}
716
717fn factorize_svd_full_rank(
718    t: &IdxTensor,
719    left_inds: &[DynIndex],
720    canonical: Canonical,
721) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
722    let svd_options = SvdOptions::full_rank();
723    factorize_svd_with_options(t, left_inds, canonical, &svd_options)
724}
725
726fn factorize_svd_with_options(
727    t: &IdxTensor,
728    left_inds: &[DynIndex],
729    canonical: Canonical,
730    svd_options: &SvdOptions,
731) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
732    let result = svd_for_factorize(t, left_inds, svd_options)?;
733    assemble_svd_factors(result, canonical)
734}
735
736/// Absorb the SVD factors into left/right canonical form.
737///
738/// Shared by the host and context-scoped SVD factorization paths: contracts
739/// `S` into one side and renames the leftover `sim` leg back to `bond`.
740fn assemble_svd_factors(
741    result: SvdFactorizeResult,
742    canonical: Canonical,
743) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
744    let u = result.u;
745    let s = result.s;
746    let vh = result.vh;
747    let bond_index = result.bond_index;
748    let singular_values = result.singular_values;
749    let rank = result.rank;
750    // Internal SVD plumbing: svd_for_factorize keeps the legacy convention
751    // (vh shares the original `bond` leg), so S's leftover `sim` leg is
752    // renamed back to `bond` after each contraction. The public svd_with API
753    // instead returns V with the `sim` leg and needs no compensation.
754    let sim_bond_index = s.indices[1].clone();
755
756    match canonical {
757        Canonical::Left => {
758            // L = U (orthogonal), R = S * V^H
759            let right_contracted = contract_pair(&s, &vh)
760                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
761            let right = right_contracted
762                .replaceind(&sim_bond_index, &bond_index)
763                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
764            Ok(FactorizeResult::new(
765                u,
766                right,
767                bond_index,
768                Some(singular_values),
769                rank,
770            ))
771        }
772        Canonical::Right => {
773            // L = U * S, R = V^H
774            let left_contracted = contract_pair(&u, &s)
775                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
776            let left = left_contracted
777                .replaceind(&sim_bond_index, &bond_index)
778                .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
779            Ok(FactorizeResult::new(
780                left,
781                vh,
782                bond_index,
783                Some(singular_values),
784                rank,
785            ))
786        }
787    }
788}
789
790/// QR factorization implementation.
791fn factorize_qr(
792    t: &IdxTensor,
793    left_inds: &[DynIndex],
794    options: &FactorizeOptions,
795) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
796    if options.canonical == Canonical::Right {
797        return Err(FactorizeError::UnsupportedCanonical(
798            "QR only supports Canonical::Left (would need LQ for right)",
799        ));
800    }
801
802    let qr_options = if let Some(rtol) = options.qr_rtol {
803        QrOptions::new().with_rtol(rtol)
804    } else {
805        QrOptions::new()
806    };
807
808    factorize_qr_with_options(t, left_inds, &qr_options)
809}
810
811fn factorize_qr_full_rank(
812    t: &IdxTensor,
813    left_inds: &[DynIndex],
814    canonical: Canonical,
815) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
816    if canonical == Canonical::Right {
817        return Err(FactorizeError::UnsupportedCanonical(
818            "QR only supports Canonical::Left (would need LQ for right)",
819        ));
820    }
821
822    factorize_qr_with_options(t, left_inds, &QrOptions::full_rank())
823}
824
825fn factorize_qr_with_options(
826    t: &IdxTensor,
827    left_inds: &[DynIndex],
828    qr_options: &QrOptions,
829) -> Result<FactorizeResult<IdxTensor>, FactorizeError> {
830    let (q, r) = qr_with::<f64>(t, left_inds, qr_options)?;
831
832    // Get bond index from Q tensor (last index)
833    let bond_index = q
834        .indices
835        .last()
836        .cloned()
837        .ok_or_else(|| anyhow::anyhow!("QR factorization returned a rank-0 Q tensor"))?;
838    // Rank is the last dimension of Q
839    let q_dims = q.dims();
840    let rank = *q_dims
841        .last()
842        .ok_or_else(|| anyhow::anyhow!("QR factorization returned Q with no dimensions"))?;
843
844    Ok(FactorizeResult::new(q, r, bond_index, None, rank))
845}
846
847/// LU factorization implementation.
848fn factorize_lu<T>(
849    t: &IdxTensor,
850    left_inds: &[DynIndex],
851    options: &FactorizeOptions,
852) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
853where
854    T: TensorElement
855        + ComplexFloat
856        + Default
857        + From<<T as ComplexFloat>::Real>
858        + MatrixScalar
859        + crate::MatrixLuciScalar
860        + 'static,
861    <T as ComplexFloat>::Real: Into<f64> + 'static,
862{
863    factorize_lu_with_options::<T>(
864        t,
865        left_inds,
866        options.canonical,
867        options.max_bond_dim.unwrap_or(usize::MAX),
868        1e-14,
869    )
870}
871
872fn factorize_lu_full_rank<T>(
873    t: &IdxTensor,
874    left_inds: &[DynIndex],
875    canonical: Canonical,
876) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
877where
878    T: TensorElement
879        + ComplexFloat
880        + Default
881        + From<<T as ComplexFloat>::Real>
882        + MatrixScalar
883        + crate::MatrixLuciScalar
884        + 'static,
885    <T as ComplexFloat>::Real: Into<f64> + 'static,
886{
887    factorize_lu_with_options::<T>(t, left_inds, canonical, usize::MAX, 0.0)
888}
889
890fn factorize_lu_with_options<T>(
891    t: &IdxTensor,
892    left_inds: &[DynIndex],
893    canonical: Canonical,
894    max_bond_dim: usize,
895    rel_tol: f64,
896) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
897where
898    T: TensorElement
899        + ComplexFloat
900        + Default
901        + From<<T as ComplexFloat>::Real>
902        + MatrixScalar
903        + crate::MatrixLuciScalar
904        + 'static,
905    <T as ComplexFloat>::Real: Into<f64> + 'static,
906{
907    // Unfold tensor into matrix
908    let (a_tensor, _, m, n, left_indices, right_indices) = unfold_split(t, left_inds)
909        .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))?;
910
911    // Convert to Matrix type for rrLU without rebuilding a zero-filled matrix.
912    let a_matrix = native_tensor_to_matrix::<T>(&a_tensor, m, n)?;
913
914    // Set up LU options
915    let left_orthogonal = canonical == Canonical::Left;
916    let lu_options = RrLUOptions {
917        max_bond_dim,
918        rel_tol,
919        abs_tol: 0.0,
920        left_orthogonal,
921    };
922
923    // Perform LU decomposition
924    let mut a_matrix = a_matrix;
925    let lu = rrlu_mut(&mut a_matrix, Some(lu_options))?;
926    let rank = lu.npivots();
927
928    // Extract L and U matrices (permuted)
929    let l_matrix = lu.left(true);
930    let u_matrix = lu.right(true);
931
932    // Create bond index
933    let bond_index = DynIndex::new_bond(rank)
934        .map_err(|e| anyhow::anyhow!("Failed to create bond index: {:?}", e))?;
935
936    // Convert L matrix back to tensor
937    let l_vec = l_matrix.into_col_major_vec();
938    let mut l_indices = left_indices.clone();
939    l_indices.push(bond_index.clone());
940    let left = IdxTensor::from_dense(l_indices, l_vec)
941        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
942
943    // Convert U matrix back to tensor
944    let u_vec = u_matrix.into_col_major_vec();
945    let mut r_indices = vec![bond_index.clone()];
946    r_indices.extend_from_slice(&right_indices);
947    let right = IdxTensor::from_dense(r_indices, u_vec)
948        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
949
950    Ok(FactorizeResult::new(left, right, bond_index, None, rank))
951}
952
953/// CI (Cross Interpolation) factorization implementation.
954fn factorize_ci<T>(
955    t: &IdxTensor,
956    left_inds: &[DynIndex],
957    options: &FactorizeOptions,
958) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
959where
960    T: TensorElement
961        + ComplexFloat
962        + Default
963        + From<<T as ComplexFloat>::Real>
964        + MatrixScalar
965        + crate::MatrixLuciScalar
966        + 'static,
967    <T as ComplexFloat>::Real: Into<f64> + 'static,
968{
969    factorize_ci_with_options::<T>(
970        t,
971        left_inds,
972        options.canonical,
973        options.max_bond_dim.unwrap_or(usize::MAX),
974        1e-14,
975    )
976}
977
978fn factorize_ci_full_rank<T>(
979    t: &IdxTensor,
980    left_inds: &[DynIndex],
981    canonical: Canonical,
982) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
983where
984    T: TensorElement
985        + ComplexFloat
986        + Default
987        + From<<T as ComplexFloat>::Real>
988        + MatrixScalar
989        + crate::MatrixLuciScalar
990        + 'static,
991    <T as ComplexFloat>::Real: Into<f64> + 'static,
992{
993    factorize_ci_with_options::<T>(t, left_inds, canonical, usize::MAX, 0.0)
994}
995
996// CI-factorization default seam.
997//
998// The default LU/CI factorization implementations in this module consume the
999// TCI substrate (rrlu, MatrixLUCI, MatrixLuciScalar) that was absorbed from
1000// tensor4all-tcicore when that crate was dissolved into tensor4all-core
1001// (#639); the substrate now lives in this crate and the inverted
1002// core -> tcicore dependency no longer exists. IdxTensor data is unfolded
1003// into a column-major eager matrix at this boundary, and fixed-pivot CI
1004// factors are rebuilt from
1005// that primal value.
1006fn factorize_ci_with_options<T>(
1007    t: &IdxTensor,
1008    left_inds: &[DynIndex],
1009    canonical: Canonical,
1010    max_bond_dim: usize,
1011    rel_tol: f64,
1012) -> Result<FactorizeResult<IdxTensor>, FactorizeError>
1013where
1014    T: TensorElement
1015        + ComplexFloat
1016        + Default
1017        + From<<T as ComplexFloat>::Real>
1018        + MatrixScalar
1019        + crate::MatrixLuciScalar
1020        + 'static,
1021    <T as ComplexFloat>::Real: Into<f64> + 'static,
1022{
1023    // Unfold tensor into an eager matrix. Pivot selection is primal-only, but
1024    // fixed-pivot CI factors are rebuilt from this eager value below.
1025    let (matrix_inner, _, m, n, left_indices, right_indices) = unfold_split_inner(t, left_inds)
1026        .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))?;
1027
1028    // Convert to Matrix type for MatrixLUCI.
1029    let a_matrix = eager_tensor_to_matrix::<T>(&matrix_inner, m, n)?;
1030
1031    // Set up LU options for CI
1032    let left_orthogonal = canonical == Canonical::Left;
1033    let lu_options = RrLUOptions {
1034        max_bond_dim,
1035        rel_tol,
1036        abs_tol: 0.0,
1037        left_orthogonal,
1038    };
1039
1040    // Perform CI decomposition and reuse its backend factors directly. The
1041    // previous path gathered pivot blocks into eager tensors and solved them
1042    // element-wise at this boundary, duplicating work already done by the
1043    // backend factorization.
1044    let factors = matrix_luci_factors_from_matrix_owned(a_matrix, Some(lu_options))?;
1045    let rank = factors.rank;
1046    let (left, right, bond_index) =
1047        matrix_luci_factors_to_idx_tensors(factors, &left_indices, &right_indices)?;
1048
1049    Ok(FactorizeResult::new(left, right, bond_index, None, rank))
1050}
1051
1052fn matrix_luci_factors_to_idx_tensors<T>(
1053    factors: MatrixLuciFactors<T>,
1054    left_indices: &[DynIndex],
1055    right_indices: &[DynIndex],
1056) -> Result<(IdxTensor, IdxTensor, DynIndex), FactorizeError>
1057where
1058    T: TensorElement + Clone,
1059{
1060    let bond_index = DynIndex::new_bond(factors.rank)
1061        .map_err(|e| anyhow::anyhow!("Failed to create bond index: {:?}", e))?;
1062
1063    let mut l_indices = left_indices.to_vec();
1064    l_indices.push(bond_index.clone());
1065    let left = IdxTensor::from_dense(l_indices, factors.left.into_col_major_vec())
1066        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1067
1068    let mut r_indices = vec![bond_index.clone()];
1069    r_indices.extend_from_slice(right_indices);
1070    let right = IdxTensor::from_dense(r_indices, factors.right.into_col_major_vec())
1071        .map_err(|e| FactorizeError::ComputationError(anyhow::Error::new(e)))?;
1072
1073    Ok((left, right, bond_index))
1074}
1075
1076/// Convert a native rank-2 tensor into a backend [`Matrix`].
1077fn native_tensor_to_matrix<T>(
1078    tensor: &tenferro::Tensor,
1079    m: usize,
1080    n: usize,
1081) -> Result<Matrix<T>, FactorizeError>
1082where
1083    T: TensorElement + MatrixScalar + Copy,
1084{
1085    let data = T::dense_values_from_native_col_major(tensor).map_err(|e| {
1086        FactorizeError::ComputationError(anyhow::anyhow!(
1087            "failed to extract dense matrix entries from native tensor: {e}"
1088        ))
1089    })?;
1090    matrix_from_col_major_values(data, m, n, "native")
1091}
1092
1093fn eager_tensor_to_matrix<T>(
1094    tensor: &EagerTensor,
1095    m: usize,
1096    n: usize,
1097) -> Result<Matrix<T>, FactorizeError>
1098where
1099    T: TensorElement + MatrixScalar + Copy,
1100{
1101    let values = tensor
1102        .value()
1103        .map_err(|source| FactorizeError::ComputationError(anyhow::Error::new(source)))?
1104        .as_slice::<T>()
1105        .map_err(|source| FactorizeError::ComputationError(anyhow::Error::new(source)))?
1106        .to_vec();
1107    matrix_from_col_major_values(values, m, n, "eager")
1108}
1109
1110fn matrix_from_col_major_values<T>(
1111    data: Vec<T>,
1112    m: usize,
1113    n: usize,
1114    source: &str,
1115) -> Result<Matrix<T>, FactorizeError>
1116where
1117    T: MatrixScalar + Copy,
1118{
1119    Matrix::try_from_col_major_vec(m, n, data).map_err(|error| {
1120        FactorizeError::ComputationError(anyhow::anyhow!(
1121            "{source} matrix conversion failed for shape ({m}, {n}): {error}"
1122        ))
1123    })
1124}
1125
1126#[cfg(test)]
1127fn checked_matrix_len(m: usize, n: usize, source: &str) -> Result<usize, FactorizeError> {
1128    m.checked_mul(n).ok_or_else(|| {
1129        FactorizeError::ComputationError(anyhow::anyhow!(
1130            "{source} matrix shape ({m}, {n}) overflows usize"
1131        ))
1132    })
1133}
1134
1135#[cfg(test)]
1136mod tests;