use symplex::certificates::{
BoxBound, Outcome, ParamBound, PolyhedronOpts, Ray, SosOpts, prove_nonnegative_on_box,
prove_nonnegative_on_halfline, prove_nonnegative_on_polyhedron, prove_sos,
};
use symplex::linprog::{q, qi};
use symplex::num_rational::Ratio;
use symplex::prelude::*;
fn close(actual: f64, expected: f64, label: &str) {
assert!(
(actual - expected).abs() <= 1e-12,
"{label}: got {actual:.17e}, expected {expected:.17e}"
);
}
fn at_one_third(e: &Ex, x: &Ex) -> f64 {
let ctx = x.context();
e.subs(x, &ctx.rational(1, 3))
.eval()
.eval_f64()
.unwrap_or_else(|err| panic!("{e} at x = 1/3 did not evaluate: {err:?}"))
}
fn err_is_computation_failed<T: std::fmt::Debug>(r: &Result<T, SymplexError>, label: &str) {
assert!(
matches!(r, Err(SymplexError::ComputationFailed { .. })),
"{label}: expected a ComputationFailed error, got {r:?}"
);
}
fn err_is_invalid<T: std::fmt::Debug>(r: &Result<T, SymplexError>, label: &str) {
assert!(
matches!(r, Err(SymplexError::InvalidArgument { .. })),
"{label}: expected an InvalidArgument error, got {r:?}"
);
}
#[test]
fn fourier_series_of_x_is_the_sawtooth_sine_series() {
let ctx = Context::new();
let x = ctx.symbol("x");
let series = x.fourier_series(&x, 3);
let expected = 2 * x.sin() - (2 * &x).sin() + ctx.rational(2, 3) * (3 * &x).sin();
assert_eq!(series.equals(&expected), Some(true), "got {series}");
}
#[test]
fn fourier_series_of_x_squared_is_pi_squared_over_3_minus_alternating_cosines() {
let ctx = Context::new();
let x = ctx.symbol("x");
let series = x.powi(2).fourier_series(&x, 3);
let expected =
ctx.pi().powi(2) / 3 - 4 * x.cos() + (2 * &x).cos() - ctx.rational(4, 9) * (3 * &x).cos();
assert_eq!(series.equals(&expected), Some(true), "got {series}");
close(
at_one_third(&series, &x),
0.05579325105083256,
"x^2 partial sum at 1/3",
);
}
#[test]
fn fourier_series_of_cos_x_reproduces_cos_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let series = x.cos().fourier_series(&x, 3);
assert_eq!(series.equals(&x.cos()), Some(true), "got {series}");
}
#[test]
fn fourier_series_of_sign_x_is_the_square_wave_odd_sine_series() {
let ctx = Context::new();
let x = ctx.symbol("x");
let series = x.sign().fourier_series(&x, 3);
let expected = 4 / ctx.pi() * (x.sin() + (3 * &x).sin() / 3);
assert_eq!(series.equals(&expected), Some(true), "got {series}");
}
#[test]
fn fourier_series_on_symmetric_interval_agrees_with_fourier_series() {
let ctx = Context::new();
let x = ctx.symbol("x");
let pi = ctx.pi();
let fixed = x.powi(2).fourier_series(&x, 3);
let general = x
.powi(2)
.fourier_series_on(&x, &(-&pi), &pi, 3)
.expect("x^2 has closed-form coefficients")
.truncate(3);
assert_eq!(fixed.equals(&general), Some(true), "{fixed} vs {general}");
close(
at_one_third(&fixed, &x) - at_one_third(&general, &x),
0.0,
"difference at 1/3",
);
}
#[test]
fn fourier_series_of_exp_ax_specialises_to_the_exp_half_x_series() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let series = (&a * &x).exp().fourier_series(&x, 1);
assert!(!series.has_unevaluated(), "{series}");
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
close(
at_one_third(&at_half, &x),
1.294773709082117,
"exp(x/2) one-harmonic partial sum at 1/3",
);
}
#[test]
fn fourier_series_of_cosh_ax_has_only_cosine_terms() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let series = (&a * &x).cosh().fourier_series(&x, 1);
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
let expected = 2 * (ctx.pi() / 2).sinh() / ctx.pi() * (1 - ctx.rational(2, 5) * x.cos());
assert_eq!(at_half.equals(&expected), Some(true), "got {at_half}");
close(
at_one_third(&at_half, &x),
0.9112878127970689,
"cosh(x/2) one-harmonic partial sum at 1/3",
);
}
#[test]
fn fourier_series_of_x_exp_ax_matches_sympy_numerically() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let series = (&x * (&a * &x).exp()).fourier_series(&x, 1);
assert!(!series.has_unevaluated(), "{series}");
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
close(
at_one_third(&at_half, &x),
0.5336773090573503,
"x exp(x/2) one-harmonic partial sum at 1/3",
);
}
#[test]
fn fourier_series_fallback_for_exp_2ax_has_the_mean_value_of_f() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let pi = ctx.pi();
let f = (2 * &a * &x).exp();
let direct = f
.fourier_series_on(&x, &(-&pi), &pi, 1)
.expect("closed-form coefficients")
.truncate(1);
let v = direct
.subs(&a, &ctx.rational(1, 2))
.subs(&x, &ctx.rational(1, 3))
.eval();
assert!(
v.eval_decimal(25)
.unwrap()
.starts_with("1.40513575105615064273916"),
"{direct}"
);
let series = f.fourier_series(&x, 1);
assert!(
!series.has_unevaluated(),
"fallback left an unevaluated node: {series}"
);
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
let mean = ((at_half.subs(&x, &ctx.int(0)) + at_half.subs(&x, &pi)) / 2).eval();
let expected = (pi.exp() - (-&pi).exp()) / (2 * &pi);
assert_eq!(mean.equals(&expected), Some(true), "mean = {mean}");
close(mean.eval_f64().unwrap(), 3.676077910374978, "sinh(pi)/pi");
}
#[test]
fn fourier_series_fallback_for_exp_ax_two_harmonics_has_the_mean_value_of_f() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let pi = ctx.pi();
let f = (&a * &x).exp();
let direct = f
.fourier_series_on(&x, &(-&pi), &pi, 2)
.expect("closed-form coefficients")
.truncate(2);
let v = direct
.subs(&a, &ctx.rational(1, 2))
.subs(&x, &ctx.rational(1, 3))
.eval();
assert!(
v.eval_decimal(25)
.unwrap()
.starts_with("1.00390187253532574801704"),
"{direct}"
);
let series = f.fourier_series(&x, 2);
assert!(!series.has_unevaluated(), "{series}");
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
let samples = [ctx.int(0), &pi / 2, pi.clone(), 3 * &pi / 2];
let mut total = ctx.int(0);
for s in &samples {
total = (&total + at_half.subs(&x, s)).eval();
}
let mean = (&total / 4).eval();
let expected = ((&pi / 2).exp() - (-&pi / 2).exp()) / π
assert_eq!(mean.equals(&expected), Some(true), "mean = {mean}");
close(
mean.eval_f64().unwrap(),
1.465052383336635,
"2 sinh(pi/2)/pi",
);
}
#[test]
fn fourier_series_fallback_for_exp_2ax_is_evaluable() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let series = (2 * &a * &x).exp().fourier_series(&x, 1);
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
close(
at_one_third(&at_half, &x),
1.405135751056151,
"exp(x) one-harmonic partial sum at 1/3",
);
}
#[test]
fn fourier_series_fallback_second_harmonic_of_exp_ax_is_evaluable() {
let ctx = Context::new();
let x = ctx.symbol("x");
let a = ctx.symbol("a");
let series = (&a * &x).exp().fourier_series(&x, 2);
let at_half = series.subs(&a, &ctx.rational(1, 2)).eval();
close(
at_one_third(&at_half, &x),
1.003901872535326,
"exp(x/2) two-harmonic partial sum at 1/3",
);
}
#[test]
fn fourier_series_of_ln_2_plus_cos_x_is_not_nan() {
let ctx = Context::new();
let x = ctx.symbol("x");
let series = (2 + x.cos()).ln().fourier_series(&x, 2);
assert!(series.has_unevaluated(), "{series}");
close(
at_one_third(&series, &x),
1.073787450967885,
"ln(2 + cos x) two-harmonic partial sum at 1/3",
);
}
#[test]
fn matrix_lll_reduces_the_textbook_basis_like_sympy() {
let ctx = Context::new();
let b = matrix![ctx, [1, 1, 1], [-1, 0, 2], [3, 5, 6]];
let reduced = b.lll(Ratio::new(3, 4)).unwrap();
assert_eq!(reduced, matrix![ctx, [0, 1, 0], [1, 0, 1], [-1, 0, 2]]);
}
#[test]
fn matrix_lll_result_is_a_unimodular_transform_of_the_input() {
let ctx = Context::new();
let b = matrix![ctx, [1, 1, 1], [-1, 0, 2], [3, 5, 6]];
let r = b.lll(Ratio::new(3, 4)).unwrap();
let t = (&r * &b.inv().unwrap()).eval();
assert_eq!(t, matrix![ctx, [-4, -1, 1], [5, 1, -1], [0, 1, 0]]);
assert_eq!(t.det().unwrap(), ctx.int(1));
assert_eq!(b.det().unwrap(), ctx.int(-3));
assert_eq!(r.det().unwrap(), ctx.int(-3));
}
#[test]
fn matrix_lll_of_the_identity_is_the_identity() {
let ctx = Context::new();
let id = Matrix::identity(&ctx, 3);
assert_eq!(id.lll(Ratio::new(3, 4)).unwrap(), id);
}
#[test]
fn matrix_lll_reduces_a_skewed_2d_basis_to_short_vectors() {
let ctx = Context::new();
let b = matrix![ctx, [201, 37], [1648, 297]];
let r = b.lll(Ratio::new(3, 4)).unwrap();
assert_eq!(r, matrix![ctx, [1, 32], [40, 1]]);
assert_eq!(r.det().unwrap(), ctx.int(-1279));
}
#[test]
fn matrix_lll_rejects_delta_outside_the_open_interval() {
let ctx = Context::new();
let b = matrix![ctx, [1, 1, 1], [-1, 0, 2], [3, 5, 6]];
err_is_invalid(&b.lll(Ratio::new(1, 4)), "delta = 1/4");
err_is_invalid(&b.lll(Ratio::new(1, 1)), "delta = 1");
assert!(
b.lll(Ratio::new(99, 100)).is_ok(),
"delta = 99/100 is valid"
);
}
#[test]
fn matrix_lll_rejects_dependent_rows_and_non_integer_entries() {
let ctx = Context::new();
err_is_invalid(
&matrix![ctx, [1, 2], [2, 4]].lll(Ratio::new(3, 4)),
"dependent rows",
);
let t = ctx.symbol("t");
err_is_invalid(
&(matrix![ctx, [1, 2], [3, 4]] * &t)
.eval()
.lll(Ratio::new(3, 4)),
"symbolic entries",
);
}
#[test]
fn qmatrix_set_changes_the_determinant_exactly() {
let mut a = QMatrix::from_i64(&[&[2, 1, 0], &[1, 3, 1], &[0, 1, 4]]).unwrap();
assert_eq!(a.det().unwrap(), qi(18));
a.set(1, 1, q(7, 2));
assert_eq!(*a.get(1, 1), q(7, 2));
assert_eq!(a.det().unwrap(), qi(22));
}
#[test]
fn qmatrix_get_mut_writes_through_to_as_slice() {
let mut a = QMatrix::from_i64(&[&[2, 1, 0], &[1, 3, 1], &[0, 1, 4]]).unwrap();
*a.get_mut(0, 2) = q(-5, 3);
assert_eq!(a.det().unwrap(), q(49, 3));
let flat = a.as_slice();
assert_eq!(flat.len(), 9);
assert_eq!(flat[2], q(-5, 3));
assert_eq!(
flat,
&[
qi(2),
qi(1),
q(-5, 3),
qi(1),
qi(3),
qi(1),
qi(0),
qi(1),
qi(4)
]
);
}
#[test]
fn outcome_proved_exposes_the_certificate_and_nothing_else() {
let ctx = Context::new();
let x = ctx.symbol("x");
let out =
prove_nonnegative_on_halfline(&(x.powi(2) + 1), &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
assert!(out.is_proved());
assert!(!out.is_refuted());
assert!(!out.is_unknown());
let cert = out.certificate().expect("proved");
assert!(cert.verify());
assert_eq!(cert.polya_power(), 0);
assert!(out.refutation().is_none());
assert!(out.unknown().is_none());
assert_eq!(format!("{out}"), format!("proved: {cert}"));
assert_eq!(format!("{out}"), "proved: x^2 + 1 = x^2 + 1, x ≥ 0");
}
#[test]
fn outcome_refuted_exposes_the_counterexample() {
let ctx = Context::new();
let x = ctx.symbol("x");
let out = prove_nonnegative_on_halfline(&(&x - 1), &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
assert!(out.is_refuted());
assert!(!out.is_proved());
assert!(!out.is_unknown());
let (point, value) = out.refutation().expect("refuted");
assert_eq!(point.len(), 1);
assert_eq!(point[0].0, x);
assert_eq!(point[0].1, qi(0));
assert_eq!(*value, qi(-1));
assert!(out.certificate().is_none());
assert!(out.unknown().is_none());
assert_eq!(format!("{out}"), "refuted at (x = 0): value -1");
}
#[test]
fn outcome_unknown_exposes_the_search_account() {
let ctx = Context::new();
let x = ctx.symbol("x");
let out =
prove_nonnegative_on_halfline(&(x.powi(2) - &x + 1), &x, &ctx.int(0), Ray::AtLeast, 0)
.unwrap();
assert!(out.is_unknown());
assert!(!out.is_proved());
assert!(!out.is_refuted());
assert_eq!(out.unknown().expect("unknown").max_polya_power, 0);
assert!(out.certificate().is_none());
assert!(out.refutation().is_none());
assert_eq!(
format!("{out}"),
"unknown: non-negative by Sturm's theorem, but no certificate up to Pólya power 0"
);
let proved =
prove_nonnegative_on_halfline(&(x.powi(2) - &x + 1), &x, &ctx.int(0), Ray::AtLeast, 10)
.unwrap();
assert!(proved.is_proved(), "{proved}");
}
#[test]
fn outcome_map_certificate_touches_only_the_proved_variant() {
let ctx = Context::new();
let x = ctx.symbol("x");
let goal = x.powi(2) + 1;
let proved = prove_nonnegative_on_halfline(&goal, &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
assert_eq!(
proved.map_certificate(|c| c.polya_power()),
Outcome::Proved(0u32)
);
let refuted =
prove_nonnegative_on_halfline(&(&x - 1), &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
match refuted.map_certificate(|c| c.polya_power()) {
Outcome::Refuted { point, value, .. } => {
assert_eq!(point, vec![(x.clone(), qi(0))]);
assert_eq!(value, qi(-1));
}
other => panic!("refuted must stay refuted: {other}"),
}
let unknown =
prove_nonnegative_on_halfline(&(x.powi(2) - &x + 1), &x, &ctx.int(0), Ray::AtLeast, 0)
.unwrap();
let account = unknown.unknown().expect("unknown").clone();
assert_eq!(
unknown.map_certificate(|c| c.polya_power()),
Outcome::Unknown(account)
);
}
#[test]
fn outcome_into_certificate_moves_the_certificate_out() {
let ctx = Context::new();
let x = ctx.symbol("x");
let proved =
prove_nonnegative_on_halfline(&(x.powi(2) + 1), &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
let cert = proved.into_certificate().expect("proved");
assert!(cert.verify());
assert_eq!(cert.polya_power(), 0);
let refuted =
prove_nonnegative_on_halfline(&(&x - 1), &x, &ctx.int(0), Ray::AtLeast, 10).unwrap();
assert!(refuted.into_certificate().is_none());
let unknown =
prove_nonnegative_on_halfline(&(x.powi(2) - &x + 1), &x, &ctx.int(0), Ray::AtLeast, 0)
.unwrap();
assert!(unknown.into_certificate().is_none());
}
#[test]
fn outcome_display_of_a_parametric_refutation_names_the_parameter() {
let ctx = Context::new();
let (j, r) = (ctx.symbol("j"), ctx.symbol("r"));
let hyps = [r.clone(), ctx.rational(1, 2) - &r];
let param = ParamBound {
var: j.clone(),
lower: ctx.int(1),
};
let out = prove_nonnegative_on_polyhedron(
&(&j - &j * &r * 2 - 1),
&hyps,
Some(¶m),
&PolyhedronOpts::default(),
)
.unwrap();
match &out {
Outcome::Refuted {
point,
value,
param_value,
..
} => {
assert_eq!(point.len(), 2);
assert_eq!(point[0], (r.clone(), q(1, 2)));
assert_eq!(point[1], (j.clone(), qi(1)));
assert_eq!(*value, qi(-1));
assert_eq!(*param_value, Some(qi(1)));
}
other => panic!("{other}"),
}
assert_eq!(
format!("{out}"),
"refuted at (r = 1/2, j = 1): value -1 (parameter 1)"
);
}
#[test]
fn box_prover_refutes_x_minus_2_at_the_origin_and_proves_x_times_1_minus_x() {
let ctx = Context::new();
let x = ctx.symbol("x");
let bounds = [BoxBound {
var: x.clone(),
lo: ctx.int(0),
hi: ctx.int(1),
}];
let refuted = prove_nonnegative_on_box(&(&x - 2), &bounds, 1).unwrap();
let (point, value) = refuted.refutation().expect("refuted");
assert_eq!(point, &[(x.clone(), qi(0))]);
assert_eq!(*value, qi(-2));
assert_eq!(format!("{refuted}"), "refuted at (x = 0): value -2");
let proved = prove_nonnegative_on_box(&(&x * (1 - &x)), &bounds, 2).unwrap();
let cert = proved.certificate().expect("proved");
assert!(cert.verify());
assert!(format!("{proved}").starts_with("proved: "));
assert!(format!("{proved}").ends_with(", 0 ≤ x ≤ 1"));
}
#[test]
fn sos_prover_reports_motzkin_as_unknown_with_a_reason() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let motzkin = x.powi(4) * y.powi(2) + x.powi(2) * y.powi(4) - 3 * x.powi(2) * y.powi(2) + 1;
let out = prove_sos(&motzkin, &[x, y], &SosOpts::default()).unwrap();
assert!(out.is_unknown(), "{out}");
let account = out.unknown().expect("unknown");
assert!(!account.reason.is_empty());
assert_eq!(account.budget_exhausted, None);
assert_eq!(format!("{out}"), format!("unknown: {}", account.reason));
assert!(out.certificate().is_none());
assert!(out.refutation().is_none());
}
#[test]
fn limit_of_a_nan_or_zoo_constant_is_an_error() {
let ctx = Context::new();
let x = ctx.symbol("x");
err_is_computation_failed(&ctx.nan().try_limit(&x, &ctx.int(0)), "limit of nan");
err_is_computation_failed(
&ctx.complex_infinity().try_limit(&x, &ctx.int(0)),
"limit of zoo",
);
assert!(ctx.nan().limit(&x, &ctx.int(0)).has_unevaluated());
}
#[test]
fn limit_at_a_nan_or_zoo_point_is_an_error() {
let ctx = Context::new();
let x = ctx.symbol("x");
err_is_computation_failed(&x.try_limit(&x, &ctx.nan()), "point nan");
err_is_computation_failed(&x.try_limit(&x, &ctx.complex_infinity()), "point zoo");
let e = x.try_limit(&x, &ctx.nan()).unwrap_err().to_string();
assert!(e.contains("finite or ±∞"), "{e}");
}
#[test]
fn one_sided_limits_of_atanh_at_its_poles() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.atanh().limit_left(&x, &ctx.int(1)), ctx.infinity());
assert_eq!(x.atanh().limit_right(&x, &ctx.int(-1)), ctx.neg_infinity());
}
#[test]
fn one_sided_limits_of_tan_at_pi_over_2() {
let ctx = Context::new();
let x = ctx.symbol("x");
let half_pi = ctx.pi() / 2;
assert_eq!(x.tan().limit_left(&x, &half_pi), ctx.infinity());
assert_eq!(
x.tan().limit_dir(&x, &half_pi, Direction::Right),
ctx.neg_infinity()
);
}
#[test]
fn one_sided_limits_of_floor_and_ceiling_at_an_integer() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.floor().limit_right(&x, &ctx.int(2)), ctx.int(2));
assert_eq!(x.floor().limit_left(&x, &ctx.int(2)), ctx.int(1));
assert_eq!(x.ceiling().limit_right(&x, &ctx.int(2)), ctx.int(3));
assert!(x.floor().try_limit(&x, &ctx.int(2)).is_err());
}
#[test]
fn one_sided_limits_of_gamma_and_digamma_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.gamma().limit_right(&x, &ctx.int(0)), ctx.infinity());
assert_eq!(x.digamma().limit_right(&x, &ctx.int(0)), ctx.neg_infinity());
assert_eq!(
(&x + 1).gamma().limit_right(&x, &ctx.int(-1)),
ctx.infinity()
);
}
#[test]
fn right_limits_of_powers_of_one_over_x_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
assert_eq!(
(1 / &x).pow(&ctx.rational(1, 2)).limit_right(&x, &zero),
ctx.infinity()
);
assert_eq!((-1 / &x).powi(3).limit_right(&x, &zero), ctx.neg_infinity());
assert_eq!((-1 / &x).powi(2).limit_right(&x, &zero), ctx.infinity());
assert_eq!(
(1 / &x).pow(&ctx.rational(-1, 2)).limit_right(&x, &zero),
ctx.int(0)
);
}
#[test]
fn right_limits_of_exponentials_and_shifts_of_one_over_x_at_zero() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
assert_eq!(
ctx.int(2).pow(&(1 / &x)).limit_right(&x, &zero),
ctx.infinity()
);
assert_eq!(
ctx.int(2).pow(&(-1 / &x)).limit_right(&x, &zero),
ctx.int(0)
);
assert_eq!((3 - 1 / &x).limit_right(&x, &zero), ctx.neg_infinity());
assert_eq!(x.ln().limit_right(&x, &zero), ctx.neg_infinity());
}
#[test]
fn right_limit_of_x_to_the_x_at_zero_is_one() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.pow(&x).limit_right(&x, &ctx.int(0)), ctx.int(1));
}
#[test]
fn two_sided_limit_of_sign_at_zero_reports_the_differing_sides() {
let ctx = Context::new();
let x = ctx.symbol("x");
let zero = ctx.int(0);
assert_eq!(x.sign().limit_right(&x, &zero), ctx.int(1));
assert_eq!(x.sign().limit_left(&x, &zero), ctx.int(-1));
let e = x.sign().try_limit(&x, &zero).unwrap_err().to_string();
assert!(e.contains("left = -1, right = 1"), "{e}");
assert_eq!(x.heaviside().limit_right(&x, &zero), ctx.int(1));
}
#[test]
fn limit_of_factorial_at_minus_one_half_is_its_value_there() {
let ctx = Context::new();
let x = ctx.symbol("x");
let l = x.factorial().limit(&x, &ctx.rational(-1, 2));
assert!(!l.has_unevaluated(), "{l}");
assert_eq!(l, ctx.rational(-1, 2).factorial());
}
#[test]
fn factorial_of_minus_one_half_evaluates_to_sqrt_pi() {
let ctx = Context::new();
let f = ctx.rational(-1, 2).factorial();
close(
f.eval_f64().unwrap(),
1.772453850905516,
"(-1/2)! = sqrt(pi)",
);
}
#[test]
fn right_limit_of_factorial_at_minus_one_is_infinite() {
let ctx = Context::new();
let x = ctx.symbol("x");
let f = x.factorial();
assert_eq!(f.limit_right(&x, &ctx.int(-1)), ctx.infinity());
assert_eq!(f.limit_left(&x, &ctx.int(-1)), ctx.neg_infinity());
assert_eq!(f.limit_right(&x, &ctx.int(-2)), ctx.neg_infinity());
assert_eq!(f.limit_left(&x, &ctx.int(-2)), ctx.infinity());
}
#[test]
fn latex_of_tanh_and_the_inverse_hyperbolics() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.tanh().to_latex(), r"\tanh\left(x\right)");
assert_eq!(x.asinh().to_latex(), r"\operatorname{asinh}\left(x\right)");
assert_eq!(x.acosh().to_latex(), r"\operatorname{acosh}\left(x\right)");
assert_eq!(x.atanh().to_latex(), r"\operatorname{atanh}\left(x\right)");
}
#[test]
fn latex_of_sign_heaviside_dirac_gamma_family_and_error_functions() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.sign().to_latex(), r"\operatorname{sgn}\left(x\right)");
assert_eq!(x.heaviside().to_latex(), r"\operatorname{H}\left(x\right)");
assert_eq!(x.dirac_delta().to_latex(), r"\delta\left(x\right)");
assert_eq!(x.gamma().to_latex(), r"\Gamma\left(x\right)");
assert_eq!(x.log_gamma().to_latex(), r"\ln \Gamma\left(x\right)");
assert_eq!(x.digamma().to_latex(), r"\psi\left(x\right)");
assert_eq!(x.erf().to_latex(), r"\operatorname{erf}\left(x\right)");
assert_eq!(x.erfc().to_latex(), r"\operatorname{erfc}\left(x\right)");
assert_eq!(x.lambertw().to_latex(), r"\operatorname{W}\left(x\right)");
}
#[test]
fn latex_of_floor_and_ceiling() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(x.floor().to_latex(), r"\lfloor x\rfloor");
assert_eq!(x.ceiling().to_latex(), r"\lceil x\rceil");
}
#[test]
fn latex_of_factorial_parenthesises_a_compound_argument() {
let ctx = Context::new();
let n = ctx.symbol("n");
assert_eq!(n.factorial().to_latex(), "n!");
assert_eq!((&n + 1).factorial().to_latex(), r"\left(n + 1\right)!");
}
#[test]
fn latex_of_binomial_beta_and_atan2() {
let ctx = Context::new();
let (x, y, n) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("n"));
assert_eq!(n.binomial(&ctx.int(2)).to_latex(), r"\binom{n}{2}");
assert_eq!(x.beta(&y).to_latex(), r"\mathrm{B}\left(x, y\right)");
assert_eq!(
y.atan2(&x).to_latex(),
r"\operatorname{atan2}\left(y, x\right)"
);
}
#[test]
fn latex_of_min_and_max_keep_argument_order() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
assert_eq!(
Ex::min_of(&ctx, [x.clone(), y.clone()]).to_latex(),
r"\min\left(x, y\right)"
);
assert_eq!(
Ex::max_of(&ctx, [x.clone(), y.clone(), ctx.int(1)]).to_latex(),
r"\max\left(x, y, 1\right)"
);
assert_eq!(x.min_with(&y).to_latex(), r"\min\left(x, y\right)");
}
#[test]
fn latex_of_formal_limit_and_residue_nodes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lim = (1 / &x).limit(&x, &ctx.int(0));
assert!(lim.has_unevaluated());
assert_eq!(lim.to_latex(), r"\lim_{x \to 0} \frac{1}{x}");
let res = (1 / &x).exp().residue(&x, &ctx.int(0));
assert!(res.has_unevaluated());
assert_eq!(
res.to_latex(),
r"\operatorname{Res}_{x=0} \exp\left(\frac{1}{x}\right)"
);
}
#[test]
fn latex_of_formal_laplace_transform_and_integral_nodes() {
let ctx = Context::new();
let (t, s) = (ctx.symbol("t"), ctx.symbol("s"));
let lap = t.tan().exp().laplace(&t, &s);
assert!(lap.has_unevaluated());
assert_eq!(
lap.to_latex(),
r"\mathcal{L}\left\{\exp\left(\tan\left(t\right)\right)\right\}"
);
let int = (t.exp() * t.sin() * t.ln()).integrate(&t);
assert!(int.has_unevaluated());
assert_eq!(
int.to_latex(),
r"\int \sin\left(t\right) \exp\left(t\right) \ln\left(t\right)\, dt"
);
}
#[test]
fn latex_of_complex_infinity_and_nan() {
let ctx = Context::new();
assert_eq!(ctx.complex_infinity().to_latex(), r"\tilde{\infty}");
assert_eq!(ctx.nan().to_latex(), r"\text{NaN}");
}
#[test]
fn latex_of_sum_closes_the_subscript_brace() {
let ctx = Context::new();
let n = ctx.symbol("n");
let sum = Ex::symbolic_sum(&(1 / n.powi(2)), &n, &ctx.int(1), &ctx.infinity());
assert_eq!(sum.to_latex(), r"\sum_{n=1}^{\infty} \frac{1}{n^{2}}");
}
#[test]
fn latex_of_product_closes_the_subscript_brace() {
let ctx = Context::new();
let (k, n) = (ctx.symbol("k"), ctx.symbol("n"));
let prod = Ex::symbolic_product(&k, &k, &ctx.int(1), &n);
assert_eq!(prod.to_latex(), r"\prod_{k=1}^{n} k");
}
const MATHML_OPEN: &str = "<math xmlns=\"http://www.w3.org/1998/Math/MathML\">";
fn mathml_body(e: &Ex) -> String {
let xml = e.to_mathml().unwrap_or_else(|err| panic!("{e}: {err:?}"));
assert!(
xml.starts_with(MATHML_OPEN) && xml.ends_with("</math>"),
"{xml}"
);
xml[MATHML_OPEN.len()..xml.len() - "</math>".len()].to_string()
}
fn mathml_apply(head: &str, arg: &str) -> String {
format!("<mrow>{head}<mo>⁡</mo><mrow><mo>(</mo>{arg}<mo>)</mo></mrow></mrow>")
}
#[test]
fn mathml_of_tanh_the_inverse_hyperbolics_and_the_named_special_functions() {
let ctx = Context::new();
let x = ctx.symbol("x");
for (e, name) in [
(x.tanh(), "tanh"),
(x.asinh(), "asinh"),
(x.acosh(), "acosh"),
(x.atanh(), "atanh"),
(x.sign(), "sgn"),
(x.heaviside(), "H"),
(x.erf(), "erf"),
(x.erfc(), "erfc"),
(x.lambertw(), "W"),
(x.arg(), "arg"),
(x.si(), "Si"),
(x.ci(), "Ci"),
(x.ei(), "Ei"),
(x.li(), "li"),
] {
assert_eq!(
mathml_body(&e),
mathml_apply(&format!("<mi>{name}</mi>"), "<mi>x</mi>"),
"{e}"
);
}
}
#[test]
fn mathml_of_dirac_gamma_loggamma_digamma_and_zeta_use_greek_character_references() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(
mathml_body(&x.dirac_delta()),
mathml_apply("<mi>δ</mi>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.gamma()),
mathml_apply("<mi>Γ</mi>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.log_gamma()),
mathml_apply("<mrow><mi>ln</mi><mi>Γ</mi></mrow>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.digamma()),
mathml_apply("<mi>ψ</mi>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.zeta()),
mathml_apply("<mi>ζ</mi>", "<mi>x</mi>")
);
}
#[test]
fn mathml_of_re_im_and_conjugate() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(
mathml_body(&x.re()),
mathml_apply("<mi>ℜ</mi>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.im()),
mathml_apply("<mi>ℑ</mi>", "<mi>x</mi>")
);
assert_eq!(
mathml_body(&x.conjugate()),
"<mover><mi>x</mi><mo>¯</mo></mover>"
);
}
#[test]
fn mathml_of_floor_and_ceiling_use_the_bracket_characters() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert_eq!(
mathml_body(&x.floor()),
"<mrow><mo>⌊</mo><mi>x</mi><mo>⌋</mo></mrow>"
);
assert_eq!(
mathml_body(&x.ceiling()),
"<mrow><mo>⌈</mo><mi>x</mi><mo>⌉</mo></mrow>"
);
}
#[test]
fn mathml_of_factorial_parenthesises_a_compound_argument() {
let ctx = Context::new();
let n = ctx.symbol("n");
assert_eq!(
mathml_body(&n.factorial()),
"<mrow><mi>n</mi><mo>!</mo></mrow>"
);
assert_eq!(
mathml_body(&(&n + 1).factorial()),
"<mrow><mrow><mo>(</mo><mrow><mi>n</mi><mo>+</mo><mn>1</mn></mrow><mo>)</mo></mrow><mo>!</mo></mrow>"
);
}
#[test]
fn mathml_of_binomial_beta_and_atan2() {
let ctx = Context::new();
let (x, y, n) = (ctx.symbol("x"), ctx.symbol("y"), ctx.symbol("n"));
assert_eq!(
mathml_body(&n.binomial(&ctx.int(2))),
"<mrow><mo>(</mo><mfrac linethickness=\"0\"><mi>n</mi><mn>2</mn></mfrac><mo>)</mo></mrow>"
);
assert_eq!(
mathml_body(&x.beta(&y)),
mathml_apply("<mi>B</mi>", "<mi>x</mi><mo>,</mo><mi>y</mi>")
);
assert_eq!(
mathml_body(&y.atan2(&x)),
mathml_apply("<mi>atan2</mi>", "<mi>y</mi><mo>,</mo><mi>x</mi>")
);
}
#[test]
fn mathml_of_min_max_polygamma_and_kronecker_delta() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
assert_eq!(
mathml_body(&Ex::min_of(&ctx, [x.clone(), y.clone()])),
mathml_apply("<mi>min</mi>", "<mi>x</mi><mo>,</mo><mi>y</mi>")
);
assert_eq!(
mathml_body(&Ex::max_of(&ctx, [x.clone(), y.clone(), ctx.int(1)])),
mathml_apply(
"<mi>max</mi>",
"<mi>x</mi><mo>,</mo><mi>y</mi><mo>,</mo><mn>1</mn>"
)
);
assert_eq!(
mathml_body(&x.polygamma(&ctx.int(2))),
mathml_apply(
"<msup><mi>ψ</mi><mrow><mo>(</mo><mn>2</mn><mo>)</mo></mrow></msup>",
"<mi>x</mi>"
)
);
assert_eq!(
mathml_body(&x.kronecker_delta(&y)),
"<msub><mi>δ</mi><mrow><mi>x</mi><mi>y</mi></mrow></msub>"
);
}
#[test]
fn mathml_of_sum_and_product_use_munderover() {
let ctx = Context::new();
let (k, n) = (ctx.symbol("k"), ctx.symbol("n"));
let sum = Ex::symbolic_sum(&(1 / n.powi(2)), &n, &ctx.int(1), &ctx.infinity());
assert_eq!(
mathml_body(&sum),
"<mrow><munderover><mo>∑</mo><mrow><mi>n</mi><mo>=</mo><mn>1</mn></mrow><mi>∞</mi></munderover><mfrac><mn>1</mn><msup><mi>n</mi><mn>2</mn></msup></mfrac></mrow>"
);
let prod = Ex::symbolic_product(&k, &k, &ctx.int(1), &n);
assert_eq!(
mathml_body(&prod),
"<mrow><munderover><mo>∏</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><mi>k</mi></mrow>"
);
}
#[test]
fn mathml_of_formal_limit_and_residue_nodes() {
let ctx = Context::new();
let x = ctx.symbol("x");
let lim = (1 / &x).limit(&x, &ctx.int(0));
assert_eq!(
mathml_body(&lim),
"<mrow><munder><mo>lim</mo><mrow><mi>x</mi><mo>→</mo><mn>0</mn></mrow></munder><mfrac><mn>1</mn><mi>x</mi></mfrac></mrow>"
);
let res = (1 / &x).exp().residue(&x, &ctx.int(0));
assert_eq!(
mathml_body(&res),
format!(
"<mrow><munder><mi>Res</mi><mrow><mi>x</mi><mo>=</mo><mn>0</mn></mrow></munder>{}</mrow>",
mathml_apply("<mi>exp</mi>", "<mfrac><mn>1</mn><mi>x</mi></mfrac>")
)
);
}
#[test]
fn mathml_of_formal_laplace_transform_and_integral_nodes() {
let ctx = Context::new();
let (t, s) = (ctx.symbol("t"), ctx.symbol("s"));
let lap = t.tan().exp().laplace(&t, &s);
let body = mathml_apply("<mi>exp</mi>", &mathml_apply("<mi>tan</mi>", "<mi>t</mi>"));
assert_eq!(
mathml_body(&lap),
format!(
"<mrow><mi mathvariant=\"script\">L</mi><mo>⁡</mo><mrow><mo>{{</mo>{body}<mo>}}</mo></mrow></mrow>"
)
);
let int = t.tan().exp().integrate(&t);
assert!(int.has_unevaluated());
assert_eq!(
mathml_body(&int),
format!("<mrow><mo>∫</mo>{body}<mi>d</mi><mi>t</mi></mrow>")
);
}
#[test]
fn mathml_of_complex_infinity_and_nan() {
let ctx = Context::new();
assert_eq!(
mathml_body(&ctx.complex_infinity()),
"<mover><mi>∞</mi><mo>~</mo></mover>"
);
assert_eq!(mathml_body(&ctx.nan()), "<mtext>NaN</mtext>");
}
fn one_of_each_node_kind(ctx: &Context) -> Vec<Ex> {
let x = ctx.symbol("x");
let y = ctx.symbol("y");
let n = ctx.symbol("n");
let p = ctx.symbol_with("p", &[Assumption::Positive]);
vec![
x.tanh(),
x.asinh(),
x.acosh(),
x.atanh(),
x.sign(),
x.heaviside(),
x.dirac_delta(),
x.gamma(),
x.log_gamma(),
x.digamma(),
x.erf(),
x.erfc(),
x.lambertw(),
x.floor(),
x.ceiling(),
x.beta(&y),
y.atan2(&x),
x.polygamma(&ctx.int(2)),
x.kronecker_delta(&y),
x.zeta(),
x.conjugate(),
x.re(),
x.im(),
x.arg(),
x.si(),
x.ci(),
x.ei(),
x.li(),
Ex::min_of(ctx, [x.clone(), y.clone()]),
Ex::max_of(ctx, [x.clone(), y.clone(), ctx.int(1)]),
Ex::symbolic_sum(&(1 / n.powi(2)), &n, &ctx.int(1), &ctx.infinity()),
Ex::symbolic_product(&n, &n, &ctx.int(1), &ctx.int(5)),
(1 / &x).limit(&x, &ctx.int(0)),
(1 / &x).exp().residue(&x, &ctx.int(0)),
x.tan().exp().laplace(&x, &y),
x.tan().exp().integrate(&x),
Ex::piecewise(&[(&x, &x.gt(&ctx.int(0))), (&(-&x), &ctx.bool_true())]),
ctx.physical_constant("c", ctx.int(299_792_458)),
ctx.complex_infinity(),
ctx.nan(),
ctx.e(),
ctx.euler_gamma(),
ctx.catalan(),
ctx.golden_ratio(),
&p * &x,
]
}
#[test]
fn compact_transfers_every_node_kind_and_keeps_assumptions() {
let ctx = Context::new();
let mut roots = one_of_each_node_kind(&ctx);
let x = ctx.symbol("x");
roots.push((&x + 1).factorial());
roots.push(x.binomial(&ctx.int(2)));
let numeric = ctx.rational(1, 2).tanh()
+ ctx.rational(7, 3).floor()
+ ctx.int(1).atan2(&ctx.int(2))
+ ctx.rational(1, 2).gamma()
+ Ex::max_of(&ctx, [ctx.int(1), ctx.int(3)]);
let before = numeric.eval_f64().unwrap();
roots.push(numeric);
let p_index = roots.len() - 4;
assert_eq!(roots[p_index].to_string(), "p*x");
let (new_ctx, copies) = ctx.compact(&roots);
assert_eq!(copies.len(), roots.len());
for (old, new) in roots.iter().zip(&copies) {
assert_eq!(old.to_string(), new.to_string());
assert_eq!(old.to_latex(), new.to_latex());
assert_eq!(old.to_tree(), new.to_tree(), "{old}");
}
let after = copies.last().unwrap().eval_f64().unwrap();
assert_eq!(after, before);
close(after, 7.698218617166332, "numeric composite");
let new_p = new_ctx.symbol("p");
assert_eq!(new_p.is_positive(), Some(true));
assert_eq!(copies[p_index].is_positive(), None);
assert!(new_ctx.node_count() < ctx.node_count());
}
#[test]
fn tree_round_trip_is_the_identity_for_every_node_kind() {
let ctx = Context::new();
for e in one_of_each_node_kind(&ctx) {
let tree = e.to_tree();
let back = ctx.from_tree(&tree);
assert_eq!(back, e, "{e}: tree {tree:?}");
let json = e.to_json().unwrap();
assert_eq!(ctx.from_json(&json).unwrap(), e, "{e}");
}
}
#[test]
fn tree_round_trip_preserves_factorial_and_binomial() {
let ctx = Context::new();
let n = ctx.symbol("n");
let fact = (&n + 1).factorial();
let back = ctx.from_tree(&fact.to_tree());
assert_eq!(back, fact, "{back}");
assert_eq!(back.subs(&n, &ctx.int(4)).eval(), ctx.int(120));
let binom = n.binomial(&ctx.int(2));
let back = ctx.from_tree(&binom.to_tree());
assert_eq!(back, binom, "{back}");
assert_eq!(back.subs(&n, &ctx.int(5)).eval(), ctx.int(10));
}
#[test]
fn liveness_ratio_and_should_compact_on_a_small_arena() {
let ctx = Context::new();
let x = ctx.symbol("x");
let _ = (&x + 1).powi(6).expand();
let root = &x + 1;
let ratio = ctx.liveness_ratio(std::slice::from_ref(&root));
assert!(ratio > 0.0 && ratio < 1.0, "liveness = {ratio}");
assert!(!ctx.should_compact(std::slice::from_ref(&root)));
assert!(ctx.node_count() < 100_000);
let (new_ctx, new_roots) = ctx.compact(std::slice::from_ref(&root));
let new_ratio = new_ctx.liveness_ratio(&new_roots);
assert!(new_ratio >= ratio, "{new_ratio} < {ratio}");
}
#[test]
fn parsing_a_large_exact_power_does_not_overflow_the_stack() {
let handle = std::thread::Builder::new()
.stack_size(2 * 1024 * 1024)
.spawn(|| {
let ctx = Context::new();
let e = symplex::parse::parse(&ctx, "7.4**77.4**74").expect("parses");
let shown = format!("{e}");
assert!(!shown.is_empty());
let big = ctx.rational(387, 5).powi(74);
let p = ctx.rational(37, 5).pow(&big);
assert!(!format!("{p}").is_empty());
assert!(!format!("{}", ctx.int(5).pow(&-&big)).is_empty());
})
.expect("spawn");
handle.join().expect("no stack overflow");
}