1use 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
59pub use crate::tensor_like::{
61 Canonical, FactorizeAlg, FactorizeError, FactorizeOptions, FactorizeResult,
62};
63
64pub 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
126pub(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 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
369pub 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
446pub 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
524pub 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
610fn 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
689fn 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
698pub 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
786pub 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
922fn 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
1029fn 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
1065fn 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 let sim_bond_index = s.indices[1].clone();
1085
1086 match canonical {
1087 Canonical::Left => {
1088 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 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
1118fn 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 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 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
1175fn 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 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 let a_matrix = native_tensor_to_matrix::<T>(&a_tensor, m, n)?;
1241
1242 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 let mut a_matrix = a_matrix;
1253 let lu = rrlu_mut(&mut a_matrix, Some(lu_options))?;
1254 let rank = lu.npivots();
1255
1256 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 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 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
1287fn 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
1330fn 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 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 let a_matrix = eager_tensor_to_matrix::<T>(&matrix_inner, m, n)?;
1364
1365 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 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
1392fn 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
1439fn 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;