use num_traits::Float;
use crate::neural::NeuralError;
#[derive(Debug, Clone, Copy)]
pub struct RbfCenter<S: Float + Copy, const D: usize> {
pub center: [S; D],
pub sigma: S,
}
impl<S: Float + Copy, const D: usize> RbfCenter<S, D> {
pub fn new(center: [S; D], sigma: S) -> Result<Self, NeuralError> {
if sigma <= S::zero() {
return Err(NeuralError::InvalidDimension);
}
Ok(Self { center, sigma })
}
}
pub fn gaussian_rbf<S: Float + Copy, const D: usize>(x: &[S; D], center: &[S; D], sigma: S) -> S {
let mut sq_dist = S::zero();
for i in 0..D {
let diff = x[i] - center[i];
sq_dist = sq_dist + diff * diff;
}
let two = S::from(2.0).unwrap_or(S::one());
let denom = two * sigma * sigma;
(-sq_dist / denom).exp()
}
#[derive(Clone)]
pub struct RbfNetwork<S: Float + Copy, const D: usize, const K: usize> {
pub centers: [RbfCenter<S, D>; K],
pub weights: [S; K],
}
impl<S: Float + Copy, const D: usize, const K: usize> RbfNetwork<S, D, K> {
pub fn new(centers: [RbfCenter<S, D>; K]) -> Self {
Self {
centers,
weights: [S::zero(); K],
}
}
pub fn with_weights(centers: [RbfCenter<S, D>; K], weights: [S; K]) -> Self {
Self { centers, weights }
}
pub fn forward(&self, x: &[S; D]) -> S {
let mut out = S::zero();
for k in 0..K {
let phi = gaussian_rbf(x, &self.centers[k].center, self.centers[k].sigma);
out = out + self.weights[k] * phi;
}
out
}
fn eval_basis(&self, x: &[S; D]) -> [S; K] {
core::array::from_fn(|k| gaussian_rbf(x, &self.centers[k].center, self.centers[k].sigma))
}
pub fn train_step(&mut self, x: &[S; D], target: S, lr: S) -> Result<S, NeuralError> {
let phi = self.eval_basis(x);
let prediction = self
.weights
.iter()
.zip(phi.iter())
.fold(S::zero(), |acc, (&w, &p)| acc + w * p);
let error = prediction - target;
let loss = error * error;
let two = S::from(2.0).unwrap_or(S::one());
let grad_scale = two * error;
for (wk, &pk) in self.weights.iter_mut().zip(phi.iter()) {
let dw = grad_scale * pk;
if !dw.is_finite() {
return Err(NeuralError::NumericalOverflow);
}
*wk = *wk - lr * dw;
}
Ok(loss)
}
pub fn reset_weights(&mut self) {
for w in self.weights.iter_mut() {
*w = S::zero();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gaussian_rbf_at_center_is_one() {
let center = [1.0_f64, 2.0, 3.0];
let x = [1.0_f64, 2.0, 3.0];
let v = gaussian_rbf::<f64, 3>(&x, ¢er, 1.0);
assert!(
(v - 1.0).abs() < 1e-12,
"Gaussian RBF at centre should be 1, got {v}"
);
}
#[test]
fn gaussian_rbf_at_three_sigma_approx_zero_point_011() {
let center = [0.0_f64];
let sigma = 1.0_f64;
let x = [3.0_f64 * sigma];
let v = gaussian_rbf::<f64, 1>(&x, ¢er, sigma);
let expected = (-9.0_f64 / 2.0).exp();
assert!(
(v - expected).abs() < 1e-8,
"at 3σ: expected ≈{expected:.5}, got {v:.5}"
);
assert!((v - 0.011109).abs() < 1e-4, "at 3σ ≈ 0.011, got {v:.5}");
}
#[test]
fn gaussian_rbf_decreases_with_distance() {
let center = [0.0_f64];
let sigma = 1.0_f64;
let v0 = gaussian_rbf::<f64, 1>(&[0.0], ¢er, sigma);
let v1 = gaussian_rbf::<f64, 1>(&[1.0], ¢er, sigma);
let v2 = gaussian_rbf::<f64, 1>(&[2.0], ¢er, sigma);
assert!(
v0 > v1 && v1 > v2,
"Gaussian should decrease: {v0:.4} > {v1:.4} > {v2:.4}"
);
}
#[test]
fn rbf_center_rejects_non_positive_sigma() {
let result = RbfCenter::<f64, 2>::new([0.0, 0.0], 0.0);
assert!(result.is_err(), "sigma=0 should be rejected");
let result2 = RbfCenter::<f64, 2>::new([0.0, 0.0], -1.0);
assert!(result2.is_err(), "negative sigma should be rejected");
}
#[test]
fn rbf_network_forward_zero_weights() {
let c = RbfCenter::new([0.0_f64], 1.0).expect("valid");
let net = RbfNetwork::<f64, 1, 1>::new([c]);
let y = net.forward(&[0.5]);
assert_eq!(y, 0.0, "zero weights should give zero output");
}
#[test]
fn rbf_network_regression_sin() {
const K: usize = 6;
let sigma = 0.8_f64;
let centers: [RbfCenter<f64, 1>; K] = core::array::from_fn(|k| {
let pos = core::f64::consts::PI * (k as f64) / ((K - 1) as f64);
RbfCenter::new([pos], sigma).expect("valid")
});
let mut net = RbfNetwork::<f64, 1, K>::new(centers);
let lr = 0.05_f64;
for _ in 0..3000 {
for k in 0..20_usize {
let x_val = core::f64::consts::PI * (k as f64) / 19.0;
let target = x_val.sin();
net.train_step(&[x_val], target, lr).expect("train step");
}
}
let mut max_err = 0.0_f64;
for k in 0..10_usize {
let x_val = core::f64::consts::PI * (k as f64) / 9.0;
let pred = net.forward(&[x_val]);
let err = (pred - x_val.sin()).abs();
if err > max_err {
max_err = err;
}
}
assert!(
max_err < 0.15,
"RBF sin regression max error too large: {max_err:.4}"
);
}
#[test]
fn rbf_network_loss_decreases() {
let c = RbfCenter::new([0.0_f64], 1.0).expect("valid");
let mut net = RbfNetwork::<f64, 1, 1>::new([c]);
let lr = 0.1_f64;
let x = [0.0_f64];
let target = 1.0_f64;
let initial_loss = net.train_step(&x, target, lr).expect("step");
for _ in 0..100 {
net.train_step(&x, target, lr).expect("step");
}
let final_loss = net.train_step(&x, target, lr).expect("step");
assert!(
final_loss < initial_loss + 1e-6 || final_loss < 1e-6,
"loss should decrease: initial={initial_loss:.4}, final={final_loss:.4}"
);
}
#[test]
fn rbf_network_copy_type() {
let c = RbfCenter::new([1.0_f64, 2.0], 0.5).expect("valid");
let c2 = c;
assert!((c2.center[0] - 1.0).abs() < 1e-12);
}
}