1use faer::Mat;
52use pounce_common::types::{Index, Number};
53use pounce_nlp::mpcc::ComplementarityPair;
54use pounce_nlp::return_codes::ApplicationReturnStatus;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MpccStage {
59 Exact,
65 Continuation,
70 Penalty,
72 Branch,
77}
78
79impl MpccStage {
80 pub fn label(self) -> &'static str {
81 match self {
82 Self::Exact => "exact product",
83 Self::Continuation => "Scholtes continuation",
84 Self::Penalty => "l1 penalty",
85 Self::Branch => "branch NLP",
86 }
87 }
88}
89
90pub const STAGES: [MpccStage; 3] = [
91 MpccStage::Continuation,
92 MpccStage::Exact,
93 MpccStage::Penalty,
94];
95
96pub const SCHOLTES_SCHEDULE: [Number; 9] = [1e0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8];
100
101pub fn warm_start_assignments(final_mu: Number) -> Vec<String> {
108 let mu = if final_mu.is_finite() && final_mu > 0.0 {
109 final_mu.clamp(1e-9, 1e-1)
110 } else {
111 1e-6
112 };
113 let p = 1e-9;
114 vec![
115 "warm_start_init_point yes\n".to_string(),
116 format!("mu_init {mu:e}\n"),
117 format!("warm_start_bound_push {p:e}\n"),
118 format!("warm_start_bound_frac {p:e}\n"),
119 format!("warm_start_slack_bound_push {p:e}\n"),
120 format!("warm_start_slack_bound_frac {p:e}\n"),
121 format!("warm_start_mult_bound_push {p:e}\n"),
122 ]
123}
124
125pub const EXACT_STAGE_TAU: Number = 1e-6;
133
134pub const EXACT_BUDGET_FACTOR: usize = 3;
147pub const EXACT_BUDGET_FLOOR: usize = 200;
149pub const FINISH_BUDGET_FLOOR: usize = 500;
157
158pub fn exact_stage_budget(answered: bool, continuation_iterations: usize) -> Option<usize> {
163 answered.then(|| {
164 EXACT_BUDGET_FACTOR
165 .saturating_mul(continuation_iterations)
166 .max(EXACT_BUDGET_FLOOR)
167 })
168}
169
170pub fn finish_budget(schedule_iterations: usize) -> usize {
173 schedule_iterations.max(FINISH_BUDGET_FLOOR)
174}
175
176pub const SAME_ANSWER_REL_TOL: Number = 1e-4;
182
183pub const CORNER_SLACK_FACTOR: Number = 10.0;
191
192pub fn relaxation_slack(products: &[Number], product_lambda: &[Number]) -> Number {
203 products
204 .iter()
205 .zip(product_lambda)
206 .map(|(&p, &l)| {
207 if p.is_finite() && l.is_finite() {
208 l.abs() * p.max(0.0)
209 } else {
210 Number::INFINITY
211 }
212 })
213 .sum()
214}
215
216#[allow(clippy::too_many_arguments)]
239pub fn complementarity_gap(
240 x: &[Number],
241 z_l: &[Number],
242 z_u: &[Number],
243 x_l: &[Number],
244 x_u: &[Number],
245 g: &[Number],
246 lambda: &[Number],
247 g_l: &[Number],
248 g_u: &[Number],
249) -> Number {
250 let n = x.len();
251 let m = g.len();
252 if [z_l.len(), z_u.len(), x_l.len(), x_u.len()] != [n; 4]
253 || [lambda.len(), g_l.len(), g_u.len()] != [m; 3]
254 {
255 return Number::INFINITY;
256 }
257 let term = |mult: Number, v: Number, lo: Number, hi: Number| -> Number {
260 if !mult.is_finite() || !v.is_finite() {
261 return Number::INFINITY;
262 }
263 let d = [lo, hi]
264 .into_iter()
265 .filter(|b| b.is_finite())
266 .map(|b| (v - b).abs())
267 .fold(Number::INFINITY, Number::min);
268 if d.is_finite() { mult.abs() * d } else { 0.0 }
269 };
270 let bounds: Number = (0..n)
271 .map(|i| {
272 let lo = term(z_l[i], x[i], x_l[i], Number::INFINITY);
273 let hi = term(z_u[i], x[i], Number::NEG_INFINITY, x_u[i]);
274 lo + hi
275 })
276 .sum();
277 let rows: Number = (0..m).map(|j| term(lambda[j], g[j], g_l[j], g_u[j])).sum();
278 bounds + rows
279}
280
281pub fn same_answer_allowance(f_a: Number, f_b: Number, slack: Number, gap: Number) -> Number {
291 SAME_ANSWER_REL_TOL * f_a.abs().max(f_b.abs())
292 + CORNER_SLACK_FACTOR * slack.max(0.0)
293 + gap.max(0.0)
294}
295
296pub fn finish_drifted(f_finish: Number, f_relaxed: Number, relaxed_slack: Number) -> bool {
318 if !f_relaxed.is_finite() || !relaxed_slack.is_finite() {
319 return false;
320 }
321 !f_finish.is_finite()
322 || f_finish > f_relaxed + same_answer_allowance(f_relaxed, f_relaxed, relaxed_slack, 0.0)
323}
324
325pub fn answers_agree(f_a: Number, f_b: Number, slack: Number, gap: Number) -> bool {
335 f_a.is_finite()
336 && f_b.is_finite()
337 && slack.is_finite()
338 && gap.is_finite()
339 && (f_a - f_b).abs() <= same_answer_allowance(f_a, f_b, slack, gap)
340}
341
342pub const RUNG_RETRY_FLOOR: usize = 200;
345
346pub fn budgeted_max_iter(budget: usize, caller_max_iter: Index) -> Index {
349 let b = Index::try_from(budget).unwrap_or(Index::MAX);
350 b.min(caller_max_iter.max(0))
351}
352
353pub const TOUCHED_OPTIONS: [&str; 13] = [
356 "max_iter",
357 "mu_strategy_fallback",
358 "warm_start_init_point",
359 "mu_init",
360 "warm_start_bound_push",
361 "warm_start_bound_frac",
362 "warm_start_slack_bound_push",
363 "warm_start_slack_bound_frac",
364 "warm_start_mult_bound_push",
365 "l1_exact_penalty_barrier",
366 "bound_relax_factor",
367 "honor_original_bounds",
368 "perturb_slack_max_rows",
369];
370
371pub const SAFE_DEFAULTS: [(&str, &str); 3] = [
389 ("bound_relax_factor", "0"),
390 ("honor_original_bounds", "yes"),
391 ("perturb_slack_max_rows", "0"),
392];
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub enum Reported {
397 Answer(MpccStage),
400 WeakFallback(MpccStage),
402 FeasibleUncertified(MpccStage),
405 Verdict,
409}
410
411pub fn describe_outcome(
413 reported: Reported,
414 n_solves: usize,
415 total_iterations: usize,
416 max_abs_product: Number,
417 certificate: Option<&Certificate>,
418) -> String {
419 let solves = format!(
420 "{n_solves} lowered solve{}, {total_iterations} iterations in all",
421 if n_solves == 1 { "" } else { "s" }
422 );
423 let head = match reported {
424 Reported::Answer(stage) => format!(
425 "pounce: complementarity: answered by the {} formulation ({solves}, violation = \
426 {max_abs_product:.1e})",
427 stage.label(),
428 ),
429 Reported::WeakFallback(stage) => format!(
430 "pounce: complementarity: no formulation produced a strongly stationary answer \
431 ({solves}); reporting the {} formulation's weakly stationary point (violation = \
432 {max_abs_product:.1e})",
433 stage.label(),
434 ),
435 Reported::FeasibleUncertified(stage) => format!(
436 "pounce: complementarity: no formulation produced a stationary answer ({solves}); \
437 reporting the {} formulation's feasible point (violation = {max_abs_product:.1e})",
438 stage.label(),
439 ),
440 Reported::Verdict => format!(
441 "pounce: complementarity: no formulation produced an answer ({solves}); reporting \
442 the exact-product solve's verdict"
443 ),
444 };
445 let tail = match certificate {
446 None => "; stationarity not evaluated".to_string(),
447 Some(c) => {
448 let class = match c.class {
449 StationarityClass::S => "S (strongly stationary)",
450 StationarityClass::W => {
451 "W only (a biactive pair has a wrong-sign multiplier: a branch may still descend)"
452 }
453 StationarityClass::None if !c.feasible => {
454 return format!(
455 "{head}; {} pair{}, {} biactive; stationarity: none (the point violates a \
456 row or bound by {:.1e}, beyond constr_viol_tol).",
457 c.n_pairs,
458 if c.n_pairs == 1 { "" } else { "s" },
459 c.n_biactive,
460 c.primal_violation
461 );
462 }
463 StationarityClass::None => "none (not a stationary point of the model)",
464 StationarityClass::NotSearched => {
465 "not determined (the model or its multiplier set is too large to certify)"
466 }
467 };
468 format!(
469 "; {} pair{}, {} biactive; stationarity: {class}.",
470 c.n_pairs,
471 if c.n_pairs == 1 { "" } else { "s" },
472 c.n_biactive
473 )
474 }
475 };
476 head + &tail
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
482pub enum Strategy {
483 Both,
484 Exact,
485 Continuation,
486}
487
488impl Strategy {
489 pub fn parse(s: &str) -> Option<Strategy> {
492 match s {
493 "both" => Some(Strategy::Both),
494 "exact" => Some(Strategy::Exact),
495 "continuation" => Some(Strategy::Continuation),
496 _ => None,
497 }
498 }
499
500 pub fn name(self) -> &'static str {
502 match self {
503 Strategy::Both => "both",
504 Strategy::Exact => "exact",
505 Strategy::Continuation => "continuation",
506 }
507 }
508
509 pub fn runs(self, stage: MpccStage) -> bool {
510 !matches!(
511 (self, stage),
512 (Strategy::Exact, MpccStage::Continuation) | (Strategy::Continuation, MpccStage::Exact)
513 )
514 }
515}
516
517pub fn converged(status: ApplicationReturnStatus) -> bool {
519 matches!(
520 status,
521 ApplicationReturnStatus::SolveSucceeded
522 | ApplicationReturnStatus::SolvedToAcceptableLevel
523 | ApplicationReturnStatus::FeasiblePointFound
524 )
525}
526
527pub fn compared(stage: MpccStage) -> bool {
538 matches!(stage, MpccStage::Continuation | MpccStage::Exact)
539}
540
541pub fn answers(class: StationarityClass) -> bool {
548 matches!(class, StationarityClass::S | StationarityClass::NotSearched)
549}
550
551pub fn kept_if_nothing_better(class: StationarityClass) -> bool {
557 class == StationarityClass::W
558}
559
560pub fn accepts(
562 status: ApplicationReturnStatus,
563 max_abs_product: Number,
564 constr_viol_tol: Number,
565) -> bool {
566 converged(status) && max_abs_product.is_finite() && max_abs_product <= constr_viol_tol
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575pub enum StationarityClass {
576 S,
579 W,
583 None,
586 NotSearched,
592}
593
594#[derive(Debug, Clone)]
596pub struct Certificate {
597 pub class: StationarityClass,
598 pub n_pairs: usize,
599 pub n_biactive: usize,
600 pub n_neither: usize,
602 pub multipliers_unique: bool,
605 pub residual: Number,
607 pub resid_tol: Number,
610 pub tight: bool,
617 pub primal_violation: Number,
620 pub feasible: bool,
623 pub worst_biactive_sign: Number,
626}
627
628pub struct CertificateInput<'a> {
631 pub x: &'a [Number],
632 pub grad_f: &'a [Number],
633 pub g: &'a [Number],
634 pub jac: &'a [(usize, usize, Number)],
635 pub x_l: &'a [Number],
636 pub x_u: &'a [Number],
637 pub g_l: &'a [Number],
638 pub g_u: &'a [Number],
639 pub pairs: &'a [ComplementarityPair],
640 pub solver_tol: Number,
642 pub feas_tol: Number,
645}
646
647pub const MAX_SEARCHED_COLUMNS: usize = 600;
650
651pub const MAX_DENSE_ENTRIES: usize = 4_000_000;
661
662pub const DENSE_ENTRIES: usize = 250_000;
675
676const INF: Number = 1e19;
677
678#[derive(Clone, Copy, PartialEq)]
679enum Sign {
680 Free,
681 NonNeg,
682 Biactive,
684}
685
686pub fn certify(inp: &CertificateInput<'_>) -> Certificate {
717 certify_routed(inp, None)
718}
719
720struct PairActivity {
723 mixed: Vec<bool>,
724 g_side: Vec<(Number, Number)>,
726 h_side: Vec<(Number, Number)>,
727 hv: Vec<Number>,
728 g_act: Vec<bool>,
729 h_act: Vec<bool>,
730}
731
732fn pair_activity(inp: &CertificateInput<'_>, corner: Number) -> PairActivity {
733 let side = |lo: Number, hi: Number| -> (Number, Number) {
737 if lo > -INF { (1.0, lo) } else { (-1.0, hi) }
738 };
739 let mixed: Vec<bool> = inp
744 .pairs
745 .iter()
746 .map(|p| {
747 pounce_nlp::mpcc::is_mixed(
748 inp.x_l[p.var],
749 inp.x_u[p.var],
750 inp.g_l[p.row],
751 inp.g_u[p.row],
752 )
753 })
754 .collect();
755 let g_side: Vec<(Number, Number)> = inp
756 .pairs
757 .iter()
758 .zip(&mixed)
759 .map(|(p, &mx)| {
760 let (lo, hi) = (inp.x_l[p.var], inp.x_u[p.var]);
761 if mx {
762 if inp.x[p.var] - lo <= hi - inp.x[p.var] {
763 (1.0, lo)
764 } else {
765 (-1.0, hi)
766 }
767 } else if lo > -INF && hi < INF {
768 if inp.g_l[p.row] > -INF {
772 (1.0, lo)
773 } else {
774 (-1.0, hi)
775 }
776 } else {
777 side(lo, hi)
778 }
779 })
780 .collect();
781 let h_side: Vec<(Number, Number)> = inp
782 .pairs
783 .iter()
784 .zip(&mixed)
785 .zip(&g_side)
786 .map(|((p, &mx), &(sg, _))| {
787 if mx {
788 (sg, 0.0)
789 } else {
790 side(inp.g_l[p.row], inp.g_u[p.row])
791 }
792 })
793 .collect();
794 let gv: Vec<Number> = inp
795 .pairs
796 .iter()
797 .zip(&g_side)
798 .map(|(p, &(s, b))| s * (inp.x[p.var] - b))
799 .collect();
800 let hv: Vec<Number> = inp
801 .pairs
802 .iter()
803 .zip(&h_side)
804 .map(|(p, &(s, c))| s * (inp.g[p.row] - c))
805 .collect();
806 let gs = gv.iter().fold(1.0_f64, |a, v| a.max(v.abs()));
807 let hs = hv.iter().fold(1.0_f64, |a, v| a.max(v.abs()));
808 let g_act: Vec<bool> = gv.iter().map(|v| v.abs() <= corner * gs).collect();
809 let h_act: Vec<bool> = hv.iter().map(|v| v.abs() <= corner * hs).collect();
810 PairActivity {
811 mixed,
812 g_side,
813 h_side,
814 hv,
815 g_act,
816 h_act,
817 }
818}
819
820fn certify_routed(inp: &CertificateInput<'_>, force_sparse: Option<bool>) -> Certificate {
823 let n = inp.x.len();
824 let m = inp.g.len();
825 let corner = inp.solver_tol.max(0.0).sqrt();
826 let row_tol = corner;
827
828 let PairActivity {
829 mixed,
830 g_side,
831 h_side,
832 hv,
833 g_act,
834 h_act,
835 } = pair_activity(inp, corner);
836 let n_biactive = g_act.iter().zip(&h_act).filter(|(a, b)| **a && **b).count();
837 let n_neither = g_act
838 .iter()
839 .zip(&h_act)
840 .filter(|(a, b)| !**a && !**b)
841 .count();
842
843 let viol = |v: Number, lo: Number, hi: Number| {
848 let below = if lo > -INF { lo - v } else { 0.0 };
849 let above = if hi < INF { v - hi } else { 0.0 };
850 below.max(above).max(0.0)
851 };
852 let mixed_sign = (0..inp.pairs.len())
855 .filter(|&k| mixed[k] && g_act[k])
856 .map(|k| (-hv[k]).max(0.0));
857 let primal_violation = (0..m)
858 .map(|i| viol(inp.g[i], inp.g_l[i], inp.g_u[i]))
859 .chain((0..n).map(|j| viol(inp.x[j], inp.x_l[j], inp.x_u[j])))
860 .chain(mixed_sign)
861 .fold(0.0, Number::max);
862 let feasible = primal_violation <= inp.feas_tol;
863
864 let mut pair_row = vec![None; m];
865 let mut pair_var = vec![None; n];
866 for (k, p) in inp.pairs.iter().enumerate() {
867 pair_row[p.row] = Some(k);
868 pair_var[p.var] = Some(k);
869 }
870 let mut rows_of: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m];
871 for &(r, c, v) in inp.jac {
872 rows_of[r].push((c, v));
873 }
874
875 let mut cols: Vec<(Vec<(usize, Number)>, Sign)> = Vec::new();
877 for i in 0..m {
878 if let Some(k) = pair_row[i] {
879 let sh = h_side[k].0;
880 let grad_h: Vec<(usize, Number)> =
881 rows_of[i].iter().map(|&(c, v)| (c, sh * v)).collect();
882 match (g_act[k], h_act[k]) {
883 (_, false) => {}
884 (true, true) => cols.push((grad_h, Sign::Biactive)),
885 (false, true) => cols.push((grad_h, Sign::Free)),
886 }
887 continue;
888 }
889 let (lo, hi) = (inp.g_l[i], inp.g_u[i]);
890 let at_lo = lo > -INF && inp.g[i] - lo <= row_tol;
891 let at_hi = hi < INF && hi - inp.g[i] <= row_tol;
892 let neg: Vec<(usize, Number)> = rows_of[i].iter().map(|&(c, v)| (c, -v)).collect();
893 match (at_lo, at_hi) {
894 (true, true) => cols.push((rows_of[i].clone(), Sign::Free)),
895 (true, false) => cols.push((rows_of[i].clone(), Sign::NonNeg)),
896 (false, true) => cols.push((neg, Sign::NonNeg)),
897 (false, false) => {}
898 }
899 }
900 for (j, pv) in pair_var.iter().enumerate() {
901 if let Some(k) = *pv {
902 let sg = g_side[k].0;
903 match (g_act[k], h_act[k]) {
904 (false, _) => {}
905 (true, true) => cols.push((vec![(j, sg)], Sign::Biactive)),
906 (true, false) => cols.push((vec![(j, sg)], Sign::Free)),
907 }
908 if !mixed[k] {
912 let (lo, hi) = (inp.x_l[j], inp.x_u[j]);
913 let other_active = if sg > 0.0 {
914 hi < INF && hi - inp.x[j] <= row_tol
915 } else {
916 lo > -INF && inp.x[j] - lo <= row_tol
917 };
918 if other_active {
919 cols.push((vec![(j, -sg)], Sign::NonNeg));
920 }
921 }
922 continue;
923 }
924 let (lo, hi) = (inp.x_l[j], inp.x_u[j]);
925 let at_lo = lo > -INF && inp.x[j] - lo <= row_tol;
926 let at_hi = hi < INF && hi - inp.x[j] <= row_tol;
927 match (at_lo, at_hi) {
928 (true, true) => cols.push((vec![(j, 1.0)], Sign::Free)),
929 (true, false) => cols.push((vec![(j, 1.0)], Sign::NonNeg)),
930 (false, true) => cols.push((vec![(j, -1.0)], Sign::NonNeg)),
931 (false, false) => {}
932 }
933 }
934
935 let b = inp.grad_f.to_vec();
936 let k = cols.len();
937 let col_max = cols
938 .iter()
939 .flat_map(|(ents, _)| ents)
940 .fold(0.0_f64, |m, &(_, v)| m.max(v.abs()));
941 let dual_scale = 1.0_f64
942 .max(b.iter().fold(0.0_f64, |m, v| m.max(v.abs())))
943 .max(col_max);
944 let resid_tol = inp.solver_tol.max(1e-6) * dual_scale;
945 let sign_tol = corner * dual_scale;
952 let accept_tol = resid_tol + sign_tol;
959 let signs: Vec<Sign> = cols.iter().map(|(_, s)| *s).collect();
960
961 let mut cert = Certificate {
962 class: StationarityClass::None,
963 n_pairs: inp.pairs.len(),
964 n_biactive,
965 n_neither,
966 multipliers_unique: true,
967 residual: Number::INFINITY,
968 resid_tol: accept_tol,
969 tight: false,
970 primal_violation,
971 feasible,
972 worst_biactive_sign: 0.0,
973 };
974 if n_neither > 0 || !feasible {
975 return cert;
978 }
979
980 let classify_unique = |cert: &mut Certificate, theta: &[Number]| {
982 let ordinary_ok = signs
983 .iter()
984 .zip(theta)
985 .all(|(s, t)| *s != Sign::NonNeg || *t >= -sign_tol);
986 if !ordinary_ok {
987 return;
988 }
989 let worst = signs
990 .iter()
991 .zip(theta)
992 .filter(|(s, _)| **s == Sign::Biactive)
993 .fold(0.0_f64, |w, (_, t)| w.min(*t));
994 cert.worst_biactive_sign = worst;
995 cert.class = if worst >= -sign_tol {
996 StationarityClass::S
997 } else {
998 StationarityClass::W
999 };
1000 };
1001
1002 let sparse = force_sparse.unwrap_or(n.saturating_mul(k) > DENSE_ENTRIES);
1003 if sparse {
1004 let entries: Vec<Vec<(usize, Number)>> = cols.iter().map(|(e, _)| e.clone()).collect();
1005 match sparse_least_squares(n, &entries, &b) {
1006 LeastSquares::Solved(theta) => {
1007 let mut r = b.clone();
1008 for (ents, t) in entries.iter().zip(&theta) {
1009 for &(row, v) in ents {
1010 r[row] -= v * t;
1011 }
1012 }
1013 cert.residual = r.iter().fold(0.0, |m: Number, v| m.max(v.abs()));
1014 cert.tight = cert.residual <= resid_tol;
1015 if cert.residual <= accept_tol {
1016 classify_unique(&mut cert, &theta);
1017 }
1018 return cert;
1019 }
1020 LeastSquares::RankDeficient if n.saturating_mul(k) <= MAX_DENSE_ENTRIES => {
1021 }
1023 LeastSquares::RankDeficient | LeastSquares::Failed => {
1024 cert.multipliers_unique = false;
1025 cert.residual = Number::NAN;
1026 cert.class = StationarityClass::NotSearched;
1027 cert.tight = true; return cert;
1029 }
1030 }
1031 }
1032
1033 let mut a = Mat::<Number>::from_fn(n, k, |_, _| 0.0);
1034 for (c, (ents, _)) in cols.iter().enumerate() {
1035 for &(r, v) in ents {
1036 a[(r, c)] += v;
1037 }
1038 }
1039
1040 let (theta, rank) = lstsq(&a, &b);
1043 let r_free = resid_inf(&a, &theta, &b);
1044 cert.residual = r_free;
1045 cert.tight = r_free <= resid_tol;
1046 if r_free > accept_tol {
1047 return cert; }
1049 if rank == k {
1050 classify_unique(&mut cert, &theta);
1051 return cert;
1052 }
1053
1054 cert.multipliers_unique = false;
1056 let constrained = signs.iter().filter(|s| **s != Sign::Free).count();
1057 if constrained > MAX_SEARCHED_COLUMNS {
1058 cert.class = StationarityClass::NotSearched;
1059 cert.tight = true; return cert;
1061 }
1062 for (class, biactive_signed) in [(StationarityClass::S, true), (StationarityClass::W, false)] {
1063 let nonneg: Vec<bool> = signs
1064 .iter()
1065 .map(|s| *s == Sign::NonNeg || (biactive_signed && *s == Sign::Biactive))
1066 .collect();
1067 let r = signed_lstsq_residual(&a, &b, &nonneg);
1070 if r <= accept_tol {
1071 cert.class = class;
1072 cert.residual = r;
1073 cert.tight = r_free <= resid_tol;
1079 return cert;
1080 }
1081 }
1082 cert
1083}
1084
1085#[derive(Debug, Clone, PartialEq)]
1087enum LeastSquares {
1088 Solved(Vec<Number>),
1090 RankDeficient,
1092 Failed,
1094}
1095
1096fn sparse_least_squares(n: usize, cols: &[Vec<(usize, Number)>], b: &[Number]) -> LeastSquares {
1112 use pounce_linsol::{ESymSolverStatus, SparseSymLinearSolverInterface};
1113
1114 let k = cols.len();
1115 if b.len() != n || cols.iter().flatten().any(|&(r, _)| r >= n) {
1116 return LeastSquares::Failed;
1117 }
1118 if k == 0 {
1119 return LeastSquares::Solved(Vec::new());
1120 }
1121 if k > n {
1122 return LeastSquares::RankDeficient;
1123 }
1124 let a_max = cols
1125 .iter()
1126 .flatten()
1127 .fold(0.0_f64, |m, &(_, v)| m.max(v.abs()));
1128 let s = a_max.max(1.0);
1129 let mut trip: Vec<(Index, Index, Number)> = (1..=n as Index).map(|i| (i, i, s)).collect();
1132 for (j, c) in cols.iter().enumerate() {
1133 let mut c = c.clone();
1134 c.sort_by_key(|&(r, _)| r);
1135 for (r, v) in c {
1136 let (row, col) = ((n + j + 1) as Index, (r + 1) as Index);
1137 match trip.last_mut() {
1138 Some(last) if last.0 == row && last.1 == col => last.2 += v,
1139 _ => trip.push((row, col, v)),
1140 }
1141 }
1142 }
1143 let irn: Vec<Index> = trip.iter().map(|t| t.0).collect();
1144 let jcn: Vec<Index> = trip.iter().map(|t| t.1).collect();
1145 let mut solver = pounce_feral::FeralSolverInterface::new();
1146 let dim = (n + k) as Index;
1147 if solver.initialize_structure(dim, irn.len() as Index, &irn, &jcn) != ESymSolverStatus::Success
1148 {
1149 return LeastSquares::Failed;
1150 }
1151 for (slot, t) in solver.values_array_mut().iter_mut().zip(&trip) {
1152 *slot = t.2;
1153 }
1154 let mut rhs0 = vec![0.0; n + k];
1155 rhs0[..n].copy_from_slice(b);
1156 let mut z = rhs0.clone();
1157 match solver.multi_solve(true, &irn, &jcn, 1, &mut z, true, k as Index) {
1158 ESymSolverStatus::Success => {}
1159 ESymSolverStatus::Singular | ESymSolverStatus::WrongInertia => {
1160 return LeastSquares::RankDeficient;
1161 }
1162 _ => return LeastSquares::Failed,
1163 }
1164 for _ in 0..REFINEMENT_STEPS {
1168 let mut corr = rhs0.clone();
1169 for &(r, c, v) in &trip {
1170 let (r, c) = ((r - 1) as usize, (c - 1) as usize);
1171 corr[r] -= v * z[c];
1172 if r != c {
1173 corr[c] -= v * z[r];
1174 }
1175 }
1176 if solver.multi_solve(false, &irn, &jcn, 1, &mut corr, false, 0)
1177 != ESymSolverStatus::Success
1178 {
1179 return LeastSquares::Failed;
1180 }
1181 z.iter_mut().zip(&corr).for_each(|(zi, ci)| *zi += ci);
1182 }
1183 if z.iter().all(|v| v.is_finite()) {
1184 LeastSquares::Solved(z[n..].to_vec())
1185 } else {
1186 LeastSquares::Failed
1187 }
1188}
1189
1190const REFINEMENT_STEPS: usize = 2;
1191
1192fn lstsq(a: &Mat<Number>, b: &[Number]) -> (Vec<Number>, usize) {
1195 let (n, k) = (a.nrows(), a.ncols());
1196 if k == 0 {
1197 return (Vec::new(), 0);
1198 }
1199 let Ok(svd) = a.thin_svd() else {
1200 return (vec![0.0; k], 0);
1201 };
1202 let s: Vec<Number> = svd.S().column_vector().iter().copied().collect();
1203 let smax = s.first().copied().unwrap_or(0.0);
1204 let tol = smax * (n.max(k) as Number) * Number::EPSILON;
1205 let u = svd.U();
1206 let v = svd.V();
1207 let mut theta = vec![0.0; k];
1208 let mut rank = 0;
1209 for (i, &si) in s.iter().enumerate() {
1210 if si <= tol {
1211 continue;
1212 }
1213 rank += 1;
1214 let coef = (0..n).map(|r| u[(r, i)] * b[r]).sum::<Number>() / si;
1215 for (c, t) in theta.iter_mut().enumerate() {
1216 *t += coef * v[(c, i)];
1217 }
1218 }
1219 (theta, rank)
1220}
1221
1222fn resid_inf(a: &Mat<Number>, theta: &[Number], b: &[Number]) -> Number {
1223 (0..a.nrows())
1224 .map(|r| ((0..a.ncols()).map(|c| a[(r, c)] * theta[c]).sum::<Number>() - b[r]).abs())
1225 .fold(0.0, Number::max)
1226}
1227
1228fn signed_lstsq_residual(a: &Mat<Number>, b: &[Number], nonneg: &[bool]) -> Number {
1232 let n = a.nrows();
1233 let free: Vec<usize> = (0..a.ncols()).filter(|&c| !nonneg[c]).collect();
1234 let cons: Vec<usize> = (0..a.ncols()).filter(|&c| nonneg[c]).collect();
1235 let basis: Vec<Vec<Number>> = if free.is_empty() {
1237 Vec::new()
1238 } else {
1239 let af = Mat::<Number>::from_fn(n, free.len(), |r, c| a[(r, free[c])]);
1240 match af.thin_svd() {
1241 Ok(svd) => {
1242 let s: Vec<Number> = svd.S().column_vector().iter().copied().collect();
1243 let smax = s.first().copied().unwrap_or(0.0);
1244 let tol = smax * (n.max(free.len()) as Number) * Number::EPSILON;
1245 let u = svd.U();
1246 (0..s.len())
1247 .filter(|&i| s[i] > tol)
1248 .map(|i| (0..n).map(|r| u[(r, i)]).collect())
1249 .collect()
1250 }
1251 Err(_) => return Number::INFINITY,
1252 }
1253 };
1254 let project = |v: &mut Vec<Number>| {
1255 for q in &basis {
1256 let d: Number = q.iter().zip(v.iter()).map(|(x, y)| x * y).sum();
1257 for (vi, qi) in v.iter_mut().zip(q) {
1258 *vi -= d * qi;
1259 }
1260 }
1261 };
1262 let mut pb = b.to_vec();
1263 project(&mut pb);
1264 let pa: Vec<Vec<Number>> = cons
1265 .iter()
1266 .map(|&c| {
1267 let mut v: Vec<Number> = (0..n).map(|r| a[(r, c)]).collect();
1268 project(&mut v);
1269 v
1270 })
1271 .collect();
1272 let x = nnls(&pa, &pb, n);
1273 (0..n)
1274 .map(|r| {
1275 (pb[r]
1276 - pa.iter()
1277 .zip(&x)
1278 .map(|(col, xi)| col[r] * xi)
1279 .sum::<Number>())
1280 .abs()
1281 })
1282 .fold(0.0, Number::max)
1283}
1284
1285fn nnls(cols: &[Vec<Number>], b: &[Number], n: usize) -> Vec<Number> {
1287 let k = cols.len();
1288 let mut x = vec![0.0; k];
1289 if k == 0 {
1290 return x;
1291 }
1292 let mut passive = vec![false; k];
1293 let resid = |x: &[Number]| -> Vec<Number> {
1294 (0..n)
1295 .map(|r| b[r] - cols.iter().zip(x).map(|(c, xi)| c[r] * xi).sum::<Number>())
1296 .collect()
1297 };
1298 let grad = |res: &[Number]| -> Vec<Number> {
1299 cols.iter()
1300 .map(|c| c.iter().zip(res).map(|(a, r)| a * r).sum())
1301 .collect()
1302 };
1303 let scale = cols
1304 .iter()
1305 .flatten()
1306 .fold(0.0_f64, |m, v| m.max(v.abs()))
1307 .max(1.0)
1308 * b.iter().fold(0.0_f64, |m, v| m.max(v.abs())).max(1.0);
1309 let wtol = 1e-12 * scale * (n.max(k) as Number);
1310 let mut blocked = vec![false; k];
1315 for _outer in 0..3 * k + 10 {
1316 let w = grad(&resid(&x));
1317 let Some((t, wt)) = (0..k)
1318 .filter(|&j| !passive[j] && !blocked[j])
1319 .map(|j| (j, w[j]))
1320 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
1321 else {
1322 break;
1323 };
1324 if wt <= wtol {
1325 break;
1326 }
1327 passive[t] = true;
1328 for _inner in 0..3 * k + 10 {
1329 let p: Vec<usize> = (0..k).filter(|&j| passive[j]).collect();
1330 let ap = Mat::<Number>::from_fn(n, p.len(), |r, c| cols[p[c]][r]);
1331 let (z, _) = lstsq(&ap, b);
1332 if z.iter().all(|&v| v > 0.0) {
1333 for (i, &j) in p.iter().enumerate() {
1334 x[j] = z[i];
1335 }
1336 break;
1337 }
1338 let mut alpha = 1.0_f64;
1339 for (i, &j) in p.iter().enumerate() {
1340 if z[i] <= 0.0 {
1341 let d = x[j] - z[i];
1342 if d > 0.0 {
1343 alpha = alpha.min(x[j] / d);
1344 }
1345 }
1346 }
1347 for (i, &j) in p.iter().enumerate() {
1348 x[j] += alpha * (z[i] - x[j]);
1349 if x[j] <= 1e-15 * scale {
1350 x[j] = 0.0;
1351 passive[j] = false;
1352 }
1353 }
1354 }
1355 if passive[t] {
1356 blocked.fill(false);
1357 } else {
1358 blocked[t] = true;
1359 }
1360 }
1361 x
1362}
1363
1364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1370pub enum HeldSide {
1371 G,
1372 H,
1373}
1374
1375#[derive(Debug, Clone, PartialEq)]
1377pub enum BVerdict {
1378 B,
1380 NotB {
1383 held: Vec<HeldSide>,
1384 decrease: Number,
1385 },
1386 NotChecked(&'static str),
1388}
1389
1390#[derive(Debug, Clone, PartialEq)]
1392pub struct BCheck {
1393 pub verdict: BVerdict,
1394 pub n_biactive: usize,
1395 pub branches_solved: usize,
1397 pub enumerated: bool,
1401}
1402
1403pub const MAX_ENUMERATED_BIACTIVE: usize = 10;
1406
1407pub fn check_b_stationarity(inp: &CertificateInput<'_>) -> BCheck {
1426 let corner = inp.solver_tol.max(0.0).sqrt();
1427 let act = pair_activity(inp, corner);
1428 let np = inp.pairs.len();
1429 let biactive: Vec<usize> = (0..np).filter(|&k| act.g_act[k] && act.h_act[k]).collect();
1430 let mut out = BCheck {
1431 verdict: BVerdict::NotChecked(""),
1432 n_biactive: biactive.len(),
1433 branches_solved: 0,
1434 enumerated: false,
1435 };
1436 if (0..np).any(|k| !act.g_act[k] && !act.h_act[k]) {
1437 out.verdict = BVerdict::NotChecked("a pair is not complementary at the point");
1438 return out;
1439 }
1440 let scale = inp.grad_f.iter().fold(1.0_f64, |a, v| a.max(v.abs()));
1441 let band = corner * scale;
1442 let held_of = |choice: &dyn Fn(usize) -> Option<HeldSide>| -> Vec<Option<HeldSide>> {
1444 (0..np)
1445 .map(|k| match (act.g_act[k], act.h_act[k]) {
1446 (true, false) => Some(HeldSide::G),
1447 (false, true) => Some(HeldSide::H),
1448 _ => choice(k),
1449 })
1450 .collect()
1451 };
1452 let full = |h: &[Option<HeldSide>]| -> Vec<HeldSide> {
1453 h.iter().map(|s| s.unwrap_or(HeldSide::G)).collect()
1454 };
1455
1456 if biactive.len() <= MAX_ENUMERATED_BIACTIVE {
1457 out.enumerated = true;
1458 let mut best: Option<(Number, Vec<HeldSide>)> = None;
1459 for mask in 0..(1usize << biactive.len()) {
1460 let held = held_of(&|k| {
1461 let bit = biactive.iter().position(|&b| b == k).unwrap();
1462 Some(if mask >> bit & 1 == 0 {
1463 HeldSide::G
1464 } else {
1465 HeldSide::H
1466 })
1467 });
1468 let Some((v, _)) = branch_lp(inp, &act, &held) else {
1469 out.verdict = BVerdict::NotChecked("a branch LP did not solve");
1470 return out;
1471 };
1472 out.branches_solved += 1;
1473 if best.as_ref().is_none_or(|(b, _)| v < *b) {
1474 best = Some((v, full(&held)));
1475 }
1476 }
1477 out.verdict = match best {
1478 Some((v, held)) if v < -band => BVerdict::NotB { held, decrease: v },
1479 _ => BVerdict::B,
1480 };
1481 return out;
1482 }
1483
1484 let relaxed = held_of(&|_| None);
1486 let Some((_, d)) = branch_lp(inp, &act, &relaxed) else {
1487 out.verdict = BVerdict::NotChecked("the relaxed LP did not solve");
1488 return out;
1489 };
1490 out.branches_solved += 1;
1491 let dg = |k: usize| act.g_side[k].0 * d[inp.pairs[k].var];
1492 let dh = |k: usize| {
1493 let i = inp.pairs[k].row;
1494 act.h_side[k].0
1495 * inp
1496 .jac
1497 .iter()
1498 .filter(|&&(r, _, _)| r == i)
1499 .map(|&(_, c, v)| v * d[c])
1500 .sum::<Number>()
1501 };
1502 let held = held_of(&|k| {
1503 Some(if dg(k) <= dh(k) {
1504 HeldSide::G
1505 } else {
1506 HeldSide::H
1507 })
1508 });
1509 let Some((v, _)) = branch_lp(inp, &act, &held) else {
1510 out.verdict = BVerdict::NotChecked("a branch LP did not solve");
1511 return out;
1512 };
1513 out.branches_solved += 1;
1514 out.verdict = if v < -band {
1515 BVerdict::NotB {
1516 held: full(&held),
1517 decrease: v,
1518 }
1519 } else {
1520 BVerdict::NotChecked(
1521 "too many biactive pairs to enumerate; the heuristic branch does not descend",
1522 )
1523 };
1524 out
1525}
1526
1527fn branch_lp(
1531 inp: &CertificateInput<'_>,
1532 act: &PairActivity,
1533 held: &[Option<HeldSide>],
1534) -> Option<(Number, Vec<Number>)> {
1535 use pounce_convex::{QpOptions, QpProblem, QpStatus, Triplet, solve_qp_ipm};
1536 let n = inp.x.len();
1537 let m = inp.g.len();
1538 let row_tol = inp.solver_tol.max(0.0).sqrt();
1539 let mut rows_of: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m];
1540 for &(r, c, v) in inp.jac {
1541 rows_of[r].push((c, v));
1542 }
1543 let (mut lb, mut ub) = (vec![-1.0; n], vec![1.0; n]);
1544 let (mut a, mut b, mut g, mut h) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
1545 let eq = |coef: &[(usize, Number)], s: Number, a: &mut Vec<Triplet>, b: &mut Vec<Number>| {
1546 let r = b.len();
1547 a.extend(coef.iter().map(|&(c, v)| Triplet {
1548 row: r,
1549 col: c,
1550 val: s * v,
1551 }));
1552 b.push(0.0);
1553 };
1554 let geq = |coef: &[(usize, Number)], s: Number, g: &mut Vec<Triplet>, h: &mut Vec<Number>| {
1556 let r = h.len();
1557 g.extend(coef.iter().map(|&(c, v)| Triplet {
1558 row: r,
1559 col: c,
1560 val: -s * v,
1561 }));
1562 h.push(0.0);
1563 };
1564 let mut pair_row = vec![None; m];
1565 let mut pair_var = vec![None; n];
1566 for (k, p) in inp.pairs.iter().enumerate() {
1567 pair_row[p.row] = Some(k);
1568 pair_var[p.var] = Some(k);
1569 }
1570 for i in (0..m).filter(|&i| pair_row[i].is_none()) {
1572 let (lo, hi) = (inp.g_l[i], inp.g_u[i]);
1573 let at_lo = lo > -INF && inp.g[i] - lo <= row_tol;
1574 let at_hi = hi < INF && hi - inp.g[i] <= row_tol;
1575 match (at_lo, at_hi) {
1576 (true, true) => eq(&rows_of[i], 1.0, &mut a, &mut b),
1577 (true, false) => geq(&rows_of[i], 1.0, &mut g, &mut h),
1578 (false, true) => geq(&rows_of[i], -1.0, &mut g, &mut h),
1579 (false, false) => {}
1580 }
1581 }
1582 for j in 0..n {
1584 let (lo, hi) = (inp.x_l[j], inp.x_u[j]);
1585 let (mut at_lo, mut at_hi) = (
1586 lo > -INF && inp.x[j] - lo <= row_tol,
1587 hi < INF && hi - inp.x[j] <= row_tol,
1588 );
1589 if let Some(k) = pair_var[j] {
1590 if act.mixed[k] {
1591 continue; }
1593 if act.g_side[k].0 > 0.0 {
1595 at_lo = false;
1596 } else {
1597 at_hi = false;
1598 }
1599 }
1600 if at_lo {
1601 lb[j] = 0.0;
1602 }
1603 if at_hi {
1604 ub[j] = 0.0;
1605 }
1606 }
1607 for (k, p) in inp.pairs.iter().enumerate() {
1609 let (sg, sh) = (act.g_side[k].0, act.h_side[k].0);
1610 let gcoef = [(p.var, 1.0)];
1611 let hcoef = &rows_of[p.row];
1612 match held[k] {
1613 Some(HeldSide::G) => {
1614 lb[p.var] = 0.0;
1615 ub[p.var] = 0.0;
1616 if act.h_act[k] {
1617 geq(hcoef, sh, &mut g, &mut h);
1618 }
1619 }
1620 Some(HeldSide::H) => {
1621 eq(hcoef, 1.0, &mut a, &mut b);
1622 if act.g_act[k] {
1623 geq(&gcoef, sg, &mut g, &mut h);
1624 }
1625 }
1626 None => {
1627 geq(&gcoef, sg, &mut g, &mut h);
1628 geq(hcoef, sh, &mut g, &mut h);
1629 }
1630 }
1631 }
1632 let prob = QpProblem {
1633 n,
1634 p_lower: Vec::new(),
1635 c: inp.grad_f.to_vec(),
1636 a,
1637 b,
1638 g,
1639 h,
1640 lb,
1641 ub,
1642 };
1643 let opts = QpOptions::default();
1644 let sol = solve_qp_ipm(&prob, &opts, || {
1645 Box::new(pounce_feral::FeralSolverInterface::new())
1646 as Box<dyn pounce_linsol::SparseSymLinearSolverInterface>
1647 });
1648 matches!(sol.status, QpStatus::Optimal).then_some((sol.obj, sol.x))
1649}
1650
1651pub fn branch_bounds(
1655 inp: &CertificateInput<'_>,
1656 held: &[HeldSide],
1657) -> pounce_nlp::mpcc::BranchBounds {
1658 let corner = inp.solver_tol.max(0.0).sqrt();
1659 let act = pair_activity(inp, corner);
1660 let mut out = pounce_nlp::mpcc::BranchBounds::default();
1661 for (k, p) in inp.pairs.iter().enumerate() {
1662 let ((sg, bnd), (_, c)) = (act.g_side[k], act.h_side[k]);
1663 match held[k] {
1664 HeldSide::G => {
1665 out.vars.push((p.var, bnd, bnd));
1666 if act.mixed[k] {
1667 out.rows.push(if sg > 0.0 {
1669 (p.row, 0.0, 2.0 * INF)
1670 } else {
1671 (p.row, -2.0 * INF, 0.0)
1672 });
1673 }
1674 }
1675 HeldSide::H => out.rows.push((p.row, c, c)),
1676 }
1677 }
1678 out
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use super::*;
1684
1685 #[test]
1689 fn nnls_on_near_dependent_columns_terminates_at_the_fit() {
1690 let a = vec![1.0, 2.0, -1.0];
1691 let a2: Vec<Number> = a.iter().map(|v| v * (1.0 + 1e-13)).collect();
1692 let neg: Vec<Number> = a.iter().map(|v| -v).collect();
1693 let b = vec![2.0, 4.0, -2.0];
1694 let x = nnls(&[a.clone(), a2.clone(), neg], &b, 3);
1695 assert!(x.iter().all(|&v| v >= 0.0));
1696 let r: Number = (0..3)
1697 .map(|i| (b[i] - x[0] * a[i] - x[1] * a2[i] + x[2] * a[i]).powi(2))
1698 .sum();
1699 assert!(r.sqrt() < 1e-9, "residual {}", r.sqrt());
1700 }
1701
1702 #[test]
1703 fn a_converged_lowering_with_a_large_product_is_not_an_answer() {
1704 let ok = ApplicationReturnStatus::SolveSucceeded;
1705 assert!(accepts(ok, 1e-9, 1e-4));
1706 assert!(!accepts(ok, 1e-3, 1e-4));
1707 assert!(!accepts(ok, Number::NAN, 1e-4));
1708 assert!(!accepts(
1709 ApplicationReturnStatus::InfeasibleProblemDetected,
1710 0.0,
1711 1e-4
1712 ));
1713 assert!(accepts(
1714 ApplicationReturnStatus::FeasiblePointFound,
1715 0.0,
1716 1e-4
1717 ));
1718 }
1719
1720 #[test]
1721 fn warm_mu_is_clamped_and_never_zero() {
1722 let mu = |v| {
1723 warm_start_assignments(v)
1724 .into_iter()
1725 .find(|a| a.starts_with("mu_init"))
1726 .unwrap()
1727 };
1728 assert_eq!(mu(1e-12), "mu_init 1e-9\n");
1729 assert_eq!(mu(3.0), "mu_init 1e-1\n");
1730 assert_eq!(mu(0.0), "mu_init 1e-6\n");
1731 assert_eq!(mu(Number::NAN), "mu_init 1e-6\n");
1732 }
1733
1734 #[test]
1738 fn the_exact_stage_is_budgeted_only_after_an_answer() {
1739 assert_eq!(exact_stage_budget(false, 0), None);
1740 assert_eq!(exact_stage_budget(false, 5000), None);
1741 assert_eq!(exact_stage_budget(true, 0), Some(EXACT_BUDGET_FLOOR));
1742 assert_eq!(exact_stage_budget(true, 10), Some(EXACT_BUDGET_FLOOR));
1743 assert_eq!(exact_stage_budget(true, 189), Some(3 * 189));
1744 assert_eq!(exact_stage_budget(true, usize::MAX), Some(usize::MAX));
1745 }
1746
1747 fn scholtes4_relaxed(c: Number, tau: Number) -> (Number, Number) {
1753 let lambda = c / tau.sqrt();
1754 (-2.0 * c * tau.sqrt(), relaxation_slack(&[tau], &[lambda]))
1755 }
1756
1757 #[test]
1758 fn relaxation_slack_is_the_first_order_value_of_the_products() {
1759 assert_eq!(relaxation_slack(&[], &[]), 0.0);
1760 assert!((relaxation_slack(&[1e-8, 2e-8], &[1e4, -3.0]) - (1e-4 + 6e-8)).abs() < 1e-18);
1761 assert_eq!(relaxation_slack(&[-0.5], &[7.0]), 0.0);
1763 assert!(relaxation_slack(&[1e-8], &[Number::NAN]).is_infinite());
1764 assert!(relaxation_slack(&[Number::INFINITY], &[1.0]).is_infinite());
1765 }
1766
1767 #[test]
1768 fn a_finish_that_drifts_above_its_relaxed_start_is_flagged() {
1769 let slack = relaxation_slack(&[5e-9; 4], &[30.0; 4]);
1772 assert!(finish_drifted(0.783, 0.65304, slack));
1773 assert!(!finish_drifted(0.65304 + 1e-6, 0.65304, slack));
1776 assert!(!finish_drifted(0.6529, 0.65304, slack));
1777 let (f_s4, s_s4) = scholtes4_relaxed(1.0, 1e-8);
1781 assert!(!finish_drifted(3e-11, f_s4, s_s4));
1782 assert!(!finish_drifted(
1783 2e-9,
1784 -9.999818e-5,
1785 relaxation_slack(&[1e-8], &[1e4])
1786 ));
1787 assert!(finish_drifted(Number::NAN, 1.0, 1e-8));
1788 assert!(!finish_drifted(1.0, Number::INFINITY, 1e-8));
1789 assert!(!finish_drifted(5.0, 1.0, Number::INFINITY));
1790 }
1791
1792 #[test]
1798 fn the_drift_verdict_does_not_change_with_the_objectives_scale() {
1799 for c in [1e-6, 1e-3, 0.1, 1.0, 10.0, 1e3, 1e6] {
1800 let (fr, slack) = scholtes4_relaxed(c, 1e-8);
1801 assert!(!finish_drifted(c * 3e-11, fr, slack), "scholtes4 x{c}");
1802 assert!(!finish_drifted(0.0, fr, slack), "scholtes4 x{c}");
1803 assert!(finish_drifted(-1e3 * fr, fr, slack), "scholtes4 x{c}");
1806 let pack = relaxation_slack(&[5e-9; 4], &[30.0 * c; 4]);
1807 assert!(finish_drifted(0.783 * c, 0.65304 * c, pack), "pack x{c}");
1808 assert!(!finish_drifted(0.6529 * c, 0.65304 * c, pack), "pack x{c}");
1809 }
1810 assert!(!finish_drifted(
1812 3e-10,
1813 -2e-3,
1814 relaxation_slack(&[1e-8], &[1e5])
1815 ));
1816 }
1817
1818 #[test]
1819 fn answers_agree_within_the_relative_tolerance() {
1820 assert!(answers_agree(0.65304, 0.65310, 0.0, 0.0));
1822 assert!(!answers_agree(953.23, 964.12, 0.0, 0.0));
1824 assert!(answers_agree(1e-9, -5e-5, 2.5e-5, 0.0));
1828 assert!(!answers_agree(f64::NAN, 0.0, 0.0, 0.0));
1829 assert!(!answers_agree(0.0, 0.0, f64::INFINITY, 0.0));
1830 assert!(answers_agree(0.0, 0.0, 0.0, 0.0));
1831 assert!(!answers_agree(0.0, 0.0, 0.0, f64::INFINITY));
1832 }
1833
1834 #[test]
1840 fn two_answers_at_zero_agree_within_their_gaps() {
1841 for c in [1e-6, 1.0, 1e6] {
1842 assert!(
1843 answers_agree(c * 1e-11, c * 5e-20, 0.0, c * 3.5e-11),
1844 "x{c}"
1845 );
1846 assert!(!answers_agree(c * 1e-11, c * 5e-20, 0.0, 0.0), "x{c}");
1847 assert!(
1848 !answers_agree(c * 1e-9, c * 5e-20, 0.0, c * 3.5e-11),
1849 "x{c}"
1850 );
1851 }
1852 }
1853
1854 #[test]
1855 fn the_complementarity_gap_sums_multiplier_times_distance() {
1856 let inf = Number::INFINITY;
1857 let gap = complementarity_gap(
1860 &[1e-9, 2.0, 5.0],
1861 &[0.01, 0.0, 0.0],
1862 &[0.0, -4.0, 0.0],
1863 &[0.0, -inf, -inf],
1864 &[inf, 3.0, inf],
1865 &[0.5, 1.0, 7.0],
1866 &[-1e-8, 30.0, 0.0],
1867 &[0.0, 1.0, -inf],
1868 &[inf, 1.0, inf],
1869 );
1870 assert!((gap - (1e-11 + 4.0 + 0.5e-8)).abs() < 1e-15, "{gap:e}");
1871 let scaled = complementarity_gap(
1873 &[1e-9],
1874 &[1e3 * 0.01],
1875 &[0.0],
1876 &[0.0],
1877 &[inf],
1878 &[],
1879 &[],
1880 &[],
1881 &[],
1882 );
1883 assert!((scaled - 1e3 * 1e-11).abs() < 1e-20, "{scaled:e}");
1884 assert!(
1886 complementarity_gap(&[1.0], &[], &[0.0], &[0.0], &[inf], &[], &[], &[], &[])
1887 .is_infinite()
1888 );
1889 assert!(
1890 complementarity_gap(
1891 &[1.0],
1892 &[f64::NAN],
1893 &[0.0],
1894 &[0.0],
1895 &[inf],
1896 &[],
1897 &[],
1898 &[],
1899 &[]
1900 )
1901 .is_infinite()
1902 );
1903 assert_eq!(
1904 complementarity_gap(&[], &[], &[], &[], &[], &[], &[], &[], &[]),
1905 0.0
1906 );
1907 }
1908
1909 #[test]
1914 fn answers_agree_does_not_change_with_the_objectives_scale() {
1915 for c in [1e-6, 1e-3, 0.1, 1.0, 10.0, 1e3, 1e6] {
1916 assert!(answers_agree(c * 1e-9, -c * 5e-5, c * 2.5e-5, 0.0), "x{c}");
1917 assert!(!answers_agree(c * 1e-9, -c * 5e-5, 0.0, 0.0), "x{c}");
1918 assert!(answers_agree(c * 0.65304, c * 0.65310, 0.0, 0.0), "x{c}");
1919 assert!(!answers_agree(c * 953.23, c * 964.12, 0.0, 0.0), "x{c}");
1920 let (fr, slack) = scholtes4_relaxed(c, 1e-8);
1921 assert!(answers_agree(c * 3e-11, fr, slack, 0.0), "scholtes4 x{c}");
1922 }
1923 }
1924
1925 #[test]
1926 fn the_finish_budget_has_a_floor_and_follows_the_schedule() {
1927 assert_eq!(finish_budget(0), FINISH_BUDGET_FLOOR);
1928 assert_eq!(finish_budget(721), 721);
1929 }
1930
1931 #[test]
1933 fn a_budget_never_exceeds_the_callers_max_iter() {
1934 assert_eq!(budgeted_max_iter(567, 3000), 567);
1935 assert_eq!(budgeted_max_iter(567, 100), 100);
1936 assert_eq!(budgeted_max_iter(usize::MAX, 3000), 3000);
1937 assert_eq!(budgeted_max_iter(10, -1), 0);
1938 }
1939
1940 #[test]
1941 fn every_assignment_is_restored() {
1942 for a in warm_start_assignments(1e-5) {
1943 let name = a.split_whitespace().next().unwrap();
1944 assert!(
1945 TOUCHED_OPTIONS.contains(&name),
1946 "{name} is written but not restored"
1947 );
1948 }
1949 }
1950
1951 const NI: Number = -1e20;
1957 const PI: Number = 1e20;
1958
1959 #[derive(Clone)]
1960 struct Model {
1961 x_l: Vec<Number>,
1962 x_u: Vec<Number>,
1963 g_l: Vec<Number>,
1964 g_u: Vec<Number>,
1965 pairs: Vec<ComplementarityPair>,
1966 jac: Vec<(usize, usize, Number)>,
1967 }
1968
1969 fn run(m: &Model, x: &[Number], grad_f: &[Number], g: &[Number]) -> Certificate {
1972 let inp = CertificateInput {
1973 x,
1974 grad_f,
1975 g,
1976 jac: &m.jac,
1977 x_l: &m.x_l,
1978 x_u: &m.x_u,
1979 g_l: &m.g_l,
1980 g_u: &m.g_u,
1981 pairs: &m.pairs,
1982 solver_tol: 1e-8,
1983 feas_tol: 1e-4,
1984 };
1985 let dense = certify_routed(&inp, Some(false));
1986 let sparse = certify_routed(&inp, Some(true));
1987 assert_eq!(
1988 (dense.class, dense.multipliers_unique),
1989 (sparse.class, sparse.multipliers_unique),
1990 "dense {dense:?}\nsparse {sparse:?}"
1991 );
1992 if dense.multipliers_unique && dense.residual.is_finite() {
1995 assert!(
1996 (dense.residual - sparse.residual).abs() <= 1e-6 * dense.resid_tol,
1997 "dense {dense:?}\nsparse {sparse:?}"
1998 );
1999 }
2000 dense
2001 }
2002
2003 #[test]
2009 fn a_mixed_pair_is_certified_from_the_bound_it_is_at() {
2010 let m = Model {
2011 x_l: vec![0.0, NI],
2012 x_u: vec![1.0, PI],
2013 g_l: vec![NI],
2014 g_u: vec![PI],
2015 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2016 jac: vec![(0, 0, -1.0), (0, 1, 1.0)],
2017 };
2018 let g = |x: &[Number]| vec![x[1] - x[0]];
2019
2020 let x = [0.0, 2.0];
2023 let c = run(&m, &x, &[-1.0, 0.0], &g(&x));
2024 assert_eq!(c.class, StationarityClass::S, "{c:?}");
2025 assert!(c.feasible, "{c:?}");
2026
2027 let x = [1.0, 0.0];
2029 let c = run(&m, &x, &[-2.0, 0.0], &g(&x));
2030 assert_eq!(c.class, StationarityClass::S, "{c:?}");
2031
2032 let x = [0.5, 0.5];
2034 let c = run(&m, &x, &[-1.0, 1.0], &g(&x));
2035 assert_eq!(c.class, StationarityClass::S, "{c:?}");
2036
2037 let x = [0.0, -1.0];
2039 let c = run(&m, &x, &[0.0, 0.0], &g(&x));
2040 assert_eq!(c.class, StationarityClass::None, "{c:?}");
2041 assert!(
2042 !c.feasible && (c.primal_violation - 1.0).abs() < 1e-12,
2043 "{c:?}"
2044 );
2045
2046 let x = [0.5, 1.0];
2048 let c = run(&m, &x, &[0.0, 0.0], &g(&x));
2049 assert_eq!(
2050 (c.class, c.n_neither),
2051 (StationarityClass::None, 1),
2052 "{c:?}"
2053 );
2054 }
2055
2056 #[test]
2061 fn a_pair_variables_ordinary_second_bound_carries_a_multiplier() {
2062 let m = Model {
2063 x_l: vec![0.0, NI],
2064 x_u: vec![1.0, PI],
2065 g_l: vec![0.0],
2066 g_u: vec![PI],
2067 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2068 jac: vec![(0, 1, 1.0)],
2069 };
2070 let x = [1.0, 0.0];
2071 let c = run(&m, &x, &[-2.0, -2.0], &[x[1]]);
2072 assert_eq!(c.class, StationarityClass::S, "{c:?}");
2073 let x = [0.0, 1.0];
2076 let c = run(&m, &x, &[-4.0, 0.0], &[x[1]]);
2077 assert_eq!(c.class, StationarityClass::S, "{c:?}");
2078 }
2079
2080 fn input<'a>(
2081 m: &'a Model,
2082 x: &'a [Number],
2083 grad_f: &'a [Number],
2084 g: &'a [Number],
2085 ) -> CertificateInput<'a> {
2086 CertificateInput {
2087 x,
2088 grad_f,
2089 g,
2090 jac: &m.jac,
2091 x_l: &m.x_l,
2092 x_u: &m.x_u,
2093 g_l: &m.g_l,
2094 g_u: &m.g_u,
2095 pairs: &m.pairs,
2096 solver_tol: 1e-8,
2097 feas_tol: 1e-4,
2098 }
2099 }
2100
2101 #[test]
2105 fn ralph1s_origin_is_b_stationary() {
2106 let m = Model {
2107 x_l: vec![0.0, 0.0],
2108 x_u: vec![PI, PI],
2109 g_l: vec![0.0],
2110 g_u: vec![PI],
2111 pairs: vec![ComplementarityPair { row: 0, var: 1 }],
2112 jac: vec![(0, 0, -1.0), (0, 1, 1.0)],
2113 };
2114 let (x, gf, g) = ([0.0, 0.0], [2.0, -1.0], [0.0]);
2115 assert_eq!(run(&m, &x, &gf, &g).class, StationarityClass::W);
2116 let b = check_b_stationarity(&input(&m, &x, &gf, &g));
2117 assert_eq!(b.verdict, BVerdict::B, "{b:?}");
2118 assert_eq!(
2119 (b.n_biactive, b.branches_solved, b.enumerated),
2120 (1, 2, true)
2121 );
2122 }
2123
2124 #[test]
2128 fn a_biactive_corner_with_descent_is_not_b_stationary() {
2129 let m = Model {
2130 x_l: vec![0.0, NI],
2131 x_u: vec![PI, PI],
2132 g_l: vec![0.0],
2133 g_u: vec![PI],
2134 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2135 jac: vec![(0, 1, 1.0)],
2136 };
2137 let (x, gf, g) = ([0.0, 0.0], [-2.0, -2.0], [0.0]);
2138 let inp = input(&m, &x, &gf, &g);
2139 let b = check_b_stationarity(&inp);
2140 let BVerdict::NotB { held, decrease } = &b.verdict else {
2141 panic!("{b:?}")
2142 };
2143 assert!((decrease + 2.0).abs() < 1e-6, "{b:?}");
2144 let bb = branch_bounds(&inp, held);
2145 match held[0] {
2146 HeldSide::G => assert_eq!(bb.vars, vec![(0, 0.0, 0.0)], "{bb:?}"),
2147 HeldSide::H => assert_eq!(bb.rows, vec![(0, 0.0, 0.0)], "{bb:?}"),
2148 }
2149 let (x, gf, g) = ([0.0, 1.0], [-2.0, 0.0], [1.0]);
2152 assert_eq!(
2153 check_b_stationarity(&input(&m, &x, &gf, &g)).verdict,
2154 BVerdict::B
2155 );
2156 }
2157
2158 #[test]
2161 fn past_the_enumeration_cap_the_heuristic_finds_a_descent() {
2162 let k = MAX_ENUMERATED_BIACTIVE + 1;
2163 let n = 2 * k;
2164 let m = Model {
2165 x_l: (0..n).map(|j| if j < k { 0.0 } else { NI }).collect(),
2166 x_u: vec![PI; n],
2167 g_l: vec![0.0; k],
2168 g_u: vec![PI; k],
2169 pairs: (0..k)
2170 .map(|i| ComplementarityPair { row: i, var: i })
2171 .collect(),
2172 jac: (0..k).map(|i| (i, k + i, 1.0)).collect(),
2173 };
2174 let (x, gf, g) = (vec![0.0; n], vec![-1.0; n], vec![0.0; k]);
2175 let b = check_b_stationarity(&input(&m, &x, &gf, &g));
2176 assert!(!b.enumerated && b.branches_solved == 2, "{b:?}");
2177 assert!(matches!(b.verdict, BVerdict::NotB { .. }), "{b:?}");
2178 }
2179
2180 #[test]
2181 fn a_point_where_a_pair_is_not_complementary_is_not_checked() {
2182 let m = Model {
2183 x_l: vec![0.0, NI],
2184 x_u: vec![PI, PI],
2185 g_l: vec![0.0],
2186 g_u: vec![PI],
2187 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2188 jac: vec![(0, 1, 1.0)],
2189 };
2190 let (x, gf, g) = ([1.0, 1.0], [0.0, 0.0], [1.0]);
2191 let b = check_b_stationarity(&input(&m, &x, &gf, &g));
2192 assert!(matches!(b.verdict, BVerdict::NotChecked(_)), "{b:?}");
2193 }
2194
2195 #[allow(clippy::type_complexity)]
2200 fn complementarity() -> (Model, impl Fn(&[Number]) -> (Vec<Number>, Vec<Number>)) {
2201 let mut jac = vec![
2202 (0, 1, 0.5),
2203 (0, 2, 1.0),
2204 (0, 3, -0.5),
2205 (0, 4, 1.0),
2206 (1, 0, 3.0),
2207 (1, 1, -1.0),
2208 (1, 5, -1.0),
2209 (2, 0, -1.0),
2210 (2, 1, 0.5),
2211 (2, 6, -1.0),
2212 (3, 0, -1.0),
2213 (3, 1, -1.0),
2214 (3, 7, -1.0),
2215 ];
2216 for k in 0..3 {
2217 jac.push((4 + k, 5 + k, 1.0));
2218 }
2219 let m = Model {
2220 x_l: vec![0.0, 0.0, 0.0, 0.0, 0.0, NI, NI, NI],
2221 x_u: vec![PI; 8],
2222 g_l: vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2223 g_u: vec![0.0, 0.0, 0.0, 0.0, PI, PI, PI],
2224 pairs: (0..3)
2225 .map(|k| ComplementarityPair {
2226 row: 4 + k,
2227 var: 2 + k,
2228 })
2229 .collect(),
2230 jac,
2231 };
2232 let eval = |x: &[Number]| {
2233 let grad = vec![
2234 2.0 * (x[0] - 5.0),
2235 4.0 * (2.0 * x[1] + 1.0),
2236 0.0,
2237 0.0,
2238 0.0,
2239 0.0,
2240 0.0,
2241 0.0,
2242 ];
2243 let g = vec![
2244 2.0 * (x[1] - 1.0) - 1.5 * x[1] + x[2] - 0.5 * x[3] + x[4],
2245 3.0 * x[0] - x[1] - 3.0 - x[5],
2246 -x[0] + 0.5 * x[1] + 4.0 - x[6],
2247 -x[0] - x[1] + 7.0 - x[7],
2248 x[5],
2249 x[6],
2250 x[7],
2251 ];
2252 (grad, g)
2253 };
2254 (m, eval)
2255 }
2256
2257 #[test]
2264 fn a_large_system_with_unique_multipliers_is_certified_sparsely() {
2265 let n = 2001;
2266 let m = Model {
2267 x_l: vec![0.0; n],
2268 x_u: vec![PI; n],
2269 g_l: vec![],
2270 g_u: vec![],
2271 pairs: vec![],
2272 jac: vec![],
2273 };
2274 assert!(n * n > MAX_DENSE_ENTRIES);
2275 let mut grad = vec![1.0; n];
2276 let cert = |grad: &[Number]| {
2277 certify(&CertificateInput {
2278 x: &vec![0.0; n],
2279 grad_f: grad,
2280 g: &[],
2281 jac: &m.jac,
2282 x_l: &m.x_l,
2283 x_u: &m.x_u,
2284 g_l: &m.g_l,
2285 g_u: &m.g_u,
2286 pairs: &m.pairs,
2287 solver_tol: 1e-8,
2288 feas_tol: 1e-4,
2289 })
2290 };
2291 let c = cert(&grad);
2292 assert_eq!(
2293 (c.class, c.multipliers_unique),
2294 (StationarityClass::S, true),
2295 "{c:?}"
2296 );
2297 grad[7] = -1.0;
2298 let c = cert(&grad);
2299 assert_eq!(c.class, StationarityClass::None, "{c:?}");
2300 }
2301
2302 #[test]
2310 fn a_badly_scaled_column_gets_the_same_verdict_on_both_paths() {
2311 for eps in [1e-3, 1e-5, 1e-7, 1e-9, 1e-12, 1e-14, 0.0] {
2312 let m = Model {
2313 x_l: vec![0.0, -2e19],
2314 x_u: vec![2e19, 2e19],
2315 g_l: vec![0.0],
2316 g_u: vec![2e19],
2317 pairs: vec![],
2318 jac: vec![(0, 0, 1.0), (0, 1, eps)],
2319 };
2320 let c = run(&m, &[0.0, 0.0], &[2.0, eps], &[0.0]);
2322 assert_eq!(c.class, StationarityClass::S, "{eps:e}: {c:?}");
2323 assert_eq!(c.multipliers_unique, eps != 0.0, "{eps:e}: {c:?}");
2324 let cols = vec![vec![(0, 1.0)], vec![(0, 1.0), (1, eps)]];
2325 match sparse_least_squares(2, &cols, &[2.0, eps]) {
2326 LeastSquares::Solved(theta) => {
2327 assert!(eps != 0.0, "{theta:?}");
2328 assert!(
2329 theta.iter().all(|t| (t - 1.0).abs() < 1e-12),
2330 "{eps:e}: {theta:?}"
2331 );
2332 }
2333 other => assert!(
2334 eps == 0.0 && other == LeastSquares::RankDeficient,
2335 "{eps:e}: {other:?}"
2336 ),
2337 }
2338 }
2339 }
2340
2341 #[test]
2349 fn the_residual_band_is_the_same_on_the_unique_path() {
2350 let m = Model {
2351 x_l: vec![-2e19],
2352 x_u: vec![2e19],
2353 g_l: vec![],
2354 g_u: vec![],
2355 pairs: vec![],
2356 jac: vec![],
2357 };
2358 for (r, class, tight) in [
2361 (5e-7, StationarityClass::S, true),
2362 (2e-5, StationarityClass::S, false),
2363 (9e-5, StationarityClass::S, false),
2364 (2e-4, StationarityClass::None, false),
2365 ] {
2366 let c = run(&m, &[0.3], &[r], &[]);
2367 assert_eq!((c.class, c.tight), (class, tight), "{r:e}: {c:?}");
2368 assert!((c.resid_tol - (1e-6 + 1e-4)).abs() < 1e-15, "{c:?}");
2369 }
2370 }
2371
2372 #[test]
2377 fn a_point_violating_a_row_beyond_constr_viol_tol_is_not_certified() {
2378 let m = Model {
2379 x_l: vec![-2e19],
2380 x_u: vec![2e19],
2381 g_l: vec![1.0],
2382 g_u: vec![2e19],
2383 pairs: vec![],
2384 jac: vec![(0, 0, 1.0)],
2385 };
2386 let x = 1.0 - 5e-4;
2387 let c = run(&m, &[x], &[1.0], &[x]);
2388 assert_eq!(
2389 (c.class, c.feasible),
2390 (StationarityClass::None, false),
2391 "{c:?}"
2392 );
2393 assert!((c.primal_violation - 5e-4).abs() < 1e-12, "{c:?}");
2394 let x = 1.0 - 5e-5;
2396 let c = run(&m, &[x], &[1.0], &[x]);
2397 assert_eq!((c.class, c.feasible), (StationarityClass::S, true), "{c:?}");
2398 let m = Model {
2400 x_l: vec![0.0],
2401 g_l: vec![],
2402 g_u: vec![],
2403 jac: vec![],
2404 ..m
2405 };
2406 let c = run(&m, &[-2e-3], &[1.0], &[]);
2407 assert_eq!(
2408 (c.class, c.feasible),
2409 (StationarityClass::None, false),
2410 "{c:?}"
2411 );
2412 }
2413
2414 #[test]
2419 fn a_large_rank_deficient_system_is_not_searched() {
2420 let n = 2001;
2421 let c = certify(&CertificateInput {
2422 x: &vec![0.0; n],
2423 grad_f: &vec![1.0; n],
2424 g: &[0.0],
2425 jac: &[(0, 0, 1.0)],
2426 x_l: &vec![0.0; n],
2427 x_u: &vec![PI; n],
2428 g_l: &[0.0],
2429 g_u: &[2e19],
2430 pairs: &[],
2431 solver_tol: 1e-8,
2432 feas_tol: 1e-4,
2433 });
2434 assert_eq!(
2435 (c.class, c.multipliers_unique),
2436 (StationarityClass::NotSearched, false),
2437 "{c:?}"
2438 );
2439 assert!(n * (n + 1) > MAX_DENSE_ENTRIES);
2440 }
2441
2442 #[test]
2451 fn the_set_search_band_is_resid_tol_plus_the_sign_band() {
2452 let m = Model {
2453 x_l: vec![NI],
2454 x_u: vec![PI],
2455 g_l: vec![0.0, 0.0],
2456 g_u: vec![PI, PI],
2457 pairs: vec![],
2458 jac: vec![(0, 0, 1.0), (1, 0, 1.0)],
2459 };
2460 let inside = run(&m, &[0.0], &[-5e-5], &[0.0, 0.0]);
2461 assert!(!inside.multipliers_unique, "{inside:?}");
2462 assert_eq!(inside.class, StationarityClass::S, "{inside:?}");
2463 let outside = run(&m, &[0.0], &[-2e-4], &[0.0, 0.0]);
2464 assert!(!outside.multipliers_unique, "{outside:?}");
2465 assert_eq!(outside.class, StationarityClass::None, "{outside:?}");
2466 }
2467
2468 #[test]
2469 fn certify_matches_the_python_classifier_on_complementarity() {
2470 let (m, eval) = complementarity();
2471 for (x, want, nb) in [
2472 (
2473 [1.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0, 6.0],
2474 StationarityClass::S,
2475 0,
2476 ), (
2478 [5.0, 2.0, 0.0, 0.0, 1.0, 10.0, 0.0, 0.0],
2479 StationarityClass::S,
2480 1,
2481 ), (
2483 [3.0, 4.0, 0.0, 0.0, 0.0, 2.0, 3.0, 0.0],
2484 StationarityClass::W,
2485 1,
2486 ), ] {
2488 let (grad, g) = eval(&x);
2489 assert!(
2490 g[..4].iter().all(|v| v.abs() < 1e-12),
2491 "{x:?} is not feasible: {g:?}"
2492 );
2493 let c = run(&m, &x, &grad, &g);
2494 assert_eq!((c.class, c.n_biactive), (want, nb), "{x:?}: {c:?}");
2495 }
2496 }
2497
2498 #[test]
2499 fn certify_reads_kkt_on_a_plain_nlp() {
2500 let m = Model {
2502 x_l: vec![NI; 2],
2503 x_u: vec![PI; 2],
2504 g_l: vec![1.0],
2505 g_u: vec![1.0],
2506 pairs: vec![],
2507 jac: vec![],
2508 };
2509 let at = |x: [Number; 2]| {
2510 let mut mm = Model {
2511 jac: vec![(0, 0, 2.0 * x[0]), (0, 1, 2.0 * x[1])],
2512 ..m.clone()
2513 };
2514 mm.jac.retain(|e| e.2 != 0.0);
2515 let grad = [4.0 * x[0] - 1.0, 4.0 * x[1]];
2516 run(&mm, &x, &grad, &[x[0] * x[0] + x[1] * x[1]]).class
2517 };
2518 assert_eq!(at([1.0, 0.0]), StationarityClass::S);
2519 assert_eq!(at([0.0, 1.0]), StationarityClass::None); }
2521
2522 #[test]
2523 fn certify_ralph1_ctrap_and_qpec_small() {
2524 let m = Model {
2526 x_l: vec![0.0, 0.0],
2527 x_u: vec![PI; 2],
2528 g_l: vec![0.0],
2529 g_u: vec![PI],
2530 pairs: vec![ComplementarityPair { row: 0, var: 1 }],
2531 jac: vec![(0, 0, -1.0), (0, 1, 1.0)],
2532 };
2533 let c = run(&m, &[0.0, 0.0], &[2.0, -1.0], &[0.0]);
2534 assert_eq!((c.class, c.n_biactive), (StationarityClass::W, 1), "{c:?}");
2535
2536 let mirrored = Model {
2539 x_l: vec![NI, NI],
2540 x_u: vec![0.0, 0.0],
2541 g_l: vec![NI],
2542 g_u: vec![0.0],
2543 pairs: vec![ComplementarityPair { row: 0, var: 1 }],
2544 jac: vec![(0, 0, -1.0), (0, 1, 1.0)],
2545 };
2546 let c = run(&mirrored, &[0.0, 0.0], &[-2.0, 1.0], &[0.0]);
2547 assert_eq!((c.class, c.n_biactive), (StationarityClass::W, 1), "{c:?}");
2548
2549 let m = Model {
2551 x_l: vec![0.0, NI],
2552 x_u: vec![PI; 2],
2553 g_l: vec![0.0],
2554 g_u: vec![PI],
2555 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2556 jac: vec![(0, 1, 1.0)],
2557 };
2558 let c = run(&m, &[0.0, 0.0], &[-1.0, -1.0], &[0.0]);
2559 assert_eq!(c.class, StationarityClass::W, "{c:?}");
2560 let c = run(&m, &[0.5, 0.0], &[0.0, -1.0], &[0.0]);
2562 assert_eq!((c.class, c.n_biactive), (StationarityClass::S, 0), "{c:?}");
2563
2564 let m = Model {
2566 x_l: vec![0.0, 0.0, 0.0],
2567 x_u: vec![2.0, PI, PI],
2568 g_l: vec![0.0, 0.0],
2569 g_u: vec![PI, PI],
2570 pairs: vec![
2571 ComplementarityPair { row: 0, var: 1 },
2572 ComplementarityPair { row: 1, var: 2 },
2573 ],
2574 jac: vec![(0, 0, -1.0), (0, 1, 2.0), (1, 0, 1.0), (1, 2, 2.0)],
2575 };
2576 let c = run(&m, &[1.0, 1.0, 0.0], &[0.0, 0.0, 0.0], &[0.0, 0.0]);
2577 assert_eq!((c.class, c.n_biactive), (StationarityClass::S, 1), "{c:?}");
2578 let m = Model {
2581 x_l: vec![NI, NI],
2582 x_u: vec![0.0, PI],
2583 g_l: vec![NI],
2584 g_u: vec![0.0],
2585 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2586 jac: vec![(0, 1, 1.0)],
2587 };
2588 let c = run(&m, &[-0.5, 0.0], &[0.0, 1.0], &[0.0]);
2589 assert_eq!((c.class, c.n_biactive), (StationarityClass::S, 0), "{c:?}");
2590 let c = run(&m, &[0.0, 0.0], &[1.0, 1.0], &[0.0]);
2591 assert_eq!(c.class, StationarityClass::W, "{c:?}");
2592 }
2593
2594 #[test]
2602 fn certify_searches_the_multiplier_set_when_it_is_not_a_point() {
2603 let m = Model {
2604 x_l: vec![0.0, NI],
2605 x_u: vec![PI; 2],
2606 g_l: vec![0.0, 0.0],
2607 g_u: vec![PI, 0.0],
2608 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2609 jac: vec![(0, 1, 1.0), (1, 1, 1.0)],
2610 };
2611 let c = run(&m, &[0.0, 0.0], &[1.0, -0.1], &[0.0, 0.0]);
2612 assert!(!c.multipliers_unique, "{c:?}");
2613 assert_eq!((c.class, c.n_biactive), (StationarityClass::S, 1), "{c:?}");
2614 let a = Mat::<Number>::from_fn(2, 3, |r, col| match (r, col) {
2616 (0, 0) => 1.0,
2617 (1, 1) | (1, 2) => 1.0,
2618 _ => 0.0,
2619 });
2620 let (theta, _) = lstsq(&a, &[1.0, -0.1]);
2621 assert!(theta[1] < 0.0, "{theta:?}");
2622 }
2623
2624 #[test]
2625 fn certify_refuses_a_point_where_a_pair_is_not_complementary() {
2626 let m = Model {
2627 x_l: vec![0.0, NI],
2628 x_u: vec![PI; 2],
2629 g_l: vec![0.0],
2630 g_u: vec![PI],
2631 pairs: vec![ComplementarityPair { row: 0, var: 0 }],
2632 jac: vec![(0, 1, 1.0)],
2633 };
2634 let c = run(&m, &[0.5, 0.5], &[0.0, 0.0], &[0.5]);
2635 assert_eq!(
2636 (c.class, c.n_neither),
2637 (StationarityClass::None, 1),
2638 "{c:?}"
2639 );
2640 }
2641}