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 crate::defaults::idx_tensor::unfold_split_inner;
11use crate::defaults::DynIndex;
12use crate::index_like::IndexLike;
13use crate::truncation::{
14    validate_svd_truncation_options, validate_svd_truncation_policy, SingularValueMeasure,
15    SvdTruncationOptionsError, SvdTruncationPolicy, ThresholdScale, TruncationRule,
16};
17use crate::IdxTensor;
18use num_complex::Complex64;
19use std::sync::Mutex;
20use tenferro::DType;
21use tenferro_ad::EagerTensor;
22use tenferro_linalg::EagerTensorLinalgExt;
23#[cfg(test)]
24use tensor4all_tensorbackend::native_tensor_primal_to_dense_col_major;
25use thiserror::Error;
26
27/// Error type for SVD operations in tensor4all-linalg.
28#[derive(Debug, Error)]
29pub enum SvdError {
30    /// SVD computation failed.
31    #[error("SVD computation failed: {0}")]
32    ComputationError(#[from] anyhow::Error),
33    /// Invalid truncation threshold value (must be finite and non-negative).
34    #[error("Invalid SVD truncation threshold: {0}. Threshold must be finite and non-negative.")]
35    InvalidThreshold(f64),
36    /// `max_bond_dim` is set to zero.
37    #[error("max_bond_dim must be at least 1 when set")]
38    InvalidMaxBondDim,
39}
40
41/// Options for SVD decomposition with truncation control.
42///
43/// # Examples
44///
45/// ```
46/// use tensor4all_core::svd::{SvdOptions, svd_with};
47/// use tensor4all_core::{DynIndex, SvdTruncationPolicy, IdxTensor};
48///
49/// let i = DynIndex::new_dyn(3);
50/// let j = DynIndex::new_dyn(3);
51/// let data: Vec<f64> = (0..9).map(|x| x as f64).collect();
52/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
53///
54/// let opts = SvdOptions::new().with_policy(SvdTruncationPolicy::new(1e-10));
55/// let (u, s, v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
56///
57/// // U has left index + bond, S is diagonal bond x bond, V has right index + bond
58/// assert_eq!(u.dims()[0], 3);
59/// assert_eq!(s.dims().len(), 2);
60/// ```
61#[derive(Debug, Clone, Copy)]
62pub struct SvdOptions {
63    /// Maximum retained rank after policy-based truncation.
64    pub max_bond_dim: Option<usize>,
65    /// Per-call SVD truncation policy.
66    /// If `None`, the global default policy is used.
67    pub policy: Option<SvdTruncationPolicy>,
68    truncate: bool,
69}
70
71impl Default for SvdOptions {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl SvdOptions {
78    /// Create new SVD options with no overrides.
79    #[must_use]
80    pub fn new() -> Self {
81        Self {
82            max_bond_dim: None,
83            policy: None,
84            truncate: true,
85        }
86    }
87
88    /// Set the maximum retained rank.
89    #[must_use]
90    pub fn with_max_bond_dim(mut self, max_bond_dim: usize) -> Self {
91        self.max_bond_dim = Some(max_bond_dim);
92        self
93    }
94
95    /// Set the SVD truncation policy override.
96    #[must_use]
97    pub fn with_policy(mut self, policy: SvdTruncationPolicy) -> Self {
98        self.policy = Some(policy);
99        self
100    }
101
102    pub(crate) fn full_rank() -> Self {
103        Self {
104            max_bond_dim: None,
105            policy: None,
106            truncate: false,
107        }
108    }
109}
110
111fn default_policy_guard() -> std::sync::MutexGuard<'static, SvdTruncationPolicy> {
112    match DEFAULT_SVD_TRUNCATION_POLICY.lock() {
113        Ok(guard) => guard,
114        Err(poisoned) => poisoned.into_inner(),
115    }
116}
117
118// Default value: relative per-value threshold 1e-12.
119static DEFAULT_SVD_TRUNCATION_POLICY: Mutex<SvdTruncationPolicy> =
120    Mutex::new(SvdTruncationPolicy::new(1e-12));
121
122/// Get the global default truncation policy for SVD.
123///
124/// The default policy is `SvdTruncationPolicy::new(1e-12)`.
125#[must_use]
126pub fn default_svd_truncation_policy() -> SvdTruncationPolicy {
127    *default_policy_guard()
128}
129
130/// Set the global default truncation policy for SVD.
131///
132/// # Arguments
133/// * `policy` - SVD truncation policy to use when `SvdOptions::policy` is `None`
134///
135/// # Errors
136/// Returns `SvdError::InvalidThreshold` if `policy.threshold` is invalid.
137pub fn set_default_svd_truncation_policy(policy: SvdTruncationPolicy) -> Result<(), SvdError> {
138    validate_svd_truncation_policy(policy).map_err(|e| SvdError::InvalidThreshold(e.0))?;
139    *default_policy_guard() = policy;
140    Ok(())
141}
142
143fn singular_value_measure(value: f64, measure: SingularValueMeasure) -> f64 {
144    match measure {
145        SingularValueMeasure::Value => value,
146        SingularValueMeasure::SquaredValue => value * value,
147    }
148}
149
150/// Compute the retained rank based on an explicit SVD truncation policy.
151pub(crate) fn compute_retained_rank(s_vec: &[f64], policy: &SvdTruncationPolicy) -> usize {
152    if s_vec.is_empty() {
153        return 1;
154    }
155
156    let measured: Vec<f64> = s_vec
157        .iter()
158        .map(|&value| singular_value_measure(value, policy.measure))
159        .collect();
160    if measured.iter().all(|&value| value == 0.0) {
161        return 1;
162    }
163
164    let retained = match (policy.scale, policy.rule) {
165        (ThresholdScale::Relative, TruncationRule::PerValue) => {
166            let reference = measured.iter().copied().fold(0.0_f64, f64::max);
167            measured
168                .iter()
169                .take_while(|&&value| reference > 0.0 && value / reference > policy.threshold)
170                .count()
171        }
172        (ThresholdScale::Absolute, TruncationRule::PerValue) => measured
173            .iter()
174            .take_while(|&&value| value > policy.threshold)
175            .count(),
176        (ThresholdScale::Relative, TruncationRule::DiscardedTailSum) => {
177            let total: f64 = measured.iter().sum();
178            if total == 0.0 {
179                1
180            } else {
181                let mut discarded = 0.0;
182                let mut keep = measured.len();
183                for (i, value) in measured.iter().enumerate().rev() {
184                    if (discarded + value) / total <= policy.threshold {
185                        discarded += value;
186                        keep = i;
187                    } else {
188                        break;
189                    }
190                }
191                keep
192            }
193        }
194        (ThresholdScale::Absolute, TruncationRule::DiscardedTailSum) => {
195            let mut discarded = 0.0;
196            let mut keep = measured.len();
197            for (i, value) in measured.iter().enumerate().rev() {
198                if discarded + value <= policy.threshold {
199                    discarded += value;
200                    keep = i;
201                } else {
202                    break;
203                }
204            }
205            keep
206        }
207    };
208
209    retained.max(1)
210}
211
212#[cfg(test)]
213fn singular_values_from_native(tensor: &tenferro::Tensor) -> Result<Vec<f64>, SvdError> {
214    match tensor.dtype() {
215        DType::F64 => native_tensor_primal_to_dense_col_major::<f64>(tensor)
216            .map_err(|e| SvdError::ComputationError(e.source)),
217        DType::C64 => native_tensor_primal_to_dense_col_major::<Complex64>(tensor)
218            .map(|values| values.into_iter().map(|value| value.re).collect())
219            .map_err(|e| SvdError::ComputationError(e.source)),
220        other => Err(SvdError::ComputationError(anyhow::anyhow!(
221            "native SVD returned unsupported singular-value scalar type {other:?}"
222        ))),
223    }
224}
225
226fn singular_values_from_eager(tensor: &EagerTensor) -> Result<Vec<f64>, SvdError> {
227    let value = tensor
228        .value()
229        .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source)))?;
230    match tensor.dtype() {
231        DType::F64 => value
232            .as_slice::<f64>()
233            .map(|values| values.to_vec())
234            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
235        DType::C64 => value
236            .as_slice::<Complex64>()
237            .map(|values| values.iter().map(|value| value.re).collect())
238            .map_err(|source| SvdError::ComputationError(anyhow::Error::new(source))),
239        other => Err(SvdError::ComputationError(anyhow::anyhow!(
240            "eager SVD returned unsupported singular-value scalar type {other:?}"
241        ))),
242    }
243}
244
245type SvdTruncatedEagerResult = (
246    EagerTensor,
247    EagerTensor,
248    EagerTensor,
249    Vec<f64>,
250    DynIndex,
251    Vec<DynIndex>,
252    Vec<DynIndex>,
253);
254
255fn svd_truncated_inner(
256    t: &IdxTensor,
257    left_inds: &[DynIndex],
258    options: &SvdOptions,
259) -> Result<SvdTruncatedEagerResult, SvdError> {
260    let (matrix_inner, _, m, n, left_indices, right_indices) = unfold_split_inner(t, left_inds)
261        .map_err(|e| anyhow::anyhow!("Failed to unfold tensor: {}", e))
262        .map_err(SvdError::ComputationError)?;
263    let k = m.min(n);
264
265    let (mut u_inner, mut s_inner, mut vt_inner) = matrix_inner
266        .svd()
267        .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
268    let s_full = singular_values_from_eager(&s_inner)?;
269    let mut r = if options.truncate {
270        // Shared root guard: reject `max_bond_dim == Some(0)` and invalid
271        // explicit policies before any truncation, so the caller cannot
272        // silently clamp every factorization to rank one.
273        validate_svd_truncation_options(options.max_bond_dim, options.policy).map_err(|error| {
274            match error {
275                SvdTruncationOptionsError::ZeroMaxBondDim => SvdError::InvalidMaxBondDim,
276                SvdTruncationOptionsError::InvalidThreshold(threshold) => {
277                    SvdError::InvalidThreshold(threshold)
278                }
279            }
280        })?;
281        let policy = options.policy.unwrap_or_else(default_svd_truncation_policy);
282        validate_svd_truncation_policy(policy).map_err(|e| SvdError::InvalidThreshold(e.0))?;
283
284        let mut retained = compute_retained_rank(&s_full, &policy);
285        if let Some(max_bond_dim) = options.max_bond_dim {
286            retained = retained.min(max_bond_dim);
287        }
288        retained.max(1)
289    } else {
290        k.max(1)
291    };
292    r = r.min(s_full.len());
293    if r < k {
294        let keep: Vec<usize> = (0..r).collect();
295        u_inner = u_inner
296            .take_axis(1, &keep)
297            .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
298        s_inner = s_inner
299            .take_axis(0, &keep)
300            .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
301        vt_inner = vt_inner
302            .take_axis(0, &keep)
303            .map_err(|e| SvdError::ComputationError(anyhow::anyhow!("{e}")))?;
304    }
305
306    let bond_index = DynIndex::new_bond(r)
307        .map_err(|e| anyhow::anyhow!("Failed to create Link index: {:?}", e))
308        .map_err(SvdError::ComputationError)?;
309    let singular_values = s_full[..r].to_vec();
310
311    Ok((
312        u_inner,
313        s_inner,
314        vt_inner,
315        singular_values,
316        bond_index,
317        left_indices,
318        right_indices,
319    ))
320}
321
322/// Compute SVD decomposition of a tensor with arbitrary rank, returning (U, S, V).
323///
324/// # Errors
325///
326/// Returns an error when the operation fails (a shape or index mismatch, or
327/// /// a backend failure).
328///
329/// # Examples
330///
331/// ```
332/// use tensor4all_core::{IdxTensor, DynIndex, svd};
333///
334/// // Create a 2x3 matrix (rank-1 outer product: all-ones)
335/// let i = DynIndex::new_dyn(2);
336/// let j = DynIndex::new_dyn(3);
337/// let data = vec![1.0_f64; 6]; // all-ones 2x3 matrix
338/// let t = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
339///
340/// let (u, s, v) = svd::<f64>(&t, &[i.clone()]).unwrap();
341///
342/// // U: shape (left_dim, bond) = (2, bond)
343/// assert_eq!(u.dims()[0], 2);
344/// // V: shape (right_dim, bond) = (3, bond)
345/// assert_eq!(v.dims()[0], 3);
346/// // S is a diagonal matrix (bond × bond)
347/// assert_eq!(s.dims().len(), 2);
348/// ```
349pub fn svd<T>(
350    t: &IdxTensor,
351    left_inds: &[DynIndex],
352) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
353    svd_with::<T>(t, left_inds, &SvdOptions::default())
354}
355
356/// Compute SVD decomposition of a tensor with arbitrary rank, returning (U, S, V).
357///
358/// This function allows per-call control of the truncation policy via `SvdOptions`.
359/// If `options.policy` is `None`, it uses the global default policy.
360///
361/// # Errors
362///
363/// Returns an error when the operation fails (a shape or index mismatch, or
364/// /// a backend failure).
365///
366/// # Examples
367///
368/// ```
369/// use tensor4all_core::{DynIndex, IdxTensor};
370/// use tensor4all_core::svd::{SvdOptions, svd_with};
371///
372/// let i = DynIndex::new_dyn(4);
373/// let j = DynIndex::new_dyn(4);
374/// // Rank-1 matrix
375/// let mut data = vec![0.0_f64; 16];
376/// data[0] = 1.0;
377/// let tensor = IdxTensor::from_dense(vec![i.clone(), j.clone()], data).unwrap();
378///
379/// use tensor4all_core::SvdTruncationPolicy;
380///
381/// // Truncate with a relative per-value threshold => rank 1
382/// let opts = SvdOptions::new().with_policy(SvdTruncationPolicy::new(1e-10));
383/// let (u, s, _v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
384/// assert_eq!(s.dims()[0], 1);  // rank-1
385///
386/// // Truncate with max_bond_dim => capped
387/// let opts = SvdOptions::new().with_max_bond_dim(2);
388/// let (_u, s, _v) = svd_with::<f64>(&tensor, &[i.clone()], &opts).unwrap();
389/// assert!(s.dims()[0] <= 2);
390/// ```
391pub fn svd_with<T>(
392    t: &IdxTensor,
393    left_inds: &[DynIndex],
394    options: &SvdOptions,
395) -> Result<(IdxTensor, IdxTensor, IdxTensor), SvdError> {
396    let (u_inner, s_inner, vt_inner, _singular_values, bond_index, left_indices, right_indices) =
397        svd_truncated_inner(t, left_inds, options)?;
398
399    let mut u_indices = left_indices;
400    u_indices.push(bond_index.clone());
401    let u_dims: Vec<usize> = u_indices.iter().map(|idx| idx.dim).collect();
402    let u_reshaped = u_inner.reshape(&u_dims).map_err(|e| {
403        SvdError::ComputationError(anyhow::anyhow!("eager SVD U reshape failed: {e}"))
404    })?;
405    let u = IdxTensor::from_inner(u_indices, u_reshaped).map_err(SvdError::ComputationError)?;
406
407    // S carries a fresh `sim` leg (ITensors convention S: [l, l'], V: l');
408    // the SAME sim instance is shared with V^H so the returned triple
409    // reconstructs under plain contraction:
410    // U [left..., bond] · S [bond, sim] · V^H [sim, right...].
411    let sim_bond_index = bond_index.sim();
412    let s_indices = vec![bond_index.clone(), sim_bond_index.clone()];
413    let s = IdxTensor::from_diag_inner(s_indices, s_inner).map_err(SvdError::ComputationError)?;
414
415    let mut vh_indices = vec![sim_bond_index];
416    vh_indices.extend(right_indices);
417    let vh_dims: Vec<usize> = vh_indices.iter().map(|idx| idx.dim).collect();
418    let vt_reshaped = vt_inner.reshape(&vh_dims).map_err(|e| {
419        SvdError::ComputationError(anyhow::anyhow!("eager SVD V^T reshape failed: {e}"))
420    })?;
421    let vh = IdxTensor::from_inner(vh_indices, vt_reshaped).map_err(SvdError::ComputationError)?;
422    let perm: Vec<usize> = (1..vh.indices.len()).chain(std::iter::once(0)).collect();
423    let v = vh
424        .conj()
425        .permute(&perm)
426        .map_err(|e| SvdError::ComputationError(anyhow::Error::new(e)))?;
427
428    Ok((u, s, v))
429}
430
431/// SVD result for factorization, returning `V^H` directly.
432pub(crate) struct SvdFactorizeResult {
433    pub u: IdxTensor,
434    pub s: IdxTensor,
435    pub vh: IdxTensor,
436    pub bond_index: DynIndex,
437    pub singular_values: Vec<f64>,
438    pub rank: usize,
439}
440
441/// Compute truncated SVD for factorization, returning `V^H` instead of `V`.
442pub(crate) fn svd_for_factorize(
443    t: &IdxTensor,
444    left_inds: &[DynIndex],
445    options: &SvdOptions,
446) -> Result<SvdFactorizeResult, SvdError> {
447    let (u_inner, s_inner, vt_inner, singular_values, bond_index, left_indices, right_indices) =
448        svd_truncated_inner(t, left_inds, options)?;
449    let rank = singular_values.len();
450
451    let mut u_indices = left_indices;
452    u_indices.push(bond_index.clone());
453    let u_dims: Vec<usize> = u_indices.iter().map(|idx| idx.dim).collect();
454    let u_reshaped = u_inner.reshape(&u_dims).map_err(|e| {
455        SvdError::ComputationError(anyhow::anyhow!("eager SVD U reshape failed: {e}"))
456    })?;
457    let u = IdxTensor::from_inner(u_indices, u_reshaped).map_err(SvdError::ComputationError)?;
458
459    let s_indices = vec![bond_index.clone(), bond_index.sim()];
460    let s = IdxTensor::from_diag_inner(s_indices, s_inner).map_err(SvdError::ComputationError)?;
461
462    let mut vh_indices = vec![bond_index.clone()];
463    vh_indices.extend(right_indices);
464    let vh_dims: Vec<usize> = vh_indices.iter().map(|idx| idx.dim).collect();
465    let vt_reshaped = vt_inner.reshape(&vh_dims).map_err(|e| {
466        SvdError::ComputationError(anyhow::anyhow!("eager SVD V^T reshape failed: {e}"))
467    })?;
468    let vh = IdxTensor::from_inner(vh_indices, vt_reshaped).map_err(SvdError::ComputationError)?;
469
470    Ok(SvdFactorizeResult {
471        u,
472        s,
473        vh,
474        bond_index,
475        singular_values,
476        rank,
477    })
478}
479
480#[cfg(test)]
481mod tests;