1use crate::KernelError;
4
5pub trait ScalarKernel {
11 #[must_use]
16 fn covariance(&self, x: f64, y: f64) -> f64;
17}
18
19#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct RbfKernel {
30 signal_variance: f64,
31 length_scale: f64,
32}
33
34impl RbfKernel {
35 pub fn new(signal_variance: f64, length_scale: f64) -> Result<Self, KernelError> {
42 if !signal_variance.is_finite() {
43 return Err(KernelError::NonFiniteSignalVariance);
44 }
45 if signal_variance <= 0.0 {
46 return Err(KernelError::NonPositiveSignalVariance);
47 }
48 if !length_scale.is_finite() {
49 return Err(KernelError::NonFiniteLengthScale);
50 }
51 if length_scale <= 0.0 {
52 return Err(KernelError::NonPositiveLengthScale);
53 }
54
55 Ok(Self {
56 signal_variance,
57 length_scale,
58 })
59 }
60
61 #[must_use]
63 pub const fn signal_variance(&self) -> f64 {
64 self.signal_variance
65 }
66
67 #[must_use]
69 pub const fn length_scale(&self) -> f64 {
70 self.length_scale
71 }
72}
73
74impl ScalarKernel for RbfKernel {
75 fn covariance(&self, x: f64, y: f64) -> f64 {
76 let scaled_distance = (x - y) / self.length_scale;
77 self.signal_variance * (-0.5 * scaled_distance * scaled_distance).exp()
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::{RbfKernel, ScalarKernel};
84 use crate::KernelError;
85
86 const TOLERANCE: f64 = 1.0e-12;
87
88 fn assert_close(actual: f64, expected: f64) {
89 let scale = expected.abs().max(1.0);
90 assert!(
91 (actual - expected).abs() <= TOLERANCE * scale,
92 "expected {expected:.16e}, got {actual:.16e}"
93 );
94 }
95
96 #[test]
97 fn rejects_invalid_signal_variance() {
98 assert_eq!(
99 RbfKernel::new(f64::NAN, 1.0),
100 Err(KernelError::NonFiniteSignalVariance)
101 );
102 assert_eq!(
103 RbfKernel::new(f64::INFINITY, 1.0),
104 Err(KernelError::NonFiniteSignalVariance)
105 );
106 assert_eq!(
107 RbfKernel::new(0.0, 1.0),
108 Err(KernelError::NonPositiveSignalVariance)
109 );
110 assert_eq!(
111 RbfKernel::new(-1.0, 1.0),
112 Err(KernelError::NonPositiveSignalVariance)
113 );
114 }
115
116 #[test]
117 fn rejects_invalid_length_scale() {
118 assert_eq!(
119 RbfKernel::new(1.0, f64::NAN),
120 Err(KernelError::NonFiniteLengthScale)
121 );
122 assert_eq!(
123 RbfKernel::new(1.0, f64::INFINITY),
124 Err(KernelError::NonFiniteLengthScale)
125 );
126 assert_eq!(
127 RbfKernel::new(1.0, 0.0),
128 Err(KernelError::NonPositiveLengthScale)
129 );
130 assert_eq!(
131 RbfKernel::new(1.0, -1.0),
132 Err(KernelError::NonPositiveLengthScale)
133 );
134 }
135
136 #[test]
137 fn exposes_validated_parameters() {
138 let kernel = RbfKernel::new(2.5, 0.75).expect("parameters are valid");
139
140 assert_close(kernel.signal_variance(), 2.5);
141 assert_close(kernel.length_scale(), 0.75);
142 }
143
144 #[test]
145 fn diagonal_equals_signal_variance() {
146 let kernel = RbfKernel::new(3.0, 1.4).expect("parameters are valid");
147
148 for x in [-10.0, -1.0, 0.0, 0.5, 12.0] {
149 assert_close(kernel.covariance(x, x), 3.0);
150 }
151 }
152
153 #[test]
154 fn covariance_is_symmetric() {
155 let kernel = RbfKernel::new(1.7, 0.9).expect("parameters are valid");
156
157 for (x, y) in [(-3.0, 0.5), (-0.2, 4.1), (1.0, 1.0), (8.0, -2.0)] {
158 assert_close(kernel.covariance(x, y), kernel.covariance(y, x));
159 }
160 }
161
162 #[test]
163 fn covariance_is_stationary() {
164 let kernel = RbfKernel::new(1.2, 2.3).expect("parameters are valid");
165 let shift = 17.0;
166
167 assert_close(
168 kernel.covariance(-1.5, 3.25),
169 kernel.covariance(-1.5 + shift, 3.25 + shift),
170 );
171 }
172
173 #[test]
174 fn covariance_decays_with_distance() {
175 let kernel = RbfKernel::new(2.0, 1.0).expect("parameters are valid");
176 let at_zero = kernel.covariance(0.0, 0.0);
177 let at_one = kernel.covariance(0.0, 1.0);
178 let at_two = kernel.covariance(0.0, 2.0);
179
180 assert!(at_zero > at_one);
181 assert!(at_one > at_two);
182 assert!(at_two > 0.0);
183 }
184
185 #[test]
186 fn representative_gram_matrix_is_positive_semidefinite() {
187 let kernel = RbfKernel::new(1.3, 0.8).expect("parameters are valid");
188 let points = [-1.0, -0.25, 0.5, 2.0];
189 let coefficient_sets = [
190 [1.0, 0.0, 0.0, 0.0],
191 [1.0, -1.0, 0.5, 0.25],
192 [-2.0, 0.75, 1.25, -0.5],
193 [1.0, 1.0, 1.0, 1.0],
194 ];
195
196 for coefficients in coefficient_sets {
197 let quadratic_form = coefficients
198 .iter()
199 .enumerate()
200 .map(|(i, &left)| {
201 coefficients
202 .iter()
203 .enumerate()
204 .map(|(j, &right)| left * kernel.covariance(points[i], points[j]) * right)
205 .sum::<f64>()
206 })
207 .sum::<f64>();
208
209 assert!(
210 quadratic_form >= -TOLERANCE,
211 "quadratic form should be non-negative, got {quadratic_form:.16e}"
212 );
213 }
214 }
215}