use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Signed, ToPrimitive, Zero};
use symplex::matrix::{QMatrix, ZMatrix};
use symplex::optimize::{
RootOpts, bisect, brent_root, linear_fit, poly_fit, poly_fit_exact, trapezoid,
};
use symplex::prelude::*;
use symplex::units::*;
fn q(x: f64) -> Ratio<BigInt> {
Ratio::from_float(x).unwrap()
}
#[test]
fn bracketing_root_finders_refuse_a_pole() {
let opts = RootOpts::default();
for r in [
brent_root(f64::tan, 1.0, 2.0, &opts),
bisect(f64::tan, 1.0, 2.0, &opts),
] {
assert!(
matches!(r, Err(SymplexError::ComputationFailed { .. })),
"{r:?}"
);
}
let recip = |x: f64| 1.0 / (x - 0.3) + 2.0;
assert!(brent_root(recip, 0.25, 0.31, &opts).is_err());
assert!(bisect(recip, 0.25, 0.31, &opts).is_err());
assert!((brent_root(recip, -1.0, 0.25, &opts).unwrap() + 0.2).abs() < 1e-12);
assert!((brent_root(f64::tan, 3.0, 3.3, &opts).unwrap() - std::f64::consts::PI).abs() < 1e-12);
assert!((bisect(f64::tan, 3.0, 3.3, &opts).unwrap() - std::f64::consts::PI).abs() < 1e-11);
}
#[test]
fn root_finders_survive_brackets_at_the_float_limits() {
let opts = RootOpts::default();
let r = 3.691_732_523_024_782_6;
let f = |x: f64| (0.005_101_480_868_353_455 * (x - r)).atan();
let x = brent_root(f, -f64::MAX, f64::MAX, &opts).unwrap();
assert!((x - r).abs() < 1e-11, "{x}");
let g = |x: f64| (7_738_637.162_667_296 * (x - 2.290_885_218_528_304)).atan();
let x = bisect(g, -f64::MAX, f64::MAX, &opts).unwrap();
assert!((x - 2.290_885_218_528_304).abs() < 1e-11, "{x}");
}
#[test]
fn brent_converges_on_a_triple_root() {
let opts = RootOpts::default();
for (r, s, a, b) in [
(
-3.732_691_433_039,
-46.993_907_865_85,
-4.233_456_654_491,
-3.730_208_542_931,
),
(
0.202_733_970_612,
10.603_170_319_504,
-81.679_041_210_82,
10.874_921_150_73,
),
] {
let x = brent_root(|x: f64| s * (x - r).powi(3), a, b, &opts).unwrap();
assert!(
(x - r).abs() <= opts.xtol + opts.rtol * r.abs(),
"{x} vs {r}"
);
}
}
#[test]
fn zero_tolerance_ends_at_adjacent_floats() {
let opts = RootOpts {
xtol: 0.0,
rtol: 0.0,
..RootOpts::default()
};
let f = |x: f64| x.exp() - 0.016_703_290_333_597_003;
let up = |x: f64| f64::from_bits(x.to_bits() - 1); let down = |x: f64| f64::from_bits(x.to_bits() + 1);
for x in [
brent_root(f, -5.535_953_805_763_644, -4.091_989_668_365_889, &opts).unwrap(),
bisect(f, -5.535_953_805_763_644, -4.091_989_668_365_889, &opts).unwrap(),
] {
let changes = |y: f64| (f(x) > 0.0) != (f(y) > 0.0) || f(x) == 0.0;
assert!(changes(up(x)) || changes(down(x)), "{x}");
}
}
fn fitted_value_error(xs: &[f64], ys: &[f64], c: &[f64], e: &[Ratio<BigInt>]) -> f64 {
let (mut err2, mut y2) = (Ratio::<BigInt>::zero(), Ratio::<BigInt>::zero());
for (&x, &y) in xs.iter().zip(ys) {
let (mut pw, mut d) = (Ratio::<BigInt>::one(), Ratio::<BigInt>::zero());
for (cj, ej) in c.iter().zip(e) {
d += (q(*cj) - ej) * &pw;
pw *= q(x);
}
err2 += &d * &d;
y2 += q(y) * q(y);
}
(err2.to_f64().unwrap() / y2.to_f64().unwrap()).sqrt()
}
#[test]
fn polynomial_fits_far_from_the_origin_are_accurate() {
let ys: Vec<f64> = (0..13)
.map(|k| {
let t = f64::from(k);
2.0 - 0.5 * t + 0.25 * t * t + if k % 2 == 0 { 1e-3 } else { -1e-3 }
})
.collect();
let xs: Vec<f64> = (0..13).map(|k| 1e9 + 60.0 * f64::from(k)).collect();
let fit = linear_fit(&xs, &ys).unwrap();
assert!(
(fit.slope - 0.041_666_666_666_666_664).abs() < 1e-16,
"{}",
fit.slope
);
assert!(
(fit.intercept + 41_666_670.166_589_744).abs() < 1e-6,
"{}",
fit.intercept
);
let xs: Vec<f64> = (0..13).map(|k| 1e4 + 0.01 * f64::from(k)).collect();
let pts: Vec<_> = xs.iter().zip(&ys).map(|(&x, &y)| (q(x), q(y))).collect();
let exact = poly_fit_exact(&pts, 3).unwrap();
let c = poly_fit(&xs, &ys, 3).unwrap();
assert!(fitted_value_error(&xs, &ys, &c, &exact) < 2e-6);
let xs: Vec<f64> = (0..13).map(|k| 1000.0 + 0.1 * f64::from(k)).collect();
let pts: Vec<_> = xs.iter().zip(&ys).map(|(&x, &y)| (q(x), q(y))).collect();
let exact = poly_fit_exact(&pts, 2).unwrap();
let c = poly_fit(&xs, &ys, 2).unwrap();
assert!(fitted_value_error(&xs, &ys, &c, &exact) < 3e-10);
}
#[test]
fn trapezoid_refuses_unsorted_abscissae() {
assert!(matches!(
trapezoid(&[0.0, 2.0, 1.0], &[0.0, 2.0, 1.0]),
Err(SymplexError::InvalidArgument { .. })
));
assert!(trapezoid(&[1.0, 2.0], &[f64::NAN, 1.0]).is_err());
assert_eq!(trapezoid(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0]).unwrap(), 2.0);
assert_eq!(trapezoid(&[2.0, 1.0, 0.0], &[2.0, 1.0, 0.0]).unwrap(), -2.0);
assert_eq!(
trapezoid(&[0.0, 0.0, 1.0, 1.0], &[0.0, 1.0, 1.0, 2.0]).unwrap(),
1.0
);
}
fn lcg_matrix(n: usize, seed: u64) -> ZMatrix {
let mut s = seed;
let rows: Vec<Vec<BigInt>> = (0..n)
.map(|_| {
(0..n)
.map(|_| {
s = s
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
BigInt::from(i64::try_from((s >> 33) % 19).unwrap() - 9)
})
.collect()
})
.collect();
ZMatrix::new(rows).unwrap()
}
fn is_integral(m: &QMatrix) -> bool {
m.iter().all(|v| v.is_integer())
}
#[test]
fn normal_forms_of_a_40_by_40_matrix_are_fast_and_exact() {
let a = lcg_matrix(40, 7);
let h = a.hermite_normal_form();
let n = 40;
let mut diag = BigInt::one();
for i in 0..n {
let p = h.get(i, i);
assert!(p.is_positive(), "pivot {i}");
diag *= p;
for k in 0..i {
assert!(h.get(i, k).is_zero(), "below diagonal ({i},{k})");
assert!(
!h.get(k, i).is_negative() && h.get(k, i) < p,
"not reduced ({k},{i})"
);
}
}
assert_eq!(diag, a.det().unwrap().abs());
let (aq, hq) = (a.to_qmatrix(), h.to_qmatrix());
assert!(is_integral(&(&hq * &aq.inv().unwrap())));
assert!(is_integral(&(&aq * &hq.inv().unwrap())));
let s = a.smith_normal_form();
let t = h.smith_normal_form_with_transforms();
assert_eq!(s, t.s);
assert_eq!(&(&t.u * &h) * &t.v, t.s);
}
#[test]
fn integer_kernel_of_a_wide_matrix_is_fast_and_saturated() {
let square = lcg_matrix(40, 11);
let a = ZMatrix::new(square.to_rows().into_iter().take(30).collect()).unwrap();
let kernel = a.integer_nullspace();
assert_eq!(kernel.len(), 10);
for k in &kernel {
assert!((&a * k).is_zero());
}
let rows: Vec<Vec<BigInt>> = kernel
.iter()
.map(|k| (0..40).map(|i| k.get(i, 0).clone()).collect())
.collect();
let s = ZMatrix::new(rows).unwrap().smith_normal_form();
assert!((0..10).all(|i| s.get(i, i).is_one()), "{s:?}");
}
#[test]
fn dimensional_calculus_with_converted_variables() {
let ctx = Context::new();
let tau = ctx.symbol("tau");
let xi = ctx.symbol("xi");
let theta = ctx.symbol("theta");
let position = Length::meters(&(&tau * &tau)).as_qty();
let v = diff_qty(&position, &Time::minutes(&tau).as_qty());
assert!(
(v.inner() - &tau / 30).expand().is_zero_structural(),
"{}",
v.inner()
);
let w = integrate_qty(
&Force::newtons(&ctx.int(5)).as_qty(),
&Length::kilometers(&xi).as_qty(),
);
assert!(
(w.inner() - &xi * 5000).expand().is_zero_structural(),
"{}",
w.inner()
);
let minutes = Time::minutes(&tau);
let by_method = Length::meters(&(&tau * &tau)).diff(&minutes);
assert!(
(&by_method - &tau / 30).expand().is_zero_structural(),
"{by_method}"
);
let by_qty = Length::meters(&(&tau * &tau)).as_qty().integrate(&minutes);
assert!(
(&by_qty - tau.powi(3) * 20).expand().is_zero_structural(),
"{by_qty}"
);
let heat = Energy::joules(&(&theta * 3)).as_qty();
let capacity = diff_qty(&heat, &Temperature::from_celsius(&theta).as_qty());
assert_eq!(capacity.inner(), &ctx.int(3));
let t = Time::symbol(&ctx, "t").as_qty();
let x = Length::meters(&ctx.symbol("t").powi(2)).as_qty();
assert_eq!(format!("{}", diff_qty(&x, &t).inner()), "2*t");
}
#[test]
fn dimension_inference_of_arg_product_series_and_laplace() {
let ctx = Context::new();
let (x, t, k, s) = (
ctx.symbol("x"),
ctx.symbol("t"),
ctx.symbol("k"),
ctx.symbol("s"),
);
let f = ctx.apply("f", &[&t]).unwrap();
let dims = DimMap::new()
.with("x", ConstDim::LENGTH)
.with("t", ConstDim::TIME)
.with("k", ConstDim::DIMENSIONLESS)
.with("s", ConstDim::FREQUENCY)
.with("f(t)", ConstDim::LENGTH);
let d = |e: &Ex| infer_dimension(e, &dims);
assert_eq!(d(&x.arg()), Ok(ConstDim::DIMENSIONLESS));
let product = Ex::symbolic_product(&x, &k, &ctx.int(1), &ctx.int(3));
assert_eq!(d(&product), Ok(ConstDim::VOLUME));
assert!(d(&Ex::symbolic_product(&x, &k, &ctx.int(1), &k)).is_err());
assert_eq!(d(&f.series(&t, &ctx.int(0), 3)), Ok(ConstDim::LENGTH));
assert_eq!(
d(&f.laplace(&t, &s)),
Ok(ConstDim::LENGTH.mul(ConstDim::TIME))
);
let wrong = DimMap::new()
.with("t", ConstDim::TIME)
.with("s", ConstDim::TIME)
.with("f(t)", ConstDim::LENGTH);
assert!(infer_dimension(&f.laplace(&t, &s), &wrong).is_err());
}