use symplex::certificates::{SosOpts, prove_sos};
use symplex::linprog::q;
use symplex::optimize::poly_fit;
use symplex::polysys::solve_numeric_system;
use symplex::prelude::*;
use symplex::stats::Rng;
use symplex::stats::cox::{CoxOpts, cox_ph};
use symplex::stats::multivariate::{MultivariateNormal, pca_f64};
use symplex::stats::regression::{LogitOpts, logit, mnlogit};
use symplex::stats::survival::Observation;
fn close(actual: f64, expected: f64, label: &str) {
assert!(
(actual - expected).abs() < 1e-9 * expected.abs().max(1.0),
"{label}: {actual} vs {expected}"
);
}
#[test]
fn logit_doc_values_unchanged() {
let y: Vec<bool> = (0..20).map(|i| matches!(i, 7..=9 | 13..=19)).collect();
let x: Vec<Vec<f64>> = (0..20)
.map(|i| vec![if i < 10 { 0.0 } else { 1.0 }])
.collect();
let fit = logit(&y, &x, true, &LogitOpts::default()).unwrap();
close(fit.coefficients[0], (3.0f64 / 7.0).ln(), "β₀");
close(fit.coefficients[1], (49.0f64 / 9.0).ln(), "β₁");
close(fit.standard_errors[0], 0.6900655593423543, "se₀");
close(fit.standard_errors[1], 0.9759000729485332, "se₁");
close(fit.p_values[0], 0.21950281228300073, "p₀");
close(fit.p_values[1], 0.08248537711586468, "p₁");
close(fit.cov_params[0][0], 10.0 / 21.0, "cov₀₀");
close(fit.cov_params[0][1], -10.0 / 21.0, "cov₀₁");
close(fit.cov_params[1][0], -10.0 / 21.0, "cov₁₀");
close(fit.cov_params[1][1], 20.0 / 21.0, "cov₁₁");
close(fit.log_likelihood, -12.217286041097868, "llf");
let collinear: Vec<Vec<f64>> = x.iter().map(|r| vec![r[0], 2.0 * r[0]]).collect();
assert!(matches!(
logit(&y, &collinear, true, &LogitOpts::default()),
Err(SymplexError::InvalidArgument { .. })
));
}
#[test]
fn mnlogit_doc_values_unchanged() {
let y = [
0, 0, 1, 0, 0, 1, 2, 0, 1, 1, 2, 0, 1, 2, 1, 2, 1, 2, 2, 1, 2, 0, 2, 2,
];
let x: Vec<Vec<f64>> = (1..=24).map(|i| vec![f64::from(i)]).collect();
let fit = mnlogit(&y, &x, true, &LogitOpts::default()).unwrap();
assert!(fit.converged);
close(fit.coefficients[0][0], -0.9175964754951935, "β₁₀");
close(fit.coefficients[0][1], 0.10985702371911016, "β₁₁");
close(fit.coefficients[1][0], -2.875029245270399, "β₂₀");
close(fit.coefficients[1][1], 0.2536273262970427, "β₂₁");
close(fit.standard_errors[0][0], 1.0202923117484362, "se₁₀");
close(fit.standard_errors[0][1], 0.09311682562597019, "se₁₁");
close(fit.standard_errors[1][0], 1.4432124419606307, "se₂₀");
close(fit.standard_errors[1][1], 0.1081682371602044, "se₂₁");
close(fit.p_values[0][0], 0.36846804499407193, "p₁₀");
close(fit.p_values[1][1], 0.019039911083865633, "p₂₁");
close(fit.log_likelihood, -22.117379463121267, "llf");
}
#[test]
fn cox_doc_values_unchanged() {
let obs = Observation::from_i64(
&[4, 7, 2, 9, 12, 5, 15, 3, 11, 8],
&[
true, true, true, false, true, true, false, true, true, false,
],
);
let x: Vec<Vec<f64>> = [3.0, 1.0, 5.0, 2.0, 0.0, 4.0, 1.0, 6.0, 2.0, 3.0]
.iter()
.map(|&v| vec![v])
.collect();
let fit = cox_ph(&obs, &x, &CoxOpts::default()).unwrap();
close(fit.coefficients[0], 0.875_980_988_764_909_6, "β");
close(fit.standard_errors[0], 0.380_902_829_654_236_7, "se");
close(fit.log_likelihood, -8.465_216_276_861_835, "llf");
close(
fit.cov_params[0][0],
0.380_902_829_654_236_7 * 0.380_902_829_654_236_7,
"cov",
);
close(
fit.z_values[0],
0.875_980_988_764_909_6 / 0.380_902_829_654_236_7,
"z",
);
assert!(
fit.p_values[0] > 0.02 && fit.p_values[0] < 0.022,
"{}",
fit.p_values[0]
);
assert_eq!(fit.concordance().unwrap(), q(31, 38));
}
fn mvn2(ctx: &Context) -> MultivariateNormal {
MultivariateNormal::try_new(vec![ctx.int(1), ctx.int(2)], matrix![ctx, [2, 1], [1, 3]]).unwrap()
}
#[test]
fn mvn_precision_density_and_sampling_unchanged() {
let ctx = Context::new();
let mvn = mvn2(&ctx);
let prec = mvn.precision().unwrap();
assert_eq!(*prec.get(0, 0), ctx.rational(3, 5));
assert_eq!(*prec.get(0, 1), ctx.rational(-1, 5));
assert_eq!(*prec.get(1, 1), ctx.rational(2, 5));
let d = mvn
.density(&[ctx.rational(3, 2), ctx.rational(5, 2)])
.unwrap()
.eval_f64()
.unwrap();
close(d, 0.06603330634679298, "pdf(1.5, 2.5)");
let n = 4000;
let draws = mvn.sample(n, &mut Rng::new(42)).unwrap();
assert_eq!(draws, mvn.sample(n, &mut Rng::new(42)).unwrap());
let mean = |j: usize| draws.iter().map(|x| x[j]).sum::<f64>() / n as f64;
let cov = |i: usize, j: usize| {
let (mi, mj) = (mean(i), mean(j));
draws.iter().map(|x| (x[i] - mi) * (x[j] - mj)).sum::<f64>() / (n as f64 - 1.0)
};
assert!((mean(0) - 1.0).abs() < 0.1 && (mean(1) - 2.0).abs() < 0.1);
assert!((cov(0, 0) - 2.0).abs() < 0.25 && (cov(1, 1) - 3.0).abs() < 0.25);
assert!((cov(0, 1) - 1.0).abs() < 0.25);
let degenerate =
MultivariateNormal::new(vec![ctx.int(0), ctx.int(0)], matrix![ctx, [1, 1], [1, 1]]);
assert!(matches!(
degenerate.sample(1, &mut Rng::new(1)),
Err(SymplexError::ComputationFailed { .. })
));
}
#[test]
fn pca_f64_matches_numpy_eigh() {
let cov = vec![
vec![4.0, 1.0, 0.5, 0.0],
vec![1.0, 3.0, 0.25, 0.5],
vec![0.5, 0.25, 2.0, 1.0],
vec![0.0, 0.5, 1.0, 5.0],
];
let p = pca_f64(&cov).unwrap();
let expected = [
5.5446889571830384,
4.5024046887344715,
2.3618374429898186,
1.5910689110926703,
];
for (i, e) in expected.iter().enumerate() {
close(p.eigenvalues[i], *e, &format!("λ{i}"));
}
let v0 = [
0.30225746834694595,
0.3150155112535152,
0.3037565246562271,
0.8468397866343743,
];
for (j, c) in v0.iter().enumerate() {
close(p.components[0][j], *c, &format!("v0{j}"));
}
close(p.explained_variance_ratio[0], 0.39604921122735987, "ratio₀");
let e = pca_f64(&[vec![2.0, -1.0], vec![-1.0, 2.0]]).unwrap();
close(e.eigenvalues[0], 3.0, "λ₀");
close(e.eigenvalues[1], 1.0, "λ₁");
assert!(
pca_f64(&[vec![1.0, 2.0], vec![3.0, 4.0]]).is_err(),
"not symmetric"
);
}
#[test]
fn poly_fit_is_householder_least_squares() {
let c = poly_fit(&[0.0, 1.0, 2.0, 3.0], &[1.0, 3.0, 2.0, 5.0], 1).unwrap();
close(c[0], 1.1, "intercept");
close(c[1], 1.1, "slope");
let xs: Vec<f64> = (0..6).map(f64::from).collect();
let ys: Vec<f64> = xs.iter().map(|x| 1.0 + 2.0 * x + 3.0 * x * x).collect();
let c = poly_fit(&xs, &ys, 2).unwrap();
close(c[0], 1.0, "c₀");
close(c[1], 2.0, "c₁");
close(c[2], 3.0, "c₂");
assert!(matches!(
poly_fit(&[0.0, 1.0, 0.0, 1.0], &[0.0, 1.0, 0.0, 1.0], 2),
Err(SymplexError::ComputationFailed { .. })
));
}
#[test]
fn newton_system_solves_through_partial_pivoting() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let eqs = [&x.powi(2) + &y.powi(2) - 1, &y - &x];
let sol = solve_numeric_system(&eqs, &[x.clone(), y.clone()], &[1.0, 1.0]).unwrap();
let r = std::f64::consts::FRAC_1_SQRT_2;
assert!((sol[0] - r).abs() < 1e-10 && (sol[1] - r).abs() < 1e-10);
let eqs = [&y - 1, &x + &y - 3];
let sol = solve_numeric_system(&eqs, &[x.clone(), y.clone()], &[0.0, 0.0]).unwrap();
assert!((sol[0] - 2.0).abs() < 1e-10 && (sol[1] - 1.0).abs() < 1e-10);
let eqs = [&x + &y, 2 * (&x + &y) - 1];
assert!(matches!(
solve_numeric_system(&eqs, &[x, y], &[0.0, 0.0]),
Err(SymplexError::ComputationFailed { .. })
));
}
#[test]
fn heurisch_outcomes_unchanged() {
let ctx = Context::new();
let x = ctx.symbol("x");
for e in [x.ln().sin(), x.exp() * x.cos().powi(2), x.exp() * x.tan()] {
let s = e.integrate(&x).to_string();
assert!(s.starts_with("Integral("), "{e}: {s}");
}
assert_eq!(
(&x * x.exp()).integrate(&x).to_string(),
"x*exp(x) - exp(x)"
);
assert_eq!(
(x.ln().ln() / &x).integrate(&x).to_string(),
"-ln(x) + ln(x)*ln(ln(x))"
);
}
#[test]
fn amgm_sos_certificate_is_byte_identical() {
let ctx = Context::new();
let (x, y, z) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("z"));
let amgm = x.powi(4) + y.powi(4) + z.powi(4) - &x * &y * &z * 4 + 1;
let out = prove_sos(&amgm, &[x, y, z], &SosOpts::default()).unwrap();
let sos = out.certificate().unwrap();
assert!(sos.verify());
assert_eq!(
sos.to_string(),
"x^4 + y^4 + z^4 - 4*x*y*z + 1 = (-1/3*x^2 - 1/3*y^2 - 1/3*z^2 + 1)^2 + 2/3*(-y*z + x)^2 + 2/3*(-x*z + y)^2 + 2/3*(-x*y + z)^2 + 2/3*(-x^2 + y^2)^2 + 8/9*(-1/2*x^2 - 1/2*y^2 + z^2)^2"
);
}