uncertain_numerics/
kernel_integral.rs1use crate::{GaussianMeasure, RbfKernel};
4
5pub trait KernelIntegral<M> {
16 #[must_use]
18 fn kernel_integral(&self, measure: &M) -> f64;
19}
20
21impl KernelIntegral<GaussianMeasure> for RbfKernel {
22 fn kernel_integral(&self, measure: &GaussianMeasure) -> f64 {
23 let length_scale_squared = self.length_scale() * self.length_scale();
24 let denominator = length_scale_squared + 2.0 * measure.variance();
25
26 self.signal_variance() * (length_scale_squared / denominator).sqrt()
27 }
28}
29
30#[cfg(test)]
31mod tests {
32 use super::KernelIntegral;
33 use crate::{ContinuousProbabilityMeasure, GaussianMeasure, RbfKernel, ScalarKernel};
34
35 const TOLERANCE: f64 = 1.0e-12;
36 const QUADRATURE_TOLERANCE: f64 = 1.0e-8;
37
38 fn assert_close(actual: f64, expected: f64, tolerance: f64) {
39 let scale = expected.abs().max(1.0);
40 assert!(
41 (actual - expected).abs() <= tolerance * scale,
42 "expected {expected:.16e}, got {actual:.16e}"
43 );
44 }
45
46 fn simpson_weights(intervals: u32) -> Vec<f64> {
47 assert!(intervals > 0);
48 assert_eq!(intervals % 2, 0);
49
50 let capacity = usize::try_from(intervals + 1).expect("interval count fits in usize");
51 let mut weights = Vec::with_capacity(capacity);
52 for index in 0..=intervals {
53 let weight = if index == 0 || index == intervals {
54 1.0
55 } else if index % 2 == 0 {
56 2.0
57 } else {
58 4.0
59 };
60 weights.push(weight);
61 }
62 weights
63 }
64
65 fn simpson_double_integral<F>(function: F, lower: f64, upper: f64, intervals: u32) -> f64
66 where
67 F: Fn(f64, f64) -> f64,
68 {
69 let step = (upper - lower) / f64::from(intervals);
70 let weights = simpson_weights(intervals);
71 let mut weighted_sum = 0.0;
72
73 for (i, &weight_x) in weights.iter().enumerate() {
74 let i_u32 = u32::try_from(i).expect("index fits in u32");
75 let x = lower + f64::from(i_u32) * step;
76
77 for (j, &weight_y) in weights.iter().enumerate() {
78 let j_u32 = u32::try_from(j).expect("index fits in u32");
79 let y = lower + f64::from(j_u32) * step;
80 weighted_sum += weight_x * weight_y * function(x, y);
81 }
82 }
83
84 weighted_sum * step * step / 9.0
85 }
86
87 #[test]
88 fn kernel_integral_matches_closed_form() {
89 let kernel = RbfKernel::new(2.5, 0.75).expect("kernel parameters are valid");
90 let measure = GaussianMeasure::new(1.25, 1.6).expect("measure parameters are valid");
91 let length_scale_squared = kernel.length_scale() * kernel.length_scale();
92 let expected = kernel.signal_variance()
93 * (length_scale_squared / (length_scale_squared + 2.0 * measure.variance())).sqrt();
94
95 assert_close(kernel.kernel_integral(&measure), expected, TOLERANCE);
96 }
97
98 #[test]
99 fn kernel_integral_is_invariant_to_measure_mean() {
100 let kernel = RbfKernel::new(1.8, 0.9).expect("kernel parameters are valid");
101 let left = GaussianMeasure::new(-10.0, 2.0).expect("measure parameters are valid");
102 let right = GaussianMeasure::new(25.0, 2.0).expect("measure parameters are valid");
103
104 assert_close(
105 kernel.kernel_integral(&left),
106 kernel.kernel_integral(&right),
107 TOLERANCE,
108 );
109 }
110
111 #[test]
112 fn kernel_integral_is_positive_and_bounded_by_signal_variance() {
113 let kernel = RbfKernel::new(3.0, 0.6).expect("kernel parameters are valid");
114
115 for variance in [0.01, 0.25, 1.0, 10.0, 100.0] {
116 let measure =
117 GaussianMeasure::new(0.0, variance).expect("measure parameters are valid");
118 let integral = kernel.kernel_integral(&measure);
119
120 assert!(integral > 0.0);
121 assert!(integral <= kernel.signal_variance());
122 }
123 }
124
125 #[test]
126 fn kernel_integral_decreases_as_measure_variance_increases() {
127 let kernel = RbfKernel::new(1.0, 1.0).expect("kernel parameters are valid");
128 let narrow = GaussianMeasure::new(0.0, 0.1).expect("measure parameters are valid");
129 let medium = GaussianMeasure::new(0.0, 1.0).expect("measure parameters are valid");
130 let wide = GaussianMeasure::new(0.0, 10.0).expect("measure parameters are valid");
131
132 assert!(kernel.kernel_integral(&narrow) > kernel.kernel_integral(&medium));
133 assert!(kernel.kernel_integral(&medium) > kernel.kernel_integral(&wide));
134 }
135
136 #[test]
137 fn analytic_kernel_integral_matches_independent_numerical_quadrature() {
138 let cases = [
139 (1.0, 1.0, 0.0, 1.0),
140 (2.5, 0.4, -1.0, 2.0),
141 (0.7, 3.0, 4.0, 0.25),
142 ];
143
144 for (signal_variance, length_scale, measure_mean, measure_variance) in cases {
145 let kernel =
146 RbfKernel::new(signal_variance, length_scale).expect("kernel parameters are valid");
147 let measure = GaussianMeasure::new(measure_mean, measure_variance)
148 .expect("measure parameters are valid");
149 let standard_deviation = measure.standard_deviation();
150 let lower = measure.mean() - 8.0 * standard_deviation;
151 let upper = measure.mean() + 8.0 * standard_deviation;
152
153 let numerical = simpson_double_integral(
154 |x, y| kernel.covariance(x, y) * measure.density(x) * measure.density(y),
155 lower,
156 upper,
157 400,
158 );
159 let analytic = kernel.kernel_integral(&measure);
160
161 assert_close(analytic, numerical, QUADRATURE_TOLERANCE);
162 }
163 }
164}