use greeners_core::error::GreenersError;
use greeners_core::linalg::LinalgInverse as _;
use ndarray::{Array1, Array2};
use statrs::distribution::{ContinuousCDF, StudentsT};
use std::fmt;
#[derive(Debug)]
pub struct NlsResult {
pub params: Array1<f64>,
pub std_errors: Array1<f64>,
pub t_values: Array1<f64>,
pub p_values: Array1<f64>,
pub rss: f64,
pub r_squared: f64,
pub adj_r_squared: f64,
pub n_obs: usize,
pub df_resid: usize,
pub n_params: usize,
pub n_iter: usize,
pub converged: bool,
pub residuals: Array1<f64>,
pub fitted: Array1<f64>,
pub sigma: f64,
pub param_names: Option<Vec<String>>,
}
impl fmt::Display for NlsResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n{:=^78}", " Nonlinear Least Squares ")?;
writeln!(f, "{:<20} {:>12}", "Observations:", self.n_obs)?;
writeln!(f, "{:<20} {:>12}", "Parameters:", self.n_params)?;
writeln!(f, "{:<20} {:>12}", "Iterations:", self.n_iter)?;
writeln!(
f,
"{:<20} {:>12}",
"Converged:",
if self.converged { "yes" } else { "no" }
)?;
writeln!(f, "{:<20} {:>12.6}", "RSS:", self.rss)?;
writeln!(f, "{:<20} {:>12.6}", "R-squared:", self.r_squared)?;
writeln!(f, "{:<20} {:>12.6}", "Adj R-sq:", self.adj_r_squared)?;
writeln!(f, "{:<20} {:>12.6}", "Sigma:", self.sigma)?;
writeln!(f, "\n{:-^78}", "")?;
let header = format!(
"{:<12} {:>12} {:>12} {:>10} {:>10}",
"Parameter", "Coef.", "Std.Err.", "t", "P>|t|"
);
writeln!(f, "{header}")?;
writeln!(f, "{:-^78}", "")?;
for i in 0..self.n_params {
let name = self
.param_names
.as_ref()
.and_then(|n| n.get(i).cloned())
.unwrap_or_else(|| format!("p{}", i));
let t_str = if self.t_values[i].is_nan() || self.t_values[i].is_infinite() {
format!("{:>10}", "—")
} else {
format!("{:>10.3}", self.t_values[i])
};
let p_str = if self.p_values[i].is_nan() || self.p_values[i].is_infinite() {
format!("{:>10}", "—")
} else {
format!("{:>10.4}", self.p_values[i])
};
writeln!(
f,
"{:<12} {:>12.6} {:>12.6} {t_str} {p_str}",
name, self.params[i], self.std_errors[i]
)?;
}
write!(f, "{:=^78}", "")
}
}
pub struct NLS;
impl NLS {
pub fn fit(
y: &Array1<f64>,
x: &Array2<f64>,
predict: &dyn Fn(&[f64], &[f64]) -> f64,
start: &[f64],
max_iter: usize,
tol: f64,
) -> Result<NlsResult, GreenersError> {
let n = y.len();
let k = start.len();
if n == 0 || k == 0 {
return Err(GreenersError::InvalidOperation(
"NLS: empty data or parameters".into(),
));
}
if x.nrows() != n {
return Err(GreenersError::ShapeMismatch(format!(
"y (n={n}) and x (nrows={}) must have same number of rows",
x.nrows()
)));
}
let mut params = start.to_vec();
let mut residuals = vec![0.0_f64; n];
let mut fitted = vec![0.0_f64; n];
let compute_rss = |params: &[f64], residuals: &mut [f64], fitted: &mut [f64]| -> f64 {
let mut rss = 0.0;
for i in 0..n {
let x_row: Vec<f64> = x.row(i).to_vec();
let pred = predict(params, &x_row);
fitted[i] = pred;
let r = y[i] - pred;
residuals[i] = r;
rss += r * r;
}
rss
};
let mut rss = compute_rss(¶ms, &mut residuals, &mut fitted);
let mut lambda = 1e-3_f64;
let mut n_iter = 0;
let mut converged = false;
for iter in 0..max_iter {
n_iter = iter + 1;
let mut jacobian = Array2::<f64>::zeros((n, k));
let eps = 1e-8_f64;
for j in 0..k {
let mut params_pert = params.clone();
let h = eps * (1.0 + params[j].abs());
params_pert[j] += h;
for i in 0..n {
let x_row: Vec<f64> = x.row(i).to_vec();
let pred_pert = predict(¶ms_pert, &x_row);
jacobian[(i, j)] = -(pred_pert - fitted[i]) / h;
}
}
let jt = jacobian.t();
let jtj = jt.dot(&jacobian);
let jtr = jt.dot(&Array1::from(residuals.clone()));
let mut a = jtj.clone();
for j in 0..k {
a[(j, j)] += lambda * jtj[(j, j)].abs().max(1e-10);
}
let delta = match a.inv() {
Ok(a_inv) => -a_inv.dot(&jtr),
Err(_) => {
lambda *= 10.0;
if lambda > 1e12 {
break;
}
continue;
}
};
let mut params_trial = params.clone();
for j in 0..k {
params_trial[j] += delta[j];
}
let mut residuals_trial = vec![0.0_f64; n];
let mut fitted_trial = vec![0.0_f64; n];
let rss_trial = compute_rss(¶ms_trial, &mut residuals_trial, &mut fitted_trial);
if rss_trial <= rss {
params = params_trial;
residuals = residuals_trial;
fitted = fitted_trial;
let improvement = (rss - rss_trial) / rss.max(1e-15);
rss = rss_trial;
lambda *= 0.5;
if improvement < tol {
converged = true;
break;
}
} else {
lambda *= 10.0;
if lambda > 1e12 {
break;
}
}
}
let mut jacobian = Array2::<f64>::zeros((n, k));
let eps = 1e-8_f64;
for j in 0..k {
let mut params_pert = params.clone();
let h = eps * (1.0 + params[j].abs());
params_pert[j] += h;
for i in 0..n {
let x_row: Vec<f64> = x.row(i).to_vec();
let pred_pert = predict(¶ms_pert, &x_row);
jacobian[(i, j)] = -(pred_pert - fitted[i]) / h;
}
}
let jt = jacobian.t();
let jtj = jt.dot(&jacobian);
let df_resid = n - k;
let sigma2 = rss / df_resid.max(1) as f64;
let cov = match jtj.inv() {
Ok(inv) => inv * sigma2,
Err(_) => Array2::zeros((k, k)),
};
let std_errors = cov.diag().mapv(|v| v.sqrt());
let t_values = Array1::from(params.clone()) / &std_errors;
let t_dist = StudentsT::new(0.0, 1.0, df_resid as f64)
.map_err(|e| GreenersError::InvalidOperation(e.to_string()))?;
let p_values = t_values.mapv(|t| {
if t.is_nan() || t.is_infinite() {
f64::NAN
} else {
2.0 * (1.0 - t_dist.cdf(t.abs()))
}
});
let y_mean = y.mean().unwrap_or(0.0);
let tss = y.mapv(|v| (v - y_mean).powi(2)).sum();
let r_squared = if tss > 1e-15 { 1.0 - rss / tss } else { 0.0 };
let adj_r_squared = 1.0 - (1.0 - r_squared) * (n - 1) as f64 / df_resid.max(1) as f64;
Ok(NlsResult {
params: Array1::from(params),
std_errors,
t_values,
p_values,
rss,
r_squared,
adj_r_squared,
n_obs: n,
df_resid,
n_params: k,
n_iter,
converged,
residuals: Array1::from(residuals),
fitted: Array1::from(fitted),
sigma: sigma2.sqrt(),
param_names: None,
})
}
pub fn fit_with_names(
y: &Array1<f64>,
x: &Array2<f64>,
predict: &dyn Fn(&[f64], &[f64]) -> f64,
start: &[f64],
param_names: Vec<String>,
max_iter: usize,
tol: f64,
) -> Result<NlsResult, GreenersError> {
let mut result = Self::fit(y, x, predict, start, max_iter, tol)?;
result.param_names = Some(param_names);
Ok(result)
}
}
pub fn predict_exp(params: &[f64], x: &[f64]) -> f64 {
params[0] * (params[1] * x[0]).exp()
}
pub fn predict_power(params: &[f64], x: &[f64]) -> f64 {
if x[0] > 0.0 {
params[0] * x[0].powf(params[1])
} else {
0.0
}
}
pub fn predict_logistic(params: &[f64], x: &[f64]) -> f64 {
params[0] / (1.0 + (-(params[1] * (x[0] - params[2]))).exp())
}
pub fn predict_cobb_douglas(params: &[f64], x: &[f64]) -> f64 {
let a = params[0];
let mut result = a;
for (i, xi) in x.iter().enumerate() {
if *xi > 0.0 {
result *= xi.powf(params[1 + i]);
}
}
result
}
pub fn predict_ces(params: &[f64], x: &[f64]) -> f64 {
let a = params[0];
let b1 = params[1].clamp(1e-10, 1.0 - 1e-10);
let rho = params[2];
let b2 = 1.0 - b1;
let inner = b1 * x[0].max(1e-10).powf(rho) + b2 * x[1].max(1e-10).powf(rho);
a * inner.max(1e-10).powf(1.0 / rho)
}