1use pounce_common::options_list::OptionsList;
163use pounce_common::types::{Index, Number};
164use pounce_linsol::{ESymSolverStatus, SparseSymLinearSolverInterface};
165use pounce_nlp::SolveStatistics;
166use pounce_nlp::alg_types::SolverReturn;
167use pounce_nlp::constant_derivatives::DerivativeProof;
168use pounce_nlp::tnlp::{
169 BoundsInfo, IndexStyle, IpoptCq, IpoptData, ScalingRequest, Solution, SparsityRequest,
170 StartingPoint, TNLP,
171};
172use std::cell::RefCell;
173use std::rc::Rc;
174
175pub const SQUARE_SYSTEM_RUNG_LABEL: &str = "square_system_lm";
178
179pub const MAX_LAMBDA_TRIALS: usize = 11;
183
184pub const LM_ITERATION_CAP: usize = 200;
187
188const LAMBDA_FLOOR: Number = 1e-16;
192
193pub const STATIONARITY_TOL: Number = 1e-12;
202
203pub trait ResidualSystem {
205 fn eval_c(&mut self, x: &[Number], c: &mut [Number]) -> bool;
207 fn eval_jac(&mut self, x: &[Number], vals: &mut [Number]) -> bool;
209}
210
211#[derive(Debug, Clone)]
213pub struct LmProblem {
214 pub n: usize,
215 pub m: usize,
216 pub jac_rows: Vec<usize>,
218 pub jac_cols: Vec<usize>,
219 pub lb: Vec<Number>,
221 pub ub: Vec<Number>,
222}
223
224#[derive(Debug, Clone)]
226pub struct FeasibilityTest {
227 pub row_scale: Vec<Number>,
229 pub tol: Number,
231 pub constr_viol_tol: Number,
233}
234
235impl FeasibilityTest {
236 pub fn violations(&self, c: &[Number]) -> (Number, Number) {
238 let mut scaled: Number = 0.0;
239 let mut unscaled: Number = 0.0;
240 for (i, &ci) in c.iter().enumerate() {
241 let a = ci.abs();
242 if !a.is_finite() {
244 return (Number::INFINITY, Number::INFINITY);
245 }
246 unscaled = unscaled.max(a);
247 scaled = scaled.max(self.row_scale.get(i).copied().unwrap_or(1.0) * a);
248 }
249 (scaled, unscaled)
250 }
251
252 pub fn passes(&self, c: &[Number]) -> bool {
253 let (s, u) = self.violations(c);
254 s <= self.tol && u <= self.constr_viol_tol
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum LmStatus {
261 Converged,
263 IterationLimit,
265 Stationary,
270 TrustRegionCollapsed,
273 EvaluationFailed,
276 LinearSolverFailed,
278 CpuTimeExceeded,
280 WallTimeExceeded,
282 UserRequestedStop,
284}
285
286#[derive(Debug, Clone)]
288pub struct LmResult {
289 pub status: LmStatus,
290 pub x: Vec<Number>,
292 pub c: Vec<Number>,
294 pub iterations: usize,
295 pub residual_evals: usize,
296 pub jacobian_evals: usize,
297 pub factorizations: usize,
298}
299
300struct Augmented {
302 slot: Vec<usize>,
304 uniq: Vec<(usize, usize)>,
306 irn: Vec<Index>,
309 jcn: Vec<Index>,
310}
311
312impl Augmented {
313 fn new(p: &LmProblem) -> Self {
314 let mut index = std::collections::HashMap::with_capacity(p.jac_rows.len());
315 let mut uniq = Vec::new();
316 let mut slot = Vec::with_capacity(p.jac_rows.len());
317 for (&i, &j) in p.jac_rows.iter().zip(&p.jac_cols) {
318 let next = uniq.len();
319 let s = *index.entry((i, j)).or_insert(next);
320 if s == next {
321 uniq.push((i, j));
322 }
323 slot.push(s);
324 }
325 let nnz = p.n + uniq.len() + p.m;
326 let mut irn = Vec::with_capacity(nnz);
327 let mut jcn = Vec::with_capacity(nnz);
328 for j in 0..p.n {
329 irn.push((j + 1) as Index);
330 jcn.push((j + 1) as Index);
331 }
332 for &(i, j) in &uniq {
333 irn.push((p.n + i + 1) as Index);
334 jcn.push((j + 1) as Index);
335 }
336 for i in 0..p.m {
337 irn.push((p.n + i + 1) as Index);
338 jcn.push((p.n + i + 1) as Index);
339 }
340 Self {
341 slot,
342 uniq,
343 irn,
344 jcn,
345 }
346 }
347
348 fn merge(&self, raw: &[Number], out: &mut [Number]) {
349 out.iter_mut().for_each(|v| *v = 0.0);
350 for (k, &v) in raw.iter().enumerate() {
351 out[self.slot[k]] += v;
352 }
353 }
354}
355
356fn norm2(v: &[Number]) -> Number {
357 v.iter().map(|a| a * a).sum::<Number>().sqrt()
358}
359
360fn project(x: &mut [Number], lb: &[Number], ub: &[Number]) {
361 for j in 0..x.len() {
362 if x[j] < lb[j] {
363 x[j] = lb[j];
364 }
365 if x[j] > ub[j] {
366 x[j] = ub[j];
367 }
368 }
369}
370
371struct StepSolver {
374 backend: Box<dyn SparseSymLinearSolverInterface>,
375 aug: Augmented,
376 n: usize,
377 m: usize,
378 a: Vec<Number>,
380 factorizations: usize,
381}
382
383impl StepSolver {
384 fn solve(&mut self, lambda: Number, c: &[Number]) -> Option<(Vec<Number>, Vec<Number>)> {
387 let (n, m) = (self.n, self.m);
388 {
389 let vals = self.backend.values_array_mut();
390 for v in vals.iter_mut().take(n) {
391 *v = lambda;
392 }
393 vals[n..n + self.a.len()].copy_from_slice(&self.a);
394 for v in vals.iter_mut().skip(n + self.a.len()) {
395 *v = -1.0;
396 }
397 }
398 let mut rhs = vec![0.0; n + m];
399 for i in 0..m {
400 rhs[n + i] = -c[i];
401 }
402 self.factorizations += 1;
403 let st =
404 self.backend
405 .multi_solve(true, &self.aug.irn, &self.aug.jcn, 1, &mut rhs, false, 0);
406 if st != ESymSolverStatus::Success || rhs.iter().any(|v| !v.is_finite()) {
407 return None;
408 }
409 let p: Vec<Number> = rhs[..n].to_vec();
410 let mut rhs2 = vec![0.0; n + m];
411 rhs2[..n].copy_from_slice(&p);
412 let st =
413 self.backend
414 .multi_solve(false, &self.aug.irn, &self.aug.jcn, 1, &mut rhs2, false, 0);
415 if st != ESymSolverStatus::Success || rhs2.iter().any(|v| !v.is_finite()) {
416 return None;
417 }
418 Some((p, rhs2[..n].to_vec()))
419 }
420
421 fn scaled_gradient_norm(&self, c: &[Number]) -> Number {
424 let mut g = vec![0.0; self.n];
425 for (k, &(i, j)) in self.aug.uniq.iter().enumerate() {
426 g[j] += self.a[k] * c[i];
427 }
428 norm2(&g)
429 }
430
431 fn is_stationary(&self, c: &[Number], x: &[Number], lb: &[Number], ub: &[Number]) -> bool {
434 let cn = norm2(c);
435 if cn.is_nan() || cn <= 0.0 {
437 return false;
438 }
439 let mut g = vec![0.0; self.n];
440 for (k, &(i, j)) in self.aug.uniq.iter().enumerate() {
441 g[j] += self.a[k] * c[i];
442 }
443 g.iter().enumerate().all(|(j, &gj)| {
444 let blocked = (gj > 0.0 && x[j] <= lb[j]) || (gj < 0.0 && x[j] >= ub[j]);
447 blocked || gj.abs() <= STATIONARITY_TOL * cn
448 })
449 }
450
451 fn trust_region_step(
454 &mut self,
455 c: &[Number],
456 delta: Number,
457 lambda_prev: Number,
458 ) -> Option<(Vec<Number>, Number)> {
459 let mut lo: Number = 0.0;
461 if let Some((p, _)) = self.solve(LAMBDA_FLOOR, c)
462 && norm2(&p) <= 1.1 * delta
463 {
464 return Some((p, 0.0));
465 }
466 let gnorm = self.scaled_gradient_norm(c);
467 if !(gnorm.is_finite() && gnorm > 0.0) {
468 return None;
469 }
470 let mut hi = gnorm / delta;
471 let mut lambda = if lambda_prev > LAMBDA_FLOOR {
472 lambda_prev.clamp(LAMBDA_FLOOR, hi)
473 } else {
474 (1e-3 * hi).max(LAMBDA_FLOOR)
475 };
476 let mut best: Option<(Vec<Number>, Number)> = None;
477 for _ in 0..MAX_LAMBDA_TRIALS - 1 {
478 let Some((p, u)) = self.solve(lambda, c) else {
479 lo = lambda;
482 lambda = (10.0 * lambda).max(1e-12);
483 continue;
484 };
485 let pn = norm2(&p);
486 let fits = (pn - delta).abs() <= 0.1 * delta;
487 if pn <= 1.1 * delta {
490 best = Some((p.clone(), lambda));
491 }
492 if fits {
493 return Some((p, lambda));
494 }
495 if pn > delta {
496 lo = lo.max(lambda);
497 } else {
498 hi = hi.min(lambda);
499 }
500 let pu: Number = p.iter().zip(&u).map(|(a, b)| a * b).sum();
501 let newton = if pu > 0.0 {
502 lambda + (pn * pn / pu) * ((pn - delta) / delta)
503 } else {
504 Number::NAN
505 };
506 lambda = if newton.is_finite() && newton > lo && newton < hi {
507 newton
508 } else {
509 (lo * hi).sqrt().max(1e-3 * hi).max(LAMBDA_FLOOR)
511 };
512 }
513 if best.is_none() {
514 if let Some((p, _)) = self.solve(hi, c) {
517 best = Some((p, hi));
518 }
519 }
520 best
521 }
522}
523
524#[derive(Debug)]
526pub struct LmIterate<'a> {
527 pub iter: usize,
531 pub x: &'a [Number],
532 pub c: &'a [Number],
533 pub step_inf_norm: Number,
536 pub lambda: Number,
539 pub accepted: bool,
541}
542
543#[derive(Default)]
546pub struct LmHooks<'a> {
547 pub deadline: Option<pounce_common::timing::Deadline>,
551 pub on_iterate: Option<&'a mut dyn FnMut(&LmIterate<'_>) -> bool>,
557}
558
559impl LmHooks<'_> {
560 fn fire(&mut self, it: &LmIterate<'_>) -> bool {
561 match self.on_iterate.as_mut() {
562 Some(f) => f(it),
563 None => true,
564 }
565 }
566
567 fn time_exceeded(&self) -> Option<LmStatus> {
568 use pounce_common::timing::DeadlineKind;
569 match self.deadline.as_ref().and_then(|d| d.exceeded()) {
570 Some(DeadlineKind::Cpu) => Some(LmStatus::CpuTimeExceeded),
571 Some(DeadlineKind::Wall) => Some(LmStatus::WallTimeExceeded),
572 None => None,
573 }
574 }
575}
576
577pub fn bounded_levenberg_marquardt(
580 model: &mut dyn ResidualSystem,
581 prob: &LmProblem,
582 x0: &[Number],
583 test: &FeasibilityTest,
584 max_iter: usize,
585 mut backend: Box<dyn SparseSymLinearSolverInterface>,
586 mut hooks: LmHooks<'_>,
587) -> LmResult {
588 let (n, m) = (prob.n, prob.m);
589 let mut x = x0.to_vec();
590 project(&mut x, &prob.lb, &prob.ub);
591 let mut c = vec![0.0; m];
592 let mut out = LmResult {
593 status: LmStatus::EvaluationFailed,
594 x: x.clone(),
595 c: c.clone(),
596 iterations: 0,
597 residual_evals: 0,
598 jacobian_evals: 0,
599 factorizations: 0,
600 };
601 out.residual_evals += 1;
602 if !model.eval_c(&x, &mut c) || c.iter().any(|v| !v.is_finite()) {
603 return out;
604 }
605 out.c = c.clone();
606 let start = LmIterate {
609 iter: 0,
610 x: &x,
611 c: &c,
612 step_inf_norm: 0.0,
613 lambda: 0.0,
614 accepted: false,
615 };
616 if !hooks.fire(&start) {
617 out.status = LmStatus::UserRequestedStop;
618 return out;
619 }
620 if test.passes(&c) {
621 out.status = LmStatus::Converged;
622 return out;
623 }
624 if let Some(st) = hooks.time_exceeded() {
625 out.status = st;
626 return out;
627 }
628
629 let aug = Augmented::new(prob);
630 let nnz = aug.irn.len();
631 if backend.initialize_structure((n + m) as Index, nnz as Index, &aug.irn, &aug.jcn)
632 != ESymSolverStatus::Success
633 {
634 out.status = LmStatus::LinearSolverFailed;
635 return out;
636 }
637 let n_uniq = aug.uniq.len();
638 let mut solver = StepSolver {
639 backend,
640 aug,
641 n,
642 m,
643 a: vec![0.0; n_uniq],
644 factorizations: 0,
645 };
646
647 let mut raw = vec![0.0; prob.jac_rows.len()];
648 let mut jac = vec![0.0; n_uniq];
649 let mut d = vec![0.0; n];
650 let refresh = |model: &mut dyn ResidualSystem,
653 x: &[Number],
654 raw: &mut [Number],
655 jac: &mut [Number],
656 d: &mut [Number],
657 solver: &mut StepSolver|
658 -> bool {
659 if !model.eval_jac(x, raw) || raw.iter().any(|v| !v.is_finite()) {
660 return false;
661 }
662 solver.aug.merge(raw, jac);
663 let mut col = vec![0.0; d.len()];
664 for (k, &(_, j)) in solver.aug.uniq.iter().enumerate() {
665 col[j] += jac[k] * jac[k];
666 }
667 for j in 0..d.len() {
668 d[j] = d[j].max(col[j].sqrt());
669 }
670 for (k, &(_, j)) in solver.aug.uniq.iter().enumerate() {
671 let dj = if d[j] > 0.0 { d[j] } else { 1.0 };
672 solver.a[k] = jac[k] / dj;
673 }
674 true
675 };
676 out.jacobian_evals += 1;
677 if !refresh(model, &x, &mut raw, &mut jac, &mut d, &mut solver) {
678 out.factorizations = solver.factorizations;
679 return out;
680 }
681 let dscale = |d: &[Number], j: usize| if d[j] > 0.0 { d[j] } else { 1.0 };
682 let scaled_norm = |d: &[Number], v: &[Number]| -> Number {
683 v.iter()
684 .enumerate()
685 .map(|(j, &vj)| (dscale(d, j) * vj).powi(2))
686 .sum::<Number>()
687 .sqrt()
688 };
689 let xnorm = scaled_norm(&d, &x);
691 let mut delta = if xnorm > 0.0 { 100.0 * xnorm } else { 100.0 };
692 let mut lambda: Number = 0.0;
693 let mut ct = vec![0.0; m];
694 let status = loop {
695 let iter = out.iterations;
697 if iter >= max_iter {
698 break LmStatus::IterationLimit;
699 }
700 if solver.is_stationary(&c, &x, &prob.lb, &prob.ub) {
701 break LmStatus::Stationary;
702 }
703 out.iterations = iter + 1;
704 let Some((pt, lam)) = solver.trust_region_step(&c, delta, lambda) else {
705 break LmStatus::LinearSolverFailed;
706 };
707 lambda = lam;
708 let mut xt: Vec<Number> = (0..n).map(|j| x[j] + pt[j] / dscale(&d, j)).collect();
709 project(&mut xt, &prob.lb, &prob.ub);
710 let s: Vec<Number> = (0..n).map(|j| xt[j] - x[j]).collect();
711 let snorm = scaled_norm(&d, &s);
712
713 out.residual_evals += 1;
714 let ok = model.eval_c(&xt, &mut ct) && ct.iter().all(|v| v.is_finite());
715 let rho = if ok {
716 let phi: Number = c.iter().map(|v| v * v).sum();
717 let phit: Number = ct.iter().map(|v| v * v).sum();
718 let mut lin = c.clone();
720 for (k, &(i, j)) in solver.aug.uniq.iter().enumerate() {
721 lin[i] += jac[k] * s[j];
722 }
723 let pred = phi - lin.iter().map(|v| v * v).sum::<Number>();
724 if pred > 0.0 {
725 (phi - phit) / pred
726 } else {
727 Number::NEG_INFINITY
728 }
729 } else {
730 Number::NEG_INFINITY
731 };
732
733 if !ok {
734 delta *= 0.25;
735 } else if rho < 0.25 {
736 delta = 0.5 * if snorm > 0.0 { delta.min(snorm) } else { delta };
737 } else if rho > 0.75 || lambda == 0.0 {
738 delta = delta.max(2.0 * snorm);
739 }
740
741 let accepted = rho >= 1e-4;
742 if accepted {
743 x = xt;
744 c.copy_from_slice(&ct);
745 }
746 let here = LmIterate {
747 iter: out.iterations,
748 x: &x,
749 c: &c,
750 step_inf_norm: if accepted {
751 s.iter().fold(0.0, |a: Number, v| a.max(v.abs()))
752 } else {
753 0.0
754 },
755 lambda,
756 accepted,
757 };
758 if !hooks.fire(&here) {
759 break LmStatus::UserRequestedStop;
760 }
761 if accepted && test.passes(&c) {
762 break LmStatus::Converged;
763 }
764 if let Some(st) = hooks.time_exceeded() {
765 break st;
766 }
767 if accepted {
768 out.jacobian_evals += 1;
769 if !refresh(model, &x, &mut raw, &mut jac, &mut d, &mut solver) {
770 break LmStatus::EvaluationFailed;
771 }
772 }
773 let floor = 1e-15 * scaled_norm(&d, &x).max(1.0);
774 if delta.is_nan() || delta <= floor {
776 break LmStatus::TrustRegionCollapsed;
777 }
778 };
779 out.status = status;
780 out.x = x;
781 out.c = c;
782 out.factorizations = solver.factorizations;
783 out
784}
785
786#[derive(Debug, Clone, PartialEq)]
788pub enum SquareSystemDecline {
789 Disabled,
791 Unavailable,
793 NotEqualityConstrained,
795 NotSquare { free: usize, rows: usize },
797 ObjectiveNotConstant,
799}
800
801#[derive(Debug, Clone)]
804pub struct SquareSystemPlan {
805 n_full: usize,
806 m: usize,
807 free: Vec<usize>,
809 x_full: Vec<Number>,
812 x_scaling: Vec<Number>,
818 jac_row_full: Vec<usize>,
821 g_rhs: Vec<Number>,
822 kept: Vec<(usize, usize, usize)>,
825 nnz_full: usize,
826 lb: Vec<Number>,
827 ub: Vec<Number>,
828}
829
830fn opt_num(options: &OptionsList, tag: &str, default: Number) -> Number {
836 options
837 .get_numeric_value(tag, "")
838 .ok()
839 .and_then(|(v, found)| found.then_some(v))
840 .unwrap_or(default)
841}
842
843pub fn is_enabled(options: &OptionsList) -> bool {
846 options
847 .get_bool_value("square_system_recovery", "")
848 .map(|(v, _found)| v)
849 .unwrap_or(true)
850}
851
852pub fn probe(
857 tnlp: &Rc<RefCell<dyn TNLP>>,
858 options: &OptionsList,
859) -> Result<SquareSystemPlan, SquareSystemDecline> {
860 if !is_enabled(options) {
861 return Err(SquareSystemDecline::Disabled);
862 }
863 let lo_inf = opt_num(options, "nlp_lower_bound_inf", -1e19);
864 let up_inf = opt_num(options, "nlp_upper_bound_inf", 1e19);
865 let mut t = tnlp.borrow_mut();
866 let info = t.get_nlp_info().ok_or(SquareSystemDecline::Unavailable)?;
867 let n = info.n.max(0) as usize;
868 let m = info.m.max(0) as usize;
869 if m == 0 {
870 return Err(SquareSystemDecline::NotEqualityConstrained);
871 }
872 let mut x_l = vec![0.0; n];
873 let mut x_u = vec![0.0; n];
874 let mut g_l = vec![0.0; m];
875 let mut g_u = vec![0.0; m];
876 if !t.get_bounds_info(BoundsInfo {
877 x_l: &mut x_l,
878 x_u: &mut x_u,
879 g_l: &mut g_l,
880 g_u: &mut g_u,
881 }) {
882 return Err(SquareSystemDecline::Unavailable);
883 }
884 if (0..m).any(|i| !(g_l[i].is_finite() && g_l[i] == g_u[i])) {
885 return Err(SquareSystemDecline::NotEqualityConstrained);
886 }
887 let lb: Vec<Number> = x_l
888 .iter()
889 .map(|&v| if v <= lo_inf { Number::NEG_INFINITY } else { v })
890 .collect();
891 let ub: Vec<Number> = x_u
892 .iter()
893 .map(|&v| if v >= up_inf { Number::INFINITY } else { v })
894 .collect();
895 if (0..n).any(|j| lb[j] > ub[j]) {
896 return Err(SquareSystemDecline::Unavailable);
897 }
898 let free: Vec<usize> = (0..n).filter(|&j| lb[j] < ub[j]).collect();
899 if free.len() != m {
900 return Err(SquareSystemDecline::NotSquare {
901 free: free.len(),
902 rows: m,
903 });
904 }
905 if t.derivative_proofs().grad_f == DerivativeProof::Varying {
908 return Err(SquareSystemDecline::ObjectiveNotConstant);
909 }
910 let mut x = vec![0.0; n];
911 let mut z_l = vec![0.0; n];
912 let mut z_u = vec![0.0; n];
913 let mut lam = vec![0.0; m];
914 if !t.get_starting_point(StartingPoint {
915 init_x: true,
916 x: &mut x,
917 init_z: false,
918 z_l: &mut z_l,
919 z_u: &mut z_u,
920 init_lambda: false,
921 lambda: &mut lam,
922 }) || x.iter().any(|v| !v.is_finite())
923 {
924 return Err(SquareSystemDecline::Unavailable);
925 }
926 for j in 0..n {
927 if lb[j] == ub[j] {
928 x[j] = lb[j];
929 }
930 }
931 let x_scaling = x.clone();
932 project(&mut x, &lb, &ub);
933 let mut grad = vec![0.0; n];
934 if !t.eval_grad_f(&x, true, &mut grad) {
935 return Err(SquareSystemDecline::Unavailable);
936 }
937 if grad.iter().any(|&v| v != 0.0) {
941 return Err(SquareSystemDecline::ObjectiveNotConstant);
942 }
943 let nnz = info.nnz_jac_g.max(0) as usize;
944 let mut irow = vec![0 as Index; nnz];
945 let mut jcol = vec![0 as Index; nnz];
946 if !t.eval_jac_g(
947 None,
948 false,
949 SparsityRequest::Structure {
950 irow: &mut irow,
951 jcol: &mut jcol,
952 },
953 ) {
954 return Err(SquareSystemDecline::Unavailable);
955 }
956 let index_base: Index = match info.index_style {
957 IndexStyle::Fortran => 1,
958 _ => 0,
959 };
960 let mut free_of = vec![usize::MAX; n];
961 for (k, &j) in free.iter().enumerate() {
962 free_of[j] = k;
963 }
964 let mut kept = Vec::with_capacity(nnz);
965 let mut jac_row_full = Vec::with_capacity(nnz);
966 for k in 0..nnz {
967 let i = (irow[k] - index_base) as usize;
968 let j = (jcol[k] - index_base) as usize;
969 if i >= m || j >= n {
970 return Err(SquareSystemDecline::Unavailable);
971 }
972 jac_row_full.push(i);
973 if free_of[j] != usize::MAX {
974 kept.push((k, i, free_of[j]));
975 }
976 }
977 Ok(SquareSystemPlan {
978 n_full: n,
979 m,
980 lb: free.iter().map(|&j| lb[j]).collect(),
981 ub: free.iter().map(|&j| ub[j]).collect(),
982 free,
983 x_full: x,
984 x_scaling,
985 jac_row_full,
986 g_rhs: g_l,
987 kept,
988 nnz_full: nnz,
989 })
990}
991
992struct TnlpResidual<'a> {
994 tnlp: &'a Rc<RefCell<dyn TNLP>>,
995 plan: &'a SquareSystemPlan,
996 x_full: Vec<Number>,
997 g: Vec<Number>,
998 jac_full: Vec<Number>,
999}
1000
1001impl TnlpResidual<'_> {
1002 fn scatter(&mut self, x: &[Number]) {
1003 for (k, &j) in self.plan.free.iter().enumerate() {
1004 self.x_full[j] = x[k];
1005 }
1006 }
1007}
1008
1009impl ResidualSystem for TnlpResidual<'_> {
1010 fn eval_c(&mut self, x: &[Number], c: &mut [Number]) -> bool {
1011 self.scatter(x);
1012 if !self
1013 .tnlp
1014 .borrow_mut()
1015 .eval_g(&self.x_full, true, &mut self.g)
1016 {
1017 return false;
1018 }
1019 for ((ci, gi), ri) in c.iter_mut().zip(&self.g).zip(&self.plan.g_rhs) {
1020 *ci = gi - ri;
1021 }
1022 true
1023 }
1024
1025 fn eval_jac(&mut self, x: &[Number], vals: &mut [Number]) -> bool {
1026 self.scatter(x);
1027 if !self.tnlp.borrow_mut().eval_jac_g(
1028 Some(&self.x_full),
1029 true,
1030 SparsityRequest::Values {
1031 values: &mut self.jac_full,
1032 },
1033 ) {
1034 return false;
1035 }
1036 for (q, &(k, _, _)) in self.plan.kept.iter().enumerate() {
1037 vals[q] = self.jac_full[k];
1038 }
1039 true
1040 }
1041}
1042
1043struct IpmScaling {
1046 row_scale: Vec<Number>,
1048 computed_obj_scale: Number,
1051 total_obj_scale: Number,
1054 start_obj_grad_max: Number,
1057 evals: (usize, usize),
1059}
1060
1061fn ipm_scaling(
1089 tnlp: &Rc<RefCell<dyn TNLP>>,
1090 options: &OptionsList,
1091 plan: &SquareSystemPlan,
1092) -> IpmScaling {
1093 use pounce_nlp::orig_ipopt_nlp::{
1094 gradient_obj_scale, gradient_row_scale, gradient_scaling_fires,
1095 };
1096 let m = plan.m;
1097 let user_obj = opt_num(options, "obj_scaling_factor", 1.0);
1098 let min_value = opt_num(options, "nlp_scaling_min_value", 1e-8);
1099 let unscaled = IpmScaling {
1100 row_scale: vec![1.0; m],
1101 computed_obj_scale: 1.0,
1102 total_obj_scale: user_obj,
1103 start_obj_grad_max: Number::NAN,
1104 evals: (0, 0),
1105 };
1106 let method = options
1107 .get_string_value("nlp_scaling_method", "")
1108 .ok()
1109 .and_then(|(v, found)| found.then_some(v))
1110 .unwrap_or_else(|| "gradient-based".to_string());
1111 match method.as_str() {
1112 "none" => unscaled,
1113 "user-scaling" | "curvature-based" => {
1114 let mut obj = 1.0;
1115 let mut use_x = false;
1116 let mut xs = vec![1.0; plan.n_full];
1117 let mut use_g = false;
1118 let mut gs = vec![1.0; m];
1119 let ok = tnlp.borrow_mut().get_scaling_parameters(ScalingRequest {
1120 obj_scaling: &mut obj,
1121 use_x_scaling: &mut use_x,
1122 x_scaling: &mut xs,
1123 use_g_scaling: &mut use_g,
1124 g_scaling: &mut gs,
1125 });
1126 if !ok {
1127 return unscaled;
1128 }
1129 let df = if obj.abs() < min_value {
1130 min_value
1131 } else {
1132 obj
1133 };
1134 IpmScaling {
1135 row_scale: if use_g {
1136 gs.iter()
1137 .map(|&v| if v < min_value { min_value } else { v })
1138 .collect()
1139 } else {
1140 vec![1.0; m]
1141 },
1142 computed_obj_scale: 1.0,
1143 total_obj_scale: df * user_obj,
1144 start_obj_grad_max: Number::NAN,
1145 evals: (0, 0),
1146 }
1147 }
1148 _ => {
1149 let max_grad = opt_num(options, "nlp_scaling_max_gradient", 100.0);
1150 let obj_target = opt_num(options, "nlp_scaling_obj_target_gradient", 0.0);
1151 let constr_target = opt_num(options, "nlp_scaling_constr_target_gradient", 0.0);
1152 let x = &plan.x_scaling;
1153 let mut grad = vec![0.0; plan.n_full];
1154 let mut df = 1.0;
1155 let mut grad_max = Number::NAN;
1156 if tnlp.borrow_mut().eval_grad_f(x, true, &mut grad) {
1157 let gm = plan
1158 .free
1159 .iter()
1160 .fold(0.0 as Number, |a, &j| a.max(grad[j].abs()));
1161 grad_max = gm;
1162 if gm.is_finite() {
1163 df = gradient_obj_scale(gm, max_grad, min_value, obj_target);
1164 }
1165 }
1166 let mut vals = vec![0.0; plan.nnz_full];
1167 let row_scale = if tnlp.borrow_mut().eval_jac_g(
1168 Some(x),
1169 true,
1170 SparsityRequest::Values { values: &mut vals },
1171 ) {
1172 let mut row_max = vec![Number::MIN_POSITIVE; m];
1173 for (k, &i) in plan.jac_row_full.iter().enumerate() {
1174 row_max[i] = row_max[i].max(vals[k].abs());
1175 }
1176 if gradient_scaling_fires(&row_max, max_grad, constr_target) {
1177 row_max
1178 .iter()
1179 .map(|&g| gradient_row_scale(g, max_grad, min_value, constr_target))
1180 .collect()
1181 } else {
1182 vec![1.0; m]
1183 }
1184 } else {
1185 vec![1.0; m]
1186 };
1187 IpmScaling {
1188 row_scale,
1189 computed_obj_scale: df,
1190 total_obj_scale: df * user_obj,
1191 start_obj_grad_max: grad_max,
1192 evals: (1, 1),
1193 }
1194 }
1195 }
1196}
1197
1198#[derive(Debug, Clone)]
1200pub struct SquareSystemOutcome {
1201 pub lm: LmResult,
1202 pub accepted: bool,
1205 pub reason: String,
1207 pub statistics: SolveStatistics,
1209 pub violation: (Number, Number),
1211}
1212
1213#[derive(Debug, Clone, Copy, Default)]
1215pub struct RunOptions {
1216 pub capture_iterations: bool,
1222}
1223
1224pub fn run(
1237 plan: &SquareSystemPlan,
1238 tnlp: &Rc<RefCell<dyn TNLP>>,
1239 options: &OptionsList,
1240 backend: Box<dyn SparseSymLinearSolverInterface>,
1241 run_opts: RunOptions,
1242) -> SquareSystemOutcome {
1243 use pounce_nlp::solve_statistics::IterRecord;
1244 let wall0 = pounce_common::utils::wallclock_time();
1245 let cpu0 = pounce_common::utils::cpu_time();
1246 let deadline = pounce_common::timing::Deadline::new(
1247 opt_num(options, "max_wall_time", 1e6),
1248 opt_num(options, "max_cpu_time", 1e6),
1249 );
1250 let max_iter = options
1251 .get_integer_value("max_iter", "")
1252 .ok()
1253 .and_then(|(v, found)| found.then_some(v.max(0) as usize))
1254 .unwrap_or(3000)
1255 .min(LM_ITERATION_CAP);
1256 let prob = LmProblem {
1257 n: plan.free.len(),
1258 m: plan.m,
1259 jac_rows: plan.kept.iter().map(|&(_, i, _)| i).collect(),
1260 jac_cols: plan.kept.iter().map(|&(_, _, j)| j).collect(),
1261 lb: plan.lb.clone(),
1262 ub: plan.ub.clone(),
1263 };
1264 let x0: Vec<Number> = plan.free.iter().map(|&j| plan.x_full[j]).collect();
1265 let mut model = TnlpResidual {
1266 tnlp,
1267 plan,
1268 x_full: plan.x_full.clone(),
1269 g: vec![0.0; plan.m],
1270 jac_full: vec![0.0; plan.nnz_full],
1271 };
1272 let fail = |lm: LmResult, reason: String| SquareSystemOutcome {
1273 violation: (Number::NAN, Number::NAN),
1274 lm,
1275 accepted: false,
1276 reason,
1277 statistics: SolveStatistics::default(),
1278 };
1279 let scaling = ipm_scaling(tnlp, options, plan);
1280 let test = FeasibilityTest {
1281 row_scale: scaling.row_scale.clone(),
1282 tol: opt_num(options, "tol", 1e-8),
1283 constr_viol_tol: opt_num(options, "constr_viol_tol", 1e-4),
1284 };
1285
1286 let wants_callback = tnlp.borrow().wants_intermediate_callback();
1291 let mut records: Vec<IterRecord> = Vec::new();
1292 let mut extra_f_evals = 0usize;
1293 let mut extra_grad_evals = 0usize;
1294 let mut observe = |it: &LmIterate<'_>| -> bool {
1295 if !(wants_callback || run_opts.capture_iterations) {
1296 return true;
1297 }
1298 let mut xf = plan.x_full.clone();
1299 for (k, &j) in plan.free.iter().enumerate() {
1300 xf[j] = it.x[k];
1301 }
1302 extra_f_evals += 1;
1303 let obj = tnlp.borrow_mut().eval_f(&xf, true).unwrap_or(Number::NAN);
1304 let mut grad = vec![0.0; plan.n_full];
1305 extra_grad_evals += 1;
1306 let inf_du = if tnlp.borrow_mut().eval_grad_f(&xf, false, &mut grad) {
1307 plan.free
1308 .iter()
1309 .fold(0.0 as Number, |a, &j| a.max(grad[j].abs()))
1310 } else {
1311 Number::NAN
1312 };
1313 let (scaled, unscaled) = test.violations(it.c);
1314 let alpha = if it.accepted { 1.0 } else { 0.0 };
1315 if run_opts.capture_iterations {
1316 records.push(IterRecord {
1317 iter: it.iter as Index,
1318 objective: obj,
1319 inf_pr: unscaled,
1320 inf_pr_internal: scaled,
1321 inf_du,
1322 mu: 0.0,
1323 d_norm: it.step_inf_norm,
1324 regularization: it.lambda,
1325 alpha_dual: 0.0,
1326 alpha_primal: alpha,
1327 alpha_primal_char: if it.iter == 0 { ' ' } else { 'L' },
1328 ls_trials: 0,
1329 phase: Default::default(),
1330 });
1331 }
1332 if wants_callback {
1333 return tnlp.borrow_mut().intermediate_callback(
1334 pounce_nlp::tnlp::IterStats {
1335 mode: pounce_nlp::AlgorithmMode::RegularMode,
1336 iter: it.iter as Index,
1337 obj_value: obj,
1338 inf_pr: scaled,
1339 inf_du,
1340 mu: 0.0,
1341 d_norm: it.step_inf_norm,
1342 regularization_size: it.lambda,
1343 alpha_du: 0.0,
1344 alpha_pr: alpha,
1345 ls_trials: 0,
1346 },
1347 &IpoptData::default(),
1348 &IpoptCq::default(),
1349 );
1350 }
1351 true
1352 };
1353 let lm = bounded_levenberg_marquardt(
1354 &mut model,
1355 &prob,
1356 &x0,
1357 &test,
1358 max_iter,
1359 backend,
1360 LmHooks {
1361 deadline: Some(deadline),
1362 on_iterate: Some(&mut observe),
1363 },
1364 );
1365 if lm.status != LmStatus::Converged {
1366 let reason = match lm.status {
1367 LmStatus::CpuTimeExceeded => format!(
1368 "max_cpu_time reached after {} iteration(s), before a root",
1369 lm.iterations
1370 ),
1371 LmStatus::WallTimeExceeded => format!(
1372 "max_wall_time reached after {} iteration(s), before a root",
1373 lm.iterations
1374 ),
1375 LmStatus::UserRequestedStop => format!(
1376 "the intermediate callback requested a stop after {} iteration(s)",
1377 lm.iterations
1378 ),
1379 st => format!("{st:?} after {} iteration(s)", lm.iterations),
1380 };
1381 return fail(lm, reason);
1382 }
1383
1384 model.scatter(&lm.x);
1387 let x_full = model.x_full.clone();
1388 let mut g = vec![0.0; plan.m];
1389 let mut c = vec![0.0; plan.m];
1390 if !tnlp.borrow_mut().eval_g(&x_full, true, &mut g) {
1391 return fail(lm, "eval_g failed at the returned point".into());
1392 }
1393 for i in 0..plan.m {
1394 c[i] = g[i] - plan.g_rhs[i];
1395 }
1396 if !test.passes(&c) {
1397 return fail(
1398 lm,
1399 "the returned point does not pass the feasibility test".into(),
1400 );
1401 }
1402 if (0..prob.n).any(|k| !(lm.x[k] >= prob.lb[k] && lm.x[k] <= prob.ub[k])) {
1403 return fail(lm, "the returned point is outside the variable box".into());
1404 }
1405 let mut grad = vec![0.0; plan.n_full];
1406 if !tnlp.borrow_mut().eval_grad_f(&x_full, false, &mut grad) || grad.iter().any(|&v| v != 0.0) {
1407 return fail(
1408 lm,
1409 "the objective gradient is nonzero at the returned point".into(),
1410 );
1411 }
1412 let Some(obj) = tnlp.borrow_mut().eval_f(&x_full, false) else {
1413 return fail(lm, "eval_f failed at the returned point".into());
1414 };
1415 if !obj.is_finite() {
1416 return fail(
1417 lm,
1418 "the objective is not finite at the returned point".into(),
1419 );
1420 }
1421
1422 let (scaled, unscaled) = test.violations(&c);
1423 let zeros_n = vec![0.0; plan.n_full];
1424 let zeros_m = vec![0.0; plan.m];
1425 tnlp.borrow_mut().finalize_solution(
1426 Solution {
1427 status: SolverReturn::Success,
1428 x: &x_full,
1429 z_l: &zeros_n,
1430 z_u: &zeros_n,
1431 g: &g,
1432 lambda: &zeros_m,
1433 obj_value: obj,
1434 },
1435 &IpoptData::default(),
1436 &IpoptCq::default(),
1437 );
1438
1439 pounce_observability::extend_active_capture(&records);
1442 let statistics = SolveStatistics {
1443 iteration_count: lm.iterations as Index,
1444 total_cpu_time_secs: pounce_common::utils::cpu_time() - cpu0,
1445 total_wallclock_time_secs: pounce_common::utils::wallclock_time() - wall0,
1446 num_obj_evals: (1 + extra_f_evals) as Index,
1450 num_constr_evals: (lm.residual_evals + 1) as Index,
1451 num_obj_grad_evals: (1 + extra_grad_evals + scaling.evals.0) as Index,
1452 num_constr_jac_evals: (lm.jacobian_evals + scaling.evals.1) as Index,
1453 final_objective: obj,
1454 final_scaled_objective: scaling.total_obj_scale * obj,
1456 final_obj_scaling_factor: scaling.computed_obj_scale,
1457 final_total_obj_scaling_factor: scaling.total_obj_scale.abs(),
1458 start_obj_grad_max: scaling.start_obj_grad_max,
1459 final_dual_inf: 0.0,
1460 final_constr_viol: scaled,
1461 final_compl: 0.0,
1462 final_kkt_error: scaled,
1463 final_unscaled_dual_inf: 0.0,
1464 final_unscaled_constr_viol: unscaled,
1465 final_unscaled_compl: 0.0,
1466 final_unscaled_kkt_error: unscaled,
1467 final_kkt_error_above_noise: scaled,
1468 final_declared_constr_viol: unscaled,
1469 final_declared_box_viol: 0.0,
1470 final_mu: 0.0,
1471 iterations: records,
1472 ..SolveStatistics::default()
1473 };
1474 SquareSystemOutcome {
1475 lm,
1476 accepted: true,
1477 reason: String::new(),
1478 statistics,
1479 violation: (scaled, unscaled),
1480 }
1481}
1482
1483pub fn default_backend(options: &OptionsList) -> Box<dyn SparseSymLinearSolverInterface> {
1488 Box::new(pounce_feral::FeralSolverInterface::with_config(
1489 crate::application::feral_config_from_options(options),
1490 ))
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496
1497 struct Sys<F, J>
1499 where
1500 F: FnMut(&[Number], &mut [Number]),
1501 J: FnMut(&[Number], &mut [Number]),
1502 {
1503 f: F,
1504 j: J,
1505 }
1506
1507 impl<F, J> ResidualSystem for Sys<F, J>
1508 where
1509 F: FnMut(&[Number], &mut [Number]),
1510 J: FnMut(&[Number], &mut [Number]),
1511 {
1512 fn eval_c(&mut self, x: &[Number], c: &mut [Number]) -> bool {
1513 (self.f)(x, c);
1514 true
1515 }
1516 fn eval_jac(&mut self, x: &[Number], v: &mut [Number]) -> bool {
1517 (self.j)(x, v);
1518 true
1519 }
1520 }
1521
1522 fn dense2(lb: [Number; 2], ub: [Number; 2]) -> LmProblem {
1523 LmProblem {
1524 n: 2,
1525 m: 2,
1526 jac_rows: vec![0, 0, 1, 1],
1527 jac_cols: vec![0, 1, 0, 1],
1528 lb: lb.to_vec(),
1529 ub: ub.to_vec(),
1530 }
1531 }
1532
1533 fn strict() -> FeasibilityTest {
1534 FeasibilityTest {
1535 row_scale: vec![1.0, 1.0],
1536 tol: 1e-10,
1537 constr_viol_tol: 1e-10,
1538 }
1539 }
1540
1541 fn backend() -> Box<dyn SparseSymLinearSolverInterface> {
1542 Box::new(pounce_feral::FeralSolverInterface::with_config(
1543 pounce_feral::FeralConfig::default(),
1544 ))
1545 }
1546
1547 #[test]
1553 fn converges_to_a_root_on_an_active_bound() {
1554 let prob = dense2([-10.0, -10.0], [1.0, 10.0]);
1555 let mut sys = Sys {
1556 f: |x: &[Number], c: &mut [Number]| {
1557 c[0] = x[0] * x[0] + x[1] * x[1] - 2.0;
1558 c[1] = x[0] - x[1];
1559 },
1560 j: |x: &[Number], v: &mut [Number]| {
1561 v.copy_from_slice(&[2.0 * x[0], 2.0 * x[1], 1.0, -1.0]);
1562 },
1563 };
1564 let r = bounded_levenberg_marquardt(
1565 &mut sys,
1566 &prob,
1567 &[0.2, 3.0],
1568 &strict(),
1569 200,
1570 backend(),
1571 LmHooks::default(),
1572 );
1573 assert_eq!(r.status, LmStatus::Converged, "{r:?}");
1574 assert!(r.x[0] <= 1.0, "x0 = {} left the box", r.x[0]);
1575 assert!(
1576 (r.x[0] - 1.0).abs() < 1e-9 && (r.x[1] - 1.0).abs() < 1e-9,
1577 "{:?}",
1578 r.x
1579 );
1580 }
1581
1582 #[test]
1587 fn a_root_outside_the_box_is_not_reported_as_converged() {
1588 let prob = dense2([-10.0, -10.0], [1.0, 10.0]);
1589 let mut sys = Sys {
1590 f: |x: &[Number], c: &mut [Number]| {
1591 c[0] = x[0] - 2.0;
1592 c[1] = x[1];
1593 },
1594 j: |_x: &[Number], v: &mut [Number]| v.copy_from_slice(&[1.0, 0.0, 0.0, 1.0]),
1595 };
1596 let r = bounded_levenberg_marquardt(
1597 &mut sys,
1598 &prob,
1599 &[0.0, 0.5],
1600 &strict(),
1601 200,
1602 backend(),
1603 LmHooks::default(),
1604 );
1605 assert_eq!(r.status, LmStatus::Stationary, "{r:?}");
1606 assert!(r.x[0] <= 1.0);
1607 assert!(
1608 (r.c[0] + 1.0).abs() < 1e-8,
1609 "should stall at the box's nearest point: {r:?}"
1610 );
1611 }
1612
1613 #[test]
1618 fn a_zero_jacobian_start_stops_on_stationarity_at_once() {
1619 let prob = dense2([Number::NEG_INFINITY; 2], [Number::INFINITY; 2]);
1620 let mut sys = Sys {
1621 f: |x: &[Number], c: &mut [Number]| {
1622 c[0] = x[0] * x[0] + x[1] * x[1] + 25.0;
1623 c[1] = x[0] * x[1] - 9.0;
1624 },
1625 j: |x: &[Number], v: &mut [Number]| {
1626 v.copy_from_slice(&[2.0 * x[0], 2.0 * x[1], x[1], x[0]]);
1627 },
1628 };
1629 let r = bounded_levenberg_marquardt(
1630 &mut sys,
1631 &prob,
1632 &[0.0, 0.0],
1633 &strict(),
1634 200,
1635 backend(),
1636 LmHooks::default(),
1637 );
1638 assert_eq!(r.status, LmStatus::Stationary, "{r:?}");
1639 assert_eq!(r.iterations, 0);
1640 }
1641
1642 #[test]
1650 fn converges_on_a_rank_deficient_non_isolated_root_set() {
1651 let prob = dense2([Number::NEG_INFINITY; 2], [Number::INFINITY; 2]);
1652 let mut sys = Sys {
1653 f: |x: &[Number], c: &mut [Number]| {
1654 c[0] = x[0] - x[1];
1655 c[1] = x[0] * x[0] - x[1] * x[1];
1656 },
1657 j: |x: &[Number], v: &mut [Number]| {
1658 v.copy_from_slice(&[1.0, -1.0, 2.0 * x[0], -2.0 * x[1]]);
1659 },
1660 };
1661 let r = bounded_levenberg_marquardt(
1662 &mut sys,
1663 &prob,
1664 &[3.0, -1.0],
1665 &strict(),
1666 200,
1667 backend(),
1668 LmHooks::default(),
1669 );
1670 assert_eq!(r.status, LmStatus::Converged, "{r:?}");
1671 assert!((r.x[0] - r.x[1]).abs() < 1e-10, "{:?}", r.x);
1672 }
1673
1674 #[test]
1678 fn duplicate_triplets_are_summed_and_a_repeated_row_is_tolerated() {
1679 let prob = LmProblem {
1680 n: 2,
1681 m: 2,
1682 jac_rows: vec![0, 0, 0, 1, 1],
1684 jac_cols: vec![0, 0, 1, 0, 1],
1685 lb: vec![Number::NEG_INFINITY; 2],
1686 ub: vec![Number::INFINITY; 2],
1687 };
1688 let mut sys = Sys {
1689 f: |x: &[Number], c: &mut [Number]| {
1690 c[0] = x[0] + x[1] - 2.0;
1691 c[1] = 2.0 * (x[0] + x[1] - 2.0);
1692 },
1693 j: |_x: &[Number], v: &mut [Number]| v.copy_from_slice(&[0.5, 0.5, 1.0, 2.0, 2.0]),
1694 };
1695 let r = bounded_levenberg_marquardt(
1696 &mut sys,
1697 &prob,
1698 &[5.0, 7.0],
1699 &strict(),
1700 200,
1701 backend(),
1702 LmHooks::default(),
1703 );
1704 assert_eq!(r.status, LmStatus::Converged, "{r:?}");
1705 assert!((r.x[0] + r.x[1] - 2.0).abs() < 1e-10);
1706 }
1707
1708 fn bound_root_system() -> (
1712 LmProblem,
1713 Sys<impl FnMut(&[Number], &mut [Number]), impl FnMut(&[Number], &mut [Number])>,
1714 ) {
1715 let prob = dense2([-10.0, -10.0], [1.0, 10.0]);
1716 let sys = Sys {
1717 f: |x: &[Number], c: &mut [Number]| {
1718 c[0] = x[0] * x[0] + x[1] * x[1] - 2.0;
1719 c[1] = x[0] - x[1];
1720 },
1721 j: |x: &[Number], v: &mut [Number]| {
1722 v.copy_from_slice(&[2.0 * x[0], 2.0 * x[1], 1.0, -1.0]);
1723 },
1724 };
1725 (prob, sys)
1726 }
1727
1728 #[test]
1735 fn an_exhausted_time_budget_stops_the_method_and_names_the_budget() {
1736 use pounce_common::timing::Deadline;
1737 for (deadline, want) in [
1738 (Deadline::new(0.0, 1e6), LmStatus::WallTimeExceeded),
1739 (Deadline::new(1e6, 0.0), LmStatus::CpuTimeExceeded),
1740 ] {
1741 let (prob, mut sys) = bound_root_system();
1742 let r = bounded_levenberg_marquardt(
1743 &mut sys,
1744 &prob,
1745 &[0.2, 3.0],
1746 &strict(),
1747 200,
1748 backend(),
1749 LmHooks {
1750 deadline: Some(deadline),
1751 on_iterate: None,
1752 },
1753 );
1754 assert_eq!(r.status, want, "{r:?}");
1755 assert_eq!(r.iterations, 0, "{r:?}");
1756 }
1757 }
1758
1759 #[test]
1764 fn a_root_is_not_discarded_for_being_late() {
1765 let (prob, mut sys) = bound_root_system();
1766 let r = bounded_levenberg_marquardt(
1767 &mut sys,
1768 &prob,
1769 &[1.0, 1.0],
1770 &strict(),
1771 200,
1772 backend(),
1773 LmHooks {
1774 deadline: Some(pounce_common::timing::Deadline::new(0.0, 0.0)),
1775 on_iterate: None,
1776 },
1777 );
1778 assert_eq!(r.status, LmStatus::Converged, "{r:?}");
1779 assert_eq!(r.iterations, 0);
1780 }
1781
1782 #[test]
1789 fn the_observer_sees_every_iterate_and_can_stop_the_method() {
1790 let (prob, mut sys) = bound_root_system();
1791 let mut seen: Vec<usize> = Vec::new();
1792 let mut obs = |it: &LmIterate<'_>| {
1793 seen.push(it.iter);
1794 it.iter < 2
1795 };
1796 let r = bounded_levenberg_marquardt(
1797 &mut sys,
1798 &prob,
1799 &[0.2, 3.0],
1800 &strict(),
1801 200,
1802 backend(),
1803 LmHooks {
1804 deadline: None,
1805 on_iterate: Some(&mut obs),
1806 },
1807 );
1808 assert_eq!(r.status, LmStatus::UserRequestedStop, "{r:?}");
1809 assert_eq!(r.iterations, 2);
1810 assert_eq!(seen, [0, 1, 2]);
1811
1812 let (prob, mut sys) = bound_root_system();
1815 let mut seen: Vec<usize> = Vec::new();
1816 let mut obs = |it: &LmIterate<'_>| {
1817 seen.push(it.iter);
1818 true
1819 };
1820 let r = bounded_levenberg_marquardt(
1821 &mut sys,
1822 &prob,
1823 &[0.2, 3.0],
1824 &strict(),
1825 200,
1826 backend(),
1827 LmHooks {
1828 deadline: None,
1829 on_iterate: Some(&mut obs),
1830 },
1831 );
1832 assert_eq!(r.status, LmStatus::Converged, "{r:?}");
1833 assert_eq!(seen, (0..=r.iterations).collect::<Vec<_>>());
1834 }
1835
1836 struct Fxm {
1842 finalized: Vec<Vec<Number>>,
1843 callbacks: usize,
1844 stop_at: Option<Index>,
1846 }
1847
1848 impl Fxm {
1849 fn new() -> Self {
1850 Self {
1851 finalized: Vec::new(),
1852 callbacks: 0,
1853 stop_at: None,
1854 }
1855 }
1856 }
1857
1858 impl TNLP for Fxm {
1859 fn get_nlp_info(&mut self) -> Option<pounce_nlp::tnlp::NlpInfo> {
1860 Some(pounce_nlp::tnlp::NlpInfo {
1861 n: 3,
1862 m: 2,
1863 nnz_jac_g: 6,
1864 nnz_h_lag: 0,
1865 index_style: IndexStyle::C,
1866 })
1867 }
1868 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1869 b.x_l.copy_from_slice(&[-1e20, 3.0, -1e20]);
1870 b.x_u.copy_from_slice(&[2.0, 3.0, 1e20]);
1871 b.g_l.copy_from_slice(&[2.0, 1.0]);
1872 b.g_u.copy_from_slice(&[2.0, 1.0]);
1873 true
1874 }
1875 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1876 sp.x.copy_from_slice(&[1.5, 7.0, 1.5]);
1877 true
1878 }
1879 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
1880 Some(5.0)
1881 }
1882 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
1883 g.iter_mut().for_each(|v| *v = 0.0);
1884 true
1885 }
1886 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
1887 g[0] = x[0] * x[2] - x[1];
1888 g[1] = x[0] + 0.5 * x[1] - x[2];
1889 true
1890 }
1891 fn eval_jac_g(
1892 &mut self,
1893 x: Option<&[Number]>,
1894 _new_x: bool,
1895 mode: SparsityRequest<'_>,
1896 ) -> bool {
1897 match mode {
1898 SparsityRequest::Structure { irow, jcol } => {
1899 irow.copy_from_slice(&[0, 0, 0, 1, 1, 1]);
1900 jcol.copy_from_slice(&[0, 1, 2, 0, 1, 2]);
1901 }
1902 SparsityRequest::Values { values } => {
1903 let x = x.unwrap();
1904 values.copy_from_slice(&[x[2], -1.0, x[0], 1.0, 0.5, -1.0]);
1905 }
1906 }
1907 true
1908 }
1909 fn intermediate_callback(
1910 &mut self,
1911 stats: pounce_nlp::tnlp::IterStats,
1912 _d: &IpoptData,
1913 _q: &IpoptCq,
1914 ) -> bool {
1915 self.callbacks += 1;
1916 self.stop_at.is_none_or(|k| stats.iter < k)
1917 }
1918 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
1919 self.finalized.push(sol.x.to_vec());
1920 }
1921 }
1922
1923 fn run_fxm(
1924 fxm: Fxm,
1925 opts: &[(&str, Number)],
1926 capture: bool,
1927 ) -> (SquareSystemOutcome, Rc<RefCell<Fxm>>) {
1928 let mut options = OptionsList::new();
1929 for &(tag, v) in opts {
1930 options.set_numeric_value(tag, v, true, true).unwrap();
1931 }
1932 let cell = Rc::new(RefCell::new(fxm));
1933 let tnlp: Rc<RefCell<dyn TNLP>> = cell.clone();
1934 let plan = probe(&tnlp, &options).expect("Fxm is in the class");
1935 let out = run(
1936 &plan,
1937 &tnlp,
1938 &options,
1939 backend(),
1940 RunOptions {
1941 capture_iterations: capture,
1942 },
1943 );
1944 (out, cell)
1945 }
1946
1947 #[test]
1952 fn run_promotes_and_reports_in_the_ipm_conventions() {
1953 let (out, fxm) = run_fxm(Fxm::new(), &[("obj_scaling_factor", 2.0)], true);
1954 assert!(out.accepted, "{}", out.reason);
1955 let fxm = fxm.borrow();
1956 assert_eq!(fxm.finalized.len(), 1);
1957 let x = &fxm.finalized[0];
1958 assert_eq!(x[1], 3.0, "the fixed variable keeps its slot: {x:?}");
1959 assert!(
1960 (x[0] - 2.0).abs() < 1e-12 && (x[2] - 2.5).abs() < 1e-12,
1961 "{x:?}"
1962 );
1963 let st = &out.statistics;
1964 assert_eq!(st.iterations.len(), st.iteration_count as usize + 1);
1965 assert_eq!(fxm.callbacks, st.iteration_count as usize + 1);
1966 assert_eq!(st.final_objective, 5.0);
1967 assert_eq!(st.final_scaled_objective, 10.0);
1968 }
1969
1970 #[test]
1975 fn run_honours_max_wall_time_and_does_not_promote() {
1976 for (tag, want) in [
1977 ("max_wall_time", LmStatus::WallTimeExceeded),
1978 ("max_cpu_time", LmStatus::CpuTimeExceeded),
1979 ] {
1980 let (out, fxm) = run_fxm(Fxm::new(), &[(tag, 0.0)], true);
1981 assert!(!out.accepted, "{tag}");
1982 assert_eq!(out.lm.status, want, "{tag}");
1983 assert!(out.reason.contains(tag), "{tag}: {}", out.reason);
1984 assert!(fxm.borrow().finalized.is_empty(), "{tag}");
1985 }
1986 }
1987
1988 #[test]
1993 fn run_honours_an_intermediate_callback_stop() {
1994 let mut fxm = Fxm::new();
1995 fxm.stop_at = Some(1);
1996 let (out, fxm) = run_fxm(fxm, &[], false);
1997 assert!(!out.accepted);
1998 assert_eq!(out.lm.status, LmStatus::UserRequestedStop);
1999 assert_eq!(out.lm.iterations, 1);
2000 assert_eq!(fxm.borrow().callbacks, 2, "iterates 0 and 1");
2001 assert!(fxm.borrow().finalized.is_empty());
2002 }
2003
2004 #[test]
2008 fn feasibility_test_applies_both_halves_and_rejects_nan() {
2009 let t = FeasibilityTest {
2010 row_scale: vec![1e-3, 1.0],
2011 tol: 1e-8,
2012 constr_viol_tol: 1e-4,
2013 };
2014 assert!(t.passes(&[5e-6, 1e-9]), "5e-6 · 1e-3 = 5e-9 ≤ tol");
2015 assert!(
2016 !t.passes(&[5e-4, 0.0]),
2017 "scaled passes but unscaled 5e-4 > constr_viol_tol"
2018 );
2019 assert!(!t.passes(&[0.0, 2e-8]), "unscaled row over tol");
2020 assert!(!t.passes(&[Number::NAN, 0.0]));
2021 }
2022}