#![cfg(feature = "pounce")]
use oximo::pounce::{MuStrategy, PounceOptions};
use oximo::prelude::*;
use oximo::solvers::Pounce;
fn assert_close(got: f64, want: f64, tol: f64, what: &str) {
assert!((got - want).abs() < tol, "{what}: got {got}, want {want}");
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-5 * a.abs().max(b.abs()).max(1.0)
}
#[test]
fn hs071() {
let m = Model::new("hs071");
variable!(m, 1.0 <= x1 <= 5.0, initial = 1.0);
variable!(m, 1.0 <= x2 <= 5.0, initial = 5.0);
variable!(m, 1.0 <= x3 <= 5.0, initial = 5.0);
variable!(m, 1.0 <= x4 <= 5.0, initial = 1.0);
objective!(m, Min, x1 * x4 * (x1 + x2 + x3) + x3);
constraint!(m, prod, x1 * x2 * x3 * x4 >= 25.0);
constraint!(m, ssq, x1.powi(2) + x2.powi(2) + x3.powi(2) + x4.powi(2) == 40.0);
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(res.has_solution(), "hs071 should solve");
assert_close(res.value_of(x1).unwrap(), 1.0, 1e-3, "x1");
assert_close(res.value_of(x2).unwrap(), 4.743, 1e-3, "x2");
assert_close(res.value_of(x4).unwrap(), 1.379_408, 1e-3, "x4");
assert_close(res.objective().unwrap(), 17.014, 1e-2, "objective");
}
#[test]
fn rosenbrock_unconstrained() {
let m = Model::new("rosenbrock");
variable!(m, -10.0 <= x <= 10.0, initial = -1.2);
variable!(m, -10.0 <= y <= 10.0, initial = 1.0);
objective!(m, Min, (1.0 - x).powi(2) + 100.0 * (y - x.powi(2)).powi(2));
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert_eq!(res.termination, TerminationStatus::LocallyOptimal);
assert_close(res.value_of(x).unwrap(), 1.0, 1e-4, "x");
assert_close(res.value_of(y).unwrap(), 1.0, 1e-4, "y");
assert!(res.objective().unwrap().abs() < 1e-6, "objective");
}
#[test]
fn maximize_flips_sign_back() {
let m = Model::new("max");
variable!(m, -10.0 <= x <= 10.0);
objective!(m, Max, 4.0 * x - x.powi(2));
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert_eq!(res.termination, TerminationStatus::LocallyOptimal);
assert_close(res.value_of(x).unwrap(), 2.0, 1e-4, "x");
assert_close(res.objective().unwrap(), 4.0, 1e-5, "objective");
}
#[test]
fn quadratic_constraint_qcp() {
let m = Model::new("qcp");
variable!(m, -2.0 <= x <= 2.0);
variable!(m, -2.0 <= y <= 2.0);
constraint!(m, ball, x.powi(2) + y.powi(2) <= 1.0);
objective!(m, Min, x + y);
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(res.has_solution());
let r = -1.0 / 2.0_f64.sqrt();
assert_close(res.value_of(x).unwrap(), r, 1e-4, "x");
assert_close(res.value_of(y).unwrap(), r, 1e-4, "y");
}
#[test]
fn lp_duals_match_lp_convention() {
let m = Model::new("product_mix");
variable!(m, x >= 0.0);
variable!(m, y >= 0.0);
variable!(m, z >= 0.0);
let labor = constraint!(m, labor, x + y + z <= 12.0);
let material = constraint!(m, material, 2.0 * x + y + 3.0 * z <= 16.0);
objective!(m, Max, 40.0 * x + 30.0 * y + 20.0 * z);
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(res.has_solution());
assert_close(res.objective().unwrap(), 400.0, 1e-3, "objective");
assert_close(res.value_of(x).unwrap(), 4.0, 1e-3, "x");
assert_close(res.value_of(y).unwrap(), 8.0, 1e-3, "y");
assert_close(res.dual_of(labor).unwrap(), 20.0, 1e-3, "labor dual");
assert_close(res.dual_of(material).unwrap(), 10.0, 1e-3, "material dual");
let z_id = m.variable_id("z").unwrap();
assert_close(res.reduced_costs[&z_id], -30.0, 1e-3, "z reduced cost");
assert!(res.iterations > 0, "iteration count should be reported");
}
#[test]
fn indexed_least_squares_qp() {
let t = [1.0, 2.0, 3.0, 4.0];
let n = t.len();
let m = Model::new("least_squares");
variable!(m, 0.0 <= x[i in 0..n] <= 10.0);
constraint!(m, total, sum!(x[i] for i in 0..n) == 8.0);
objective!(m, Min, sum!((x[i] - t[i]).powi(2) for i in 0..n));
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(res.has_solution());
assert_close(res.objective().unwrap(), 1.0, 1e-4, "objective");
for i in 0..n {
assert_close(res.value_of(x[i]).unwrap(), t[i] - 0.5, 1e-4, &format!("x[{i}]"));
}
}
#[test]
fn feasibility_problem_returns_feasible_point() {
let m = Model::new("feas");
variable!(m, -2.0 <= x <= 2.0);
variable!(m, -2.0 <= y <= 2.0);
constraint!(m, disk, x.powi(2) + y.powi(2) <= 1.0);
constraint!(m, line, x + y >= 1.0);
objective!(m, Feasibility);
let res = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(res.has_solution(), "feasibility solve should return a point");
let (xv, yv) = (res.value_of(x).unwrap(), res.value_of(y).unwrap());
assert!(xv * xv + yv * yv <= 1.0 + 1e-5, "inside disk: ({xv}, {yv})");
assert!(xv + yv >= 1.0 - 1e-5, "above line: ({xv}, {yv})");
}
#[test]
fn integer_models_are_rejected() {
let m = Model::new("milp");
variable!(m, 0.0 <= x <= 5.0, Int);
objective!(m, Min, x);
let err = Pounce.solve(&m, &PounceOptions::default()).unwrap_err();
assert!(matches!(err, SolverError::UnsupportedKind(ModelKind::MILP)));
}
#[test]
fn persistent_matches_cold_on_parameter_sweep() {
let m = Model::new("nlp_sweep");
param!(m, w = 1.0);
variable!(m, 0.1 <= x <= 10.0, initial = 1.0);
variable!(m, 0.1 <= y <= 10.0, initial = 1.0);
constraint!(m, prod, x * y >= 4.0);
objective!(m, Min, w * x + y);
let mut solver = Pounce.persistent();
for wv in [1.0, 2.0, 0.5] {
w.set_param_value(wv);
let warm = solver.solve(&m, &PounceOptions::default()).unwrap();
let cold = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(warm.has_solution(), "w {wv}: no solution");
assert!(close(warm.objective().unwrap(), cold.objective().unwrap()), "w {wv}: objective");
assert!(close(warm.value_of(x).unwrap(), cold.value_of(x).unwrap()), "w {wv}: x");
assert!(close(warm.value_of(y).unwrap(), cold.value_of(y).unwrap()), "w {wv}: y");
}
}
#[test]
fn typed_options_still_solve() {
let m = Model::new("hs071_opts");
variable!(m, 1.0 <= x1 <= 5.0, initial = 1.0);
variable!(m, 1.0 <= x2 <= 5.0, initial = 5.0);
variable!(m, 1.0 <= x3 <= 5.0, initial = 5.0);
variable!(m, 1.0 <= x4 <= 5.0, initial = 1.0);
objective!(m, Min, x1 * x4 * (x1 + x2 + x3) + x3);
constraint!(m, prod, x1 * x2 * x3 * x4 >= 25.0);
constraint!(m, ssq, x1.powi(2) + x2.powi(2) + x3.powi(2) + x4.powi(2) == 40.0);
let opts = PounceOptions::default()
.tol(1e-8)
.max_iter(500)
.mu_strategy(MuStrategy::Adaptive)
.mu_oracle("probing")
.presolve(false);
let res = Pounce.solve(&m, &opts).unwrap();
assert!(res.has_solution(), "hs071 should solve with typed options");
assert_close(res.objective().unwrap(), 17.014, 1e-2, "objective");
}
#[test]
fn unknown_option_is_rejected() {
let m = Model::new("reject");
variable!(m, 0.0 <= x <= 5.0, initial = 1.0);
objective!(m, Min, (x - 2.0).powi(2));
let opts = PounceOptions::default().set("not_a_real_option", 1);
let err = Pounce.solve(&m, &opts).unwrap_err();
assert!(matches!(err, SolverError::Backend(_)), "cold: got {err:?}");
let mut solver = Pounce.persistent();
let err = solver.solve(&m, &opts).unwrap_err();
assert!(matches!(err, SolverError::Backend(_)), "persistent: got {err:?}");
}
#[test]
fn verbose_captures_raw_log() {
let m = Model::new("logged");
variable!(m, -10.0 <= x <= 10.0, initial = -1.2);
objective!(m, Min, (x - 2.0).powi(2));
let quiet = Pounce.solve(&m, &PounceOptions::default()).unwrap();
assert!(quiet.raw_log.is_none(), "no log capture without verbose");
let mut opts = PounceOptions::default();
opts.universal.verbose = Some(true);
let res = Pounce.solve(&m, &opts).unwrap();
assert!(res.iterations > 0, "solve reports iterations");
let log = res.raw_log.expect("verbose solve should capture a log");
assert!(log.contains("EXIT:"), "log has the exit status: {log}");
}