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;