1pub use pounce_minima::{archive, penalty_tnlp, sampling};
11
12use crate::cli::{Args, MinimaArgs, ProblemSource};
13use crate::solve_report::{InputDescriptor, ReportBuilder, status_to_solve_result_num};
14use archive::scaled_distance;
15use pounce_algorithm::application::IpoptApplication;
16use pounce_common::types::{Index, Number};
17use pounce_minima::{IpoptLocalSolve, Minimum, Stop, find_minima};
18use pounce_nlp::return_codes::ApplicationReturnStatus;
19use pounce_nlp::tnlp::TNLP;
20use std::cell::RefCell;
21use std::path::{Path, PathBuf};
22use std::process::ExitCode;
23use std::rc::Rc;
24
25pub fn run(
30 app: &mut IpoptApplication,
31 base: &Rc<RefCell<dyn TNLP>>,
32 cfg: &MinimaArgs,
33 args: &Args,
34 sol_path: Option<&Path>,
35 dual_sign: Number,
36) -> ExitCode {
37 let Some(info) = base.borrow_mut().get_nlp_info() else {
38 eprintln!("pounce: --minima could not read problem dimensions");
39 return ExitCode::from(2);
40 };
41 let n = info.n as usize;
42 let m = info.m as usize;
43
44 println!(
45 "Searching for up to {} minima via `{}` (max {} solves, seed {})...",
46 cfg.n_minima,
47 cfg.method.as_str(),
48 cfg.max_solves_or_default(),
49 cfg.seed
50 );
51
52 let result = {
53 let mut solver = IpoptLocalSolve::new(app);
54 match find_minima(base, &mut solver, cfg) {
55 Ok(r) => r,
56 Err(e) => {
57 eprintln!("pounce: --minima {e}");
58 return ExitCode::from(2);
59 }
60 }
61 };
62 for w in &result.warnings {
63 eprintln!("pounce: --minima {w}");
64 }
65 if result.minima.is_empty() && result.n_kkt_rejected > 0 {
67 eprintln!(
68 "pounce: --minima: every converged candidate ({}) was rejected by \
69 --kkt-tol {} (unscaled dual infeasibility above kkt_tol * max(1, gradient \
70 scale)); no minima are returned. Loosen --kkt-tol, or pass --kkt-tol none.",
71 result.n_kkt_rejected,
72 cfg.kkt_tol.unwrap_or(f64::NAN)
73 );
74 }
75
76 let minima = &result.minima;
77 print_table(
78 minima,
79 &result.scale,
80 result.stop,
81 result.n_solves,
82 result.n_kkt_rejected,
83 dual_sign,
84 );
85
86 if let Some(sp) = sol_path {
89 write_sol_files(sp, minima, n, m, dual_sign);
90 }
91
92 if let Some(json_path) = &args.json_output {
95 write_json_report(
96 json_path,
97 args,
98 cfg,
99 MinimaCounts {
100 stop: result.stop,
101 n_solves: result.n_solves,
102 n_kkt_rejected: result.n_kkt_rejected,
103 },
104 minima,
105 n,
106 m,
107 &info,
108 dual_sign,
109 );
110 }
111
112 if minima.is_empty() {
113 ExitCode::from(1)
114 } else {
115 ExitCode::SUCCESS
116 }
117}
118
119fn print_table(
129 minima: &[Minimum],
130 l_scale: &[Number],
131 stop: Stop,
132 n_solves: usize,
133 n_kkt_rejected: usize,
134 obj_sign: Number,
135) {
136 println!();
137 println!(
138 "find-minima: {} distinct minim{} in {} solves ({})",
139 minima.len(),
140 if minima.len() == 1 { "um" } else { "a" },
141 n_solves,
142 stop.as_str()
143 );
144 if n_kkt_rejected > 0 {
145 println!(" {n_kkt_rejected} converged solve(s) rejected by --kkt-tol");
146 }
147 if minima.is_empty() {
148 println!(" (no accepted minima — try raising --max-solves or --patience)");
149 return;
150 }
151 println!(" rank objective dist-to-best");
152 let best = &minima[0].x;
153 for (rank, mn) in minima.iter().enumerate() {
154 let d = scaled_distance(&mn.x, best, l_scale);
155 println!(
156 " {rank:>4} {:>16.8e} {:>14.6e}",
157 obj_sign * mn.objective,
158 d
159 );
160 }
161}
162
163fn write_sol_files(sol_path: &Path, minima: &[Minimum], n: usize, m: usize, dual_sign: Number) {
180 let zeros = vec![0.0; m];
181 for (rank, mn) in minima.iter().enumerate() {
182 let path = if rank == 0 {
183 sol_path.to_path_buf()
184 } else {
185 sibling_sol_path(sol_path, rank)
186 };
187 let message = format!(
188 "POUNCE {} find-minima rank {rank}: Solve_Succeeded",
189 env!("CARGO_PKG_VERSION")
190 );
191 let lambda: Vec<Number> = if mn.lambda.len() == m {
194 mn.lambda.iter().map(|&v| dual_sign * v).collect()
195 } else {
196 zeros.clone()
197 };
198 let suffixes = bound_multiplier_suffixes(mn, n, dual_sign);
199 let payload = crate::nl_writer::SolutionFile {
200 message: &message,
201 x: &mn.x,
202 mult_g: &lambda,
203 solve_result_num: status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded),
204 suffixes: &suffixes,
205 };
206 match crate::nl_writer::write_sol_file(&path, &payload) {
207 Ok(_) => eprintln!("pounce: wrote {}", path.display()),
208 Err(e) => eprintln!("pounce: failed to write {}: {e}", path.display()),
209 }
210 }
211}
212
213fn bound_multiplier_suffixes(
220 mn: &Minimum,
221 n: usize,
222 sigma: Number,
223) -> Vec<crate::nl_writer::SolSuffix> {
224 if mn.z_l.len() != n || mn.z_u.len() != n {
225 return Vec::new();
226 }
227 let block = |name: &str, values: Vec<Number>| crate::nl_writer::SolSuffix {
228 name: name.to_string(),
229 target: crate::nl_writer::SolSuffixTarget::Var,
230 values: crate::nl_writer::SolSuffixValues::Real(values),
231 };
232 vec![
233 block("ipopt_zL_out", mn.z_l.iter().map(|&z| sigma * z).collect()),
234 block("ipopt_zU_out", mn.z_u.iter().map(|&z| -sigma * z).collect()),
235 ]
236}
237
238fn sibling_sol_path(sol_path: &Path, rank: usize) -> PathBuf {
240 let mut stub = sol_path.to_path_buf();
241 stub.set_extension(""); let base = stub.to_string_lossy().into_owned();
243 PathBuf::from(format!("{base}.min{rank:03}.sol"))
244}
245
246struct MinimaCounts {
249 stop: Stop,
250 n_solves: usize,
251 n_kkt_rejected: usize,
252}
253
254#[allow(clippy::too_many_arguments)]
257fn write_json_report(
258 json_path: &Path,
259 args: &Args,
260 cfg: &MinimaArgs,
261 counts: MinimaCounts,
262 minima: &[Minimum],
263 n: usize,
264 m: usize,
265 info: &pounce_nlp::tnlp::NlpInfo,
266 obj_sign: Number,
267) {
268 let input = match &args.problem {
269 ProblemSource::Builtin(name) => InputDescriptor::Builtin { name: name.clone() },
270 ProblemSource::NlFile(p) => InputDescriptor::NlFile {
271 path: p.clone(),
272 size_bytes: std::fs::metadata(p).ok().map(|md| md.len()),
273 },
274 };
275 let mut builder = ReportBuilder::new(args.json_detail, input);
276 builder.problem.n_variables = n as Index;
277 builder.problem.n_constraints = m as Index;
278 builder.problem.n_objectives = 1;
279 builder.problem.nnz_jac_g = Some(info.nnz_jac_g);
280 builder.problem.nnz_h_lag = Some(info.nnz_h_lag);
281 builder.problem.minimize = obj_sign > 0.0;
282 if let Some(best) = minima.first() {
283 builder.solution.status = ApplicationReturnStatus::SolveSucceeded;
284 builder.solution.solve_result_num =
285 status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded);
286 builder.solution.objective = obj_sign * best.objective;
290 builder.solution.x = best.x.clone();
291 builder.solution.lambda = if best.lambda.len() == m {
297 best.lambda.iter().map(|&v| obj_sign * v).collect()
298 } else {
299 vec![0.0; m]
300 };
301 }
302 let report = builder.finish();
303
304 let mut value = match serde_json::to_value(&report) {
307 Ok(v) => v,
308 Err(e) => {
309 eprintln!("pounce: failed to serialize minima report: {e}");
310 return;
311 }
312 };
313 let minima_json: Vec<serde_json::Value> = minima
314 .iter()
315 .map(|mn| {
316 serde_json::json!({
317 "x": mn.x,
318 "objective": obj_sign * mn.objective,
319 })
320 })
321 .collect();
322 let values: Vec<Number> = minima.iter().map(|mn| obj_sign * mn.objective).collect();
323 if let serde_json::Value::Object(map) = &mut value {
324 map.insert(
325 "minima".to_string(),
326 serde_json::json!({
327 "method": cfg.method.as_str(),
328 "status": counts.stop.as_str(),
329 "n_solves": counts.n_solves,
330 "n_kkt_rejected": counts.n_kkt_rejected,
331 "n_minima": minima.len(),
332 "minima": minima_json,
333 "values": values,
334 }),
335 );
336 }
337 match serde_json::to_string_pretty(&value) {
338 Ok(s) => match std::fs::write(json_path, s) {
339 Ok(_) => eprintln!("pounce: wrote {}", json_path.display()),
340 Err(e) => eprintln!(
341 "pounce: failed to write JSON report to {}: {e}",
342 json_path.display()
343 ),
344 },
345 Err(e) => eprintln!("pounce: failed to render minima report: {e}"),
346 }
347}