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}