Skip to main content

sparse_ir_basis/sve/
compute.rs

1//! Main SVE computation functions
2
3use crate::error::{Error, require_accuracy, require_nonzero_size};
4use crate::fpu_check::FpuGuard;
5use crate::kernel::{AbstractKernel, CentrosymmKernel, KernelProperties, SVEHints};
6use crate::matrix::Mat;
7use crate::numeric::CustomNumeric;
8
9use super::result::SVEResult;
10use super::strategy::{CentrosymmSVE, NonCentrosymmSVE, SVEStrategy};
11use super::types::{TworkType, safe_epsilon};
12
13/// Default relative cutoff for singular value truncation: `2 * T::epsilon()`
14///
15/// This is `2^-51` (about 4.44e-16) for `f64` and `2^-104` (about 4.93e-32)
16/// for `Df64`. Both SVE paths use it when `cutoff` is `None`.
17/// Convention-matched with libsparseir, whose `pre_postprocess` in
18/// `backend/cxx/include/sparseir/impl/sve_impl.ipp` (commit 4bc58ea) uses
19/// `T(2) * std::numeric_limits<T>::epsilon()` when the cutoff is NaN.
20fn default_cutoff<T: CustomNumeric>() -> T {
21    T::from_f64_unchecked(2.0) * T::epsilon()
22}
23
24/// Release unused memory back to the OS.
25///
26/// SVE computation allocates large temporary buffers for SVD.
27/// After computation, these are freed but the allocator may retain them.
28/// This function asks the allocator to return unused memory to the OS.
29///
30/// Supported platforms:
31/// - macOS: uses `malloc_zone_pressure_relief`
32/// - Linux (glibc): uses `malloc_trim`
33/// - Other platforms: no-op (memory is still freed, just not returned to OS immediately)
34#[inline]
35fn release_unused_memory() {
36    #[cfg(target_os = "macos")]
37    {
38        unsafe extern "C" {
39            fn malloc_zone_pressure_relief(zone: *mut std::ffi::c_void, goal: usize) -> usize;
40        }
41        unsafe { malloc_zone_pressure_relief(std::ptr::null_mut(), 0) };
42    }
43
44    // Only use malloc_trim on Linux with glibc (not musl)
45    #[cfg(all(target_os = "linux", target_env = "gnu"))]
46    {
47        unsafe extern "C" {
48            fn malloc_trim(pad: usize) -> i32;
49        }
50        unsafe { malloc_trim(0) };
51    }
52}
53
54/// Main SVE computation function for centrosymmetric kernels
55///
56/// Automatically chooses the appropriate SVE strategy based on kernel properties
57/// and working precision based on epsilon.
58///
59/// # Arguments
60///
61/// * `kernel` - The centrosymmetric kernel to expand
62/// * `epsilon` - Required accuracy, in (0, 1). `None` selects the best
63///   accuracy of the working precision (about 1.6e-16 in Float64X2).
64/// * `cutoff` - Relative tolerance for singular value truncation: singular
65///   values smaller than `cutoff` times the largest singular value are
66///   discarded. `None` selects `2 * machine epsilon` of the working precision,
67///   about 4.44e-16 for Float64 and 4.93e-32 for Float64X2 (the libsparseir
68///   default). The SVD of each even/odd block already discards singular values
69///   below `2 * machine epsilon` times that block's largest singular value, so
70///   a smaller `cutoff` has little effect. Must be in [0, 1].
71/// * `max_num_svals` - Maximum number of singular values to keep
72/// * `twork` - Working precision type (Auto for automatic selection)
73///
74/// # Returns
75///
76/// SVEResult containing singular functions and values
77///
78/// # Errors
79///
80/// * [`Error::InvalidParameter`] if `epsilon` is not in (0, 1), `cutoff` is
81///   not in [0, 1], or `max_num_svals` is `Some(0)`; checked before any work
82/// * [`Error::NonFiniteInput`] if the discretized kernel has a NaN or
83///   infinite entry (e.g. a cutoff Λ so small that 1/Λ overflows)
84/// * [`Error::DecompositionFailed`] if an SVD fails
85///
86/// # FPU State Warning
87///
88/// This function checks for dangerous FPU settings (Flush-to-Zero and Denormals-Are-Zero)
89/// that can cause incorrect results. If detected, it temporarily corrects the FPU state
90/// and prints a warning. If you see this warning, add `-fp-model precise` flag when
91/// compiling with Intel Fortran.
92pub fn compute_sve<K>(
93    kernel: K,
94    epsilon: Option<f64>,
95    cutoff: Option<f64>,
96    max_num_svals: Option<usize>,
97    twork: TworkType,
98) -> Result<SVEResult, Error>
99where
100    K: CentrosymmKernel + KernelProperties + Clone + 'static,
101{
102    check_sve_parameters(epsilon, cutoff, max_num_svals)?;
103
104    // Protect computation from dangerous FPU settings (FZ/DAZ)
105    // This temporarily disables FZ/DAZ and restores them after computation
106    let _fpu_guard = FpuGuard::new_protect_computation();
107
108    // Determine safe epsilon and working precision
109    let (safe_epsilon, twork_actual) = safe_epsilon(epsilon, twork);
110
111    // Dispatch based on working precision
112    let result = match twork_actual {
113        TworkType::Float64 => {
114            compute_sve_with_precision::<f64, K>(kernel, safe_epsilon, cutoff, max_num_svals)
115        }
116        TworkType::Float64X2 => compute_sve_with_precision::<crate::Df64, K>(
117            kernel,
118            safe_epsilon,
119            cutoff.map(crate::Df64::from),
120            max_num_svals,
121        ),
122        _ => panic!("Invalid TworkType: {:?}", twork_actual),
123    };
124
125    // Release temporary memory back to OS after SVE computation
126    release_unused_memory();
127
128    result
129}
130
131/// Main SVE computation function for general kernels (centrosymmetric or non-centrosymmetric)
132///
133/// Discretizes the kernel on its full domain `[-xmax, xmax] × [-ymax, ymax]`
134/// with [`NonCentrosymmSVE`], which only requires [`AbstractKernel`].
135///
136/// Centrosymmetric kernels are expanded correctly as well: their half-domain
137/// [`SVEHints`] segments are mirrored onto the full domain, so the singular
138/// values agree with [`compute_sve`] up to rounding. The even/odd block
139/// structure is not exploited, however: the SVD is taken of one matrix with
140/// twice as many rows and columns as each block used by [`compute_sve`]
141/// (asymptotically about four times the work), the singular functions carry no
142/// parity tag, and the singular functions of (nearly) degenerate singular
143/// values may mix the even and odd sectors. For kernels implementing
144/// [`CentrosymmKernel`], such as [`LogisticKernel`](crate::kernel::LogisticKernel)
145/// and [`RegularizedBoseKernel`](crate::kernel::RegularizedBoseKernel), prefer
146/// [`compute_sve`].
147///
148/// This function cannot select [`CentrosymmSVE`] by itself even when
149/// [`AbstractKernel::is_centrosymmetric`] returns true: that strategy needs
150/// the reduced kernels of [`CentrosymmKernel::compute_reduced`], and a
151/// `K: AbstractKernel` bound cannot be refined to `K: CentrosymmKernel`
152/// without trait specialization, which stable Rust does not provide.
153///
154/// # Arguments
155///
156/// * `kernel` - The kernel to expand (can be centrosymmetric or non-centrosymmetric)
157/// * `epsilon` - Required accuracy, in (0, 1). `None` selects the best
158///   accuracy of the working precision (about 1.6e-16 in Float64X2).
159/// * `cutoff` - Relative tolerance for singular value truncation: singular
160///   values smaller than `cutoff` times the largest singular value are
161///   discarded. `None` selects `2 * machine epsilon` of the working precision,
162///   about 4.44e-16 for Float64 and 4.93e-32 for Float64X2, as in
163///   [`compute_sve`]. The SVD of the full-domain matrix already discards
164///   singular values below `2 * machine epsilon` times the largest one, so a
165///   smaller `cutoff` has no effect. Must be in [0, 1].
166/// * `max_num_svals` - Maximum number of singular values to keep
167/// * `twork` - Working precision type (Auto for automatic selection)
168///
169/// # Returns
170///
171/// SVEResult containing singular functions and values
172///
173/// # Errors
174///
175/// * [`Error::InvalidParameter`] if `epsilon` is not in (0, 1), `cutoff` is
176///   not in [0, 1], or `max_num_svals` is `Some(0)`; checked before any work
177/// * [`Error::NonFiniteInput`] if the discretized kernel has a NaN or
178///   infinite entry (e.g. a cutoff Λ so small that 1/Λ overflows)
179/// * [`Error::DecompositionFailed`] if an SVD fails
180///
181/// # FPU State Warning
182///
183/// This function checks for dangerous FPU settings (Flush-to-Zero and Denormals-Are-Zero)
184/// that can cause incorrect results. If detected, it temporarily corrects the FPU state
185/// and prints a warning. If you see this warning, add `-fp-model precise` flag when
186/// compiling with Intel Fortran.
187pub fn compute_sve_general<K>(
188    kernel: K,
189    epsilon: Option<f64>,
190    cutoff: Option<f64>,
191    max_num_svals: Option<usize>,
192    twork: TworkType,
193) -> Result<SVEResult, Error>
194where
195    K: AbstractKernel + KernelProperties + Clone + 'static,
196{
197    check_sve_parameters(epsilon, cutoff, max_num_svals)?;
198
199    // Protect computation from dangerous FPU settings (FZ/DAZ)
200    // This temporarily disables FZ/DAZ and restores them after computation
201    let _fpu_guard = FpuGuard::new_protect_computation();
202
203    // Determine safe epsilon and working precision
204    let (safe_epsilon, twork_actual) = safe_epsilon(epsilon, twork);
205
206    // Dispatch based on working precision
207    let result = match twork_actual {
208        TworkType::Float64 => compute_sve_general_with_precision::<f64, K>(
209            kernel,
210            safe_epsilon,
211            cutoff,
212            max_num_svals,
213        ),
214        TworkType::Float64X2 => compute_sve_general_with_precision::<crate::Df64, K>(
215            kernel,
216            safe_epsilon,
217            cutoff.map(crate::Df64::from),
218            max_num_svals,
219        ),
220        _ => panic!("Invalid TworkType: {:?}", twork_actual),
221    };
222
223    // Release temporary memory back to OS after SVE computation
224    release_unused_memory();
225
226    result
227}
228
229/// Check the parameters shared by [`compute_sve`] and [`compute_sve_general`]
230fn check_sve_parameters(
231    epsilon: Option<f64>,
232    cutoff: Option<f64>,
233    max_num_svals: Option<usize>,
234) -> Result<(), Error> {
235    require_accuracy("epsilon", epsilon)?;
236    if let Some(c) = cutoff {
237        if !(0.0..=1.0).contains(&c) {
238            return Err(Error::InvalidParameter {
239                name: "cutoff",
240                value: format!("{c:?}"),
241                reason: "must be in [0, 1]".to_string(),
242            });
243        }
244    }
245    require_nonzero_size("max_num_svals", max_num_svals)
246}
247
248/// Compute SVE with specific precision type
249fn compute_sve_with_precision<T, K>(
250    kernel: K,
251    epsilon: f64,
252    cutoff: Option<T>,
253    max_num_svals: Option<usize>,
254) -> Result<SVEResult, Error>
255where
256    T: CustomNumeric + Send + Sync + Clone + 'static,
257    K: CentrosymmKernel + KernelProperties + Clone + 'static,
258    K::SVEHintsType<T>: SVEHints<T> + Clone,
259{
260    // 1. Determine SVE strategy (automatically chooses CentrosymmSVE for centrosymmetric kernels)
261    let sve = determine_sve::<T, K>(kernel, epsilon)?;
262
263    // 2. Compute matrices
264    let matrices = sve.matrices();
265
266    // 3. Compute SVD for each matrix
267    let mut u_list = Vec::new();
268    let mut s_list = Vec::new();
269    let mut v_list = Vec::new();
270
271    for matrix in matrices.iter() {
272        let (u, s, v) = crate::tsvd::compute_svd_dtensor(matrix)?;
273        u_list.push(u);
274        s_list.push(s);
275        v_list.push(v);
276    }
277
278    // 4. Truncate based on cutoff (default: 2 * T::epsilon(), see default_cutoff)
279    let rtol_t = cutoff.unwrap_or_else(default_cutoff::<T>);
280    let (u_trunc, s_trunc, v_trunc) = truncate(u_list, s_list, v_list, rtol_t, max_num_svals);
281
282    // 5. Post-process to create SVEResult
283    sve.postprocess(u_trunc, s_trunc, v_trunc)
284}
285
286/// Compute SVE with specific precision type for general kernels
287fn compute_sve_general_with_precision<T, K>(
288    kernel: K,
289    epsilon: f64,
290    cutoff: Option<T>,
291    max_num_svals: Option<usize>,
292) -> Result<SVEResult, Error>
293where
294    T: CustomNumeric + Send + Sync + Clone + 'static,
295    K: AbstractKernel + KernelProperties + Clone + 'static,
296    K::SVEHintsType<T>: SVEHints<T> + Clone,
297{
298    // 1. Determine SVE strategy (full-domain NonCentrosymmSVE)
299    let sve = determine_sve_general::<T, K>(kernel, epsilon)?;
300
301    // 2. Compute matrices
302    let matrices = sve.matrices();
303
304    // 3. Compute SVD for each matrix
305    let mut u_list = Vec::new();
306    let mut s_list = Vec::new();
307    let mut v_list = Vec::new();
308
309    for matrix in matrices.iter() {
310        let (u, s, v) = crate::tsvd::compute_svd_dtensor(matrix)?;
311        u_list.push(u);
312        s_list.push(s);
313        v_list.push(v);
314    }
315
316    // 4. Truncate based on cutoff (default: 2 * T::epsilon(), see default_cutoff)
317    let rtol_t = cutoff.unwrap_or_else(default_cutoff::<T>);
318    let (u_trunc, s_trunc, v_trunc) = truncate(u_list, s_list, v_list, rtol_t, max_num_svals);
319
320    // 5. Post-process to create SVEResult
321    sve.postprocess(u_trunc, s_trunc, v_trunc)
322}
323
324/// Determine the appropriate SVE strategy
325///
326/// For centrosymmetric kernels, uses CentrosymmSVE for efficient computation
327/// by exploiting even/odd symmetry.
328fn determine_sve<T, K>(kernel: K, epsilon: f64) -> Result<Box<dyn SVEStrategy<T>>, Error>
329where
330    T: CustomNumeric + Send + Sync + Clone + 'static,
331    K: CentrosymmKernel + KernelProperties + Clone + 'static,
332    K::SVEHintsType<T>: SVEHints<T> + Clone,
333{
334    // CentrosymmKernel trait implies centrosymmetric
335    Ok(Box::new(CentrosymmSVE::new(kernel, epsilon)?))
336}
337
338/// Determine the SVE strategy for general kernels
339///
340/// Always uses [`NonCentrosymmSVE`], which handles centrosymmetric kernels by
341/// mirroring their half-domain hint segments onto the full domain.
342/// [`CentrosymmSVE`] would require `K: CentrosymmKernel`, which cannot be
343/// recovered from the `K: AbstractKernel` bound (see [`compute_sve_general`]).
344fn determine_sve_general<T, K>(kernel: K, epsilon: f64) -> Result<Box<dyn SVEStrategy<T>>, Error>
345where
346    T: CustomNumeric + Send + Sync + Clone + 'static,
347    K: AbstractKernel + KernelProperties + Clone + 'static,
348    K::SVEHintsType<T>: SVEHints<T> + Clone,
349{
350    Ok(Box::new(NonCentrosymmSVE::new(kernel, epsilon)?))
351}
352
353/// Truncate SVD results based on cutoff and maximum size
354///
355/// # Arguments
356///
357/// * `u_list` - List of U matrices
358/// * `s_list` - List of singular value vectors
359/// * `v_list` - List of V matrices
360/// * `rtol` - Relative tolerance for truncation
361/// * `max_num_svals` - Maximum number of singular values to keep
362///
363/// # Returns
364///
365/// Tuple of (truncated_u_list, truncated_s_list, truncated_v_list)
366pub(crate) fn truncate<T: CustomNumeric>(
367    u_list: Vec<Mat<T>>,
368    s_list: Vec<Vec<T>>,
369    v_list: Vec<Mat<T>>,
370    rtol: T,
371    max_num_svals: Option<usize>,
372) -> (Vec<Mat<T>>, Vec<Vec<T>>, Vec<Mat<T>>) {
373    let zero = T::zero();
374
375    // Validate
376    if let Some(max) = max_num_svals {
377        if max == 0 {
378            panic!("max_num_svals must be positive");
379        }
380    }
381    if rtol < zero || rtol > T::from_f64_unchecked(1.0) {
382        panic!("rtol must be in [0, 1]");
383    }
384
385    // Find global maximum singular value
386    let mut all_svals = Vec::new();
387    for s in &s_list {
388        all_svals.extend(s.iter().copied());
389    }
390
391    let max_sval = all_svals
392        .iter()
393        .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
394        .copied()
395        .unwrap_or(zero);
396
397    // Determine cutoff
398    let cutoff = if let Some(max_count) = max_num_svals {
399        if max_count < all_svals.len() {
400            let mut sorted = all_svals.clone();
401            sorted.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
402            let nth = sorted[max_count - 1];
403            if rtol * max_sval > nth {
404                rtol * max_sval
405            } else {
406                nth
407            }
408        } else {
409            rtol * max_sval
410        }
411    } else {
412        rtol * max_sval
413    };
414
415    // Truncate each result
416    let mut u_trunc = Vec::new();
417    let mut s_trunc = Vec::new();
418    let mut v_trunc = Vec::new();
419
420    for i in 0..s_list.len() {
421        let s = &s_list[i];
422        let u = &u_list[i];
423        let v = &v_list[i];
424
425        // Count singular values above cutoff
426        let mut n_keep = 0;
427        for &val in s.iter() {
428            if val >= cutoff {
429                n_keep += 1;
430            }
431        }
432
433        // Slice U: keep first n_keep columns. Preserve one output block per
434        // input block even when all singular values in that block are removed.
435        let u_shape = *u.shape();
436        let u_sliced = Mat::<T>::from_fn([u_shape.0, n_keep], |idx| u[[idx[0], idx[1]]]);
437        u_trunc.push(u_sliced);
438
439        s_trunc.push(s[..n_keep].to_vec());
440
441        // Slice V: keep first n_keep columns
442        let v_shape = *v.shape();
443        let v_sliced = Mat::<T>::from_fn([v_shape.0, n_keep], |idx| v[[idx[0], idx[1]]]);
444        v_trunc.push(v_sliced);
445    }
446
447    (u_trunc, s_trunc, v_trunc)
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    /// The default cutoff is exactly `2 * machine epsilon` of each working
455    /// precision (issue #249); `Df64` epsilon is `f64::EPSILON^2 / 2 = 2^-105`.
456    #[test]
457    fn test_default_cutoff_is_two_machine_epsilon() {
458        assert_eq!(default_cutoff::<f64>(), 2.0 * f64::EPSILON);
459        assert_eq!(default_cutoff::<f64>(), 2f64.powi(-51));
460
461        let df64 = default_cutoff::<crate::Df64>();
462        assert_eq!(df64, crate::Df64::from(2f64.powi(-104)));
463        assert_eq!((df64.hi(), df64.lo()), (2f64.powi(-104), 0.0));
464    }
465
466    #[test]
467    fn test_truncate_by_rtol() {
468        let u = vec![Mat::<f64>::from_elem([3, 3], 1.0)];
469        let s = vec![vec![10.0, 5.0, 0.1]];
470        let v = vec![Mat::<f64>::from_elem([3, 3], 1.0)];
471
472        // rtol = 0.1, max_sval = 10.0, cutoff = 1.0
473        // Keep values >= 1.0: [10.0, 5.0]
474        let (_, s_trunc, _) = truncate(u, s, v, 0.1, None);
475
476        assert_eq!(s_trunc[0].len(), 2);
477        assert_eq!(s_trunc[0][0], 10.0);
478        assert_eq!(s_trunc[0][1], 5.0);
479    }
480
481    #[test]
482    fn test_truncate_by_max_size() {
483        let u = vec![Mat::<f64>::from_elem([3, 3], 1.0)];
484        let s = vec![vec![10.0, 5.0, 2.0]];
485        let v = vec![Mat::<f64>::from_elem([3, 3], 1.0)];
486
487        // max_num_svals = 2
488        let (_, s_trunc, _) = truncate(u, s, v, 0.0, Some(2));
489
490        assert_eq!(s_trunc[0].len(), 2);
491    }
492
493    #[test]
494    #[should_panic(expected = "max_num_svals must be positive")]
495    fn test_truncate_rejects_zero_max_size() {
496        let u = vec![Mat::<f64>::from_elem([1, 1], 1.0)];
497        let s = vec![vec![1.0]];
498        let v = vec![Mat::<f64>::from_elem([1, 1], 1.0)];
499
500        truncate(u, s, v, 0.0, Some(0));
501    }
502
503    #[test]
504    fn test_truncate_preserves_empty_blocks() {
505        let u = vec![
506            Mat::<f64>::from_elem([2, 1], 1.0),
507            Mat::<f64>::from_elem([2, 1], 2.0),
508        ];
509        let s = vec![vec![10.0], vec![1.0]];
510        let v = vec![
511            Mat::<f64>::from_elem([2, 1], 1.0),
512            Mat::<f64>::from_elem([2, 1], 2.0),
513        ];
514
515        let (u_trunc, s_trunc, v_trunc) = truncate(u, s, v, 0.5, None);
516
517        assert_eq!(s_trunc.len(), 2);
518        assert_eq!(s_trunc[0], vec![10.0]);
519        assert!(s_trunc[1].is_empty());
520        assert_eq!(*u_trunc[1].shape(), (2, 0));
521        assert_eq!(*v_trunc[1].shape(), (2, 0));
522    }
523
524    #[test]
525    #[should_panic(expected = "rtol must be in [0, 1]")]
526    fn test_truncate_invalid_rtol() {
527        let u = vec![Mat::<f64>::from_elem([1, 1], 1.0)];
528        let s = vec![vec![1.0]];
529        let v = vec![Mat::<f64>::from_elem([1, 1], 1.0)];
530
531        truncate(u, s, v, 1.5, None);
532    }
533}