use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use pounce_cli::solve_report::SolveReport;
fn pounce_exe() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pounce"))
}
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_issue557_{}_{}_{suffix}",
std::process::id(),
n
));
p
}
const M: usize = 16;
const PAIRS: usize = 20;
fn shared_cse_model_nl(m: usize, pairs: usize) -> String {
let n = m + 2;
let nzc = 3 * m;
let mut s = String::new();
s.push_str("g3 1 1 0\n");
s.push_str(&format!(" {n} {m} 1 0 0 0\n"));
s.push_str(&format!(" {m} 0\n 0 0\n"));
s.push_str(&format!(" {n} 0 0\n"));
s.push_str(" 0 0 0 1\n 0 0 0 0 0\n");
s.push_str(&format!(" {nzc} {n}\n"));
s.push_str(" 0 0\n 0 1 0 0 0\n");
s.push_str(&format!("V{n} 0 0\n"));
for _ in 0..pairs {
s.push_str("o43\no44\n");
}
s.push_str("o0\nn2\no2\nn0.01\no0\nv0\nv1\n");
for i in 0..m {
s.push_str(&format!("C{i}\no2\nv{n}\nv{}\n", i + 2));
}
s.push_str("O0 0\nn0\n");
s.push_str(&format!("x{n}\n"));
for j in 0..n {
s.push_str(&format!("{j} 1.0\n"));
}
s.push_str("r\n");
for _ in 0..m {
s.push_str("2 1\n");
}
s.push_str("b\n");
s.push_str("0 0.5 5\n0 0.5 5\n");
for _ in 0..m {
s.push_str("0 0.001 10000\n");
}
s.push_str(&format!("k{}\n", n - 1));
let mut acc = 0;
for j in 0..n - 1 {
acc += if j < 2 { m } else { 1 };
s.push_str(&format!("{acc}\n"));
}
for i in 0..m {
s.push_str(&format!("J{i} 3\n0 0.0\n1 0.0\n{} 0.0\n", i + 2));
}
s.push_str(&format!("G0 {n}\n"));
for j in 0..n {
s.push_str(&format!("{j} 1.0\n"));
}
s
}
fn solve(no_hybrid: bool) -> (SolveReport, String) {
let nl_path = tmp_path("model.nl");
std::fs::write(&nl_path, shared_cse_model_nl(M, PAIRS)).expect("write model");
let json_path = tmp_path("report.json");
let sol_path = tmp_path("model.sol");
let mut cmd = Command::new(pounce_exe());
cmd.arg(&nl_path)
.arg(&sol_path)
.arg("--json-output")
.arg(&json_path)
.env("POUNCE_DBG_TAPE_STATS", "1");
if no_hybrid {
cmd.env("POUNCE_DBG_NO_HYBRID", "1");
}
let out = cmd.output().expect("spawn pounce");
assert!(
out.status.success(),
"solve exited nonzero (no_hybrid={no_hybrid})"
);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
let stats = stderr
.lines()
.find(|l| l.contains("[hybrid stats]"))
.unwrap_or_default()
.to_string();
let text = std::fs::read_to_string(&json_path).expect("read json report");
let _ = std::fs::remove_file(&nl_path);
let _ = std::fs::remove_file(&json_path);
let _ = std::fs::remove_file(&sol_path);
(
serde_json::from_str(&text).expect("deserialize SolveReport"),
stats,
)
}
fn expected_objective() -> f64 {
1.0 + M as f64 / 2.01
}
fn assert_solved_at_optimum(report: &SolveReport, ctx: &str) {
let code = report.solution.solve_result_num;
assert!(
(0..100).contains(&code),
"{ctx}: not solved (solve_result_num={code}, status={:?})",
report.solution.status,
);
let obj = report.solution.objective;
let want = expected_objective();
assert!(
(obj - want).abs() < 1e-4 * want,
"{ctx}: objective {obj} is not the analytic optimum {want}",
);
}
#[test]
fn shared_cse_model_solves_on_the_hybrid_paths() {
let (report, stats) = solve(false);
assert_solved_at_optimum(&report, "shared-CSE model (hybrid)");
assert!(
stats.contains("hess_gate=on"),
"eval_h must take the shared-CSE path on this model; stats line was: {stats:?}"
);
assert!(
stats.contains("jac_gate=on"),
"eval_jac_g must take the shared-CSE path on this model; stats line was: {stats:?}"
);
}
#[test]
fn shared_cse_model_matches_the_flat_tape_solve() {
let (hybrid, hstats) = solve(false);
let (flat, fstats) = solve(true);
assert_solved_at_optimum(&flat, "shared-CSE model (flat reference)");
assert!(
hstats.contains("hess_gate=on"),
"hybrid run did not take the shared-CSE Hessian path: {hstats:?}"
);
assert!(
fstats.contains("hybrid not built"),
"POUNCE_DBG_NO_HYBRID run should build no hybrid tape at all: {fstats:?}"
);
let (oh, of) = (hybrid.solution.objective, flat.solution.objective);
assert!(
(oh - of).abs() <= 1e-6 * of.abs().max(1.0),
"hybrid ({oh}) and flat ({of}) solves disagree"
);
}