use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
pub fn mean(data: &ArrayView1<f64>) -> Result<f64, String> {
Ok(data.mean().unwrap_or(0.0))
}
pub fn std(data: &ArrayView1<f64>, _ddof: i32) -> Result<f64, String> {
Ok(data.std(1.0))
}
pub fn pearsonr(
x: &ArrayView1<f64>,
y: &ArrayView1<f64>,
_alternative: &str,
) -> Result<(f64, f64), String> {
if x.len() != y.len() || x.len() < 2 {
return Ok((0.0, 0.5));
}
let x_mean = x.mean().unwrap_or(0.0);
let y_mean = y.mean().unwrap_or(0.0);
let mut num = 0.0;
let mut x_sum_sq = 0.0;
let mut y_sum_sq = 0.0;
for i in 0..x.len() {
let x_diff = x[i] - x_mean;
let y_diff = y[i] - y_mean;
num += x_diff * y_diff;
x_sum_sq += x_diff * x_diff;
y_sum_sq += y_diff * y_diff;
}
let denom = (x_sum_sq * y_sum_sq).sqrt();
let corr = if denom > 1e-10 { num / denom } else { 0.0 };
Ok((corr, 0.05)) }
pub fn minimize(
objective: fn(&[f64]) -> f64,
x0: &[f64],
bounds: Option<&[(f64, f64)]>,
) -> Result<OptimizeResult, String> {
if x0.is_empty() {
return Err("minimize: x0 must not be empty".to_string());
}
let n = x0.len();
let mut x = x0.to_vec();
let mut best = objective(&x);
let mut step = 1.0_f64;
const MAX_ITERATIONS: usize = 100;
const MIN_STEP: f64 = 1e-8;
let mut iterations_used = 0;
let clamp_dim = |i: usize, val: f64| -> f64 {
bounds
.and_then(|b| b.get(i))
.map_or(val, |&(lo, hi)| val.clamp(lo, hi))
};
while step > MIN_STEP && iterations_used < MAX_ITERATIONS {
let mut improved = false;
for i in 0..n {
for delta in [step, -step] {
let mut candidate = x.clone();
candidate[i] = clamp_dim(i, candidate[i] + delta);
let value = objective(&candidate);
if value < best {
best = value;
x = candidate;
improved = true;
}
}
}
iterations_used += 1;
if !improved {
step *= 0.5;
}
}
Ok(OptimizeResult {
x,
fun: best,
success: true,
nit: iterations_used,
message: "Fallback coordinate/pattern search (no SciRS2 optimizer available)".to_string(),
})
}
pub struct OptimizeResult {
pub x: Vec<f64>,
pub fun: f64,
pub success: bool,
pub nit: usize,
pub message: String,
}
fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
let n = a.nrows();
if a.ncols() != n || b.len() != n {
return Err(format!(
"solve_linear_system: dimension mismatch (A is {}x{}, b has length {})",
a.nrows(),
a.ncols(),
b.len()
));
}
let mut aug = a.clone();
let mut rhs = b.clone();
for col in 0..n {
let mut pivot_row = col;
let mut max_val = aug[[col, col]].abs();
for row in (col + 1)..n {
if aug[[row, col]].abs() > max_val {
max_val = aug[[row, col]].abs();
pivot_row = row;
}
}
if max_val < 1e-12 {
return Err(format!(
"solve_linear_system: matrix is singular or nearly singular (pivot magnitude {max_val:.3e} at column {col})"
));
}
if pivot_row != col {
for k in 0..n {
aug.swap((col, k), (pivot_row, k));
}
rhs.swap(col, pivot_row);
}
let pivot_val = aug[[col, col]];
for k in 0..n {
aug[[col, k]] /= pivot_val;
}
rhs[col] /= pivot_val;
for row in 0..n {
if row != col {
let factor = aug[[row, col]];
if factor != 0.0 {
for k in 0..n {
let aug_col_k = aug[[col, k]];
aug[[row, k]] -= factor * aug_col_k;
}
let rhs_col = rhs[col];
rhs[row] -= factor * rhs_col;
}
}
}
}
Ok(rhs)
}
pub struct LinearRegression {
coefficients: Vec<f64>,
intercept: f64,
}
impl LinearRegression {
pub const fn new() -> Self {
Self {
coefficients: Vec::new(),
intercept: 0.0,
}
}
pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<(), String> {
let n_samples = x.nrows();
let n_features = x.ncols();
if n_samples != y.len() {
return Err(format!(
"LinearRegression::fit: x has {n_samples} rows but y has {} elements",
y.len()
));
}
if n_samples == 0 || n_features == 0 {
return Err("LinearRegression::fit: x must be non-empty".to_string());
}
let mut x_aug = Array2::<f64>::ones((n_samples, n_features + 1));
for i in 0..n_samples {
for j in 0..n_features {
x_aug[[i, j]] = x[[i, j]];
}
}
let xt = x_aug.t();
let xtx = xt.dot(&x_aug);
let xty = xt.dot(y);
let beta = solve_linear_system(&xtx, &xty)?;
self.coefficients = beta.iter().take(n_features).copied().collect();
self.intercept = beta[n_features];
Ok(())
}
pub fn predict(&self, x: &Array2<f64>) -> Array1<f64> {
let n_samples = x.nrows();
let mut predictions = Array1::<f64>::zeros(n_samples);
for i in 0..n_samples {
let mut value = self.intercept;
for (j, &coefficient) in self.coefficients.iter().enumerate() {
if j < x.ncols() {
value += coefficient * x[[i, j]];
}
}
predictions[i] = value;
}
predictions
}
}
impl Default for LinearRegression {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_minimize_actually_evaluates_objective_and_reduces_it() {
fn objective(x: &[f64]) -> f64 {
(x[0] - 3.0).powi(2) + (x[1] + 2.0).powi(2)
}
let x0 = [0.0, 0.0];
let initial_value = objective(&x0);
let result = minimize(objective, &x0, None).expect("minimize should succeed");
assert!(
result.fun < initial_value,
"fallback optimizer must actually reduce the objective (got {} vs initial {})",
result.fun,
initial_value
);
assert!(
(result.x[0] - 3.0).abs() < 0.1,
"expected x0 near 3.0, got {}",
result.x[0]
);
assert!(
(result.x[1] + 2.0).abs() < 0.1,
"expected x1 near -2.0, got {}",
result.x[1]
);
}
#[test]
fn test_minimize_respects_bounds() {
fn objective(x: &[f64]) -> f64 {
(x[0] - 100.0).powi(2)
}
let x0 = [0.0];
let bounds = [(-1.0, 1.0)];
let result = minimize(objective, &x0, Some(&bounds)).expect("minimize should succeed");
assert!(
(-1.0..=1.0).contains(&result.x[0]),
"optimizer must respect bounds, got x={}",
result.x[0]
);
}
#[test]
fn test_linear_regression_recovers_known_line() {
let x = Array2::from_shape_vec((4, 1), vec![0.0, 1.0, 2.0, 3.0]).unwrap();
let y = Array1::from_vec(vec![1.0, 3.0, 5.0, 7.0]);
let mut model = LinearRegression::new();
model
.fit(&x, &y)
.expect("fit should succeed for a well-posed system");
let predictions = model.predict(&x);
for i in 0..4 {
assert!(
(predictions[i] - y[i]).abs() < 1e-6,
"prediction {} should match training target {} at index {i}",
predictions[i],
y[i]
);
}
}
#[test]
fn test_linear_regression_rejects_mismatched_shapes() {
let x = Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).unwrap();
let y = Array1::from_vec(vec![1.0, 2.0]);
let mut model = LinearRegression::new();
assert!(
model.fit(&x, &y).is_err(),
"fit must honestly error on mismatched shapes rather than silently succeeding"
);
}
#[test]
fn test_solve_linear_system_basic() {
let a = Array2::from_shape_vec((2, 2), vec![2.0, 0.0, 0.0, 3.0]).unwrap();
let b = Array1::from_vec(vec![4.0, 9.0]);
let x = solve_linear_system(&a, &b).expect("solve should succeed");
assert!((x[0] - 2.0).abs() < 1e-9);
assert!((x[1] - 3.0).abs() < 1e-9);
}
}