pub use pounce_minima::{archive, penalty_tnlp, sampling};
use crate::cli::{Args, MinimaArgs, ProblemSource};
use crate::solve_report::{InputDescriptor, ReportBuilder, status_to_solve_result_num};
use archive::scaled_distance;
use pounce_algorithm::application::IpoptApplication;
use pounce_common::types::{Index, Number};
use pounce_minima::{IpoptLocalSolve, Minimum, Stop, find_minima};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::tnlp::TNLP;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::rc::Rc;
pub fn run(
app: &mut IpoptApplication,
base: &Rc<RefCell<dyn TNLP>>,
cfg: &MinimaArgs,
args: &Args,
sol_path: Option<&Path>,
dual_sign: Number,
) -> ExitCode {
let Some(info) = base.borrow_mut().get_nlp_info() else {
eprintln!("pounce: --minima could not read problem dimensions");
return ExitCode::from(2);
};
let n = info.n as usize;
let m = info.m as usize;
println!(
"Searching for up to {} minima via `{}` (max {} solves, seed {})...",
cfg.n_minima,
cfg.method.as_str(),
cfg.max_solves_or_default(),
cfg.seed
);
let result = {
let mut solver = IpoptLocalSolve::new(app);
match find_minima(base, &mut solver, cfg) {
Ok(r) => r,
Err(e) => {
eprintln!("pounce: --minima {e}");
return ExitCode::from(2);
}
}
};
for w in &result.warnings {
eprintln!("pounce: --minima {w}");
}
if result.minima.is_empty() && result.n_kkt_rejected > 0 {
eprintln!(
"pounce: --minima: every converged candidate ({}) was rejected by \
--kkt-tol {} (unscaled dual infeasibility above kkt_tol * max(1, gradient \
scale)); no minima are returned. Loosen --kkt-tol, or pass --kkt-tol none.",
result.n_kkt_rejected,
cfg.kkt_tol.unwrap_or(f64::NAN)
);
}
let minima = &result.minima;
print_table(
minima,
&result.scale,
result.stop,
result.n_solves,
result.n_kkt_rejected,
dual_sign,
);
if let Some(sp) = sol_path {
write_sol_files(sp, minima, n, m, dual_sign);
}
if let Some(json_path) = &args.json_output {
write_json_report(
json_path,
args,
cfg,
MinimaCounts {
stop: result.stop,
n_solves: result.n_solves,
n_kkt_rejected: result.n_kkt_rejected,
},
minima,
n,
m,
&info,
dual_sign,
);
}
if minima.is_empty() {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn print_table(
minima: &[Minimum],
l_scale: &[Number],
stop: Stop,
n_solves: usize,
n_kkt_rejected: usize,
obj_sign: Number,
) {
println!();
println!(
"find-minima: {} distinct minim{} in {} solves ({})",
minima.len(),
if minima.len() == 1 { "um" } else { "a" },
n_solves,
stop.as_str()
);
if n_kkt_rejected > 0 {
println!(" {n_kkt_rejected} converged solve(s) rejected by --kkt-tol");
}
if minima.is_empty() {
println!(" (no accepted minima — try raising --max-solves or --patience)");
return;
}
println!(" rank objective dist-to-best");
let best = &minima[0].x;
for (rank, mn) in minima.iter().enumerate() {
let d = scaled_distance(&mn.x, best, l_scale);
println!(
" {rank:>4} {:>16.8e} {:>14.6e}",
obj_sign * mn.objective,
d
);
}
}
fn write_sol_files(sol_path: &Path, minima: &[Minimum], n: usize, m: usize, dual_sign: Number) {
let zeros = vec![0.0; m];
for (rank, mn) in minima.iter().enumerate() {
let path = if rank == 0 {
sol_path.to_path_buf()
} else {
sibling_sol_path(sol_path, rank)
};
let message = format!(
"POUNCE {} find-minima rank {rank}: Solve_Succeeded",
env!("CARGO_PKG_VERSION")
);
let lambda: Vec<Number> = if mn.lambda.len() == m {
mn.lambda.iter().map(|&v| dual_sign * v).collect()
} else {
zeros.clone()
};
let suffixes = bound_multiplier_suffixes(mn, n, dual_sign);
let payload = crate::nl_writer::SolutionFile {
message: &message,
x: &mn.x,
mult_g: &lambda,
solve_result_num: status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded),
suffixes: &suffixes,
};
match crate::nl_writer::write_sol_file(&path, &payload) {
Ok(_) => eprintln!("pounce: wrote {}", path.display()),
Err(e) => eprintln!("pounce: failed to write {}: {e}", path.display()),
}
}
}
fn bound_multiplier_suffixes(
mn: &Minimum,
n: usize,
sigma: Number,
) -> Vec<crate::nl_writer::SolSuffix> {
if mn.z_l.len() != n || mn.z_u.len() != n {
return Vec::new();
}
let block = |name: &str, values: Vec<Number>| crate::nl_writer::SolSuffix {
name: name.to_string(),
target: crate::nl_writer::SolSuffixTarget::Var,
values: crate::nl_writer::SolSuffixValues::Real(values),
};
vec![
block("ipopt_zL_out", mn.z_l.iter().map(|&z| sigma * z).collect()),
block("ipopt_zU_out", mn.z_u.iter().map(|&z| -sigma * z).collect()),
]
}
fn sibling_sol_path(sol_path: &Path, rank: usize) -> PathBuf {
let mut stub = sol_path.to_path_buf();
stub.set_extension(""); let base = stub.to_string_lossy().into_owned();
PathBuf::from(format!("{base}.min{rank:03}.sol"))
}
struct MinimaCounts {
stop: Stop,
n_solves: usize,
n_kkt_rejected: usize,
}
#[allow(clippy::too_many_arguments)]
fn write_json_report(
json_path: &Path,
args: &Args,
cfg: &MinimaArgs,
counts: MinimaCounts,
minima: &[Minimum],
n: usize,
m: usize,
info: &pounce_nlp::tnlp::NlpInfo,
obj_sign: Number,
) {
let input = match &args.problem {
ProblemSource::Builtin(name) => InputDescriptor::Builtin { name: name.clone() },
ProblemSource::NlFile(p) => InputDescriptor::NlFile {
path: p.clone(),
size_bytes: std::fs::metadata(p).ok().map(|md| md.len()),
},
};
let mut builder = ReportBuilder::new(args.json_detail, input);
builder.problem.n_variables = n as Index;
builder.problem.n_constraints = m as Index;
builder.problem.n_objectives = 1;
builder.problem.nnz_jac_g = Some(info.nnz_jac_g);
builder.problem.nnz_h_lag = Some(info.nnz_h_lag);
builder.problem.minimize = obj_sign > 0.0;
if let Some(best) = minima.first() {
builder.solution.status = ApplicationReturnStatus::SolveSucceeded;
builder.solution.solve_result_num =
status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded);
builder.solution.objective = obj_sign * best.objective;
builder.solution.x = best.x.clone();
builder.solution.lambda = if best.lambda.len() == m {
best.lambda.iter().map(|&v| obj_sign * v).collect()
} else {
vec![0.0; m]
};
}
let report = builder.finish();
let mut value = match serde_json::to_value(&report) {
Ok(v) => v,
Err(e) => {
eprintln!("pounce: failed to serialize minima report: {e}");
return;
}
};
let minima_json: Vec<serde_json::Value> = minima
.iter()
.map(|mn| {
serde_json::json!({
"x": mn.x,
"objective": obj_sign * mn.objective,
})
})
.collect();
let values: Vec<Number> = minima.iter().map(|mn| obj_sign * mn.objective).collect();
if let serde_json::Value::Object(map) = &mut value {
map.insert(
"minima".to_string(),
serde_json::json!({
"method": cfg.method.as_str(),
"status": counts.stop.as_str(),
"n_solves": counts.n_solves,
"n_kkt_rejected": counts.n_kkt_rejected,
"n_minima": minima.len(),
"minima": minima_json,
"values": values,
}),
);
}
match serde_json::to_string_pretty(&value) {
Ok(s) => match std::fs::write(json_path, s) {
Ok(_) => eprintln!("pounce: wrote {}", json_path.display()),
Err(e) => eprintln!(
"pounce: failed to write JSON report to {}: {e}",
json_path.display()
),
},
Err(e) => eprintln!("pounce: failed to render minima report: {e}"),
}
}