use rustnum as np;
fn main() -> np::Result<()> {
let n: usize = 200;
let mut rng = np::random::Rng::seed(0);
let x = rng.normal(0.0, 1.0, &[n, 3]);
let true_w = np::array![2.0, -1.0, 0.5];
let noise = rng.normal(0.0, 0.1, &[n]);
let y = x.matmul(&true_w) + 4.0 + &noise;
let ones = np::ones::<f64>(&[n, 1]);
let xb = np::concatenate(&[&x, &ones], 1);
let xt = xb.t();
let xtx = xt.matmul(&xb);
let xty = xt.matmul(&y);
let w_ne = np::linalg::solve(&xtx, &xty)?;
let lr: f64 = 0.1;
let scale = 2.0 / n as f64;
let mut w_gd = np::zeros::<f64>(&[4]);
for _ in 0..500 {
let err = xb.matmul(&w_gd) - &y;
let grad = xt.matmul(&err) * scale;
w_gd -= grad * lr;
}
println!("true weights = [2, -1, 0.5, 4]");
println!("normal equations = {w_ne}");
println!("gradient descent = {w_gd}");
let max_diff = (&w_ne - &w_gd).abs().max();
println!("max |w_ne - w_gd| = {max_diff:e}");
assert!(w_ne.allclose(&w_gd, 0.0, 1e-3), "the two fits disagree");
let resid = xb.matmul(&w_ne) - &y;
println!("mean squared error = {:.5}", resid.square().mean());
Ok(())
}