sparse_ir/kernel.rs
1//! Kernel implementations for SparseIR
2//!
3//! This module provides kernel implementations for analytical continuation in quantum many-body physics.
4//! The kernels are used in Fredholm integral equations of the first kind.
5//!
6//! u(x) = integral of K(x, y) v(y) dy
7//!
8//! where x ∈ [xmin, xmax] and y ∈ [ymin, ymax].
9//!
10//! In general, the kernel is applied to a scaled spectral function rho'(y) as:
11//!
12//! integral of K(x, y) rho'(y) dy,
13//!
14//! where ρ'(y) = w(y) ρ(y). The weight function w(y) transforms the original spectral
15//! function ρ(y) into the scaled version ρ'(y) used in the integral equation.
16
17use crate::numeric::CustomNumeric;
18use crate::traits::{Statistics, StatisticsType};
19use libm::expm1 as libm_expm1;
20use simba::scalar::ComplexField;
21use std::fmt::Debug;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum SymmetryType {
25 Even,
26 Odd,
27}
28
29impl SymmetryType {
30 pub fn sign(self) -> i32 {
31 match self {
32 SymmetryType::Even => 1,
33 SymmetryType::Odd => -1,
34 }
35 }
36}
37
38/// Trait for SVE (Singular Value Expansion) hints
39///
40/// Provides discretization hints for singular value expansion of a given kernel.
41/// This includes segment information and numerical parameters for efficient computation.
42pub trait SVEHints<T>: Debug + Send + Sync
43where
44 T: Copy + Debug + Send + Sync,
45{
46 /// Get the x-axis segments for discretization
47 ///
48 /// For centrosymmetric kernels, returns only positive values (x >= 0) including the endpoints.
49 /// The returned vector contains segments from [0, xmax] where xmax is the
50 /// upper bound of the x domain.
51 ///
52 /// For non-centrosymmetric kernels, returns segments covering the full domain
53 /// [-xmax, xmax].
54 fn segments_x(&self) -> Vec<T>;
55
56 /// Get the y-axis segments for discretization
57 ///
58 /// For centrosymmetric kernels, returns only positive values (y >= 0) including the endpoints.
59 /// The returned vector contains segments from [0, ymax] where ymax is the
60 /// upper bound of the y domain.
61 ///
62 /// For non-centrosymmetric kernels, returns segments covering the full domain
63 /// [-ymax, ymax].
64 fn segments_y(&self) -> Vec<T>;
65
66 /// Get the number of singular values hint
67 fn nsvals(&self) -> usize;
68
69 /// Get the number of Gauss points for quadrature
70 fn ngauss(&self) -> usize;
71}
72
73/// Trait for kernel type properties (static characteristics)
74pub trait KernelProperties {
75 /// Associated type for SVE hints
76 type SVEHintsType<T>: SVEHints<T> + Clone
77 where
78 T: Copy + Debug + Send + Sync + CustomNumeric + 'static;
79 /// Power with which the y coordinate scales.
80 ///
81 /// For most kernels, this is 0 (no scaling).
82 /// For RegularizedBoseKernel, this is 1 (linear scaling).
83 fn ypower(&self) -> i32;
84
85 /// Convergence radius of the Matsubara basis asymptotic model
86 ///
87 /// For improved numerical accuracy, IR basis functions on Matsubara axis
88 /// can be evaluated from asymptotic expression for |n| > conv_radius.
89 fn conv_radius(&self) -> f64;
90
91 /// Get the upper bound of the x domain
92 fn xmax(&self) -> f64;
93
94 /// Get the upper bound of the y domain
95 fn ymax(&self) -> f64;
96
97 /// Weight function for given statistics.
98 ///
99 /// The kernel is applied to a scaled spectral function ρ'(y) as:
100 /// ∫ K(x, y) ρ'(y) dy,
101 /// where ρ'(y) = w(y) ρ(y).
102 ///
103 /// This function returns w(beta, omega) that transforms the original spectral
104 /// function ρ(y) into the scaled version ρ'(y) used in the integral equation.
105 ///
106 /// @param beta Inverse temperature
107 /// @param omega Frequency
108 /// @return The weight value w(beta, omega)
109 fn weight<S: StatisticsType + 'static>(&self, beta: f64, omega: f64) -> f64;
110
111 /// Inverse weight function to avoid division by zero.
112 ///
113 /// This is a safer API that returns the inverse weight.
114 ///
115 /// @param beta Inverse temperature
116 /// @param omega Frequency
117 /// @return The inverse weight value
118 fn inv_weight<S: StatisticsType + 'static>(&self, beta: f64, omega: f64) -> f64;
119
120 /// Create SVE hints for this kernel type.
121 ///
122 /// Provides discretization hints for singular value expansion computation.
123 /// The hints include segment information and numerical parameters optimized
124 /// for the specific kernel type.
125 ///
126 /// @param epsilon Target accuracy for the SVE computation
127 /// @return SVE hints specific to this kernel type
128 fn sve_hints<T>(&self, epsilon: f64) -> Self::SVEHintsType<T>
129 where
130 T: Copy + Debug + Send + Sync + CustomNumeric + 'static;
131}
132
133/// Trait for general kernels (both centrosymmetric and non-centrosymmetric)
134///
135/// This trait provides the basic interface for computing kernel values.
136/// Centrosymmetric kernels should implement `CentrosymmKernel` instead,
137/// which provides additional optimizations.
138pub trait AbstractKernel: Send + Sync {
139 /// Compute the kernel value K(x, y) with high precision
140 ///
141 /// # Arguments
142 ///
143 /// * `x` - The x coordinate (typically in [-xmax, xmax])
144 /// * `y` - The y coordinate (typically in [-ymax, ymax])
145 ///
146 /// # Returns
147 ///
148 /// The kernel value K(x, y)
149 fn compute<T: CustomNumeric + Copy + Debug>(&self, x: T, y: T) -> T;
150
151 /// Check if the kernel is centrosymmetric
152 ///
153 /// Returns true if and only if K(x, y) == K(-x, -y) for all values of x and y.
154 /// This allows the kernel to be block-diagonalized, speeding up the
155 /// singular value expansion by a factor of 4.
156 ///
157 /// # Returns
158 ///
159 /// True if the kernel is centrosymmetric, false otherwise.
160 fn is_centrosymmetric(&self) -> bool {
161 false
162 }
163}
164
165/// Trait for centrosymmetric kernels
166///
167/// Centrosymmetric kernels satisfy K(x, y) = K(-x, -y) and can be decomposed
168/// into even and odd components for efficient computation.
169pub trait CentrosymmKernel: AbstractKernel {
170 /// Compute the reduced kernel value
171 ///
172 /// K_red(x, y) = K(x, y) + sign * K(x, -y)
173 /// where sign = 1 for even symmetry and sign = -1 for odd symmetry
174 fn compute_reduced<T: CustomNumeric + Copy + Debug>(
175 &self,
176 x: T,
177 y: T,
178 symmetry: SymmetryType,
179 ) -> T;
180
181 /// Get the cutoff parameter Λ (lambda)
182 fn lambda(&self) -> f64;
183}
184
185/// Logistic kernel for fermionic analytical continuation
186///
187/// This kernel implements K(x, y) = exp(-Λy(x + 1)/2)/(1 + exp(-Λy))
188/// where x ∈ [-1, 1] and y ∈ [-1, 1]
189#[derive(Debug, Clone, Copy)]
190pub struct LogisticKernel {
191 lambda: f64,
192}
193
194impl LogisticKernel {
195 /// Create a new logistic kernel with the given cutoff parameter
196 pub fn new(lambda: f64) -> Self {
197 Self { lambda }
198 }
199
200 /// Get the cutoff parameter
201 pub fn lambda(&self) -> f64 {
202 self.lambda
203 }
204}
205
206impl KernelProperties for LogisticKernel {
207 type SVEHintsType<T>
208 = LogisticSVEHints<T>
209 where
210 T: Copy + Debug + Send + Sync + CustomNumeric + 'static;
211 fn ypower(&self) -> i32 {
212 0 // No y-power scaling for LogisticKernel
213 }
214
215 fn conv_radius(&self) -> f64 {
216 40.0 * self.lambda
217 }
218
219 fn xmax(&self) -> f64 {
220 1.0
221 }
222 fn ymax(&self) -> f64 {
223 1.0
224 }
225
226 fn weight<S: StatisticsType + 'static>(&self, beta: f64, omega: f64) -> f64 {
227 match S::STATISTICS {
228 Statistics::Fermionic => {
229 // For fermionic statistics: w(beta, omega) = 1.0
230 // The kernel K(x, y) is already in the correct form for fermions
231 1.0
232 }
233 Statistics::Bosonic => {
234 // For bosonic statistics: w(beta, omega) = 1.0 / tanh(0.5 * beta * omega)
235 // This transforms the fermionic kernel to work with bosonic correlation functions
236 // The tanh factor accounts for the different statistics
237 1.0 / (0.5 * beta * omega).tanh()
238 }
239 }
240 }
241
242 fn inv_weight<S: StatisticsType + 'static>(&self, beta: f64, omega: f64) -> f64 {
243 match S::STATISTICS {
244 Statistics::Fermionic => {
245 // For fermionic statistics: 1/w = 1.0 (safe, no division by zero)
246 1.0
247 }
248 Statistics::Bosonic => {
249 // For bosonic statistics: 1/w = tanh(0.5 * beta * omega) (safe, handles omega=0 case)
250 // This avoids division by zero when tanh(0.5 * beta * omega) approaches zero
251 (0.5 * beta * omega).tanh()
252 }
253 }
254 }
255
256 fn sve_hints<T>(&self, epsilon: f64) -> Self::SVEHintsType<T>
257 where
258 T: Copy + Debug + Send + Sync + CustomNumeric + 'static,
259 {
260 LogisticSVEHints::new(*self, epsilon)
261 }
262}
263
264pub fn compute_logistic_kernel<T: CustomNumeric>(lambda: f64, x: T, y: T) -> T {
265 let x_plus: T = T::from_f64_unchecked(1.0) + x;
266 let x_minus: T = T::from_f64_unchecked(1.0) - x;
267
268 let u_plus: T = T::from_f64_unchecked(0.5) * x_plus;
269 let u_minus: T = T::from_f64_unchecked(0.5) * x_minus;
270 let v: T = T::from_f64_unchecked(lambda) * y;
271
272 let mabs_v: T = -v.abs_as_same_type();
273 let numerator: T = if v >= T::from_f64_unchecked(0.0) {
274 (u_plus * mabs_v).exp()
275 } else {
276 (u_minus * mabs_v).exp()
277 };
278 let denominator: T = T::from_f64_unchecked(1.0) + mabs_v.exp();
279 numerator / denominator
280}
281
282fn compute_logistic_kernel_reduced_odd<T: CustomNumeric>(lambda: f64, x: T, y: T) -> T {
283 // For x * y around 0, antisymmetrization introduces cancellation, which
284 // reduces the relative precision. To combat this, we replace the
285 // values with the explicit form
286 let v_half: T = T::from_f64_unchecked(lambda * 0.5) * y;
287 // Use direct comparison to match C++ implementation (avoid precision loss from to_f64())
288 let xy_small: bool = x * v_half < T::from_f64_unchecked(1.0);
289 let cosh_finite: bool = v_half < T::from_f64_unchecked(85.0);
290 if xy_small && cosh_finite {
291 -(v_half * x).sinh() / v_half.cosh()
292 } else {
293 let k_plus = compute_logistic_kernel(lambda, x, y);
294 let k_minus = compute_logistic_kernel(lambda, x, -y);
295 k_plus - k_minus
296 }
297}
298
299impl AbstractKernel for LogisticKernel {
300 fn compute<T: CustomNumeric + Copy + Debug>(&self, x: T, y: T) -> T {
301 compute_logistic_kernel(self.lambda, x, y)
302 }
303
304 fn is_centrosymmetric(&self) -> bool {
305 true
306 }
307}
308
309impl CentrosymmKernel for LogisticKernel {
310 fn compute_reduced<T: CustomNumeric + Copy + Debug>(
311 &self,
312 x: T,
313 y: T,
314 symmetry: SymmetryType,
315 ) -> T {
316 match symmetry {
317 SymmetryType::Even => self.compute(x, y) + self.compute(x, -y),
318 SymmetryType::Odd => compute_logistic_kernel_reduced_odd(self.lambda, x, y),
319 }
320 }
321
322 fn lambda(&self) -> f64 {
323 self.lambda
324 }
325}
326
327/// SVE hints for LogisticKernel
328#[derive(Debug, Clone)]
329pub struct LogisticSVEHints<T> {
330 kernel: LogisticKernel,
331 epsilon: f64,
332 _phantom: std::marker::PhantomData<T>,
333}
334
335impl<T> LogisticSVEHints<T>
336where
337 T: Copy + Debug + Send + Sync,
338{
339 pub fn new(kernel: LogisticKernel, epsilon: f64) -> Self {
340 Self {
341 kernel,
342 epsilon,
343 _phantom: std::marker::PhantomData,
344 }
345 }
346}
347
348impl<T> SVEHints<T> for LogisticSVEHints<T>
349where
350 T: Copy + Debug + Send + Sync + CustomNumeric,
351{
352 fn segments_x(&self) -> Vec<T> {
353 // Direct implementation that generates only non-negative sample points
354 // This is equivalent to the C++ implementation but without the full symmetric array creation
355 let nzeros = std::cmp::max((15.0 * self.kernel.lambda().log10()).round() as usize, 1);
356
357 // Create a range of values
358 let mut temp = vec![0.0; nzeros];
359 for i in 0..nzeros {
360 temp[i] = 0.143 * i as f64;
361 }
362
363 // Calculate diffs using the inverse hyperbolic cosine
364 let mut diffs = vec![0.0; nzeros];
365 for i in 0..nzeros {
366 diffs[i] = 1.0 / temp[i].cosh();
367 }
368
369 // Calculate cumulative sum of diffs
370 let mut zeros = vec![0.0; nzeros];
371 zeros[0] = diffs[0];
372 for i in 1..nzeros {
373 zeros[i] = zeros[i - 1] + diffs[i];
374 }
375
376 // Normalize zeros
377 let last_zero = zeros[nzeros - 1];
378 for i in 0..nzeros {
379 zeros[i] /= last_zero;
380 }
381
382 // Create segments with only non-negative values (x >= 0) including endpoints [0, xmax]
383 let mut segments = Vec::with_capacity(nzeros + 1);
384
385 // Add 0.0 endpoint
386 segments.push(<T as CustomNumeric>::from_f64_unchecked(0.0));
387
388 // Add positive zeros (already in [0, 1] range)
389 for i in 0..nzeros {
390 segments.push(<T as CustomNumeric>::from_f64_unchecked(zeros[i]));
391 }
392
393 // Ensure segments are sorted in ascending order [0, ..., xmax]
394 segments.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
395
396 segments
397 }
398
399 fn segments_y(&self) -> Vec<T> {
400 // Direct implementation that generates only non-negative sample points
401 // This is equivalent to the C++ implementation but without the full symmetric array creation
402 let nzeros = std::cmp::max((20.0 * self.kernel.lambda().log10()).round() as usize, 2);
403
404 // Initial differences (from C++ implementation)
405 let mut diffs = vec![
406 0.01523, 0.03314, 0.04848, 0.05987, 0.06703, 0.07028, 0.07030, 0.06791, 0.06391,
407 0.05896, 0.05358, 0.04814, 0.04288, 0.03795, 0.03342, 0.02932, 0.02565, 0.02239,
408 0.01951, 0.01699,
409 ];
410
411 // Truncate diffs if necessary
412 if nzeros < diffs.len() {
413 diffs.truncate(nzeros);
414 }
415
416 // Calculate trailing differences
417 for i in 20..nzeros {
418 let x = 0.141 * i as f64;
419 diffs.push(0.25 * (-x).exp());
420 }
421
422 // Calculate cumulative sum of diffs
423 let mut zeros = Vec::with_capacity(nzeros);
424 zeros.push(diffs[0]);
425 for i in 1..nzeros {
426 zeros.push(zeros[i - 1] + diffs[i]);
427 }
428
429 // Normalize zeros
430 let last_zero = zeros[nzeros - 1];
431 for i in 0..nzeros {
432 zeros[i] /= last_zero;
433 }
434 zeros.pop(); // Remove last element
435
436 // Updated nzeros
437 let nzeros = zeros.len();
438
439 // Adjust zeros
440 for i in 0..nzeros {
441 zeros[i] -= 1.0;
442 }
443
444 // Generate segments directly from negative zeros
445 let mut segments: Vec<T> = Vec::new();
446
447 segments.push(<T as CustomNumeric>::from_f64_unchecked(1.0));
448
449 // Add absolute values of negative zeros
450 for i in 0..nzeros {
451 let abs_val = -zeros[i];
452 segments.push(<T as CustomNumeric>::from_f64_unchecked(abs_val));
453 }
454
455 if segments[segments.len() - 1].abs_as_same_type() > T::epsilon() {
456 segments.push(<T as CustomNumeric>::from_f64_unchecked(0.0));
457 }
458
459 // Sort in ascending order
460 segments.sort_by(|a, b| a.partial_cmp(b).unwrap());
461
462 segments
463 }
464
465 fn nsvals(&self) -> usize {
466 let log10_lambda = self.kernel.lambda().log10().max(1.0);
467 ((25.0 + log10_lambda) * log10_lambda).round() as usize
468 }
469
470 fn ngauss(&self) -> usize {
471 if self.epsilon >= 1e-8 { 10 } else { 16 }
472 }
473}
474
475// ============================================================================
476// RegularizedBoseKernel
477// ============================================================================
478
479/// Regularized bosonic analytical continuation kernel
480///
481/// In dimensionless variables x = 2τ/β - 1, y = βω/Λ, the integral kernel is:
482///
483/// ```text
484/// K(x, y) = y * exp(-Λ y (x + 1) / 2) / (1 - exp(-Λ y))
485/// ```
486///
487/// This kernel is used for bosonic Green's functions. The factor y regularizes
488/// the singularity at ω = 0, making the kernel well-behaved for numerical work.
489///
490/// The dimensionalized kernel is related by:
491/// ```text
492/// K(τ, ω) = ωmax * K(2τ/β - 1, ω/ωmax)
493/// ```
494/// where ωmax = Λ/β.
495///
496/// # Properties
497/// - **Centrosymmetric**: K(x, y) = K(-x, -y)
498/// - **ypower = 1**: Spectral function transforms as ρ'(y) = y * ρ(y)
499/// - **Bosonic only**: Does not support fermionic statistics
500/// - **Weight function**: w(β, ω) = 1/ω for bosonic statistics
501///
502/// # Numerical Stability
503/// The expression v / (exp(v) - 1) is evaluated using expm1 for small |v|.
504#[derive(Debug, Clone, Copy, PartialEq)]
505pub struct RegularizedBoseKernel {
506 /// Kernel cutoff parameter Λ = β × ωmax
507 pub lambda: f64,
508}
509
510/// Helper function to compute expm1(-absv) for different numeric types
511/// Uses exp_m1() for Df64 and libm::expm1 for f64
512fn _exp_m1<T: CustomNumeric>(absv: T) -> T {
513 match std::any::TypeId::of::<T>() {
514 id if id == std::any::TypeId::of::<crate::Df64>() => {
515 // Safe: we know T is Df64, use exp_m1() method from ComplexField trait
516 let df64_absv: crate::Df64 = unsafe { std::mem::transmute_copy(&absv) };
517 let expm1_result = (-df64_absv).exp_m1();
518 unsafe { std::mem::transmute_copy(&expm1_result) }
519 }
520 _ => {
521 // For f64, use libm::expm1 for better numerical stability
522 let f64_absv = absv.to_f64();
523 T::from_f64_unchecked(libm_expm1(-f64_absv))
524 }
525 }
526}
527
528impl RegularizedBoseKernel {
529 /// Create a new RegularizedBoseKernel
530 ///
531 /// # Arguments
532 /// * `lambda` - Kernel cutoff Λ (must be non-negative)
533 ///
534 /// # Panics
535 /// Panics if lambda < 0
536 pub fn new(lambda: f64) -> Self {
537 if lambda < 0.0 {
538 panic!("Kernel cutoff Λ must be non-negative, got {}", lambda);
539 }
540 Self { lambda }
541 }
542
543 /// Compute kernel value with numerical stability
544 ///
545 /// Evaluates K(x, y) = y * exp(-Λy(x+1)/2) / (1 - exp(-Λy))
546 /// using expm1 for better accuracy near y = 0.
547 ///
548 /// # Arguments
549 /// * `x` - Normalized time coordinate (x ∈ [-1, 1])
550 /// * `y` - Normalized frequency coordinate (y ∈ [-1, 1])
551 /// * `x_plus` - Precomputed x + 1 (optional, for efficiency)
552 /// * `x_minus` - Precomputed 1 - x (optional, for efficiency)
553 fn compute_impl<T>(&self, x: T, y: T, x_plus: Option<T>, x_minus: Option<T>) -> T
554 where
555 T: CustomNumeric,
556 {
557 // u_plus = (x + 1) / 2, u_minus = (1 - x) / 2
558 // x_plus and x_minus are (x+1) and (1-x), so we need to multiply by 0.5
559 let u_plus = x_plus
560 .map(|xp| T::from_f64_unchecked(0.5) * xp)
561 .unwrap_or_else(|| T::from_f64_unchecked(0.5) * (T::from_f64_unchecked(1.0) + x));
562 let u_minus = x_minus
563 .map(|xm| T::from_f64_unchecked(0.5) * xm)
564 .unwrap_or_else(|| T::from_f64_unchecked(0.5) * (T::from_f64_unchecked(1.0) - x));
565
566 let v = T::from_f64_unchecked(self.lambda) * y;
567 let absv = v.abs_as_same_type();
568
569 // Handle y ≈ 0 using Taylor expansion
570 // K(x,y) = 1/Λ - xy/2 + (1/24)Λ(3x² - 1)y² + O(y³)
571 // For |Λy| < 2e-14, use first-order approximation
572 // This avoids division by zero when exp(-|Λy|) ≈ 1
573 if absv.to_f64() < 2e-14 {
574 let term0 = T::from_f64_unchecked(1.0 / self.lambda);
575 let term1 = T::from_f64_unchecked(0.5) * x * y;
576 return term0 - term1;
577 }
578
579 // enum_val = exp(-|v| * (v >= 0 ? u_plus : u_minus))
580 let enum_val = if v >= T::from_f64_unchecked(0.0) {
581 (-absv * u_plus).exp()
582 } else {
583 (-absv * u_minus).exp()
584 };
585
586 // Handle v / (exp(v) - 1) with numerical stability using expm1
587 // Follows SparseIR.jl implementation: denom = absv / expm1(-absv)
588 // This is more accurate than exp(-absv) - 1 for small arguments
589 let denom = if absv.to_f64() >= 1e-200 {
590 let expm1_neg_absv = _exp_m1(absv);
591 absv / expm1_neg_absv
592 } else {
593 // For very small absv, use -1 (matches SparseIR.jl: -one(absv))
594 -T::from_f64_unchecked(1.0)
595 };
596
597 // K(x, y) = -1/Λ * enum_val * denom
598 // Since denom is negative (exp(-absv) < 1), final result is positive
599 T::from_f64_unchecked(-1.0 / self.lambda) * enum_val * denom
600 }
601}
602
603impl KernelProperties for RegularizedBoseKernel {
604 type SVEHintsType<T>
605 = RegularizedBoseSVEHints<T>
606 where
607 T: Copy + Debug + Send + Sync + CustomNumeric + 'static;
608
609 fn ypower(&self) -> i32 {
610 1 // Spectral function transforms as ρ'(y) = y * ρ(y)
611 }
612
613 fn conv_radius(&self) -> f64 {
614 40.0 * self.lambda
615 }
616
617 fn xmax(&self) -> f64 {
618 1.0
619 }
620
621 fn ymax(&self) -> f64 {
622 1.0
623 }
624
625 fn weight<S: StatisticsType + 'static>(&self, _beta: f64, omega: f64) -> f64 {
626 match S::STATISTICS {
627 Statistics::Fermionic => {
628 panic!("RegularizedBoseKernel does not support fermionic functions");
629 }
630 Statistics::Bosonic => {
631 // weight = 1/ω
632 if omega.abs() < 1e-300 {
633 panic!("RegularizedBoseKernel: omega too close to zero");
634 }
635 1.0 / omega
636 }
637 }
638 }
639
640 fn inv_weight<S: StatisticsType + 'static>(&self, _beta: f64, omega: f64) -> f64 {
641 match S::STATISTICS {
642 Statistics::Fermionic => {
643 panic!("RegularizedBoseKernel does not support fermionic functions");
644 }
645 Statistics::Bosonic => {
646 // inv_weight = ω (safe, no division)
647 omega
648 }
649 }
650 }
651
652 fn sve_hints<T>(&self, epsilon: f64) -> Self::SVEHintsType<T>
653 where
654 T: Copy + Debug + Send + Sync + CustomNumeric + 'static,
655 {
656 RegularizedBoseSVEHints::new(*self, epsilon)
657 }
658}
659
660impl AbstractKernel for RegularizedBoseKernel {
661 fn compute<T: CustomNumeric + Copy + Debug>(&self, x: T, y: T) -> T {
662 let x_plus = Some(T::from_f64_unchecked(1.0) + x);
663 let x_minus = Some(T::from_f64_unchecked(1.0) - x);
664 self.compute_impl(x, y, x_plus, x_minus)
665 }
666
667 fn is_centrosymmetric(&self) -> bool {
668 true
669 }
670}
671
672impl CentrosymmKernel for RegularizedBoseKernel {
673 fn compute_reduced<T: CustomNumeric + Copy + Debug>(
674 &self,
675 x: T,
676 y: T,
677 symmetry: SymmetryType,
678 ) -> T {
679 match symmetry {
680 SymmetryType::Even => self.compute(x, y) + self.compute(x, -y),
681 SymmetryType::Odd => {
682 // For RegularizedBoseKernel, use sinh formulation for numerical stability
683 // K(x,y) - K(x,-y) = -y * sinh(Λ y x / 2) / sinh(Λ y / 2)
684 let v_half = T::from_f64_unchecked(self.lambda * 0.5) * y;
685 let xv_half = x * v_half;
686 let xy_small = xv_half.to_f64().abs() < 1.0;
687 let sinh_finite = v_half.to_f64().abs() < 85.0 && v_half.to_f64().abs() > 1e-200;
688
689 if xy_small && sinh_finite {
690 // Use sinh formulation for numerical stability
691 // NOTE: Minus sign is critical! (matches Julia/C++ implementation)
692 -y * xv_half.sinh() / v_half.sinh()
693 } else {
694 // Fall back to direct computation
695 self.compute(x, y) - self.compute(x, -y)
696 }
697 }
698 }
699 }
700
701 fn lambda(&self) -> f64 {
702 self.lambda
703 }
704}
705
706/// SVE hints for RegularizedBoseKernel
707#[derive(Debug, Clone)]
708pub struct RegularizedBoseSVEHints<T> {
709 kernel: RegularizedBoseKernel,
710 epsilon: f64,
711 _phantom: std::marker::PhantomData<T>,
712}
713
714impl<T> RegularizedBoseSVEHints<T>
715where
716 T: Copy + Debug + Send + Sync,
717{
718 pub fn new(kernel: RegularizedBoseKernel, epsilon: f64) -> Self {
719 Self {
720 kernel,
721 epsilon,
722 _phantom: std::marker::PhantomData,
723 }
724 }
725}
726
727impl<T> SVEHints<T> for RegularizedBoseSVEHints<T>
728where
729 T: Copy + Debug + Send + Sync + CustomNumeric + 'static,
730{
731 fn segments_x(&self) -> Vec<T> {
732 // Returns segments for x >= 0 domain only
733 // C++/Julia: nzeros = max(round(15 * log10(lambda)), 15)
734 let nzeros = ((15.0 * self.kernel.lambda.log10()).round() as usize).max(15);
735
736 // temp[i] = 0.18 * i
737 let mut temp = vec![0.0; nzeros];
738 for i in 0..nzeros {
739 temp[i] = 0.18 * i as f64;
740 }
741
742 // diffs[i] = 1.0 / cosh(temp[i])
743 let mut diffs = vec![0.0; nzeros];
744 for i in 0..nzeros {
745 diffs[i] = 1.0 / temp[i].cosh();
746 }
747
748 // Cumulative sum
749 let mut zeros = vec![0.0; nzeros];
750 zeros[0] = diffs[0];
751 for i in 1..nzeros {
752 zeros[i] = zeros[i - 1] + diffs[i];
753 }
754
755 // Normalize
756 let last_zero = zeros[nzeros - 1];
757 for i in 0..nzeros {
758 zeros[i] /= last_zero;
759 }
760
761 // Create segments with only non-negative values: [0, zeros[0], zeros[1], ...]
762 let mut segments = Vec::with_capacity(nzeros + 1);
763 segments.push(T::from_f64_unchecked(0.0));
764 for i in 0..nzeros {
765 segments.push(T::from_f64_unchecked(zeros[i]));
766 }
767
768 // Ensure sorted (should already be sorted, but verify)
769 segments.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
770
771 segments
772 }
773
774 fn segments_y(&self) -> Vec<T> {
775 // Returns segments for y >= 0 domain only
776 // C++/Julia: nzeros = max(round(20 * log10(lambda)), 20)
777 let nzeros = ((20.0 * self.kernel.lambda.log10()).round() as usize).max(20);
778
779 // diffs[j] = 0.12 / exp(0.0337 * j * log(j + 1))
780 let mut diffs = vec![0.0; nzeros];
781 for j in 0..nzeros {
782 let j_f64 = j as f64;
783 let exponent = 0.0337 * j_f64 * (j_f64 + 1.0).ln();
784 diffs[j] = 0.12 / exponent.exp();
785 }
786
787 // Cumulative sum
788 let mut zeros = vec![0.0; nzeros];
789 zeros[0] = diffs[0];
790 for i in 1..nzeros {
791 zeros[i] = zeros[i - 1] + diffs[i];
792 }
793
794 // Normalize by last value, then remove it
795 let last_zero = zeros[nzeros - 1];
796 for i in 0..nzeros {
797 zeros[i] /= last_zero;
798 }
799 zeros.pop();
800
801 // After normalization and pop, zeros are in [0, 1) range
802 // Create segments: [0, zeros[0], zeros[1], ..., 1]
803 let mut segments = Vec::with_capacity(zeros.len() + 2);
804 segments.push(T::from_f64_unchecked(0.0));
805 for z in zeros {
806 segments.push(T::from_f64_unchecked(z));
807 }
808 segments.push(T::from_f64_unchecked(1.0));
809
810 // Ensure sorted (should already be sorted, but verify)
811 segments.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
812
813 segments
814 }
815
816 fn nsvals(&self) -> usize {
817 // C++: int(round(28 * max(1.0, log10(lambda))))
818 let log10_lambda = self.kernel.lambda.log10().max(1.0);
819 (28.0 * log10_lambda).round() as usize
820 }
821
822 fn ngauss(&self) -> usize {
823 if self.epsilon >= 1e-8 { 10 } else { 16 }
824 }
825}
826
827#[cfg(test)]
828#[path = "kernel_tests.rs"]
829mod tests;