Skip to main content

tensor4all_core/defaults/
svd.rs

1//! SVD decomposition for tensors.
2//!
3//! Provides [`svd`] and [`svd_with`] for computing truncated SVD of
4//! [`IdxTensor`] values. The tensor is unfolded into a matrix by
5//! splitting its indices into left and right groups, then the standard
6//! matrix SVD is computed and truncated according to [`SvdOptions`].
7//!
8//! This module works with concrete types (`DynIndex`, `IdxTensor`) only.
9
10use super::eager_session::with_eager_session;
11use crate::defaults::idx_tensor::{unfold_index_sets, unfold_split_inner, unfold_split_inner_in};
12use crate::defaults::DynIndex;
13use crate::index_like::IndexLike;
14use crate::truncation::{
15    validate_svd_truncation_options, validate_svd_truncation_policy, SingularValueMeasure,
16    SvdTruncationOptionsError, SvdTruncationPolicy, ThresholdScale, TruncationRule,
17};
18use crate::{ExecutionContext, IdxTensor, TensorElement};
19use num_complex::{Complex32, Complex64, ComplexFloat};
20use std::sync::Mutex;
21use tenferro::{DType, TensorScalar};
22use tenferro_ad::EagerTensor;
23use tenferro_linalg::EagerSessionLinalgExt;
24#[cfg(test)]
25use tensor4all_tensorbackend::native_tensor_primal_to_dense_col_major;
26use thiserror::Error;
27
28/// Error type for SVD operations in tensor4all-linalg.
29#[derive(Debug, Error)]
30pub enum SvdError {
31    /// SVD computation failed.
32    #[error("SVD computation failed: {0}")]
33    ComputationError(#[from] anyhow::Error),
34    /// Invalid truncation threshold value (must be finite and non-negative).
35    #[error("Invalid SVD truncation threshold: {0}. Threshold must be finite and non-negative.")]
36    InvalidThreshold(f64),
37    /// `max_bond_dim` is set to zero.
38    #[error("max_bond_dim must be at least 1 when set")]
39    InvalidMaxBondDim,
40    /// The operand storage cannot enter the runtime-free explicit-session route.
41    ///
42    /// The session routes publish plain host factors, so an AD-tracked operand would lose its graph;
43    /// they refuse it instead.
44    #[error("Unsupported storage type: {0}")]
45    UnsupportedStorage(
46        /// Description of the unsupported operand storage
47        &'static str,
48    ),
49}
50
51/// Options for SVD decomposition with truncation control.
52///
53/// # Examples
54///
55/// ```
56/// use tensor4all_core::svd::{SvdOptions, svd_with};
57/// use tensor4all_core::{DynIndex, SvdTruncationPolicy, IdxTensor};
58///
59/// let i = DynIndex::new_dyn(3);
60/// let j = DynIndex::new_dyn(3);
61/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
62/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
63///
64/// let opts = SvdOptions::new().with_policy(SvdTruncationPolicy::new(1e-10));
65/// let (u, s, v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
66///
67/// // U has left index + bond, S is diagonal bond x bond, V has right index + bond
68/// assert_eq!(u.dims()[0], 3);
69/// assert_eq!(s.dims().len(), 2);
70/// ```
71#[derive(Debug, Clone, Copy)]
72pub struct SvdOptions {
73    /// Maximum retained rank after policy-based truncation.
74    pub max_bond_dim: Option<usize>,
75    /// Per-call SVD truncation policy.
76    /// If `None`, the global default policy is used.
77    pub policy: Option<SvdTruncationPolicy>,
78    truncate: bool,
79}
80
81impl Default for SvdOptions {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87impl SvdOptions {
88    /// Create new SVD options with no overrides.
89    #[must_use]
90    pub fn new() -> Self {
91        Self {
92            max_bond_dim: None,
93            policy: None,
94            truncate: true,
95        }
96    }
97
98    /// Set the maximum retained rank.
99    #[must_use]
100    pub fn with_max_bond_dim(mut self, max_bond_dim: usize) -> Self {
101        self.max_bond_dim = Some(max_bond_dim);
102        self
103    }
104
105    /// Set the SVD truncation policy override.
106    #[must_use]
107    pub fn with_policy(mut self, policy: SvdTruncationPolicy) -> Self {
108        self.policy = Some(policy);
109        self
110    }
111
112    pub(crate) fn full_rank() -> Self {
113        Self {
114            max_bond_dim: None,
115            policy: None,
116            truncate: false,
117        }
118    }
119}
120
121fn default_policy_guard() -> std::sync::MutexGuard<'static, SvdTruncationPolicy> {
122    match DEFAULT_SVD_TRUNCATION_POLICY.lock() {
123        Ok(guard) => guard,
124        Err(poisoned) => poisoned.into_inner(),
125    }
126}
127
128// Default value: relative per-value threshold 1e-12.
129static DEFAULT_SVD_TRUNCATION_POLICY: Mutex<SvdTruncationPolicy> =
130    Mutex::new(SvdTruncationPolicy::new(1e-12));
131
132/// Get the global default truncation policy for SVD.
133///
134/// The default policy is `SvdTruncationPolicy::new(1e-12)`.
135#[must_use]
136pub fn default_svd_truncation_policy() -> SvdTruncationPolicy {
137    *default_policy_guard()
138}
139
140/// Set the global default truncation policy for SVD.
141///
142/// # Arguments
143/// * `policy` - SVD truncation policy to use when `SvdOptions::policy` is `None`
144///
145/// # Errors
146/// Returns `SvdError::InvalidThreshold` if `policy.threshold` is invalid.
147pub fn set_default_svd_truncation_policy(policy: SvdTruncationPolicy) -> Result<(), SvdError> {
148    validate_svd_truncation_policy(policy).map_err(|e| SvdError::InvalidThreshold(e.0))?;
149    *default_policy_guard() = policy;
150    Ok(())
151}
152
153fn singular_value_measure(value: f64, measure: SingularValueMeasure) -> f64 {
154    match measure {
155        SingularValueMeasure::Value => value,
156        SingularValueMeasure::SquaredValue => value * value,
157    }
158}
159
160/// Compute the retained rank based on an explicit SVD truncation policy.
161///
162/// `s_vec` must be nonincreasing: the rank is a count of leading values. The SVD wrappers sort the
163/// backend output with [`descending_order`] first.
164pub(crate) fn compute_retained_rank(s_vec: &[f64], policy: &SvdTruncationPolicy) -> usize {
165    if s_vec.is_empty() {
166        return 1;
167    }
168
169    let measured: Vec<f64> = s_vec
170        .iter()
171        .map(|&value| singular_value_measure(value, policy.measure))
172        .collect();
173    if measured.iter().all(|&value| value == 0.0) {
174        return 1;
175    }
176
177    let retained = match (policy.scale, policy.rule) {
178        (ThresholdScale::Relative, TruncationRule::PerValue) => {
179            let reference = measured.iter().copied().fold(0.0_f64, f64::max);
180            measured
181                .iter()
182                .take_while(|&&value| reference > 0.0 && value / reference > policy.threshold)
183                .count()
184        }
185        (ThresholdScale::Absolute, TruncationRule::PerValue) => measured
186            .iter()
187            .take_while(|&&value| value > policy.threshold)
188            .count(),
189        (ThresholdScale::Relative, TruncationRule::DiscardedTailSum) => {
190            let total: f64 = measured.iter().sum();
191            if total == 0.0 {
192                1
193            } else {
194                let mut discarded = 0.0;
195                let mut keep = measured.len();
196                for (i, value) in measured.iter().enumerate().rev() {
197                    if (discarded + value) / total <= policy.threshold {
198                        discarded += value;
199                        keep = i;
200                    } else {
201                        break;
202                    }
203                }
204                keep
205            }
206        }
207        (ThresholdScale::Absolute, TruncationRule::DiscardedTailSum) => {
208            let mut discarded = 0.0;
209            let mut keep = measured.len();
210            for (i, value) in measured.iter().enumerate().rev() {
211                if discarded + value <= policy.threshold {
212                    discarded += value;
213                    keep = i;
214                } else {
215                    break;
216                }
217            }
218            keep
219        }
220    };
221
222    retained.max(1)
223}
224
225#[cfg(test)]
226fn singular_values_from_native(tensor: &tenferro::Tensor) -> Result<Vec<f64>, SvdError> {
227    match tensor.dtype() {
228        DType::F32 => native_tensor_primal_to_dense_col_major::<f32>(tensor)
229            .map(|values| values.into_iter().map(f64::from).collect())
230            .map_err(|e| SvdError::ComputationError(e.source)),
231        DType::F64 => native_tensor_primal_to_dense_col_major::<f64>(tensor)
232            .map_err(|e| SvdError::ComputationError(e.source)),
233        DType::C32 => native_tensor_primal_to_dense_col_major::<Complex32>(tensor)
234            .map(|values| {
235                values
236                    .into_iter()
237                    .map(|value| f64::from(value.re))
238                    .collect()
239            })
240            .map_err(|e| SvdError::ComputationError(e.source)),
241        DType::C64 => native_tensor_primal_to_dense_col_major::<Complex64>(tensor)
242            .map(|values| values.into_iter().map(|value| value.re).collect())
243            .map_err(|e| SvdError::ComputationError(e.source)),
244        other => Err(SvdError::ComputationError(anyhow::anyhow!(
245            "native SVD returned unsupported singular-value scalar type {other:?}"
246        ))),
247    }
248}
249
250fn singular_values_from_eager(tensor: &EagerTensor) -> Result<Vec<f64>, SvdError> {
251    let value = tensor
252        .value()
253        .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source)))?;
254    match tensor.dtype() {
255        DType::F32 => value
256            .as_slice::<f32>()
257            .map(|values| values.iter().map(|&value| f64::from(value)).collect())
258            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
259        DType::F64 => value
260            .as_slice::<f64>()
261            .map(|values| values.to_vec())
262            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
263        DType::C32 => value
264            .as_slice::<Complex32>()
265            .map(|values| values.iter().map(|value| f64::from(value.re)).collect())
266            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
267        DType::C64 => value
268            .as_slice::<Complex64>()
269            .map(|values| values.iter().map(|value| value.re).collect())
270            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
271        other => Err(SvdError::ComputationError(anyhow::anyhow!(
272            "eager SVD returned unsupported singular-value scalar type {other:?}"
273        ))),
274    }
275}
276
277/// Read the singular-value decision vector of a resident factor.
278///
279/// The factor stays resident; only its values cross the explicit readback
280/// boundary through `context`.
281#[cfg(feature = "tenferro-cuda")]
282fn resident_singular_values(
283    s_inner: &EagerTensor,
284    context: &ExecutionContext,
285) -> Result<Vec<f64>, SvdError> {
286    let indices: Vec<DynIndex> = s_inner
287        .shape()
288        .iter()
289        .map(|&dim| DynIndex::new_dyn(dim))
290        .collect();
291    let decision =
292        IdxTensor::from_inner(indices, s_inner.clone()).map_err(SvdError::ComputationError)?;
293    decision
294        .read_decision_data(context)
295        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))
296}
297
298type SvdTruncatedEagerResult = (
299    EagerTensor,
300    EagerTensor,
301    EagerTensor,
302    Vec<f64>,
303    DynIndex,
304    Vec<DynIndex>,
305    Vec<DynIndex>,
306);
307
308fn svd_truncated_inner(
309    t: &IdxTensor,
310    left_inds: &[DynIndex],
311    options: &SvdOptions,
312) -> Result<SvdTruncatedEagerResult, SvdError> {
313    if t.is_cuda_resident() {
314        return Err(SvdError::ComputationError(anyhow::anyhow!(
315            "CUDA-resident input requires svd_with_in with the owning execution context"
316        )));
317    }
318    svd_truncated_inner_scoped(t, left_inds, options, None)
319}
320
321/// Compute truncated SVD in a caller-owned execution context.
322///
323/// The factorization and truncation slicing execute in `context`. With a CUDA
324/// context only the singular-value decision vector crosses the explicit
325/// readback boundary (via [`IdxTensor::read_decision_data`); with a CPU
326/// context the decision uses the same host read as [`svd_with`].
327///
328/// # Errors
329///
330/// Returns `SvdError` when the tensor does not belong to `context`, when the
331/// indices or options are invalid, or when the factorization or explicit
332/// decision readback fails.
333pub(crate) fn svd_truncated_inner_in(
334    t: &IdxTensor,
335    left_inds: &[DynIndex],
336    options: &SvdOptions,
337    context: &ExecutionContext,
338) -> Result<SvdTruncatedEagerResult, SvdError> {
339    t.validate_context(context)
340        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
341    svd_truncated_inner_scoped(t, left_inds, options, Some(context))
342}
343
344fn svd_truncated_inner_scoped(
345    t: &IdxTensor,
346    left_inds: &[DynIndex],
347    options: &SvdOptions,
348    context: Option<&ExecutionContext>,
349) -> Result<SvdTruncatedEagerResult, SvdError> {
350    let (matrix_inner, _, m, n, left_indices, right_indices) = match context {
351        Some(context) => unfold_split_inner_in(t, left_inds, context),
352        None => unfold_split_inner(t, left_inds),
353    }
354    .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))
355    .map_err(SvdError::ComputationError)?;
356    let k = m.min(n);
357
358    let (mut u_inner, mut s_inner, mut vt_inner) =
359        with_eager_session(matrix_inner.runtime(), context, |session| {
360            session.svd(&matrix_inner)
361        })
362        .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
363    #[cfg(feature = "tenferro-cuda")]
364    let s_full = if let Some(context) = context {
365        if matches!(context, ExecutionContext::Cuda(_)) {
366            resident_singular_values(&s_inner, context)?
367        } else {
368            singular_values_from_eager(&s_inner)?
369        }
370    } else {
371        singular_values_from_eager(&s_inner)?
372    };
373    #[cfg(not(feature = "tenferro-cuda"))]
374    let s_full = {
375        let _ = context;
376        singular_values_from_eager(&s_inner)?
377    };
378    let order = descending_order(&s_full);
379    let s_full = match &order {
380        Some(order) => order.iter().map(|&i| s_full[i]).collect(),
381        None => s_full,
382    };
383    let mut r = retained_svd_rank(&s_full, k, options)?;
384    r = r.min(s_full.len());
385    if r < k || order.is_some() {
386        let keep: Vec<usize> = match &order {
387            Some(order) => order[..r].to_vec(),
388            None => (0..r).collect(),
389        };
390        u_inner = with_eager_session(u_inner.runtime(), context, |session| {
391            session.take_axis(&u_inner, 1, &keep)
392        })
393        .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
394        s_inner = with_eager_session(s_inner.runtime(), context, |session| {
395            session.take_axis(&s_inner, 0, &keep)
396        })
397        .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
398        vt_inner = with_eager_session(vt_inner.runtime(), context, |session| {
399            session.take_axis(&vt_inner, 0, &keep)
400        })
401        .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
402    }
403
404    let bond_index = DynIndex::new_bond(r)
405        .map_err(|e| anyhow::anyhow!("Failed to create Link index: {:?}", e))
406        .map_err(SvdError::ComputationError)?;
407    let singular_values = s_full[..r].to_vec();
408
409    Ok((
410        u_inner,
411        s_inner,
412        vt_inner,
413        singular_values,
414        bond_index,
415        left_indices,
416        right_indices,
417    ))
418}
419
420/// Compute SVD decomposition of a tensor with arbitrary rank, returning (U, S, V).
421///
422/// # Errors
423///
424/// Returns [`SvdError::ComputationError`] when `left_inds` is not a non-empty
425/// proper subset of the tensor's indices, when the input is CUDA-resident (use
426/// [`svd_with_in`]), or when the backend SVD fails.
427///
428/// # Examples
429///
430/// ```
431/// use tensor4all_core::{IdxTensor, DynIndex, svd};
432///
433/// // Create a 2x3 matrix (rank-1 outer product: all-ones)
434/// let i = DynIndex::new_dyn(2);
435/// let j = DynIndex::new_dyn(3);
436/// let data = vec![1.0_f64; 6]; // all-ones 2x3 matrix
437/// let t = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
438///
439/// let (u, s, v) = svd::<f64>(&t, &[i.clone()]).unwrap();
440///
441/// // U: shape (left_dim, bond) = (2, bond)
442/// assert_eq!(u.dims()[0], 2);
443/// // V: shape (right_dim, bond) = (3, bond)
444/// assert_eq!(v.dims()[0], 3);
445/// // S is a diagonal matrix (bond × bond)
446/// assert_eq!(s.dims().len(), 2);
447/// ```
448pub fn svd<T>(
449    t: &IdxTensor,
450    left_inds: &[DynIndex],
451) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
452    svd_with::<T>(t, left_inds, &SvdOptions::default())
453}
454
455/// Compute SVD decomposition of a tensor with arbitrary rank, returning (U, S, V).
456///
457/// This function allows per-call control of the truncation policy via `SvdOptions`.
458/// If `options.policy` is `None`, it uses the global default policy.
459///
460/// # Errors
461///
462/// Returns [`SvdError::InvalidMaxBondDim`] when `options.max_bond_dim` is
463/// `Some(0)`, [`SvdError::InvalidThreshold`] when the truncation threshold is
464/// negative or non-finite, and [`SvdError::ComputationError`] when `left_inds`
465/// is not a non-empty proper subset of the tensor's indices, when the input is
466/// CUDA-resident, or when the backend SVD fails.
467///
468/// # Examples
469///
470/// ```
471/// use tensor4all_core::{DynIndex, IdxTensor};
472/// use tensor4all_core::svd::{SvdOptions, svd_with};
473///
474/// let i = DynIndex::new_dyn(4);
475/// let j = DynIndex::new_dyn(4);
476/// // Rank-1 matrix
477/// let mut data = vec![0.0_f64; 16];
478/// data[0] = 1.0;
479/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
480///
481/// use tensor4all_core::SvdTruncationPolicy;
482///
483/// // Truncate with a relative per-value threshold => rank 1
484/// let opts = SvdOptions::new().with_policy(SvdTruncationPolicy::new(1e-10));
485/// let (u, s, _v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
486/// assert_eq!(s.dims()[0], 1);  // rank-1
487///
488/// // Truncate with max_bond_dim => capped
489/// let opts = SvdOptions::new().with_max_bond_dim(2);
490/// let (_u, s, _v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
491/// assert!(s.dims()[0] <= 2);
492/// ```
493pub fn svd_with<T>(
494    t: &IdxTensor,
495    left_inds: &[DynIndex],
496    options: &SvdOptions,
497) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
498    let (u_inner, s_inner, vt_inner, _singular_values, bond_index, left_indices, right_indices) =
499        svd_truncated_inner(t, left_inds, options)?;
500
501    svd_assemble(
502        u_inner,
503        s_inner,
504        vt_inner,
505        bond_index,
506        left_indices,
507        right_indices,
508        None,
509    )
510}
511
512/// Compute SVD decomposition in a caller-owned execution context.
513///
514/// The factorization, truncation slicing, and result assembly all execute in
515/// `context`. With a CUDA context only the singular-value decision vector
516/// crosses the explicit readback boundary; with a CPU context the decision
517/// uses the same host read as [`svd_with`].
518///
519/// # Arguments
520///
521/// * `t` - Input tensor, which must belong to `context`.
522/// * `left_inds` - Indices to place on the left (row) side of the unfolded matrix.
523/// * `options` - SVD options including truncation policy and bond cap.
524/// * `context` - Caller-owned execution context owning the input and results.
525///
526/// # Examples
527///
528/// ```
529/// use std::sync::Arc;
530/// use tensor4all_core::svd::{SvdOptions, svd_with_in};
531/// use tensor4all_core::{DynIndex, ExecutionContext, IdxTensor, SvdTruncationPolicy};
532/// use tensor4all_tensorbackend::CpuExecutionContext;
533/// use tensor4all_tensorbackend::CpuBackend;
534///
535/// let context = ExecutionContext::Cpu(Arc::new(
536///     CpuExecutionContext::from_backend(CpuBackend::new()),
537/// ));
538/// let i = DynIndex::new_dyn(4);
539/// let j = DynIndex::new_dyn(4);
540/// let mut data = vec![0.0_f64; 16];
541/// data[0] = 1.0;
542/// let tensor = IdxTensor::from_dense_in(&context, vec![i.clone(), j.clone()], data)?;
543/// let opts = SvdOptions::new().with_policy(SvdTruncationPolicy::new(1e-10));
544/// let (u, s, _v) = svd_with_in::<f64>(&tensor, &[i.clone()], &opts, &context)?;
545/// assert_eq!(s.dims()[0], 1);
546/// assert_eq!(u.dims()[0], 4);
547/// # Ok::<(), Box<dyn std::error::Error>>(())
548/// ```
549///
550/// # Errors
551///
552/// Returns `SvdError` when the tensor does not belong to `context`, when the
553/// indices or options are invalid, or when the factorization or explicit
554/// decision readback fails. When this build cannot execute in `context`, the
555/// rejection carries
556/// [`IdxTensorError::UnsupportedExecutionContext`](crate::IdxTensorError::UnsupportedExecutionContext)
557/// as its source.
558pub fn svd_with_in<T>(
559    t: &IdxTensor,
560    left_inds: &[DynIndex],
561    options: &SvdOptions,
562    context: &ExecutionContext,
563) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
564    let (u_inner, s_inner, vt_inner, _singular_values, bond_index, left_indices, right_indices) =
565        svd_truncated_inner_in(t, left_inds, options, context)?;
566
567    svd_assemble(
568        u_inner,
569        s_inner,
570        vt_inner,
571        bond_index,
572        left_indices,
573        right_indices,
574        Some(context),
575    )
576}
577
578/// Wrap the truncated eager factors as [`IdxTensor`]s.
579fn svd_assemble(
580    u_inner: EagerTensor,
581    s_inner: EagerTensor,
582    vt_inner: EagerTensor,
583    bond_index: DynIndex,
584    left_indices: Vec<DynIndex>,
585    right_indices: Vec<DynIndex>,
586    context: Option<&ExecutionContext>,
587) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
588    let mut u_indices = left_indices;
589    u_indices.push(bond_index.clone());
590    let u_dims: Vec<usize> = u_indices.iter().map(|idx| idx.dim).collect();
591    let u_reshaped = with_eager_session(u_inner.runtime(), context, |session| {
592        session.reshape(&u_inner, &u_dims)
593    })
594    .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("eager SVD U reshape failed: {e}")))?;
595    let u = IdxTensor::from_inner(u_indices, u_reshaped).map_err(SvdError::ComputationError)?;
596
597    // S carries a fresh `sim` leg (ITensors convention S: [l, l'], V: l');
598    // the SAME sim instance is shared with V^H so the returned triple
599    // reconstructs under plain contraction:
600    // U [left..., bond] · S [bond, sim] · V^H [sim, right...].
601    let sim_bond_index = bond_index.sim();
602    let s_indices = vec![bond_index.clone(), sim_bond_index.clone()];
603    let s = IdxTensor::from_diag_inner(s_indices, s_inner).map_err(SvdError::ComputationError)?;
604
605    let mut vh_indices = vec![sim_bond_index];
606    vh_indices.extend(right_indices);
607    let vh_dims: Vec<usize> = vh_indices.iter().map(|idx| idx.dim).collect();
608    let vt_reshaped = with_eager_session(vt_inner.runtime(), context, |session| {
609        session.reshape(&vt_inner, &vh_dims)
610    })
611    .map_err(|e| {
612        SvdError::ComputationError(anyhow::anyhow!("eager SVD V^T reshape failed: {e}"))
613    })?;
614    let vh = IdxTensor::from_inner(vh_indices, vt_reshaped).map_err(SvdError::ComputationError)?;
615    let perm: Vec<usize> = (1..vh.indices.len()).chain(std::iter::once(0)).collect();
616    let v = vh
617        .conj()
618        .permute(&perm)
619        .map_err(|e| SvdError::ComputationError(anyhow::Error::new(e)))?;
620
621    Ok((u, s, v))
622}
623
624/// SVD result for factorization, returning `V^H` directly.
625pub(crate) struct SvdFactorizeResult {
626    pub u: IdxTensor,
627    pub s: IdxTensor,
628    pub vh: IdxTensor,
629    pub bond_index: DynIndex,
630    pub singular_values: Vec<f64>,
631    pub rank: usize,
632}
633
634/// The permutation that sorts `singular_values` in nonincreasing order, or `None` when they
635/// already are.
636///
637/// The truncation rule and the prefix slicing of the factors assume nonincreasing singular values,
638/// as the Gram route guarantees by sorting its eigenpairs. A backend may return them out of order
639/// (tensor4all/faer#82), so the SVD wrappers reorder the values and the matching factor columns
640/// before the rule runs. The ordered case costs one pass and no allocation.
641fn descending_order(singular_values: &[f64]) -> Option<Vec<usize>> {
642    if singular_values.windows(2).all(|pair| pair[0] >= pair[1]) {
643        return None;
644    }
645    let mut order: Vec<usize> = (0..singular_values.len()).collect();
646    order.sort_by(|&left, &right| singular_values[right].total_cmp(&singular_values[left]));
647    Some(order)
648}
649
650/// The retained rank of one SVD, from its singular values.
651///
652/// This is the one place the SVD truncation policy is applied: the eager, context-scoped and
653/// explicit-session routes all funnel their singular values through it, so a policy change cannot
654/// diverge between the frontends.
655fn retained_svd_rank(
656    singular_values: &[f64],
657    k: usize,
658    options: &SvdOptions,
659) -> Result<usize, SvdError> {
660    if !options.truncate {
661        return Ok(k.max(1));
662    }
663    // Shared root guard: reject `max_bond_dim == Some(0)` and invalid
664    // explicit policies before any truncation, so the caller cannot
665    // silently clamp every factorization to rank one.
666    validate_svd_truncation_options(options.max_bond_dim, options.policy).map_err(|error| {
667        match error {
668            SvdTruncationOptionsError::ZeroMaxBondDim => SvdError::InvalidMaxBondDim,
669            SvdTruncationOptionsError::InvalidThreshold(threshold) => {
670                SvdError::InvalidThreshold(threshold)
671            }
672        }
673    })?;
674    let policy = options.policy.unwrap_or_else(default_svd_truncation_policy);
675    validate_svd_truncation_policy(policy).map_err(|e| SvdError::InvalidThreshold(e.0))?;
676    let mut retained = compute_retained_rank(singular_values, &policy);
677    if let Some(max_bond_dim) = options.max_bond_dim {
678        retained = retained.min(max_bond_dim);
679    }
680    Ok(retained.max(1).min(k))
681}
682
683/// Truncated SVD for factorization on a caller-held explicit session.
684///
685/// The runtime-free counterpart of [`svd_for_factorize`]: the index bookkeeping, the truncation
686/// policy and the factor layout are shared with the eager route, while the decomposition runs on
687/// `session` and the factors are published as plain host-storage [`IdxTensor`]s. No session or eager
688/// runtime is entered here.
689///
690/// # Errors
691///
692/// Returns [`SvdError::UnsupportedStorage`] for an AD-tracked operand (the plain host factors would
693/// drop its graph), [`SvdError::InvalidThreshold`]/[`SvdError::InvalidMaxBondDim`] for invalid
694/// truncation options, and [`SvdError::ComputationError`] when the split, the session decomposition or
695/// a factor's publication fails.
696pub(crate) fn svd_for_factorize_on_session<T>(
697    session: &mut tensor4all_tensorbackend::explicit::Session<'_>,
698    t: &IdxTensor,
699    left_inds: &[DynIndex],
700    options: &SvdOptions,
701) -> Result<SvdFactorizeResult, SvdError>
702where
703    T: TensorElement + ComplexFloat + tensor4all_tensorbackend::StorageScalar,
704    <T as ComplexFloat>::Real: Into<f64>,
705    <T as TensorScalar>::Real: Into<f64>,
706    T: num_traits::NumCast,
707{
708    if t.tracks_grad() {
709        return Err(SvdError::UnsupportedStorage(
710            crate::defaults::idx_tensor::TRACKED_SESSION_OPERAND_REFUSAL,
711        ));
712    }
713    let (left_indices, right_indices, order) =
714        unfold_index_sets(t, left_inds).map_err(|error| {
715            SvdError::ComputationError(anyhow::anyhow!("Failed to unfold tensor: {error}"))
716        })?;
717    // Read and reorder the operand's own host payload: entering the eager
718    // runtime for a transpose would be a second session entry on this thread.
719    let (dims, data) = t
720        .host_dense_permuted::<T>(&order)
721        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
722    let left_len = left_indices.len();
723    let m = crate::defaults::idx_tensor::checked_product(&dims[..left_len])
724        .map_err(SvdError::ComputationError)?;
725    let n = crate::defaults::idx_tensor::checked_product(&dims[left_len..])
726        .map_err(SvdError::ComputationError)?;
727    let k = m.min(n);
728    let matrix = tenferro::Tensor::from_vec_col_major(vec![m, n], data)
729        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
730    let (u_value, s_value, vt_value) = session
731        .svd(&matrix)
732        .map_err(|error| SvdError::ComputationError(anyhow::anyhow!("{error}")))?;
733    // The session returns singular values in the operand's own real dtype, so a
734    // narrow operand must not be read as `f64`.
735    let s_full: Vec<f64> = s_value
736        .as_slice::<<T as TensorScalar>::Real>()
737        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?
738        .iter()
739        .map(|&value| value.into())
740        .collect();
741    let order = descending_order(&s_full);
742    let s_full = match &order {
743        Some(order) => order.iter().map(|&i| s_full[i]).collect(),
744        None => s_full,
745    };
746    // Source column of `U` (and row of `V^H`) for each kept bond position.
747    let source = |position: usize| order.as_ref().map_or(position, |order| order[position]);
748    let rank = retained_svd_rank(&s_full, k, options)?;
749    let u_payload = u_value
750        .as_slice::<T>()
751        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
752    let vt_payload = vt_value
753        .as_slice::<T>()
754        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
755
756    // `U` is column-major `[m, k]`: kept columns are the bond axis of the left factor.
757    let mut u_data = Vec::with_capacity(m * rank);
758    for column in 0..rank {
759        let start = source(column).checked_mul(m).ok_or_else(|| {
760            SvdError::ComputationError(anyhow::anyhow!("SVD U column offset overflows usize"))
761        })?;
762        let end = start.checked_add(m).ok_or_else(|| {
763            SvdError::ComputationError(anyhow::anyhow!("SVD U column end overflows usize"))
764        })?;
765        u_data.extend_from_slice(u_payload.get(start..end).ok_or_else(|| {
766            SvdError::ComputationError(anyhow::anyhow!("SVD U column {column} is out of bounds"))
767        })?);
768    }
769    // `S` is real; it takes the factor dtype so the shared assembly keeps its fast contraction path.
770    let mut s_data = Vec::with_capacity(rank);
771    for &value in s_full.iter().take(rank) {
772        s_data.push(<T as num_traits::NumCast>::from(value).ok_or_else(|| {
773            SvdError::ComputationError(anyhow::anyhow!(
774                "singular value {value} does not fit the factor scalar type"
775            ))
776        })?);
777    }
778    // `V^H` is column-major `[k, n]`: kept rows are the bond axis of the right factor.
779    let mut vt_data = Vec::with_capacity(rank * n);
780    for column in 0..n {
781        let base = column.checked_mul(k).ok_or_else(|| {
782            SvdError::ComputationError(anyhow::anyhow!("SVD V^H column offset overflows usize"))
783        })?;
784        for row in 0..rank {
785            let value = vt_payload.get(base + source(row)).ok_or_else(|| {
786                SvdError::ComputationError(anyhow::anyhow!("SVD V^H row {row} is out of bounds"))
787            })?;
788            vt_data.push(*value);
789        }
790    }
791
792    let bond_index = DynIndex::new_bond(rank).map_err(|error| {
793        SvdError::ComputationError(anyhow::anyhow!("Failed to create Link index: {error:?}"))
794    })?;
795    let mut u_indices = left_indices;
796    u_indices.push(bond_index.clone());
797    let u = IdxTensor::from_host_dense(u_indices, u_data)
798        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
799    // The session route publishes S as a dense `[bond, sim]` diagonal matrix: the compact diagonal
800    // representation is an eager construction, and the session contraction route serves dense host
801    // operands only. The rank-squared payload is negligible next to the factors.
802    let mut s_dense = vec![
803        <T as num_traits::Zero>::zero();
804        rank.checked_mul(rank).ok_or_else(|| {
805            SvdError::ComputationError(anyhow::anyhow!("SVD S payload size overflows usize"))
806        })?
807    ];
808    for (position, value) in s_data.iter().enumerate() {
809        s_dense[position * (rank + 1)] = *value;
810    }
811    let s = IdxTensor::from_host_dense(vec![bond_index.clone(), bond_index.sim()], s_dense)
812        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
813    let mut vh_indices = vec![bond_index.clone()];
814    vh_indices.extend(right_indices);
815    let vh = IdxTensor::from_host_dense(vh_indices, vt_data)
816        .map_err(|error| SvdError::ComputationError(anyhow::Error::new(error)))?;
817
818    Ok(SvdFactorizeResult {
819        u,
820        s,
821        vh,
822        bond_index,
823        singular_values: s_full[..rank].to_vec(),
824        rank,
825    })
826}
827
828/// Compute truncated SVD for factorization, returning `V^H` instead of `V`.
829pub(crate) fn svd_for_factorize(
830    t: &IdxTensor,
831    left_inds: &[DynIndex],
832    options: &SvdOptions,
833) -> Result<SvdFactorizeResult, SvdError> {
834    let (u_inner, s_inner, vt_inner, singular_values, bond_index, left_indices, right_indices) =
835        svd_truncated_inner(t, left_inds, options)?;
836    svd_factorize_assemble(
837        (
838            u_inner,
839            s_inner,
840            vt_inner,
841            singular_values,
842            bond_index,
843            left_indices,
844            right_indices,
845        ),
846        None,
847    )
848}
849
850/// Compute truncated SVD for factorization in a caller-owned context.
851///
852/// Context-scoped counterpart of [`svd_for_factorize`]: the factorization and
853/// truncation slicing execute in `context`, with only the singular-value
854/// decision vector crossing the explicit readback boundary on CUDA.
855///
856/// # Errors
857///
858/// Returns `SvdError` when the tensor does not belong to `context`, when the
859/// indices or options are invalid, or when the factorization or explicit
860/// decision readback fails.
861pub(crate) fn svd_for_factorize_in(
862    t: &IdxTensor,
863    left_inds: &[DynIndex],
864    options: &SvdOptions,
865    context: &ExecutionContext,
866) -> Result<SvdFactorizeResult, SvdError> {
867    let (u_inner, s_inner, vt_inner, singular_values, bond_index, left_indices, right_indices) =
868        svd_truncated_inner_in(t, left_inds, options, context)?;
869    svd_factorize_assemble(
870        (
871            u_inner,
872            s_inner,
873            vt_inner,
874            singular_values,
875            bond_index,
876            left_indices,
877            right_indices,
878        ),
879        Some(context),
880    )
881}
882
883/// Wrap the truncated eager factors as [`IdxTensor`]s, returning `V^H` directly.
884fn svd_factorize_assemble(
885    (
886        u_inner,
887        s_inner,
888        vt_inner,
889        singular_values,
890        bond_index,
891        left_indices,
892        right_indices,
893    ): SvdTruncatedEagerResult,
894    context: Option<&ExecutionContext>,
895) -> Result<SvdFactorizeResult, SvdError> {
896    let rank = singular_values.len();
897    let mut u_indices = left_indices;
898    u_indices.push(bond_index.clone());
899    let u_dims: Vec<usize> = u_indices.iter().map(|idx| idx.dim).collect();
900    let u_reshaped = with_eager_session(u_inner.runtime(), context, |session| {
901        session.reshape(&u_inner, &u_dims)
902    })
903    .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("eager SVD U reshape failed: {e}")))?;
904    let u = IdxTensor::from_inner(u_indices, u_reshaped).map_err(SvdError::ComputationError)?;
905
906    // The singular values are real. Contracting a real `S` with a complex
907    // factor in `assemble_svd_factors` takes the mixed-dtype einsum path,
908    // which traces and compiles a program on every call; giving `S` the
909    // factors' dtype keeps that contraction on the cached path.
910    let s_inner = if s_inner.dtype() != vt_inner.dtype() {
911        with_eager_session(s_inner.runtime(), context, |session| {
912            session.convert(&s_inner, vt_inner.dtype())
913        })
914        .map_err(|e| {
915            SvdError::ComputationError(anyhow::anyhow!("eager SVD S dtype conversion failed: {e}"))
916        })?
917    } else {
918        s_inner
919    };
920    let s_indices = vec![bond_index.clone(), bond_index.sim()];
921    // `S` stays diagonal in its compact payload on both routes: absorbing it into
922    // a factor is then a structured diagonal scaling instead of a dense rank-by-
923    // rank matrix (which would cost quadratic storage and a general GEMM for a
924    // diagonal multiply). `assemble_svd_factors` contracts it through the
925    // selected engine; see `context_svd_factor_contraction`.
926    let s = IdxTensor::from_diag_inner(s_indices, s_inner).map_err(SvdError::ComputationError)?;
927
928    let mut vh_indices = vec![bond_index.clone()];
929    vh_indices.extend(right_indices);
930    let vh_dims: Vec<usize> = vh_indices.iter().map(|idx| idx.dim).collect();
931    let vt_reshaped = with_eager_session(vt_inner.runtime(), context, |session| {
932        session.reshape(&vt_inner, &vh_dims)
933    })
934    .map_err(|e| {
935        SvdError::ComputationError(anyhow::anyhow!("eager SVD V^T reshape failed: {e}"))
936    })?;
937    let vh = IdxTensor::from_inner(vh_indices, vt_reshaped).map_err(SvdError::ComputationError)?;
938
939    Ok(SvdFactorizeResult {
940        u,
941        s,
942        vh,
943        bond_index,
944        singular_values,
945        rank,
946    })
947}
948
949#[cfg(test)]
950mod tests;