use anofox_regression::solvers::{BlsRegressor, FittedRegressor, Regressor};
use faer::{Col, Mat};
fn main() {
println!("=== Bounded Least Squares (BLS) ===\n");
non_negative_least_squares();
box_constraints();
mixture_model();
compare_constrained_unconstrained();
}
fn non_negative_least_squares() {
println!("--- Non-Negative Least Squares (NNLS) ---\n");
let n = 50;
let x = Mat::from_fn(n, 3, |i, j| match j {
0 => (i as f64) * 0.1,
1 => ((i as f64) * 0.15).sin().abs(),
2 => ((i as f64) * 0.1).cos().abs(),
_ => 0.0,
});
let y = Col::from_fn(n, |i| {
let noise = ((i as f64 * 0.7).sin()) * 0.2;
1.0 + 2.0 * x[(i, 0)] - 0.5 * x[(i, 1)] + 1.5 * x[(i, 2)] + noise
});
let model = BlsRegressor::nnls().with_intercept(true).build();
let fitted = model.fit(&x, &y).expect("fit should succeed");
println!("True model: y = 1 + 2*x1 - 0.5*x2 + 1.5*x3 + noise");
println!("NNLS constraint: all coefficients >= 0\n");
println!("Intercept: {:.4}", fitted.intercept().unwrap_or(0.0));
println!(
"Coefficient x1: {:.4} (true: 2.0)",
fitted.coefficients()[0]
);
println!(
"Coefficient x2: {:.4} (true: -0.5, constrained to 0)",
fitted.coefficients()[1]
);
println!(
"Coefficient x3: {:.4} (true: 1.5)",
fitted.coefficients()[2]
);
println!("R-squared: {:.4}", fitted.r_squared());
println!("\nNote: x2 coefficient is forced to 0 (or positive) due to NNLS constraint.");
println!();
}
fn box_constraints() {
println!("--- Custom Box Constraints ---\n");
let n = 60;
let x = Mat::from_fn(n, 2, |i, j| {
if j == 0 {
(i as f64) * 0.1
} else {
((i as f64) * 0.2).sin()
}
});
let y = Col::from_fn(n, |i| {
let noise = ((i as f64 * 0.8).sin()) * 0.3;
2.0 + 3.0 * x[(i, 0)] + 2.0 * x[(i, 1)] + noise
});
let model = BlsRegressor::builder()
.with_intercept(true)
.lower_bounds(vec![0.0, -1.0])
.upper_bounds(vec![2.0, 1.0])
.build();
let fitted = model.fit(&x, &y).expect("fit should succeed");
println!("True model: y = 2 + 3*x1 + 2*x2 + noise");
println!("Constraints: 0 <= x1 <= 2, -1 <= x2 <= 1\n");
println!("Intercept: {:.4}", fitted.intercept().unwrap_or(0.0));
println!(
"Coefficient x1: {:.4} (true: 3.0, bounded to [0, 2])",
fitted.coefficients()[0]
);
println!(
"Coefficient x2: {:.4} (true: 2.0, bounded to [-1, 1])",
fitted.coefficients()[1]
);
println!("R-squared: {:.4}", fitted.r_squared());
println!("\nNote: Coefficients are capped at their constraint bounds.");
println!();
}
fn mixture_model() {
println!("--- Mixture Model (Proportions) ---\n");
let n = 100;
let n_components = 4;
let components = Mat::from_fn(n, n_components, |i, j| match j {
0 => (1.0 + (i as f64 * 0.1).sin()).max(0.0),
1 => (1.5 - (i as f64 * 0.08).cos()).max(0.0),
2 => ((i as f64 * 0.05).exp() / 100.0).min(2.0),
3 => (2.0 - (i as f64 * 0.02)).max(0.1),
_ => 0.0,
});
let true_props = [0.3, 0.4, 0.2, 0.1];
let y = Col::from_fn(n, |i| {
let noise = ((i as f64 * 1.1).sin()) * 0.05;
let mut val = 0.0;
for j in 0..n_components {
val += true_props[j] * components[(i, j)];
}
val + noise
});
let model = BlsRegressor::nnls().with_intercept(false).build();
let fitted = model.fit(&components, &y).expect("fit should succeed");
println!("Recovering mixture proportions from observed signal");
println!("(all proportions must be >= 0)\n");
println!(
"{:<12} {:>12} {:>12}",
"Component", "True Prop", "Estimated"
);
println!("{}", "-".repeat(40));
for (i, (&true_prop, &est_prop)) in true_props
.iter()
.zip(fitted.coefficients().iter())
.enumerate()
{
println!(
"{:<12} {:>12.4} {:>12.4}",
format!("Component {}", i + 1),
true_prop,
est_prop
);
}
let est_sum: f64 = fitted.coefficients().iter().sum();
let true_sum: f64 = true_props.iter().sum();
println!("\nSum of proportions:");
println!(" True: {:.4}", true_sum);
println!(" Estimated: {:.4}", est_sum);
println!();
}
fn compare_constrained_unconstrained() {
println!("--- Constrained vs Unconstrained ---\n");
use anofox_regression::solvers::OlsRegressor;
let n = 80;
let x = Mat::from_fn(n, 3, |i, j| match j {
0 => (i as f64) * 0.1,
1 => ((i as f64) * 0.12).sin(),
2 => ((i as f64) * 0.08).cos(),
_ => 0.0,
});
let y = Col::from_fn(n, |i| {
let noise = ((i as f64 * 0.7).sin()) * 0.3;
1.0 + 1.5 * x[(i, 0)] - 0.8 * x[(i, 1)] + 0.3 * x[(i, 2)] + noise
});
let ols_model = OlsRegressor::builder().with_intercept(true).build();
let ols_fitted = ols_model.fit(&x, &y).expect("OLS fit");
let nnls_model = BlsRegressor::nnls().with_intercept(true).build();
let nnls_fitted = nnls_model.fit(&x, &y).expect("NNLS fit");
let bls_model = BlsRegressor::builder()
.with_intercept(true)
.lower_bounds(vec![0.0, -0.5, 0.0])
.upper_bounds(vec![f64::INFINITY, 0.5, f64::INFINITY])
.build();
let bls_fitted = bls_model.fit(&x, &y).expect("BLS fit");
println!("True model: y = 1 + 1.5*x1 - 0.8*x2 + 0.3*x3 + noise\n");
println!(
"{:<15} {:>10} {:>10} {:>10} {:>12}",
"Method", "x1", "x2", "x3", "R-squared"
);
println!("{}", "-".repeat(60));
println!(
"{:<15} {:>10.4} {:>10.4} {:>10.4} {:>12.4}",
"True", 1.5, -0.8, 0.3, 1.0
);
println!(
"{:<15} {:>10.4} {:>10.4} {:>10.4} {:>12.4}",
"OLS",
ols_fitted.coefficients()[0],
ols_fitted.coefficients()[1],
ols_fitted.coefficients()[2],
ols_fitted.r_squared()
);
println!(
"{:<15} {:>10.4} {:>10.4} {:>10.4} {:>12.4}",
"NNLS (>=0)",
nnls_fitted.coefficients()[0],
nnls_fitted.coefficients()[1],
nnls_fitted.coefficients()[2],
nnls_fitted.r_squared()
);
println!(
"{:<15} {:>10.4} {:>10.4} {:>10.4} {:>12.4}",
"BLS (custom)",
bls_fitted.coefficients()[0],
bls_fitted.coefficients()[1],
bls_fitted.coefficients()[2],
bls_fitted.r_squared()
);
println!("\nNote: OLS has best R-squared but may violate constraints.");
println!(" NNLS forces x2 to 0 (can't be negative).");
println!(" BLS with custom bounds [-0.5, 0.5] for x2 is a compromise.");
}