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;