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