1use crate::error::{CsError, CsResult};
41use crate::linalg::cholesky::{cholesky_factor, cholesky_solve};
42use crate::linalg::{mat_t_vec, mat_vec, norm2};
43
44#[derive(Debug, Clone)]
50pub struct RvmFit {
51 pub weights: Vec<f64>,
53 pub alpha: Vec<f64>,
55 pub relevance: Vec<usize>,
57 pub beta: f64,
59 pub iterations: usize,
61}
62
63const ALPHA_PRUNE: f64 = 1e12;
65
66pub fn rvm_fit_design(
79 phi: &[f64],
80 m: usize,
81 n: usize,
82 t: &[f64],
83 max_iter: usize,
84 tol: f64,
85) -> CsResult<RvmFit> {
86 if m == 0 || n == 0 {
87 return Err(CsError::InvalidParameter("rvm: m and n must be > 0".into()));
88 }
89 if phi.len() != m * n {
90 return Err(CsError::ShapeMismatch {
91 expected: vec![m, n],
92 got: vec![phi.len()],
93 });
94 }
95 if t.len() != m {
96 return Err(CsError::DimensionMismatch { a: t.len(), b: m });
97 }
98
99 let gram = gram_matrix(phi, m, n);
101 let phi_t_t = mat_t_vec(phi, m, n, t)?;
102
103 let mut alpha = vec![1.0_f64; n];
105 let t_var = (norm2(t) * norm2(t) / m as f64).max(1e-6);
106 let mut beta = 1.0 / (0.1 * t_var).max(1e-6); let mut mu = vec![0.0_f64; n];
108 let mut iterations = 0usize;
109
110 for _ in 0..max_iter {
111 iterations += 1;
112 let active: Vec<usize> = (0..n).filter(|&j| alpha[j] < ALPHA_PRUNE).collect();
113 if active.is_empty() {
114 break;
115 }
116 let k = active.len();
117
118 let mut h = vec![0.0_f64; k * k];
120 for (ii, &i) in active.iter().enumerate() {
121 for (jj, &j) in active.iter().enumerate() {
122 h[ii * k + jj] = beta * gram[i * n + j];
123 }
124 h[ii * k + ii] += alpha[i];
125 }
126 let l = cholesky_factor(&h, k)?;
127
128 let mut rhs = vec![0.0_f64; k];
130 for (ii, &i) in active.iter().enumerate() {
131 rhs[ii] = beta * phi_t_t[i];
132 }
133 let mu_active = cholesky_solve(&l, k, &rhs)?;
134
135 let mut sigma_diag = vec![0.0_f64; k];
137 for jj in 0..k {
138 let mut ej = vec![0.0_f64; k];
139 ej[jj] = 1.0;
140 let s = cholesky_solve(&l, k, &ej)?;
141 sigma_diag[jj] = s[jj];
142 }
143
144 let mut max_log_change = 0.0_f64;
146 let mut gamma_sum = 0.0_f64;
147 let mut new_mu = vec![0.0_f64; n];
148 for (jj, &j) in active.iter().enumerate() {
149 let mu_j = mu_active[jj];
150 new_mu[j] = mu_j;
151 let gamma_j = (1.0 - alpha[j] * sigma_diag[jj]).clamp(1e-12, 1.0);
152 gamma_sum += gamma_j;
153 let mu_sq = (mu_j * mu_j).max(1e-300);
154 let alpha_new = (gamma_j / mu_sq).min(ALPHA_PRUNE * 10.0);
155 let log_change = (alpha_new.max(1e-300).ln() - alpha[j].max(1e-300).ln()).abs();
156 max_log_change = max_log_change.max(log_change);
157 alpha[j] = alpha_new;
158 }
159 mu = new_mu;
160
161 let phi_mu = mat_vec(phi, m, n, &mu)?;
163 let mut resid_sq = 0.0_f64;
164 for i in 0..m {
165 let d = t[i] - phi_mu[i];
166 resid_sq += d * d;
167 }
168 let denom = (m as f64 - gamma_sum).max(1e-6);
169 beta = (denom / resid_sq.max(1e-12)).clamp(1e-6, 1e12);
170
171 if max_log_change < tol {
172 break;
173 }
174 }
175
176 let relevance: Vec<usize> = (0..n).filter(|&j| alpha[j] < ALPHA_PRUNE).collect();
177 for j in 0..n {
179 if alpha[j] >= ALPHA_PRUNE {
180 mu[j] = 0.0;
181 alpha[j] = f64::INFINITY;
182 }
183 }
184
185 Ok(RvmFit {
186 weights: mu,
187 alpha,
188 relevance,
189 beta,
190 iterations,
191 })
192}
193
194fn gram_matrix(phi: &[f64], m: usize, n: usize) -> Vec<f64> {
196 let mut g = vec![0.0_f64; n * n];
197 for r in 0..m {
198 let row = r * n;
199 for i in 0..n {
200 let pri = phi[row + i];
201 if pri == 0.0 {
202 continue;
203 }
204 for j in 0..n {
205 g[i * n + j] += pri * phi[row + j];
206 }
207 }
208 }
209 g
210}
211
212#[derive(Debug, Clone)]
218pub enum RvmKernel {
219 Linear,
221 Rbf {
223 length_scale: f64,
225 },
226 Polynomial {
228 coef0: f64,
230 degree: u32,
232 },
233}
234
235impl RvmKernel {
236 fn eval(&self, a: &[f64], b: &[f64]) -> f64 {
238 let dot: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
239 match *self {
240 RvmKernel::Linear => dot,
241 RvmKernel::Rbf { length_scale } => {
242 let mut sq = 0.0_f64;
243 for (x, y) in a.iter().zip(b.iter()) {
244 let d = x - y;
245 sq += d * d;
246 }
247 (-sq / (2.0 * length_scale * length_scale)).exp()
248 }
249 RvmKernel::Polynomial { coef0, degree } => (dot + coef0).powi(degree as i32),
250 }
251 }
252}
253
254#[derive(Debug, Clone)]
256pub struct RvmConfig {
257 pub kernel: RvmKernel,
259 pub use_bias: bool,
261 pub max_iter: usize,
263 pub tol: f64,
265}
266
267impl Default for RvmConfig {
268 fn default() -> Self {
269 Self {
270 kernel: RvmKernel::Rbf { length_scale: 1.0 },
271 use_bias: true,
272 max_iter: 300,
273 tol: 1e-4,
274 }
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct Rvm {
281 config: RvmConfig,
282 rv_inputs: Vec<f64>,
284 rv_weights: Vec<f64>,
286 bias: f64,
288 d: usize,
290 beta: f64,
292}
293
294impl Rvm {
295 pub fn fit(x: &[f64], n: usize, d: usize, t: &[f64], config: RvmConfig) -> CsResult<Self> {
302 if n == 0 || d == 0 {
303 return Err(CsError::InvalidParameter("rvm: n and d must be > 0".into()));
304 }
305 if x.len() != n * d {
306 return Err(CsError::ShapeMismatch {
307 expected: vec![n, d],
308 got: vec![x.len()],
309 });
310 }
311 if t.len() != n {
312 return Err(CsError::DimensionMismatch { a: t.len(), b: n });
313 }
314 if let RvmKernel::Rbf { length_scale } = config.kernel {
315 if length_scale <= 0.0 || !length_scale.is_finite() {
316 return Err(CsError::InvalidParameter(format!(
317 "rvm: RBF length_scale must be > 0, got {length_scale}"
318 )));
319 }
320 }
321
322 let n_basis = if config.use_bias { n + 1 } else { n };
324 let mut phi = vec![0.0_f64; n * n_basis];
325 for i in 0..n {
326 let xi = &x[i * d..(i + 1) * d];
327 for j in 0..n {
328 let xj = &x[j * d..(j + 1) * d];
329 phi[i * n_basis + j] = config.kernel.eval(xi, xj);
330 }
331 if config.use_bias {
332 phi[i * n_basis + n] = 1.0;
333 }
334 }
335
336 let fit = rvm_fit_design(&phi, n, n_basis, t, config.max_iter, config.tol)?;
337
338 let mut rv_inputs = Vec::new();
340 let mut rv_weights = Vec::new();
341 let mut bias = 0.0_f64;
342 for &j in &fit.relevance {
343 if config.use_bias && j == n {
344 bias = fit.weights[j];
345 } else {
346 rv_inputs.extend_from_slice(&x[j * d..(j + 1) * d]);
347 rv_weights.push(fit.weights[j]);
348 }
349 }
350
351 Ok(Self {
352 config,
353 rv_inputs,
354 rv_weights,
355 bias,
356 d,
357 beta: fit.beta,
358 })
359 }
360
361 #[must_use]
363 pub fn n_relevance(&self) -> usize {
364 self.rv_weights.len()
365 }
366
367 #[must_use]
369 pub fn noise_std(&self) -> f64 {
370 1.0 / self.beta.max(1e-300).sqrt()
371 }
372
373 pub fn predict_one(&self, query: &[f64]) -> CsResult<f64> {
378 if query.len() != self.d {
379 return Err(CsError::DimensionMismatch {
380 a: query.len(),
381 b: self.d,
382 });
383 }
384 let mut y = self.bias;
385 for (k, w) in self.rv_weights.iter().enumerate() {
386 let rv = &self.rv_inputs[k * self.d..(k + 1) * self.d];
387 y += w * self.config.kernel.eval(query, rv);
388 }
389 Ok(y)
390 }
391
392 pub fn predict(&self, queries: &[f64], n_q: usize) -> CsResult<Vec<f64>> {
397 if queries.len() != n_q * self.d {
398 return Err(CsError::ShapeMismatch {
399 expected: vec![n_q, self.d],
400 got: vec![queries.len()],
401 });
402 }
403 let mut out = Vec::with_capacity(n_q);
404 for i in 0..n_q {
405 out.push(self.predict_one(&queries[i * self.d..(i + 1) * self.d])?);
406 }
407 Ok(out)
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn fixed_design_identity_is_sparse() {
417 let phi = {
419 let mut p = vec![0.0_f64; 16];
420 for i in 0..4 {
421 p[i * 4 + i] = 1.0;
422 }
423 p
424 };
425 let t = [1.0_f64, 0.0, 0.0, 2.0];
426 let fit = rvm_fit_design(&phi, 4, 4, &t, 200, 1e-6).expect("ok");
427 assert!(fit.weights[0].abs() > 0.5, "w0 = {}", fit.weights[0]);
428 assert!(fit.weights[3].abs() > 1.0, "w3 = {}", fit.weights[3]);
429 assert!(fit.weights[1].abs() < 1e-3, "w1 = {}", fit.weights[1]);
430 assert!(fit.weights[2].abs() < 1e-3, "w2 = {}", fit.weights[2]);
431 assert!(fit.relevance.contains(&0) && fit.relevance.contains(&3));
432 assert!(!fit.relevance.contains(&1) && !fit.relevance.contains(&2));
433 }
434
435 #[test]
436 fn fixed_design_pruned_alpha_infinite() {
437 let phi = {
438 let mut p = vec![0.0_f64; 9];
439 for i in 0..3 {
440 p[i * 3 + i] = 1.0;
441 }
442 p
443 };
444 let t = [5.0_f64, 0.0, 0.0];
445 let fit = rvm_fit_design(&phi, 3, 3, &t, 200, 1e-6).expect("ok");
446 assert!(fit.alpha[1].is_infinite());
447 assert!(fit.alpha[2].is_infinite());
448 assert!(fit.alpha[0].is_finite());
449 }
450
451 #[test]
452 fn linear_kernel_recovers_line() {
453 let n = 12usize;
455 let xs: Vec<f64> = (0..n).map(|i| i as f64 / (n as f64)).collect();
456 let t: Vec<f64> = xs.iter().map(|&x| 2.0 * x).collect();
457 let cfg = RvmConfig {
458 kernel: RvmKernel::Linear,
459 use_bias: true,
460 max_iter: 400,
461 tol: 1e-6,
462 };
463 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
464 let pred = rvm.predict_one(&[0.5]).expect("ok");
465 assert!((pred - 1.0).abs() < 0.1, "pred(0.5) = {pred}");
466 }
467
468 #[test]
469 fn rbf_kernel_fits_sine() {
470 let n = 25usize;
472 let xs: Vec<f64> = (0..n)
473 .map(|i| -3.0 + 6.0 * i as f64 / (n as f64 - 1.0))
474 .collect();
475 let t: Vec<f64> = xs.iter().map(|&x| x.sin()).collect();
476 let cfg = RvmConfig {
477 kernel: RvmKernel::Rbf { length_scale: 1.0 },
478 use_bias: true,
479 max_iter: 500,
480 tol: 1e-5,
481 };
482 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
483 let pred = rvm.predict_one(&[0.0]).expect("ok");
485 assert!(pred.abs() < 0.2, "pred(0) = {pred} (sin 0 = 0)");
486 let pred2 = rvm.predict_one(&[std::f64::consts::FRAC_PI_2]).expect("ok"); assert!((pred2 - 1.0).abs() < 0.25, "pred(π/2) = {pred2}");
488 assert!(rvm.n_relevance() <= n, "n_rv = {}", rvm.n_relevance());
490 }
491
492 #[test]
493 fn predict_batch_matches_single() {
494 let n = 8usize;
495 let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.3).collect();
496 let t: Vec<f64> = xs.iter().map(|&x| 0.5 * x + 1.0).collect();
497 let cfg = RvmConfig {
498 kernel: RvmKernel::Linear,
499 ..Default::default()
500 };
501 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
502 let q = vec![0.1_f64, 0.7, 1.4];
503 let batch = rvm.predict(&q, 3).expect("ok");
504 for (i, qi) in q.iter().enumerate() {
505 let single = rvm.predict_one(&[*qi]).expect("ok");
506 assert!((batch[i] - single).abs() < 1e-12);
507 }
508 }
509
510 #[test]
511 fn multidim_input_rbf() {
512 let pts = [
514 [0.0_f64, 0.0],
515 [1.0, 0.0],
516 [0.0, 1.0],
517 [1.0, 1.0],
518 [0.5, 0.5],
519 [0.2, 0.8],
520 ];
521 let n = pts.len();
522 let mut x = Vec::new();
523 let mut t = Vec::new();
524 for p in &pts {
525 x.extend_from_slice(p);
526 t.push(p[0] + p[1]);
527 }
528 let cfg = RvmConfig {
529 kernel: RvmKernel::Rbf { length_scale: 1.0 },
530 max_iter: 400,
531 ..Default::default()
532 };
533 let rvm = Rvm::fit(&x, n, 2, &t, cfg).expect("ok");
534 let pred = rvm.predict_one(&[1.0, 1.0]).expect("ok");
535 assert!((pred - 2.0).abs() < 0.4, "pred(1,1) = {pred}");
536 }
537
538 #[test]
539 fn polynomial_kernel_quadratic() {
540 let n = 10usize;
542 let xs: Vec<f64> = (0..n).map(|i| -1.0 + 2.0 * i as f64 / 9.0).collect();
543 let t: Vec<f64> = xs.iter().map(|&x| x * x).collect();
544 let cfg = RvmConfig {
545 kernel: RvmKernel::Polynomial {
546 coef0: 1.0,
547 degree: 2,
548 },
549 use_bias: true,
550 max_iter: 500,
551 tol: 1e-6,
552 };
553 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
554 let pred = rvm.predict_one(&[0.5]).expect("ok");
555 assert!((pred - 0.25).abs() < 0.1, "pred(0.5) = {pred}");
556 }
557
558 #[test]
559 fn noise_std_positive() {
560 let n = 6usize;
561 let xs: Vec<f64> = (0..n).map(|i| i as f64).collect();
562 let t: Vec<f64> = xs.to_vec();
563 let cfg = RvmConfig {
564 kernel: RvmKernel::Linear,
565 ..Default::default()
566 };
567 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
568 assert!(rvm.noise_std() > 0.0 && rvm.noise_std().is_finite());
569 assert!(rvm.beta > 0.0);
570 }
571
572 #[test]
573 fn predict_dimension_mismatch() {
574 let n = 4usize;
575 let xs = vec![0.0_f64, 1.0, 2.0, 3.0];
576 let t = vec![0.0_f64, 1.0, 2.0, 3.0];
577 let rvm = Rvm::fit(
578 &xs,
579 n,
580 1,
581 &t,
582 RvmConfig {
583 kernel: RvmKernel::Linear,
584 ..Default::default()
585 },
586 )
587 .expect("ok");
588 assert!(matches!(
589 rvm.predict_one(&[1.0, 2.0]),
590 Err(CsError::DimensionMismatch { .. })
591 ));
592 assert!(matches!(
594 rvm.predict(&[1.0, 2.0, 3.0], 5),
595 Err(CsError::ShapeMismatch { .. })
596 ));
597 }
598
599 #[test]
600 fn fit_rejects_bad_shapes() {
601 assert!(matches!(
603 rvm_fit_design(&[1.0, 0.0, 0.0, 1.0], 2, 2, &[1.0], 10, 1e-6),
604 Err(CsError::DimensionMismatch { .. })
605 ));
606 assert!(matches!(
608 rvm_fit_design(&[1.0, 0.0, 0.0], 2, 2, &[1.0, 0.0], 10, 1e-6),
609 Err(CsError::ShapeMismatch { .. })
610 ));
611 assert!(matches!(
613 rvm_fit_design(&[], 0, 0, &[], 10, 1e-6),
614 Err(CsError::InvalidParameter(_))
615 ));
616 assert!(matches!(
618 Rvm::fit(
619 &[0.0, 1.0],
620 2,
621 1,
622 &[0.0, 1.0],
623 RvmConfig {
624 kernel: RvmKernel::Rbf { length_scale: -1.0 },
625 ..Default::default()
626 }
627 ),
628 Err(CsError::InvalidParameter(_))
629 ));
630 }
631
632 #[test]
633 fn constant_target_uses_bias() {
634 let n = 7usize;
636 let xs: Vec<f64> = (0..n).map(|i| i as f64).collect();
637 let t = vec![3.0_f64; n];
638 let cfg = RvmConfig {
639 kernel: RvmKernel::Rbf { length_scale: 1.0 },
640 use_bias: true,
641 max_iter: 400,
642 tol: 1e-6,
643 };
644 let rvm = Rvm::fit(&xs, n, 1, &t, cfg).expect("ok");
645 let pred = rvm.predict_one(&[100.0]).expect("ok"); assert!((pred - 3.0).abs() < 0.5, "pred = {pred}");
647 }
648}