pub struct Msnn {
centers: Vec<f64>,
h: f64,
a: Vec<f64>, b: Vec<f64>, }
impl Msnn {
fn raw_memberships(centers: &[f64], h: f64, x: f64) -> Vec<f64> {
let k = centers.len();
let mut phi = vec![0.0; k];
for (i, &c) in centers.iter().enumerate() {
let t = 1.0 - (x - c).abs() / h;
let mut v = t.max(0.0);
if (i == 0 && x < c) || (i == k - 1 && x > c) {
v = 1.0;
}
phi[i] = v;
}
phi
}
pub fn membership(&self, x: f64) -> Vec<f64> {
let mut phi = Self::raw_memberships(&self.centers, self.h, x);
let s: f64 = phi.iter().sum();
if s > 0.0 {
for p in &mut phi {
*p /= s;
}
}
phi
}
pub fn fit(xs: &[f64], ys: &[f64], centers: usize, lo: f64, hi: f64) -> Msnn {
let c: Vec<f64> = (0..centers).map(|i| lo + (hi - lo) * i as f64 / (centers - 1).max(1) as f64).collect();
let h = if centers > 1 { (hi - lo) / (centers - 1) as f64 } else { hi - lo };
let mut model = Msnn { centers: c, h, a: vec![0.0; centers], b: vec![0.0; centers] };
for i in 0..centers {
let (mut sw, mut swx, mut swxx, mut swy, mut swxy) = (0.0, 0.0, 0.0, 0.0, 0.0);
for (&x, &y) in xs.iter().zip(ys.iter()) {
let w = Self::raw_memberships(&model.centers, h, x)[i];
sw += w;
swx += w * x;
swxx += w * x * x;
swy += w * y;
swxy += w * x * y;
}
let det0 = swxx * sw - swx * swx;
let det = if det0.abs() < 1e-12 { det0 + 1e-9 } else { det0 };
model.a[i] = (swxy * sw - swx * swy) / det;
model.b[i] = (swxx * swy - swx * swxy) / det;
}
model
}
pub fn predict(&self, x: f64) -> f64 {
let phi = self.membership(x);
(0..self.centers.len()).map(|i| phi[i] * (self.a[i] * x + self.b[i])).sum()
}
pub fn n_centers(&self) -> usize {
self.centers.len()
}
pub fn center(&self, i: usize) -> f64 {
self.centers[i]
}
pub fn local_slope(&self, i: usize) -> f64 {
self.a[i]
}
pub fn local_line(&self, i: usize, x: f64) -> f64 {
self.a[i] * x + self.b[i]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memberships_partition_unity() {
let m = Msnn::fit(&[0.0, 1.0], &[0.0, 1.0], 6, -1.0, 1.0);
for &x in &[-2.0, -0.7, 0.0, 0.33, 0.9, 1.5] {
let s: f64 = m.membership(x).iter().sum();
assert!((s - 1.0).abs() < 1e-12, "Σφ should be 1 at x={x}, got {s}");
}
}
#[test]
fn recovers_an_affine_function_exactly() {
let xs: Vec<f64> = (0..40).map(|i| -1.0 + 2.0 * i as f64 / 39.0).collect();
let ys: Vec<f64> = xs.iter().map(|x| 2.0 * x + 1.0).collect();
let m = Msnn::fit(&xs, &ys, 6, -1.0, 1.0);
for &x in &[-0.8, -0.2, 0.5, 0.95] {
assert!((m.predict(x) - (2.0 * x + 1.0)).abs() < 1e-9, "affine recovery at {x}");
}
for i in 0..m.n_centers() {
assert!((m.local_slope(i) - 2.0).abs() < 1e-6, "every local slope should be 2");
}
}
#[test]
fn fits_a_nonlinear_curve_with_readable_local_slopes() {
let xs: Vec<f64> = (0..80).map(|i| -1.5 + 3.0 * i as f64 / 79.0).collect();
let ys: Vec<f64> = xs.iter().map(|x| (2.0 * x).sin()).collect();
let m = Msnn::fit(&xs, &ys, 10, -1.5, 1.5);
let mse: f64 = xs.iter().zip(&ys).map(|(&x, &y)| (m.predict(x) - y).powi(2)).sum::<f64>() / xs.len() as f64;
assert!(mse < 1e-3, "should fit sin(2x): mse {mse}");
for i in 2..m.n_centers() - 2 {
let c = m.center(i);
let truth = 2.0 * (2.0 * c).cos();
assert!((m.local_slope(i) - truth).abs() < 0.6, "slope at c={c}: {} vs {truth}", m.local_slope(i));
}
}
}