1pub mod archive;
22pub mod penalty_tnlp;
23pub mod sampling;
24
25use crate::cli::{Args, MinimaArgs, MinimaMethod, ProblemSource};
26use crate::seeded_tnlp::SeededTnlp;
27use crate::solve_report::{InputDescriptor, ReportBuilder, status_to_solve_result_num};
28use archive::{Archive, scaled_distance};
29use penalty_tnlp::{Kernel, PenaltyTnlp, TunnelTnlp};
30use pounce_algorithm::application::IpoptApplication;
31use pounce_common::types::{Index, Number};
32use pounce_nlp::return_codes::ApplicationReturnStatus;
33use pounce_nlp::tnlp::{BoundsInfo, IndexStyle, SparsityRequest, StartingPoint, TNLP};
34use sampling::{Sampler, clip};
35use std::cell::RefCell;
36use std::path::{Path, PathBuf};
37use std::process::ExitCode;
38use std::rc::Rc;
39
40const BOUND_INF: Number = 1e19;
42const PSD_MAX_N: usize = 256;
46
47#[derive(Clone, Copy, Debug)]
49enum Stop {
50 TargetReached,
51 Converged,
52 BudgetExhausted,
53}
54
55impl Stop {
56 fn as_str(self) -> &'static str {
57 match self {
58 Stop::TargetReached => "target_reached",
59 Stop::Converged => "converged",
60 Stop::BudgetExhausted => "budget_exhausted",
61 }
62 }
63}
64
65struct SolveOut {
67 success: bool,
68 x: Vec<Number>,
69}
70
71type SolveCapture = Rc<RefCell<Option<(Vec<Number>, Vec<Number>)>>>;
74
75struct Driver<'a> {
78 app: &'a mut IpoptApplication,
79 base: Rc<RefCell<dyn TNLP>>,
80 capture: SolveCapture,
84 cfg: &'a MinimaArgs,
85 n: usize,
86 m: usize,
87 nnz_h: usize,
88 index_offset: usize,
90 x0: Vec<Number>,
91 x_l: Vec<Number>,
92 x_u: Vec<Number>,
93 has_box: bool,
94 l_scale: Vec<Number>,
96 sampler: Sampler,
97 archive: Archive,
98 stagnant: usize,
99 n_solves: usize,
100 max_solves: usize,
101 n_samples: usize,
103 max_samples: usize,
107 psd_skipped_logged: bool,
108}
109
110impl<'a> Driver<'a> {
111 fn run_solve(
117 &mut self,
118 solve_tnlp: Rc<RefCell<dyn TNLP>>,
119 exact_hessian: bool,
120 ) -> Result<SolveOut, Stop> {
121 if self.n_solves >= self.max_solves {
122 return Err(Stop::BudgetExhausted);
123 }
124 self.n_solves += 1;
125 let line = if exact_hessian {
128 "hessian_approximation exact\n"
129 } else {
130 "hessian_approximation limited-memory\n"
131 };
132 let _ = self.app.options_mut().read_from_str(line, true);
133 *self.capture.borrow_mut() = None;
134 let status = self.app.optimize_tnlp(solve_tnlp);
135 let success = matches!(
136 status,
137 ApplicationReturnStatus::SolveSucceeded
138 | ApplicationReturnStatus::SolvedToAcceptableLevel
139 );
140 match self.capture.borrow_mut().take() {
141 Some((x, _lambda)) if success => Ok(SolveOut { success: true, x }),
146 _ => Ok(SolveOut {
149 success: false,
150 x: Vec::new(),
151 }),
152 }
153 }
154
155 fn recover_duals(&mut self, x: &[Number]) -> Vec<Number> {
164 let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(SeededTnlp::new(
165 Rc::clone(&self.base),
166 x.to_vec(),
167 )));
168 let _ = self
169 .app
170 .options_mut()
171 .read_from_str("hessian_approximation exact\n", true);
172 *self.capture.borrow_mut() = None;
173 let status = self.app.optimize_tnlp(t);
174 let ok = matches!(
175 status,
176 ApplicationReturnStatus::SolveSucceeded
177 | ApplicationReturnStatus::SolvedToAcceptableLevel
178 );
179 match self.capture.borrow_mut().take() {
180 Some((_x, lambda)) if ok && lambda.len() == self.m => lambda,
181 _ => vec![0.0; self.m],
182 }
183 }
184
185 fn solve_seeded(&mut self, seed_x: &[Number], exact: bool) -> Result<SolveOut, Stop> {
186 let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(SeededTnlp::new(
187 Rc::clone(&self.base),
188 seed_x.to_vec(),
189 )));
190 self.run_solve(t, exact)
191 }
192
193 fn solve_penalty(&mut self, seed_x: &[Number], kernel: Kernel) -> Result<SolveOut, Stop> {
194 let pen: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(PenaltyTnlp::new(
195 Rc::clone(&self.base),
196 kernel,
197 )));
198 let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(SeededTnlp::new(pen, seed_x.to_vec())));
199 self.run_solve(t, false)
200 }
201
202 fn solve_tunnel(
203 &mut self,
204 seed_x: &[Number],
205 f_ref: Number,
206 pole: Kernel,
207 ) -> Result<SolveOut, Stop> {
208 let tun: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TunnelTnlp::new(
209 Rc::clone(&self.base),
210 f_ref,
211 pole,
212 )));
213 let t: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(SeededTnlp::new(tun, seed_x.to_vec())));
214 self.run_solve(t, false)
215 }
216
217 fn clean_f(&mut self, x: &[Number]) -> Option<Number> {
219 self.base.borrow_mut().eval_f(x, true)
220 }
221
222 fn in_bounds(&self, x: &[Number]) -> bool {
232 x.iter()
233 .zip(&self.x_l)
234 .zip(&self.x_u)
235 .all(|((&xi, &lo), &hi)| coord_in_bounds(xi, lo, hi))
236 }
237
238 fn obj_hessian_dense(&mut self, x: &[Number]) -> Option<Vec<Number>> {
241 if self.n > PSD_MAX_N || self.nnz_h == 0 {
242 return None;
243 }
244 let nnz = self.nnz_h;
245 let mut irow = vec![0 as Index; nnz];
246 let mut jcol = vec![0 as Index; nnz];
247 {
248 let mut b = self.base.borrow_mut();
249 if !b.eval_h(
250 None,
251 false,
252 1.0,
253 None,
254 false,
255 SparsityRequest::Structure {
256 irow: &mut irow,
257 jcol: &mut jcol,
258 },
259 ) {
260 return None;
261 }
262 }
263 let lam = vec![0.0; self.m];
264 let mut vals = vec![0.0; nnz];
265 {
266 let mut b = self.base.borrow_mut();
267 if !b.eval_h(
268 Some(x),
269 true,
270 1.0,
271 Some(&lam),
272 true,
273 SparsityRequest::Values { values: &mut vals },
274 ) {
275 return None;
276 }
277 }
278 let n = self.n;
279 let mut dense = vec![0.0; n * n];
280 for k in 0..nnz {
281 let i = irow[k] as usize - self.index_offset;
282 let j = jcol[k] as usize - self.index_offset;
283 dense[i * n + j] += vals[k];
284 if i != j {
285 dense[j * n + i] += vals[k];
286 }
287 }
288 Some(dense)
289 }
290
291 fn is_minimum(&mut self, x: &[Number]) -> bool {
294 if self.n > PSD_MAX_N {
295 if !self.psd_skipped_logged {
296 eprintln!(
297 "pounce: --minima saddle-rejection (PSD) check skipped — n={} exceeds the \
298 dense-eigendecomposition cap ({PSD_MAX_N}); accepting converged points as minima.",
299 self.n
300 );
301 self.psd_skipped_logged = true;
302 }
303 return true;
304 }
305 let dense = match self.obj_hessian_dense(x) {
306 Some(d) => d,
307 None => return true,
308 };
309 let n = self.n;
310 let mut w = vec![0.0; n];
311 let mut v = vec![0.0; n * n];
312 if !pounce_sensitivity::symmetric_eigen(&dense, n, &mut w, &mut v) {
313 return true;
314 }
315 let min_eig = w.iter().cloned().fold(f64::INFINITY, f64::min);
316 min_eig >= -self.cfg.psd_tol
317 }
318
319 fn resolve_lengths(
323 &self,
324 spec: Option<f64>,
325 frac_default: f64,
326 frac_override: Option<f64>,
327 fallback: f64,
328 ) -> Vec<Number> {
329 match spec {
330 Some(s) => vec![s; self.n],
331 None => {
332 let frac = frac_override.unwrap_or(frac_default);
333 if self.has_box {
334 self.l_scale.iter().map(|&l| frac * l).collect()
335 } else {
336 vec![fallback; self.n]
337 }
338 }
339 }
340 }
341
342 fn auto_amplitude(&mut self, center: &[Number], sigma: &[Number], margin: f64) -> Option<f64> {
345 let h = self.obj_hessian_dense(center)?;
346 let n = self.n;
347 let mut s_mat = vec![0.0; n * n];
348 for i in 0..n {
349 for j in 0..n {
350 s_mat[i * n + j] = sigma[i] * sigma[j] * h[i * n + j];
351 }
352 }
353 let mut w = vec![0.0; n];
354 let mut v = vec![0.0; n * n];
355 if !pounce_sensitivity::symmetric_eigen(&s_mat, n, &mut w, &mut v) {
356 return None;
357 }
358 let mu_min = w.iter().cloned().fold(f64::INFINITY, f64::min);
359 Some(margin * mu_min.max(1e-12))
360 }
361
362 fn sample(&mut self, jitter: f64) -> Vec<Number> {
364 let x0 = self.x0.clone();
365 let lo = self.x_l.clone();
366 let hi = self.x_u.clone();
367 self.sampler.sample(&x0, &lo, &hi, self.has_box, jitter)
368 }
369
370 fn consider(
377 &mut self,
378 mut x: Vec<Number>,
379 mut success: bool,
380 polish: bool,
381 ) -> Result<bool, Stop> {
382 if success && polish {
383 let r = self.solve_seeded(&x, true)?;
384 success = r.success;
385 if success {
386 x = r.x;
387 }
388 }
389 if !success {
390 return self.reject();
391 }
392 let fval = match self.clean_f(&x) {
393 Some(f) => f,
394 None => return self.reject(),
395 };
396 let finite = x.iter().all(|v| v.is_finite()) && fval.is_finite();
397 let accepted =
398 finite && self.in_bounds(&x) && self.is_minimum(&x) && !self.archive.is_known(&x);
399 if accepted {
400 let lambda = self.recover_duals(&x);
405 self.archive.add(x, lambda, fval);
406 self.stagnant = 0;
407 if self.archive.len() >= self.cfg.n_minima {
408 return Err(Stop::TargetReached);
409 }
410 Ok(true)
411 } else {
412 self.reject()
413 }
414 }
415
416 fn reject(&mut self) -> Result<bool, Stop> {
417 self.stagnant += 1;
418 if self.stagnant >= self.cfg.patience {
419 return Err(Stop::Converged);
420 }
421 Ok(false)
422 }
423
424 fn note_sample(&mut self) -> Result<(), Stop> {
430 if self.n_samples >= self.max_samples {
431 return Err(Stop::BudgetExhausted);
432 }
433 self.n_samples += 1;
434 Ok(())
435 }
436
437 fn run(&mut self) -> Stop {
440 let res = match self.cfg.method {
441 MinimaMethod::Multistart => self.run_multistart(),
442 MinimaMethod::Mlsl => self.run_mlsl(),
443 MinimaMethod::Basinhopping => self.run_basinhopping(),
444 MinimaMethod::Flooding => self.run_flooding(),
445 MinimaMethod::Deflation => self.run_deflation(),
446 MinimaMethod::Tunneling => self.run_tunneling(),
447 };
448 res.err().unwrap_or(Stop::BudgetExhausted)
450 }
451
452 fn run_multistart(&mut self) -> Result<(), Stop> {
453 let jitter = self.cfg.restart_jitter.unwrap_or(1.0);
454 let x0 = self.x0.clone();
455 let r = self.solve_seeded(&x0, true)?;
456 self.consider(r.x, r.success, false)?;
457 loop {
458 let s = self.sample(jitter);
459 let r = self.solve_seeded(&s, true)?;
460 self.consider(r.x, r.success, false)?;
461 }
462 }
463
464 fn run_mlsl(&mut self) -> Result<(), Stop> {
465 let batch = self.cfg.samples_per_round.unwrap_or(20);
466 let gamma = self.cfg.gamma.unwrap_or(2.0);
467 let jitter = self.cfg.restart_jitter.unwrap_or(1.0);
468 let n = self.n;
469 let diag = (n as f64).sqrt();
470 let mut pool_x: Vec<Vec<Number>> = Vec::new();
471 let mut pool_f: Vec<Number> = Vec::new();
472 let x0 = self.x0.clone();
473 let r = self.solve_seeded(&x0, true)?;
474 self.consider(r.x, r.success, false)?;
475 loop {
476 for _ in 0..batch {
479 self.note_sample()?;
480 let s = self.sample(jitter);
481 let f = self.clean_f(&s).unwrap_or(f64::INFINITY);
482 pool_x.push(s);
483 pool_f.push(f);
484 }
485 let bign = pool_x.len();
486 let ne = bign.max(2) as f64;
487 let radius = gamma * diag * (ne.ln() / ne).powf(1.0 / n as f64);
488 let mut order: Vec<usize> = (0..bign).collect();
489 order.sort_by(|&a, &b| {
490 pool_f[a]
491 .partial_cmp(&pool_f[b])
492 .unwrap_or(std::cmp::Ordering::Equal)
493 });
494 for i in order {
495 let si = pool_x[i].clone();
496 let fi = pool_f[i];
497 let better_near = (0..bign).any(|j| {
499 j != i
500 && pool_f[j] < fi
501 && scaled_distance(&si, &pool_x[j], &self.l_scale) < radius
502 });
503 if better_near || self.archive.near_any(&si, radius) {
504 continue;
505 }
506 let r = self.solve_seeded(&si, true)?;
507 self.consider(r.x, r.success, false)?;
508 }
509 }
510 }
511
512 fn run_basinhopping(&mut self) -> Result<(), Stop> {
513 let step = self.cfg.step.unwrap_or(0.5);
514 let temperature = self.cfg.temperature.unwrap_or(1.0);
515 let x0 = self.x0.clone();
516 let r = self.solve_seeded(&x0, true)?;
517 let mut cur = if r.success { r.x.clone() } else { x0.clone() };
518 let mut cur_f = self.clean_f(&cur).unwrap_or(f64::INFINITY);
519 self.consider(r.x, r.success, false)?;
520 loop {
521 let mut trial = self.sampler.perturb(&cur, &[step]);
522 clip(&mut trial, &self.x_l, &self.x_u, self.has_box);
523 let r = self.solve_seeded(&trial, true)?;
524 if !r.success {
525 self.consider(r.x, false, false)?;
526 continue;
527 }
528 let new_x = r.x.clone();
529 self.consider(r.x, true, false)?;
530 let new_f = self.clean_f(&new_x).unwrap_or(f64::INFINITY);
531 let accept_uphill = self.sampler.uniform() < (-(new_f - cur_f) / temperature).exp();
532 if new_f < cur_f || accept_uphill {
533 cur = new_x;
534 cur_f = new_f;
535 }
536 }
537 }
538
539 fn run_flooding(&mut self) -> Result<(), Stop> {
540 let sigma = self.resolve_lengths(self.cfg.sigma, 0.1, self.cfg.sigma_frac, 0.5);
541 let inv_sigma2: Vec<f64> = sigma.iter().map(|&s| 1.0 / (s * s)).collect();
542 let amp_spec = self.cfg.amplitude;
543 let margin = self.cfg.amp_margin.unwrap_or(2.0);
544 let bump_factor = 3.0;
545 let bump_cap = 1e3;
546 let fallback_amp = 2.0;
547 let jitter = self.cfg.restart_jitter.unwrap_or(0.5);
548 let x0 = self.x0.clone();
549 let mut base_amp: Vec<f64> = Vec::new();
550 let mut mult: Vec<f64> = Vec::new();
551 let mut start = x0.clone();
552 let mut last_center: Option<usize> = None;
553 let mut fails = 0usize;
554 loop {
555 let centers = self.archive.xs.clone();
556 while base_amp.len() < centers.len() {
557 let k = base_amp.len();
558 let a = match amp_spec {
559 Some(a) => a,
560 None => self
561 .auto_amplitude(¢ers[k], &sigma, margin)
562 .unwrap_or(fallback_amp),
563 };
564 base_amp.push(a);
565 mult.push(1.0);
566 }
567 let eff: Vec<f64> = (0..centers.len()).map(|k| base_amp[k] * mult[k]).collect();
568 let polish = !centers.is_empty();
569 let solve_out = if centers.is_empty() {
570 self.solve_seeded(&start, true)?
571 } else {
572 let kernel = Kernel::Gauss {
573 centers: centers.clone(),
574 amps: eff,
575 inv_sigma2: inv_sigma2.clone(),
576 };
577 self.solve_penalty(&start, kernel)?
578 };
579 let accepted = self.consider(solve_out.x, solve_out.success, polish)?;
580 if accepted {
581 if let Some(last) = self.archive.xs.last() {
582 start = last.clone();
583 }
584 last_center = Some(self.archive.xs.len() - 1);
585 fails = 0;
586 } else if let Some(lc) = last_center {
587 if mult[lc] < bump_cap && fails < 8 {
588 mult[lc] *= bump_factor;
590 let scale: Vec<f64> = sigma.iter().map(|&s| 0.05 * s).collect();
591 start = self.sampler.perturb(¢ers[lc], &scale);
592 fails += 1;
593 continue;
594 }
595 start = self.sample(jitter);
596 last_center = None;
597 fails = 0;
598 } else {
599 start = self.sample(jitter);
600 last_center = None;
601 fails = 0;
602 }
603 }
604 }
605
606 fn run_deflation(&mut self) -> Result<(), Stop> {
607 let eta = self.cfg.eta.unwrap_or(1.0);
608 let power = self.cfg.power.unwrap_or(2.0);
609 let soft = self.cfg.soft.unwrap_or(1e-3);
610 let length = self.resolve_lengths(self.cfg.length, 0.1, self.cfg.length_frac, 0.5);
611 let inv_len2: Vec<f64> = length.iter().map(|&l| 1.0 / (l * l)).collect();
612 let jitter = self.cfg.restart_jitter.unwrap_or(0.5);
613 let q = power / 2.0;
614 let mut start = self.x0.clone();
615 loop {
616 let centers = self.archive.xs.clone();
617 let polish = !centers.is_empty();
618 let mut s = start.clone();
620 if !centers.is_empty() && self.archive.is_known(&s) {
621 let scale: Vec<f64> = length.iter().map(|&l| 0.1 * l).collect();
622 s = self.sampler.perturb(&s, &scale);
623 }
624 let solve_out = if centers.is_empty() {
625 self.solve_seeded(&s, true)?
626 } else {
627 let kernel = Kernel::Pole {
628 centers: centers.clone(),
629 eta,
630 q,
631 soft,
632 inv_len2: inv_len2.clone(),
633 };
634 self.solve_penalty(&s, kernel)?
635 };
636 let accepted = self.consider(solve_out.x, solve_out.success, polish)?;
637 if accepted {
638 if let Some(last) = self.archive.xs.last() {
639 start = last.clone();
640 }
641 } else {
642 start = self.sample(jitter);
643 }
644 }
645 }
646
647 fn run_tunneling(&mut self) -> Result<(), Stop> {
648 let eta = self.cfg.eta.unwrap_or(1.0);
649 let power = self.cfg.power.unwrap_or(2.0);
650 let soft = self.cfg.soft.unwrap_or(1e-3);
651 let length = self.resolve_lengths(self.cfg.length, 0.1, self.cfg.length_frac, 0.5);
652 let inv_len2: Vec<f64> = length.iter().map(|&l| 1.0 / (l * l)).collect();
653 let jitter = self.cfg.restart_jitter.unwrap_or(0.75);
654 let q = power / 2.0;
655 let x0 = self.x0.clone();
656 let r = self.solve_seeded(&x0, true)?;
658 self.consider(r.x, r.success, false)?;
659 loop {
660 let centers = self.archive.xs.clone();
661 let f_ref = match self.archive.fs.last() {
664 Some(&f) => f,
665 None => self.clean_f(&x0).unwrap_or(0.0),
666 };
667 let anchor = self
668 .archive
669 .xs
670 .last()
671 .cloned()
672 .unwrap_or_else(|| x0.clone());
673 let jit = vec![jitter; self.n];
674 let mut start = self.sampler.perturb(&anchor, &jit);
675 clip(&mut start, &self.x_l, &self.x_u, self.has_box);
676 let kernel = Kernel::Pole {
677 centers: centers.clone(),
678 eta,
679 q,
680 soft,
681 inv_len2: inv_len2.clone(),
682 };
683 let r = self.solve_tunnel(&start, f_ref, kernel)?;
684 self.consider(r.x, r.success, true)?;
685 }
686 }
687}
688
689struct Minimum {
691 x: Vec<Number>,
692 objective: Number,
693 lambda: Vec<Number>,
696}
697
698pub fn run(
703 app: &mut IpoptApplication,
704 base: &Rc<RefCell<dyn TNLP>>,
705 cfg: &MinimaArgs,
706 args: &Args,
707 sol_path: Option<&Path>,
708) -> ExitCode {
709 let info = match base.borrow_mut().get_nlp_info() {
710 Some(i) => i,
711 None => {
712 eprintln!("pounce: --minima could not read problem dimensions");
713 return ExitCode::from(2);
714 }
715 };
716 let n = info.n as usize;
717 let m = info.m as usize;
718 let nnz_h = info.nnz_h_lag as usize;
719 let index_offset = match info.index_style {
720 IndexStyle::Fortran => 1,
721 IndexStyle::C => 0,
722 };
723
724 let mut x_l = vec![0.0; n];
727 let mut x_u = vec![0.0; n];
728 let mut g_l = vec![0.0; m];
729 let mut g_u = vec![0.0; m];
730 base.borrow_mut().get_bounds_info(BoundsInfo {
731 x_l: &mut x_l,
732 x_u: &mut x_u,
733 g_l: &mut g_l,
734 g_u: &mut g_u,
735 });
736 let mut x0 = vec![0.0; n];
737 {
738 let mut z_l = vec![0.0; n];
739 let mut z_u = vec![0.0; n];
740 let mut lambda = vec![0.0; m];
741 base.borrow_mut().get_starting_point(StartingPoint {
742 init_x: true,
743 x: &mut x0,
744 init_z: false,
745 z_l: &mut z_l,
746 z_u: &mut z_u,
747 init_lambda: false,
748 lambda: &mut lambda,
749 });
750 }
751
752 let has_box = (0..n).all(|i| x_l[i] > -BOUND_INF && x_u[i] < BOUND_INF);
754 let l_scale: Vec<Number> = (0..n)
755 .map(|i| {
756 let w = x_u[i] - x_l[i];
757 if has_box && w > 0.0 { w } else { 1.0 }
758 })
759 .collect();
760
761 let capture: SolveCapture = Rc::new(RefCell::new(None));
768 {
769 let cap = Rc::clone(&capture);
770 app.set_on_converged(Box::new(move |data, _cq, nlp, _pd| {
771 if let Some(curr) = data.borrow().curr.clone() {
772 let nlp_ref = nlp.borrow();
773 let x = nlp_ref.lift_x_to_full(&*curr.x);
774 let lambda = nlp_ref.finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
775 *cap.borrow_mut() = Some((x, lambda));
776 }
777 }));
778 }
779
780 let max_solves = cfg.max_solves.unwrap_or(8 * cfg.n_minima);
781 let batch = cfg.samples_per_round.unwrap_or(20).max(1);
786 let max_samples = max_solves.saturating_mul(batch);
787
788 println!(
789 "Searching for up to {} minima via `{}` (max {} solves, seed {})...",
790 cfg.n_minima,
791 cfg.method.as_str(),
792 max_solves,
793 cfg.seed
794 );
795
796 let mut driver = Driver {
797 app,
798 base: Rc::clone(base),
799 capture,
800 cfg,
801 n,
802 m,
803 nnz_h,
804 index_offset,
805 x0,
806 x_l: x_l.clone(),
807 x_u: x_u.clone(),
808 has_box,
809 l_scale: l_scale.clone(),
810 sampler: Sampler::new(cfg.seed, cfg.sobol),
811 archive: Archive::new(cfg.dedup, l_scale.clone()),
812 stagnant: 0,
813 n_solves: 0,
814 max_solves,
815 n_samples: 0,
816 max_samples,
817 psd_skipped_logged: false,
818 };
819
820 let stop = driver.run();
821 let n_solves = driver.n_solves;
822
823 let order = driver.archive.order_by_objective();
825 let minima: Vec<Minimum> = order
826 .iter()
827 .map(|&i| Minimum {
828 x: driver.archive.xs[i].clone(),
829 objective: driver.archive.fs[i],
830 lambda: driver.archive.ls[i].clone(),
831 })
832 .collect();
833 let best_obj = order.first().map(|&i| driver.archive.fs[i]);
834
835 print_table(&minima, &l_scale, stop, n_solves);
836
837 if let Some(sp) = sol_path {
840 write_sol_files(sp, &minima, m);
841 }
842
843 if let Some(json_path) = &args.json_output {
846 write_json_report(json_path, args, cfg, stop, n_solves, &minima, n, m, &info);
847 }
848
849 if best_obj.is_some() {
850 ExitCode::SUCCESS
851 } else {
852 ExitCode::from(1)
853 }
854}
855
856fn print_table(minima: &[Minimum], l_scale: &[Number], stop: Stop, n_solves: usize) {
858 println!();
859 println!(
860 "find-minima: {} distinct minim{} in {} solves ({})",
861 minima.len(),
862 if minima.len() == 1 { "um" } else { "a" },
863 n_solves,
864 stop.as_str()
865 );
866 if minima.is_empty() {
867 println!(" (no accepted minima — try raising --max-solves or --patience)");
868 return;
869 }
870 println!(" rank objective dist-to-best");
871 let best = &minima[0].x;
872 for (rank, mn) in minima.iter().enumerate() {
873 let d = scaled_distance(&mn.x, best, l_scale);
874 println!(" {rank:>4} {:>16.8e} {:>14.6e}", mn.objective, d);
875 }
876}
877
878fn write_sol_files(sol_path: &Path, minima: &[Minimum], m: usize) {
880 let zeros = vec![0.0; m];
881 for (rank, mn) in minima.iter().enumerate() {
882 let path = if rank == 0 {
883 sol_path.to_path_buf()
884 } else {
885 sibling_sol_path(sol_path, rank)
886 };
887 let message = format!(
888 "POUNCE {} find-minima rank {rank}: Solve_Succeeded",
889 env!("CARGO_PKG_VERSION")
890 );
891 let lambda = if mn.lambda.len() == m {
894 &mn.lambda
895 } else {
896 &zeros
897 };
898 let payload = crate::nl_writer::SolutionFile {
899 message: &message,
900 x: &mn.x,
901 mult_g: lambda,
902 solve_result_num: status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded),
903 suffixes: &[],
904 };
905 match crate::nl_writer::write_sol_file(&path, &payload) {
906 Ok(_) => eprintln!("pounce: wrote {}", path.display()),
907 Err(e) => eprintln!("pounce: failed to write {}: {e}", path.display()),
908 }
909 }
910}
911
912fn sibling_sol_path(sol_path: &Path, rank: usize) -> PathBuf {
914 let mut stub = sol_path.to_path_buf();
915 stub.set_extension(""); let base = stub.to_string_lossy().into_owned();
917 PathBuf::from(format!("{base}.min{rank:03}.sol"))
918}
919
920#[allow(clippy::too_many_arguments)]
923fn write_json_report(
924 json_path: &Path,
925 args: &Args,
926 cfg: &MinimaArgs,
927 stop: Stop,
928 n_solves: usize,
929 minima: &[Minimum],
930 n: usize,
931 m: usize,
932 info: &pounce_nlp::tnlp::NlpInfo,
933) {
934 let input = match &args.problem {
935 ProblemSource::Builtin(name) => InputDescriptor::Builtin { name: name.clone() },
936 ProblemSource::NlFile(p) => InputDescriptor::NlFile {
937 path: p.clone(),
938 size_bytes: std::fs::metadata(p).ok().map(|md| md.len()),
939 },
940 };
941 let mut builder = ReportBuilder::new(args.json_detail, input);
942 builder.problem.n_variables = n as Index;
943 builder.problem.n_constraints = m as Index;
944 builder.problem.n_objectives = 1;
945 builder.problem.nnz_jac_g = Some(info.nnz_jac_g);
946 builder.problem.nnz_h_lag = Some(info.nnz_h_lag);
947 if let Some(best) = minima.first() {
948 builder.solution.status = ApplicationReturnStatus::SolveSucceeded;
949 builder.solution.solve_result_num =
950 status_to_solve_result_num(ApplicationReturnStatus::SolveSucceeded);
951 builder.solution.objective = best.objective;
952 builder.solution.x = best.x.clone();
953 builder.solution.lambda = if best.lambda.len() == m {
956 best.lambda.clone()
957 } else {
958 vec![0.0; m]
959 };
960 }
961 let report = builder.finish();
962
963 let mut value = match serde_json::to_value(&report) {
966 Ok(v) => v,
967 Err(e) => {
968 eprintln!("pounce: failed to serialize minima report: {e}");
969 return;
970 }
971 };
972 let minima_json: Vec<serde_json::Value> = minima
973 .iter()
974 .map(|mn| {
975 serde_json::json!({
976 "x": mn.x,
977 "objective": mn.objective,
978 })
979 })
980 .collect();
981 let values: Vec<Number> = minima.iter().map(|mn| mn.objective).collect();
982 if let serde_json::Value::Object(map) = &mut value {
983 map.insert(
984 "minima".to_string(),
985 serde_json::json!({
986 "method": cfg.method.as_str(),
987 "status": stop.as_str(),
988 "n_solves": n_solves,
989 "n_minima": minima.len(),
990 "minima": minima_json,
991 "values": values,
992 }),
993 );
994 }
995 match serde_json::to_string_pretty(&value) {
996 Ok(s) => match std::fs::write(json_path, s) {
997 Ok(_) => eprintln!("pounce: wrote {}", json_path.display()),
998 Err(e) => eprintln!(
999 "pounce: failed to write JSON report to {}: {e}",
1000 json_path.display()
1001 ),
1002 },
1003 Err(e) => eprintln!("pounce: failed to render minima report: {e}"),
1004 }
1005}
1006
1007fn coord_in_bounds(xi: Number, lo: Number, hi: Number) -> bool {
1013 const ATOL: Number = 1e-9;
1014 const RTOL: Number = 1e-6;
1015 let tol_lo = ATOL + RTOL * lo.abs().max(1.0);
1016 let tol_hi = ATOL + RTOL * hi.abs().max(1.0);
1017 xi >= lo - tol_lo && xi <= hi + tol_hi
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::coord_in_bounds;
1023
1024 #[test]
1025 fn accepts_interior_point() {
1026 assert!(coord_in_bounds(0.0, -1.0, 1.0));
1027 assert!(coord_in_bounds(250.0, 0.0, 500.0));
1028 }
1029
1030 #[test]
1031 fn accepts_bound_relaxed_point_at_large_magnitude() {
1032 assert!(coord_in_bounds(500.000005, 0.0, 500.0));
1035 assert!(coord_in_bounds(-500.000005, -500.0, 0.0));
1036 }
1037
1038 #[test]
1039 fn rejects_genuinely_outside_point() {
1040 assert!(!coord_in_bounds(500.1, 0.0, 500.0));
1041 assert!(!coord_in_bounds(-1.1, -1.0, 1.0));
1042 }
1043}