qualia_core_db/solvers/learning/gaussian_process/
mod.rs1use crate::solvers::learning::LearningError;
14use crate::solvers::linear_algebra::cholesky::{cholesky_factor, cholesky_solve};
15
16#[derive(Debug, Clone)]
18pub struct GpRegressor {
19 x_train: Vec<f64>,
20 alpha: Vec<f64>, l: Vec<f64>, length_scale: f64,
23 signal_var: f64,
24 noise_var: f64,
25 n: usize,
26 p: usize,
27}
28
29fn rbf(a: &[f64], b: &[f64], length_scale: f64, signal_var: f64) -> f64 {
31 let d2: f64 = a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum();
32 signal_var * (-d2 / (2.0 * length_scale * length_scale)).exp()
33}
34
35impl GpRegressor {
36 pub fn fit(
40 x: &[f64],
41 y: &[f64],
42 n: usize,
43 p: usize,
44 length_scale: f64,
45 signal_var: f64,
46 noise_var: f64,
47 ) -> Result<Self, LearningError> {
48 if n == 0 || p == 0 || x.len() != n * p || y.len() != n {
49 return Err(LearningError::InvalidDimension);
50 }
51 if !(length_scale > 0.0) || !(signal_var > 0.0) || !(noise_var > 0.0) {
52 return Err(LearningError::InsufficientData);
53 }
54
55 let mut k = vec![0.0; n * n];
57 for i in 0..n {
58 for j in i..n {
59 let v = rbf(
60 &x[i * p..(i + 1) * p],
61 &x[j * p..(j + 1) * p],
62 length_scale,
63 signal_var,
64 );
65 k[i * n + j] = v;
66 k[j * n + i] = v;
67 }
68 k[i * n + i] += noise_var;
69 }
70
71 let mut l = vec![0.0; n * n];
72 cholesky_factor(n, &k, &mut l).map_err(|_| LearningError::Singular)?;
73 let mut alpha = vec![0.0; n];
74 cholesky_solve(n, &l, y, &mut alpha)?;
75
76 Ok(Self {
77 x_train: x.to_vec(),
78 alpha,
79 l,
80 length_scale,
81 signal_var,
82 noise_var,
83 n,
84 p,
85 })
86 }
87
88 pub fn noise_variance(&self) -> f64 {
90 self.noise_var
91 }
92
93 pub fn predict_row(&self, x_star: &[f64]) -> (f64, f64) {
96 let mut kstar = vec![0.0; self.n];
98 for i in 0..self.n {
99 kstar[i] = rbf(
100 &self.x_train[i * self.p..(i + 1) * self.p],
101 x_star,
102 self.length_scale,
103 self.signal_var,
104 );
105 }
106 let mean: f64 = kstar.iter().zip(&self.alpha).map(|(k, a)| k * a).sum();
107 let mut v = vec![0.0; self.n];
109 let _ = cholesky_solve(self.n, &self.l, &kstar, &mut v);
110 let reduction: f64 = kstar.iter().zip(&v).map(|(k, vi)| k * vi).sum();
111 let kxx = self.signal_var; let floor = self.noise_var;
113 (mean, (kxx - reduction).max(floor))
114 }
115
116 pub fn predict(&self, x: &[f64], m: usize) -> Vec<(f64, f64)> {
118 (0..m)
119 .map(|i| self.predict_row(&x[i * self.p..(i + 1) * self.p]))
120 .collect()
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn interpolates_training_points_with_low_variance() {
130 let x: Vec<f64> = (0..8).map(|i| i as f64).collect();
133 let y: Vec<f64> = x.iter().map(|&xi| (xi * 0.7).sin()).collect();
134 let gp = GpRegressor::fit(&x, &y, 8, 1, 1.0, 1.0, 1e-6).unwrap();
135 for i in 0..8 {
136 let (m, v) = gp.predict_row(&[x[i]]);
137 assert!(
138 (m - y[i]).abs() < 1e-2,
139 "mean at train point {i}: {m} vs {}",
140 y[i]
141 );
142 assert!(v < 1e-2, "variance at train point should be small: {v}");
143 }
144 }
145
146 #[test]
147 fn variance_grows_far_from_data() {
148 let x: Vec<f64> = (0..6).map(|i| i as f64).collect();
149 let y: Vec<f64> = x.iter().map(|&xi| 0.5 * xi).collect();
150 let gp = GpRegressor::fit(&x, &y, 6, 1, 1.0, 1.0, 1e-4).unwrap();
151 let (_, v_near) = gp.predict_row(&[2.5]); let (_, v_far) = gp.predict_row(&[50.0]); assert!(
154 v_far > v_near,
155 "var_far {v_far} should exceed var_near {v_near}"
156 );
157 assert!((v_far - 1.0).abs() < 1e-3);
159 }
160
161 #[test]
162 fn predicts_a_smooth_function_between_points() {
163 let x: Vec<f64> = (0..13).map(|i| i as f64 * 0.5).collect();
165 let y: Vec<f64> = x.iter().map(|&xi| xi.sin()).collect();
166 let gp = GpRegressor::fit(&x, &y, 13, 1, 1.0, 1.0, 1e-6).unwrap();
167 let (m, _) = gp.predict_row(&[1.25]); assert!(
169 (m - (1.25f64).sin()).abs() < 0.1,
170 "interp {m} vs {}",
171 (1.25f64).sin()
172 );
173 }
174
175 #[test]
176 fn guards() {
177 assert_eq!(
178 GpRegressor::fit(&[1.0, 2.0], &[1.0], 2, 1, 1.0, 1.0, 1e-3).unwrap_err(),
179 LearningError::InvalidDimension
180 );
181 assert_eq!(
182 GpRegressor::fit(&[1.0, 2.0], &[1.0, 2.0], 2, 1, 0.0, 1.0, 1e-3).unwrap_err(),
183 LearningError::InsufficientData
184 );
185 }
186}