scirs2_interpolate/random_features/
mod.rs1pub mod feature_map;
18pub(crate) mod mod_internal;
19pub mod orthogonal;
20pub mod regressor;
21
22pub use feature_map::{FourierFeatureMap, RffKernel};
24pub use orthogonal::OrthogonalFourierFeatureMap;
25pub use regressor::RandomFeaturesRegressor;
26
27use crate::error::InterpolateError;
28
29struct Lcg {
33 state: u64,
34}
35
36impl Lcg {
37 fn new(seed: u64) -> Self {
38 Self {
39 state: seed.wrapping_add(1),
40 }
41 }
42
43 fn next_u64(&mut self) -> u64 {
44 self.state = self
45 .state
46 .wrapping_mul(6_364_136_223_846_793_005)
47 .wrapping_add(1_442_695_040_888_963_407);
48 self.state
49 }
50
51 fn next_f64(&mut self) -> f64 {
53 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
54 }
55
56 fn next_normal(&mut self) -> f64 {
58 loop {
59 let u1 = self.next_f64();
60 let u2 = self.next_f64();
61 if u1 > 1e-300 {
62 return (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
63 }
64 }
65 }
66
67 fn next_cauchy(&mut self) -> f64 {
69 loop {
70 let b = self.next_normal();
71 if b.abs() > 1e-15 {
72 return self.next_normal() / b;
73 }
74 }
75 }
76}
77
78#[non_exhaustive]
82#[derive(Debug, Clone, PartialEq)]
83pub enum KernelType {
84 Gaussian,
86 Laplacian,
88 Cauchy,
90 Matern32,
92 Matern52,
94}
95
96#[derive(Debug, Clone)]
100pub struct RandomFeatureConfig {
101 pub n_features: usize,
103 pub kernel: KernelType,
105 pub bandwidth: f64,
107 pub seed: u64,
109}
110
111impl Default for RandomFeatureConfig {
112 fn default() -> Self {
113 Self {
114 n_features: 500,
115 kernel: KernelType::Gaussian,
116 bandwidth: 1.0,
117 seed: 42,
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
131pub struct RandomFeatureMap {
132 pub weights: Vec<Vec<f64>>,
134 pub biases: Vec<f64>,
136 pub config: RandomFeatureConfig,
138 n_dims: usize,
140}
141
142impl RandomFeatureMap {
143 pub fn new(n_dims: usize, config: RandomFeatureConfig) -> Result<Self, InterpolateError> {
145 if n_dims == 0 {
146 return Err(InterpolateError::InvalidInput {
147 message: "n_dims must be > 0".to_string(),
148 });
149 }
150 if config.n_features == 0 {
151 return Err(InterpolateError::InvalidInput {
152 message: "n_features must be > 0".to_string(),
153 });
154 }
155 if config.bandwidth <= 0.0 {
156 return Err(InterpolateError::InvalidInput {
157 message: "bandwidth must be positive".to_string(),
158 });
159 }
160
161 let mut rng = Lcg::new(config.seed);
162 let d = config.n_features;
163 let bw = config.bandwidth;
164
165 let weights: Vec<Vec<f64>> = (0..d)
166 .map(|_| {
167 (0..n_dims)
168 .map(|_| match config.kernel {
169 KernelType::Gaussian => rng.next_normal() / bw,
170 KernelType::Laplacian | KernelType::Cauchy => rng.next_cauchy() / bw,
171 KernelType::Matern32 => {
172 let g = rng.next_normal() / bw;
175 let chi = {
177 let s: f64 = (0..3).map(|_| rng.next_normal().powi(2)).sum();
178 (s / 3.0).sqrt()
179 };
180 g / chi.max(1e-12)
181 }
182 KernelType::Matern52 => {
183 let g = rng.next_normal() / bw;
185 let chi = {
186 let s: f64 = (0..5).map(|_| rng.next_normal().powi(2)).sum();
187 (s / 5.0).sqrt()
188 };
189 g / chi.max(1e-12)
190 }
191 })
192 .collect()
193 })
194 .collect();
195
196 let biases: Vec<f64> = (0..d)
197 .map(|_| rng.next_f64() * 2.0 * std::f64::consts::PI)
198 .collect();
199
200 Ok(Self {
201 weights,
202 biases,
203 config,
204 n_dims,
205 })
206 }
207
208 pub fn transform(&self, x: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, InterpolateError> {
213 if x.is_empty() {
214 return Ok(Vec::new());
215 }
216 let n = x.len();
217 let d = self.config.n_features;
218 let scale = (2.0 / d as f64).sqrt();
219
220 let mut z = vec![vec![0.0f64; d]; n];
221
222 for (i, xi) in x.iter().enumerate() {
223 if xi.len() != self.n_dims {
224 return Err(InterpolateError::DimensionMismatch(format!(
225 "Expected {} dimensions, got {}",
226 self.n_dims,
227 xi.len()
228 )));
229 }
230 for j in 0..d {
231 let dot: f64 = self.weights[j]
232 .iter()
233 .zip(xi.iter())
234 .map(|(w, xv)| w * xv)
235 .sum();
236 z[i][j] = scale * (dot + self.biases[j]).cos();
237 }
238 }
239
240 Ok(z)
241 }
242
243 pub fn kernel_approx(&self, x1: &[f64], x2: &[f64]) -> Result<f64, InterpolateError> {
245 if x1.len() != self.n_dims || x2.len() != self.n_dims {
246 return Err(InterpolateError::DimensionMismatch(format!(
247 "Expected {} dimensions",
248 self.n_dims
249 )));
250 }
251 let scale = 2.0 / self.config.n_features as f64;
252 let mut result = 0.0f64;
253 for j in 0..self.config.n_features {
254 let dot1: f64 = self.weights[j]
255 .iter()
256 .zip(x1.iter())
257 .map(|(w, xv)| w * xv)
258 .sum();
259 let dot2: f64 = self.weights[j]
260 .iter()
261 .zip(x2.iter())
262 .map(|(w, xv)| w * xv)
263 .sum();
264 result += (dot1 + self.biases[j]).cos() * (dot2 + self.biases[j]).cos();
265 }
266 Ok(scale * result)
267 }
268}
269
270#[derive(Debug, Clone)]
277pub struct RandomFeatureInterpolator {
278 pub feature_map: RandomFeatureMap,
280 pub weights: Vec<f64>,
282 pub regularization: f64,
284 fitted: bool,
286}
287
288impl RandomFeatureInterpolator {
289 pub fn new(
291 n_dims: usize,
292 config: RandomFeatureConfig,
293 regularization: f64,
294 ) -> Result<Self, InterpolateError> {
295 let feature_map = RandomFeatureMap::new(n_dims, config)?;
296 Ok(Self {
297 feature_map,
298 weights: Vec::new(),
299 regularization,
300 fitted: false,
301 })
302 }
303
304 pub fn fit(&mut self, x: &[Vec<f64>], y: &[f64]) -> Result<(), InterpolateError> {
308 if x.is_empty() {
309 return Err(InterpolateError::InsufficientData(
310 "Training data is empty".to_string(),
311 ));
312 }
313 if x.len() != y.len() {
314 return Err(InterpolateError::DimensionMismatch(format!(
315 "x has {} rows but y has {} elements",
316 x.len(),
317 y.len()
318 )));
319 }
320
321 let z = self.feature_map.transform(x)?;
322 let n = z.len();
323 let d = self.feature_map.config.n_features;
324
325 let mut ztzt = vec![vec![0.0f64; d]; d];
327 for i in 0..d {
328 for j in 0..=i {
329 let val: f64 = (0..n).map(|k| z[k][i] * z[k][j]).sum();
330 ztzt[i][j] = val;
331 ztzt[j][i] = val;
332 }
333 ztzt[i][i] += self.regularization;
334 }
335
336 let mut zty = vec![0.0f64; d];
338 for j in 0..d {
339 zty[j] = (0..n).map(|k| z[k][j] * y[k]).sum();
340 }
341
342 self.weights = cholesky_solve(&ztzt, &zty)?;
344 self.fitted = true;
345 Ok(())
346 }
347
348 pub fn predict(&self, x: &[Vec<f64>]) -> Result<Vec<f64>, InterpolateError> {
350 if !self.fitted {
351 return Err(InterpolateError::InvalidState(
352 "Model not fitted yet, call fit() first".to_string(),
353 ));
354 }
355 let z = self.feature_map.transform(x)?;
356 let preds: Vec<f64> = z
357 .iter()
358 .map(|zi| zi.iter().zip(self.weights.iter()).map(|(a, b)| a * b).sum())
359 .collect();
360 Ok(preds)
361 }
362
363 pub fn kernel_error(
366 &self,
367 x: &[Vec<f64>],
368 true_kernel_fn: impl Fn(&[f64], &[f64]) -> f64,
369 ) -> Result<f64, InterpolateError> {
370 let n = x.len();
371 if n == 0 {
372 return Ok(0.0);
373 }
374 let mut total_err = 0.0f64;
375 let mut count = 0usize;
376 for i in 0..n {
377 for j in 0..n {
378 let approx = self.feature_map.kernel_approx(&x[i], &x[j])?;
379 let exact = true_kernel_fn(&x[i], &x[j]);
380 total_err += (approx - exact).abs();
381 count += 1;
382 }
383 }
384 if count == 0 {
385 Ok(0.0)
386 } else {
387 Ok(total_err / count as f64)
388 }
389 }
390}
391
392pub(crate) fn cholesky_solve(a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>, InterpolateError> {
397 let n = a.len();
398 if n == 0 {
399 return Ok(Vec::new());
400 }
401
402 let mut l = vec![vec![0.0f64; n]; n];
404 for i in 0..n {
405 for j in 0..=i {
406 let mut s: f64 = a[i][j];
407 for k in 0..j {
408 s -= l[i][k] * l[j][k];
409 }
410 if i == j {
411 if s < 0.0 {
412 return cholesky_solve_fallback(a, b);
414 }
415 l[i][j] = s.sqrt().max(1e-300);
416 } else {
417 l[i][j] = s / l[j][j].max(1e-300);
418 }
419 }
420 }
421
422 let mut y = vec![0.0f64; n];
424 for i in 0..n {
425 let mut s = b[i];
426 for k in 0..i {
427 s -= l[i][k] * y[k];
428 }
429 y[i] = s / l[i][i].max(1e-300);
430 }
431
432 let mut x = vec![0.0f64; n];
434 for i in (0..n).rev() {
435 let mut s = y[i];
436 for k in (i + 1)..n {
437 s -= l[k][i] * x[k];
438 }
439 x[i] = s / l[i][i].max(1e-300);
440 }
441
442 Ok(x)
443}
444
445fn cholesky_solve_fallback(a: &[Vec<f64>], b: &[f64]) -> Result<Vec<f64>, InterpolateError> {
447 let n = a.len();
449 let mut x = vec![0.0f64; n];
450 let mut r = b.to_vec();
451 let mut p = r.clone();
452 let mut rs_old: f64 = r.iter().map(|v| v * v).sum();
453
454 for _ in 0..(n * 10) {
455 let ap: Vec<f64> = (0..n)
457 .map(|i| (0..n).map(|j| a[i][j] * p[j]).sum())
458 .collect();
459 let pap: f64 = p.iter().zip(ap.iter()).map(|(a, b)| a * b).sum();
460 if pap.abs() < 1e-300 {
461 break;
462 }
463 let alpha = rs_old / pap;
464 for i in 0..n {
465 x[i] += alpha * p[i];
466 r[i] -= alpha * ap[i];
467 }
468 let rs_new: f64 = r.iter().map(|v| v * v).sum();
469 if rs_new.sqrt() < 1e-10 {
470 break;
471 }
472 let beta = rs_new / rs_old;
473 for i in 0..n {
474 p[i] = r[i] + beta * p[i];
475 }
476 rs_old = rs_new;
477 }
478
479 Ok(x)
480}
481
482#[cfg(test)]
485mod tests {
486 use super::*;
487
488 fn gaussian_kernel(bw: f64, x1: &[f64], x2: &[f64]) -> f64 {
489 let sq: f64 = x1.iter().zip(x2.iter()).map(|(a, b)| (a - b).powi(2)).sum();
490 (-sq / (2.0 * bw * bw)).exp()
491 }
492
493 #[test]
494 fn test_gaussian_kernel_approximation_error() {
495 let config = RandomFeatureConfig {
496 n_features: 200,
497 kernel: KernelType::Gaussian,
498 bandwidth: 1.0,
499 seed: 12345,
500 };
501 let map = RandomFeatureMap::new(2, config).expect("should create map");
502
503 let xs: Vec<Vec<f64>> = (0..5)
505 .flat_map(|i| (0..5).map(move |j| vec![i as f64 * 0.5, j as f64 * 0.5]))
506 .collect();
507
508 let mut total_err = 0.0f64;
509 let mut count = 0usize;
510 let bw = 1.0;
511 for x1 in &xs {
512 for x2 in &xs {
513 let approx = map.kernel_approx(x1, x2).expect("kernel approx");
514 let exact = gaussian_kernel(bw, x1, x2);
515 total_err += (approx - exact).abs();
516 count += 1;
517 }
518 }
519 let mean_err = total_err / count as f64;
520 assert!(
521 mean_err < 0.15,
522 "Mean kernel approximation error too large: {mean_err}"
523 );
524 }
525
526 #[test]
527 fn test_fit_predict_sin() {
528 let x_train: Vec<Vec<f64>> = (0..30)
530 .map(|i| vec![i as f64 * 2.0 * std::f64::consts::PI / 30.0])
531 .collect();
532 let y_train: Vec<f64> = x_train.iter().map(|xi| xi[0].sin()).collect();
533
534 let config = RandomFeatureConfig {
535 n_features: 100,
536 kernel: KernelType::Gaussian,
537 bandwidth: 1.0,
538 seed: 999,
539 };
540 let mut interp =
541 RandomFeatureInterpolator::new(1, config, 1e-4).expect("create interpolator");
542 interp.fit(&x_train, &y_train).expect("fit");
543
544 let x_test: Vec<Vec<f64>> = (0..10)
545 .map(|i| vec![i as f64 * 2.0 * std::f64::consts::PI / 10.0])
546 .collect();
547 let preds = interp.predict(&x_test).expect("predict");
548 assert_eq!(preds.len(), 10, "Prediction count mismatch");
549 }
550
551 #[test]
552 fn test_predict_shape_correct() {
553 let n = 15;
554 let x: Vec<Vec<f64>> = (0..n).map(|i| vec![i as f64 * 0.1]).collect();
555 let y: Vec<f64> = x.iter().map(|xi| xi[0] * 2.0 + 1.0).collect();
556
557 let config = RandomFeatureConfig {
558 n_features: 50,
559 kernel: KernelType::Laplacian,
560 bandwidth: 0.5,
561 seed: 7,
562 };
563 let mut interp = RandomFeatureInterpolator::new(1, config, 1e-3).expect("create");
564 interp.fit(&x, &y).expect("fit");
565
566 let x_new: Vec<Vec<f64>> = (0..7).map(|i| vec![i as f64 * 0.15]).collect();
567 let preds = interp.predict(&x_new).expect("predict");
568 assert_eq!(preds.len(), 7);
569 }
570
571 #[test]
572 fn test_kernel_types() {
573 for kernel in [
574 KernelType::Gaussian,
575 KernelType::Laplacian,
576 KernelType::Cauchy,
577 KernelType::Matern32,
578 KernelType::Matern52,
579 ] {
580 let config = RandomFeatureConfig {
581 n_features: 50,
582 kernel,
583 bandwidth: 1.0,
584 seed: 1,
585 };
586 let map = RandomFeatureMap::new(2, config).expect("create");
587 let x1 = vec![0.0, 0.0];
588 let x2 = vec![1.0, 1.0];
589 let k = map.kernel_approx(&x1, &x2).expect("kernel approx");
590 assert!(k.is_finite(), "Kernel value should be finite");
591 }
592 }
593
594 #[test]
595 fn test_kernel_error_gaussian() {
596 let config = RandomFeatureConfig {
597 n_features: 300,
598 kernel: KernelType::Gaussian,
599 bandwidth: 1.0,
600 seed: 42,
601 };
602 let x_train: Vec<Vec<f64>> = (0..5)
603 .map(|i| vec![i as f64 * 0.4, i as f64 * 0.2])
604 .collect();
605 let y_train: Vec<f64> = x_train.iter().map(|xi| xi[0].sin()).collect();
606
607 let mut interp = RandomFeatureInterpolator::new(2, config, 1e-4).expect("create");
608 interp.fit(&x_train, &y_train).expect("fit");
609
610 let bw = 1.0f64;
611 let err = interp
612 .kernel_error(&x_train, |x1, x2| {
613 let sq: f64 = x1.iter().zip(x2.iter()).map(|(a, b)| (a - b).powi(2)).sum();
614 (-sq / (2.0 * bw * bw)).exp()
615 })
616 .expect("kernel error");
617 assert!(err < 0.15, "Kernel error too large: {err}");
618 }
619}