use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use pounce_cli::solve_report::SolveReport;
use pounce_nlp::ApplicationReturnStatus;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
fn fixture(name: &str) -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests");
p.push("fixtures");
p.push(name);
p
}
fn tmp_path(suffix: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!(
"pounce_issue512_{}_{}_{suffix}",
std::process::id(),
n
));
p
}
fn solve(fixture_name: &str, extra_opts: &[&str]) -> SolveReport {
let json_path = tmp_path(&format!("{fixture_name}.json"));
let sol_path = tmp_path(&format!("{fixture_name}.sol"));
let mut cmd = Command::new(pounce_exe());
cmd.arg(fixture(fixture_name))
.arg(&sol_path)
.arg("--json-output")
.arg(&json_path)
.arg("print_level=0");
for opt in extra_opts {
cmd.arg(opt);
}
let _ = cmd.status().expect("spawn pounce");
let text = std::fs::read_to_string(&json_path).expect("read json report");
let _ = std::fs::remove_file(&json_path);
let _ = std::fs::remove_file(&sol_path);
serde_json::from_str(&text).expect("deserialize SolveReport")
}
const MAX_ITER: &str = "max_iter=300";
fn tight_adaptive() -> Vec<&'static str> {
vec![
"mu_strategy=adaptive",
"tol=1e-12",
"acceptable_tol=1e-15",
MAX_ITER,
]
}
#[test]
fn an_adaptive_solve_stops_at_a_flagged_tiny_step() {
let r = solve("airport.nl", &tight_adaptive());
assert_ne!(
r.solution.status,
ApplicationReturnStatus::MaximumIterationsExceeded,
"adaptive μ ran out the iteration budget at a frozen iterate \
instead of stopping at the tiny step (gh #512); \
iteration_count={}",
r.statistics.iteration_count,
);
assert!(
r.statistics.iteration_count < 300,
"iteration_count={} means the solve reached the cap; the tiny step \
is detected around iteration 16 and nothing after it moves",
r.statistics.iteration_count,
);
}
#[test]
fn the_tiny_step_exit_lands_on_a_converged_point() {
let r = solve("airport.nl", &tight_adaptive());
let s = &r.statistics;
assert!(
s.final_constr_viol < 1e-8,
"stopped at constraint violation {} — a tiny step is only a valid \
reason to stop if the iterate is feasible",
s.final_constr_viol,
);
assert!(
s.final_dual_inf < 1e-6,
"stopped at dual infeasibility {} — that is a stall, not \
convergence to best available accuracy",
s.final_dual_inf,
);
}
#[test]
fn adaptive_does_not_grind_at_a_frozen_iterate_at_default_tolerances() {
let r = solve("hs71_obj1e8.nl", &["mu_strategy=adaptive"]);
assert!(
(0..100).contains(&r.solution.solve_result_num),
"expected the known optimum to be certified, got \
solve_result_num={} ({:?})",
r.solution.solve_result_num,
r.solution.status,
);
assert!(
r.statistics.iteration_count <= 35,
"iteration_count={} — the solve reaches its optimum at ~11 \
iterations and the rest are tiny steps at a point that no longer \
moves (gh #512)",
r.statistics.iteration_count,
);
}
#[test]
fn the_monotone_route_is_untouched() {
let r = solve("hs71_obj1e8.nl", &[]);
assert!(
(0..100).contains(&r.solution.solve_result_num),
"default (monotone) solve regressed: solve_result_num={} ({:?})",
r.solution.solve_result_num,
r.solution.status,
);
}
#[test]
fn a_square_infeasible_model_survives_a_tiny_step_exit_from_restoration() {
for strategy in ["mu_strategy=monotone", "mu_strategy=adaptive"] {
let r = solve(
"issue_508_infeasible_gap_1em2.nl",
&[strategy, "tol=1e-12", "acceptable_tol=1e-15", MAX_ITER],
);
assert_eq!(
r.solution.solve_result_num, 200,
"{strategy}: a model infeasible by 1e-2 must report local \
infeasibility (AMPL 200), got {} ({:?}) — 500 tells the user \
their solver broke (gh #508, #512)",
r.solution.solve_result_num, r.solution.status,
);
}
}