Skip to main content

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