use crate::error::OptimizeResult;
use crate::result::OptimizeResults;
use scirs2_core::ndarray::{Array1, Array2, ArrayBase, Data, Ix1};
#[derive(Debug, Clone)]
pub struct WeightedOptions {
pub max_iter: usize,
pub max_nfev: Option<usize>,
pub xtol: f64,
pub ftol: f64,
pub gtol: f64,
pub diff_step: f64,
pub check_weights: bool,
}
impl Default for WeightedOptions {
fn default() -> Self {
WeightedOptions {
max_iter: 100,
max_nfev: None,
xtol: 1e-8,
ftol: 1e-8,
gtol: 1e-8,
diff_step: 1e-8,
check_weights: true,
}
}
}
#[allow(dead_code)]
pub fn weighted_least_squares<F, J, D, S1, S2, S3>(
residuals: F,
x0: &ArrayBase<S1, Ix1>,
weights: &ArrayBase<S2, Ix1>,
jacobian: Option<J>,
data: &ArrayBase<S3, Ix1>,
options: Option<WeightedOptions>,
) -> OptimizeResult<OptimizeResults<f64>>
where
F: Fn(&[f64], &[D]) -> Array1<f64>,
J: Fn(&[f64], &[D]) -> Array2<f64>,
D: Clone,
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
S3: Data<Elem = D>,
{
let options = options.unwrap_or_default();
if options.check_weights {
for &w in weights.iter() {
if w < 0.0 {
return Err(crate::error::OptimizeError::ValueError(
"Weights must be non-negative".to_string(),
));
}
}
}
weighted_gauss_newton(residuals, x0, weights, jacobian, data, &options)
}
#[allow(dead_code)]
fn weighted_gauss_newton<F, J, D, S1, S2, S3>(
residuals: F,
x0: &ArrayBase<S1, Ix1>,
weights: &ArrayBase<S2, Ix1>,
jacobian: Option<J>,
data: &ArrayBase<S3, Ix1>,
options: &WeightedOptions,
) -> OptimizeResult<OptimizeResults<f64>>
where
F: Fn(&[f64], &[D]) -> Array1<f64>,
J: Fn(&[f64], &[D]) -> Array2<f64>,
D: Clone,
S1: Data<Elem = f64>,
S2: Data<Elem = f64>,
S3: Data<Elem = D>,
{
let mut x = x0.to_owned();
let m = x.len();
let n = weights.len();
let max_nfev = options.max_nfev.unwrap_or(options.max_iter * m * 10);
let mut nfev = 0;
let mut njev = 0;
let mut iter = 0;
let sqrt_weights = weights.mapv(f64::sqrt);
let compute_numerical_jacobian =
|x_val: &Array1<f64>, res_val: &Array1<f64>| -> (Array2<f64>, usize) {
let eps = options.diff_step;
let mut jac = Array2::zeros((n, m));
let mut count = 0;
for j in 0..m {
let mut x_h = x_val.clone();
x_h[j] += eps;
let res_h = residuals(
x_h.as_slice().expect("Operation failed"),
data.as_slice().expect("Operation failed"),
);
count += 1;
for i in 0..n {
jac[[i, j]] = (res_h[i] - res_val[i]) / eps;
}
}
(jac, count)
};
while iter < options.max_iter && nfev < max_nfev {
let res = residuals(
x.as_slice().expect("Operation failed"),
data.as_slice().expect("Operation failed"),
);
nfev += 1;
let weighted_res = &res * &sqrt_weights;
let cost = 0.5 * weighted_res.iter().map(|&r| r * r).sum::<f64>();
let (jac, jac_evals) = match &jacobian {
Some(jac_fn) => {
let j = jac_fn(
x.as_slice().expect("Operation failed"),
data.as_slice().expect("Operation failed"),
);
njev += 1;
(j, 0)
}
None => {
let (j, count) = compute_numerical_jacobian(&x, &res);
nfev += count;
(j, count)
}
};
let mut weighted_jac = Array2::zeros((n, m));
for i in 0..n {
for j in 0..m {
weighted_jac[[i, j]] = jac[[i, j]] * sqrt_weights[i];
}
}
let gradient = weighted_jac.t().dot(&weighted_res);
if gradient.iter().all(|&g| g.abs() < options.gtol) {
let mut result = OptimizeResults::<f64>::default();
result.x = x;
result.fun = cost;
result.nfev = nfev;
result.njev = njev;
result.nit = iter;
result.success = true;
result.message = "Optimization terminated successfully.".to_string();
return Ok(result);
}
let jtw_j = weighted_jac.t().dot(&weighted_jac);
let neg_gradient = -&gradient;
match solve(&jtw_j, &neg_gradient) {
Some(step) => {
let mut alpha = 1.0;
let mut best_cost = cost;
let mut best_x = x.clone();
for _ in 0..10 {
let x_new = &x + &step * alpha;
let res_new = residuals(
x_new.as_slice().expect("Operation failed"),
data.as_slice().expect("Operation failed"),
);
nfev += 1;
let weighted_res_new = &res_new * &sqrt_weights;
let new_cost = 0.5 * weighted_res_new.iter().map(|&r| r * r).sum::<f64>();
if new_cost < best_cost {
best_cost = new_cost;
best_x = x_new;
break;
}
alpha *= 0.5;
}
let step_norm = step.iter().map(|&s| s * s).sum::<f64>().sqrt();
let x_norm = x.iter().map(|&xi| xi * xi).sum::<f64>().sqrt();
if step_norm < options.xtol * (1.0 + x_norm) {
let mut result = OptimizeResults::<f64>::default();
result.x = best_x;
result.fun = best_cost;
result.nfev = nfev;
result.njev = njev;
result.nit = iter;
result.success = true;
result.message = "Converged (step size tolerance)".to_string();
return Ok(result);
}
if (cost - best_cost).abs() < options.ftol * cost {
let mut result = OptimizeResults::<f64>::default();
result.x = best_x;
result.fun = best_cost;
result.nfev = nfev;
result.njev = njev;
result.nit = iter;
result.success = true;
result.message = "Converged (function tolerance)".to_string();
return Ok(result);
}
x = best_x;
}
None => {
let mut result = OptimizeResults::<f64>::default();
result.x = x;
result.fun = cost;
result.nfev = nfev;
result.njev = njev;
result.nit = iter;
result.success = false;
result.message = "Singular matrix in normal equations".to_string();
return Ok(result);
}
}
iter += 1;
}
let res_final = residuals(
x.as_slice().expect("Operation failed"),
data.as_slice().expect("Operation failed"),
);
let weighted_res_final = &res_final * &sqrt_weights;
let final_cost = 0.5 * weighted_res_final.iter().map(|&r| r * r).sum::<f64>();
let mut result = OptimizeResults::<f64>::default();
result.x = x;
result.fun = final_cost;
result.nfev = nfev;
result.njev = njev;
result.nit = iter;
result.success = false;
result.message = "Maximum iterations reached".to_string();
Ok(result)
}
#[allow(dead_code)]
fn solve(a: &Array2<f64>, b: &Array1<f64>) -> Option<Array1<f64>> {
use scirs2_linalg::solve;
solve(&a.view(), &b.view(), None).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_weighted_least_squares_simple() {
fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
let n = data.len() / 2;
let t_values = &data[0..n];
let y_values = &data[n..];
let mut res = Array1::zeros(n);
for i in 0..n {
res[i] = y_values[i] - (x[0] + x[1] * t_values[i]);
}
res
}
fn jacobian(x: &[f64], data: &[f64]) -> Array2<f64> {
let n = data.len() / 2;
let t_values = &data[0..n];
let mut jac = Array2::zeros((n, 2));
for i in 0..n {
jac[[i, 0]] = -1.0;
jac[[i, 1]] = -t_values[i];
}
jac
}
let data = array![0.0, 1.0, 2.0, 3.0, 4.0, 0.1, 0.9, 2.1, 2.9, 4.1];
let weights = array![1.0, 1.0, 1.0, 10.0, 10.0];
let x0 = array![0.0, 0.0];
let result = weighted_least_squares(residual, &x0, &weights, Some(jacobian), &data, None)
.expect("Operation failed");
assert!(result.success);
assert!((result.x[1] - 1.0).abs() < 0.1); }
#[test]
fn test_negative_weights() {
fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
array![x[0] - 1.0]
}
let x0 = array![0.0];
let weights = array![-1.0]; let data = array![];
let result = weighted_least_squares(
residual,
&x0,
&weights,
None::<fn(&[f64], &[f64]) -> Array2<f64>>,
&data,
None,
);
assert!(result.is_err());
}
#[test]
fn test_weighted_vs_unweighted() {
fn residual(x: &[f64], data: &[f64]) -> Array1<f64> {
let n = data.len() / 2;
let t_values = &data[0..n];
let y_values = &data[n..];
let mut res = Array1::zeros(n);
for i in 0..n {
res[i] = y_values[i] - (x[0] + x[1] * t_values[i]);
}
res
}
let data = array![0.0, 1.0, 2.0, 0.0, 1.0, 10.0];
let x0 = array![0.0, 0.0];
let weights_uniform = array![1.0, 1.0, 1.0];
let weights_robust = array![1.0, 1.0, 0.1];
let result_uniform = weighted_least_squares(
residual,
&x0,
&weights_uniform,
None::<fn(&[f64], &[f64]) -> Array2<f64>>,
&data,
None,
)
.expect("Operation failed");
let result_robust = weighted_least_squares(
residual,
&x0,
&weights_robust,
None::<fn(&[f64], &[f64]) -> Array2<f64>>,
&data,
None,
)
.expect("Operation failed");
assert!((result_robust.x[1] - 1.0).abs() < (result_uniform.x[1] - 1.0).abs());
}
}