1use pounce_common::options_list::OptionsList;
160use pounce_common::reg_options::{DefaultValue, RegisteredOptions};
161
162pub struct UnimplementedFeature {
164 pub feature: &'static str,
166 pub advice: &'static str,
168 pub options: &'static [&'static str],
170 pub issue: u32,
172}
173
174pub const UNIMPLEMENTED_FEATURES: &[UnimplementedFeature] = &[
176 UnimplementedFeature {
177 issue: 483,
178 feature: "the Chen-Goldfarb (CG-penalty) / inexact-Newton line search \
179 — Ipopt's `CGPenaltyLSAcceptor`",
180 advice: "pounce implements the filter line search (the default) and \
181 `line_search_method=penalty` (`IpPenaltyLSAcceptor`); tune \
182 those instead",
183 options: &[
184 "chi_cup",
185 "chi_hat",
186 "chi_tilde",
187 "delta_y_max",
188 "epsilon_c",
189 "eta_min",
190 "fast_des_fact",
191 "gamma_hat",
192 "gamma_tilde",
193 "kappa_x_dis",
194 "kappa_y_dis",
195 "min_alpha_primal",
196 "mult_diverg_feasibility_tol",
197 "mult_diverg_y_tol",
198 "never_use_fact_cgpen_direction",
199 "never_use_piecewise_penalty_ls",
200 "pen_des_fact",
201 "pen_init_fac",
202 "pen_theta_max_fact",
203 "penalty_init_max",
204 "penalty_init_min",
205 "penalty_max",
206 "penalty_update_compl_tol",
207 "penalty_update_infeasibility_tol",
208 "piecewisepenalty_gamma_infeasi",
209 "piecewisepenalty_gamma_obj",
210 "vartheta",
211 "inexact_algorithm",
212 ],
213 },
214 UnimplementedFeature {
215 issue: 551,
216 feature: "the CG-penalty acceptor's `theta_min` — the constraint-violation \
217 threshold its piecewise-penalty tests switch on. It is \
218 registered by `IpCGPenaltyLSAcceptor`, not by the filter \
219 acceptor, and pounce has no CG-penalty acceptor to point it at",
220 advice: "the filter line search has a theta_min of its own, but derives \
221 it the way upstream does — `theta_min_fact * max(1, theta_0)`, \
222 never set directly — so set `theta_min_fact` to move it",
223 options: &["theta_min"],
224 },
225 UnimplementedFeature {
226 issue: 551,
227 feature: "the `primal-and-full` / `dual-and-full` equality-multiplier \
228 step rules, which is all this tolerance configures — under \
229 them the multiplier step jumps to 1 once the max-norm of the \
230 primal step drops below it",
231 advice: "pounce implements `alpha_for_y` = `primal` (the default), \
232 `bound-mult`, `min`, `max` and `full`; `full` takes the unit \
233 multiplier step unconditionally",
234 options: &["alpha_for_y_tol"],
235 },
236 UnimplementedFeature {
237 issue: 483,
238 feature: "derivative approximation by finite differences",
239 advice: "supply `eval_grad_f` / `eval_jac_g` / `eval_h`, and check them \
240 with `derivative_test=first-order`",
241 options: &[
242 "gradient_approximation",
243 "jacobian_approximation",
244 "findiff_perturbation",
245 ],
246 },
247 UnimplementedFeature {
248 issue: 483,
249 feature: "linear-dependency detection on the equality constraints",
250 advice: "pounce's presolve removes structurally redundant rows; see \
251 `presolve`",
252 options: &[
253 "dependency_detector",
254 "dependency_detection_with_rhs",
255 "ma28_pivtol",
256 ],
257 },
258 UnimplementedFeature {
259 issue: 483,
260 feature: "the per-iteration NaN/Inf check on derivative matrices",
261 advice: "`derivative_test=first-order` checks the derivatives once, at \
262 the starting point",
263 options: &["check_derivatives_for_naninf"],
264 },
265 UnimplementedFeature {
266 issue: 483,
267 feature: "least-square initialization of *all* dual variables \
268 (the first-order-optimality fit)",
269 advice: "the equality multipliers are least-square initialized \
270 regardless (capped by `constr_mult_init_max`), and the bound \
271 multipliers take `bound_mult_init_val` — which is what \
272 `least_square_init_duals=no` asks for",
273 options: &["least_square_init_duals"],
274 },
275 UnimplementedFeature {
276 issue: 483,
277 feature: "a selectable constraint-violation norm",
278 advice: "pounce measures the violation in the 2-norm throughout",
279 options: &["constraint_violation_norm_type"],
280 },
281 UnimplementedFeature {
282 issue: 483,
283 feature: "magic steps",
284 advice: "",
285 options: &["magic_steps"],
286 },
287 UnimplementedFeature {
288 issue: 483,
289 feature: "bound replacement on the original problem",
290 advice: "",
291 options: &["replace_bounds"],
292 },
293 UnimplementedFeature {
294 issue: 483,
295 feature: "the L-BFGS augmented-system and space variants",
296 advice: "`hessian_approximation=limited-memory` uses the low-rank \
297 augmented system unconditionally",
298 options: &["hessian_approximation_space", "limited_memory_aug_solver"],
299 },
300 UnimplementedFeature {
301 issue: 483,
302 feature: "skipping the finalize-solution callback",
303 advice: "",
304 options: &["skip_finalize_solution_call"],
305 },
306 UnimplementedFeature {
307 issue: 483,
308 feature: "the dynamic HSL loader",
309 advice: "MA57 is linked at build time with `--features ma57`",
310 options: &["hsllib"],
311 },
312 UnimplementedFeature {
313 issue: 483,
314 feature: "these output controls",
315 advice: "use `print_level` (0 silences the solver) and `sb=yes` to \
316 suppress the banner",
317 options: &["suppress_all_output", "debug_print_level"],
318 },
319 UnimplementedFeature {
320 issue: 483,
321 feature: "a randomly perturbed evaluation point for the derivative \
322 checker",
323 advice: "pounce's checker tests at the (bound-projected) starting point, \
324 which is where the solve actually begins",
325 options: &["point_perturbation_radius"],
326 },
327 UnimplementedFeature {
334 issue: 551,
335 feature: "the corrector step tried inside the filter line search \
336 under the adaptive barrier strategy — Ipopt's \
337 `FilterLSAcceptor::TryCorrector`, which these four knobs \
338 select and safeguard",
339 advice: "pounce's line search takes no corrector step at all, so \
340 there is nothing for these to gate. The predictor-corrector \
341 pounce does implement is Mehrotra's, applied to the \
342 search-direction right-hand side rather than as a \
343 line-search trial: `mehrotra_algorithm=yes` is read and \
344 honoured (it also selects `mu_strategy=adaptive` and \
345 `mu_oracle=probing`)",
346 options: &[
347 "corrector_type",
348 "skip_corr_if_neg_curv",
349 "skip_corr_in_monotone_mode",
350 "corrector_compl_avrg_red_fact",
351 ],
352 },
353 UnimplementedFeature {
354 issue: 551,
355 feature: "the `expect_infeasible_problem` heuristics inside the \
356 filter line search — switching them off once the \
357 constraint violation drops below a threshold (`_ctol`), \
358 and diverting to restoration once the constraint \
359 multipliers' max-norm rises above one (`_ytol`)",
360 advice: "the restoration phase itself runs and is unaffected; \
361 pounce enters it when the line search cannot make \
362 progress, and `required_infeasibility_reduction` sets how \
363 much infeasibility reduction it must deliver before \
364 handing back. `IpBacktrackingLineSearch`'s \
365 `count_successive_shortened_steps_` machinery, which is \
366 what these two thresholds steer, has no counterpart here",
367 options: &[
368 "expect_infeasible_problem_ctol",
369 "expect_infeasible_problem_ytol",
370 ],
371 },
372 UnimplementedFeature {
373 issue: 551,
374 feature: "the special quasi-Newton update Ipopt used inside the \
375 restoration phase before Nov 2010",
376 advice: "L-BFGS runs in the restoration sub-solve; it uses the \
377 regular update procedure there, which is what \
378 `limited_memory_special_for_resto=no` — upstream's own \
379 default, and its recommendation — asks for. Only the \
380 revert to the old update is missing",
381 options: &["limited_memory_special_for_resto"],
382 },
383 UnimplementedFeature {
384 issue: 551,
385 feature: "declaring the restoration phase *failed* when it stops on \
386 the acceptable-point criteria at a primal infeasibility \
387 below a threshold",
388 advice: "restoration runs and reports failure on its own terms — \
389 the reduction guard (`required_infeasibility_reduction`), \
390 the successive-iteration cap (`max_resto_iter`), and the \
391 locally-infeasible verdicts, which measure a violation \
392 against `constr_viol_tol` relative to the offending row. \
393 There is no threshold below which a stopped restoration is \
394 reclassified as a failure, so this option has nothing to \
395 set",
396 options: &["resto_failure_feasibility_threshold"],
397 },
398 UnimplementedFeature {
399 issue: 551,
400 feature: "choosing the barrier parameter with an *oracle* when the \
401 adaptive strategy leaves free mode — Ipopt's \
402 `fix_mu_oracle_`",
403 advice: "pounce implements `fixed_mu_oracle=average_compl` (the \
404 default): the switch into fixed mode seeds μ with \
405 `adaptive_mu_monotone_init_factor · avrg_compl`, which \
406 that factor tunes. The probing / loqo / quality-function \
407 oracles are implemented, but only for `mu_oracle`, which \
408 drives μ in free mode",
409 options: &["fixed_mu_oracle"],
410 },
411 UnimplementedFeature {
412 issue: 606,
413 feature: "reuse of a previously-solved iterate or problem structure \
414 through Ipopt's `TNLP::GetWarmStartIterate` surface",
415 advice: "pounce's warm start goes through `TNLP::get_starting_point` \
416 with `warm_start_init_point=yes`, which carries the primal \
417 point and all three multiplier blocks; from Python, \
418 `pounce.WarmStart.from_info` packages it",
419 options: &["warm_start_entire_iterate", "warm_start_same_structure"],
420 },
421 UnimplementedFeature {
422 issue: 677,
423 feature: "sensitivity over more than one perturbation tier — upstream \
424 sIPOPT walks `sens_state_1`, `sens_state_2`, … one tier per \
425 step and reports a `sens_sol_state_k` for each",
426 advice: "pounce computes the single `sens_state_1` tier, which is what \
427 `n_sens_steps=1` (the default) asks for; for a multi-step \
428 parameter path, run one solve per perturbation, or drive \
429 `pounce_sensitivity::Solver::parametric_step` in a loop \
430 against the one converged factor",
431 options: &["n_sens_steps"],
432 },
433];
434
435pub struct UnimplementedValue {
445 pub option: &'static str,
447 pub value: &'static str,
449 pub feature: &'static str,
451 pub advice: &'static str,
453}
454
455pub const UNIMPLEMENTED_VALUES: &[UnimplementedValue] = &[UnimplementedValue {
457 option: "bound_mult_init_method",
458 value: "mu-based",
459 feature: "initializing each bound multiplier to mu_init divided by its \
460 own slack",
461 advice: "`bound_mult_init_method=constant` (the default) initializes them \
462 all to `bound_mult_init_val`, which you can set",
463}];
464
465pub const UNEXPLOITED_HINTS: &[&str] = &[];
485
486pub const CONVEX_UNEXPLOITED_HINTS: &[&str] = &pounce_nlp::constant_derivatives::HINT_OPTIONS;
504
505pub struct UnimplementedBackend {
512 pub backend: &'static str,
515 pub family: &'static str,
519 pub options: &'static [&'static str],
523}
524
525pub const UNIMPLEMENTED_BACKENDS: &[UnimplementedBackend] = &[
528 UnimplementedBackend {
529 backend: "the HSL MA27 sparse symmetric linear solver",
530 family: "ma27_*",
531 options: &[
532 "ma27_ignore_singularity",
533 "ma27_la_init_factor",
534 "ma27_liw_init_factor",
535 "ma27_meminc_factor",
536 "ma27_pivtol",
537 "ma27_pivtolmax",
538 "ma27_print_level",
539 "ma27_skip_inertia_check",
540 ],
541 },
542 UnimplementedBackend {
543 backend: "the HSL MA77 out-of-core sparse symmetric linear solver",
544 family: "ma77_*",
545 options: &[
546 "ma77_buffer_lpage",
547 "ma77_buffer_npage",
548 "ma77_file_size",
549 "ma77_maxstore",
550 "ma77_nemin",
551 "ma77_order",
552 "ma77_print_level",
553 "ma77_small",
554 "ma77_static",
555 "ma77_u",
556 "ma77_umax",
557 ],
558 },
559 UnimplementedBackend {
560 backend: "the HSL MA86 parallel sparse symmetric linear solver",
561 family: "ma86_*",
562 options: &[
563 "ma86_nemin",
564 "ma86_order",
565 "ma86_print_level",
566 "ma86_scaling",
567 "ma86_small",
568 "ma86_static",
569 "ma86_u",
570 "ma86_umax",
571 ],
572 },
573 UnimplementedBackend {
574 backend: "the HSL MA97 sparse symmetric linear solver",
575 family: "ma97_*",
576 options: &[
577 "ma97_dump_matrix",
578 "ma97_nemin",
579 "ma97_order",
580 "ma97_print_level",
581 "ma97_scaling",
582 "ma97_scaling1",
583 "ma97_scaling2",
584 "ma97_scaling3",
585 "ma97_small",
586 "ma97_solve_blas3",
587 "ma97_switch1",
588 "ma97_switch2",
589 "ma97_switch3",
590 "ma97_u",
591 "ma97_umax",
592 ],
593 },
594 UnimplementedBackend {
595 backend: "the MUMPS sparse symmetric linear solver",
596 family: "mumps_*",
597 options: &[
598 "mumps_dep_tol",
599 "mumps_mem_percent",
600 "mumps_mpi_communicator",
601 "mumps_permuting_scaling",
602 "mumps_pivot_order",
603 "mumps_pivtol",
604 "mumps_pivtolmax",
605 "mumps_print_level",
606 "mumps_scaling",
607 ],
608 },
609 UnimplementedBackend {
610 backend: "the Pardiso linear solver (pardiso-project.org)",
611 family: "pardiso_*",
612 options: &[
613 "pardiso_iter_coarse_size",
614 "pardiso_iter_dropping_factor",
615 "pardiso_iter_dropping_schur",
616 "pardiso_iter_inverse_norm_factor",
617 "pardiso_iter_max_levels",
618 "pardiso_iter_max_row_fill",
619 "pardiso_iter_relative_tol",
620 "pardiso_iterative",
621 "pardiso_matching_strategy",
622 "pardiso_max_droptol_corrections",
623 "pardiso_max_iter",
624 "pardiso_max_iterative_refinement_steps",
625 "pardiso_msglvl",
626 "pardiso_order",
627 "pardiso_redo_symbolic_fact_only_if_inertia_wrong",
628 "pardiso_repeated_perturbation_means_singular",
629 "pardiso_skip_inertia_check",
630 "pardisolib",
631 ],
632 },
633 UnimplementedBackend {
634 backend: "the Pardiso linear solver bundled with Intel MKL",
635 family: "pardisomkl_*",
636 options: &[
637 "pardisomkl_matching_strategy",
638 "pardisomkl_max_iterative_refinement_steps",
639 "pardisomkl_msglvl",
640 "pardisomkl_order",
641 "pardisomkl_redo_symbolic_fact_only_if_inertia_wrong",
642 "pardisomkl_repeated_perturbation_means_singular",
643 "pardisomkl_skip_inertia_check",
644 ],
645 },
646 UnimplementedBackend {
647 backend: "the SPRAL SSIDS sparse symmetric linear solver",
648 family: "spral_*",
649 options: &[
650 "spral_cpu_block_size",
651 "spral_gpu_perf_coeff",
652 "spral_ignore_numa",
653 "spral_max_load_inbalance",
654 "spral_min_gpu_work",
655 "spral_nemin",
656 "spral_order",
657 "spral_pivot_method",
658 "spral_print_level",
659 "spral_scaling",
660 "spral_scaling_1",
661 "spral_scaling_2",
662 "spral_scaling_3",
663 "spral_small",
664 "spral_small_subtree_threshold",
665 "spral_switch_1",
666 "spral_switch_2",
667 "spral_switch_3",
668 "spral_u",
669 "spral_umax",
670 "spral_use_gpu",
671 ],
672 },
673 UnimplementedBackend {
674 backend: "the WSMP sparse symmetric linear solver",
675 family: "wsmp_*",
676 options: &[
677 "wsmp_inexact_droptol",
678 "wsmp_inexact_fillin_limit",
679 "wsmp_iterative",
680 "wsmp_max_iter",
681 "wsmp_no_pivoting",
682 "wsmp_num_threads",
683 "wsmp_ordering_option",
684 "wsmp_ordering_option2",
685 "wsmp_pivtol",
686 "wsmp_pivtolmax",
687 "wsmp_scaling",
688 "wsmp_singularity_threshold",
689 "wsmp_skip_inertia_check",
690 "wsmp_write_matrix_iteration",
691 ],
692 },
693];
694
695pub fn backend_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
704 UNIMPLEMENTED_BACKENDS
705 .iter()
706 .filter_map(|group| {
707 let set: Vec<&str> = group
708 .options
709 .iter()
710 .copied()
711 .filter(|name| set_to_a_non_default(options, reg, name))
712 .collect();
713 if set.is_empty() {
714 return None;
715 }
716 let named = set
717 .iter()
718 .map(|n| format!("`{n}`"))
719 .collect::<Vec<_>>()
720 .join(", ");
721 let (verb, ignored, registered) = if set.len() == 1 {
722 ("configures", "it is ignored".to_string(), "The name is")
723 } else {
724 (
725 "configure",
726 format!("those {} are ignored", set.len()),
727 "The names are",
728 )
729 };
730 Some(format!(
731 "pounce: warning: {named} {verb} {}, which pounce does not \
732 implement, so {ignored} — as is every other `{}` option. \
733 pounce factors the KKT system with `feral` (pure Rust, the \
734 default) or MA57 (`linear_solver=ma57`, in a `--features \
735 ma57` build); no setting written for another backend \
736 transfers to either. {registered} registered so an \
737 `ipopt.opt` written for Ipopt still parses unchanged — which \
738 is why this is a warning and not an error: the solve runs, \
739 and its result is unaffected. Tracking issue: \
740 https://github.com/jkitchin/pounce/issues/551",
741 group.backend, group.family,
742 ))
743 })
744 .collect()
745}
746
747const DELIVERY_MECHANISM: &[&str] = &["option_file_name"];
756
757fn is_backend_knob(name: &str) -> bool {
759 UNIMPLEMENTED_BACKENDS
760 .iter()
761 .any(|group| group.options.contains(&name))
762}
763
764pub fn backend_only_refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
800 let asked_for_something = UNIMPLEMENTED_BACKENDS.iter().any(|group| {
801 group
802 .options
803 .iter()
804 .any(|name| set_to_a_non_default(options, reg, name))
805 });
806 if !asked_for_something {
807 return None;
808 }
809 let mentions_something_real = options
810 .names()
811 .any(|name| !is_backend_knob(name) && !DELIVERY_MECHANISM.contains(&name));
812 if mentions_something_real {
813 return None;
814 }
815 Some(
816 "pounce: error: every option this run sets configures a linear-solver \
817 backend pounce does not implement, so there is nothing left for it to \
818 act on. pounce factors the KKT system with `feral` (pure Rust, the \
819 default) or MA57 (`linear_solver=ma57`, in a `--features ma57` \
820 build); no setting written for another backend transfers to either. \
821 These names are registered so an `ipopt.opt` written for Ipopt still \
822 parses unchanged, and a file that also carries options pounce reads \
823 is warned about rather than refused — but a file that carries only \
824 these would run as if it had configured the solver when it \
825 configured nothing. Set `linear_solver=feral` (or `ma57`) if the \
826 defaults are what you want. Tracking issue: \
827 https://github.com/jkitchin/pounce/issues/551"
828 .to_string(),
829 )
830}
831
832pub(crate) fn set_to_a_non_default(
838 options: &OptionsList,
839 reg: &RegisteredOptions,
840 name: &str,
841) -> bool {
842 let Some(opt) = reg.get_option(name) else {
843 return false;
844 };
845 match &opt.default {
846 DefaultValue::String(d) => {
849 matches!(options.get_string_value(name, ""), Ok((v, true)) if !v.eq_ignore_ascii_case(d))
850 }
851 DefaultValue::Number(d) => {
852 matches!(options.get_numeric_value(name, ""), Ok((v, true)) if v != *d)
853 }
854 DefaultValue::Integer(d) => {
855 matches!(options.get_integer_value(name, ""), Ok((v, true)) if v != *d)
856 }
857 DefaultValue::None => false,
858 }
859}
860
861pub fn refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
864 for group in UNIMPLEMENTED_FEATURES {
865 for name in group.options {
866 if set_to_a_non_default(options, reg, name) {
867 let advice = if group.advice.is_empty() {
868 String::new()
869 } else {
870 format!(" Instead: {}.", group.advice)
871 };
872 return Some(format!(
873 "pounce: `{name}` configures {}, which pounce does not \
874 implement. It is registered so an ipopt.opt written for \
875 Ipopt still parses, but setting it used to do nothing at \
876 all — silently — so it is refused instead.{advice} \
877 Remove it to run. Tracking issue: \
878 https://github.com/jkitchin/pounce/issues/{}",
879 group.feature, group.issue
880 ));
881 }
882 }
883 }
884 None
885}
886
887pub fn value_refusal(options: &OptionsList) -> Option<String> {
894 for entry in UNIMPLEMENTED_VALUES {
895 let selected = matches!(
896 options.get_string_value(entry.option, ""),
897 Ok((v, true)) if v.eq_ignore_ascii_case(entry.value)
898 );
899 if !selected {
900 continue;
901 }
902 let advice = if entry.advice.is_empty() {
903 String::new()
904 } else {
905 format!(" Instead: {}.", entry.advice)
906 };
907 return Some(format!(
908 "pounce: `{}={}` selects {}, which pounce does not implement. The \
909 value is registered so an ipopt.opt written for Ipopt still \
910 parses; falling back to another mode would silently run a \
911 different initialization than the one you asked for, so it is \
912 refused instead.{advice} Tracking issue: \
913 https://github.com/jkitchin/pounce/issues/604",
914 entry.option, entry.value, entry.feature,
915 ));
916 }
917 None
918}
919
920pub fn hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
922 UNEXPLOITED_HINTS
923 .iter()
924 .filter(|name| set_to_a_non_default(options, reg, name))
925 .map(|name| {
926 format!(
927 "pounce: warning: `{name}` is a caching hint pounce does not \
928 exploit — it re-evaluates each iteration regardless. Your \
929 answer is unaffected; only the evaluation count is. \
930 (gh#483)"
931 )
932 })
933 .collect()
934}
935
936pub fn convex_hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
942 CONVEX_UNEXPLOITED_HINTS
943 .iter()
944 .filter(|name| set_to_a_non_default(options, reg, name))
945 .map(|name| {
946 format!(
947 "pounce: warning: `{name}` asserts a derivative is constant \
948 across iterates, which the NLP path reconciles against the \
949 model and exploits — but this problem routed to \
950 pounce-convex, whose LP/QP/SOCP engines are handed constant \
951 matrices to begin with and do not read the option at all. \
952 Your answer is unaffected. Use `solver_selection=nlp` if you \
953 wanted the path that acts on it. (gh#483, gh#588)"
954 )
955 })
956 .collect()
957}
958
959#[cfg(test)]
960mod tests {
961 use super::*;
962 use std::collections::BTreeSet;
963
964 fn registry() -> std::rc::Rc<RegisteredOptions> {
965 let r = RegisteredOptions::new();
966 crate::upstream_options::register_all_upstream_options(&r).expect("register");
967 r
968 }
969
970 fn fixture() -> (OptionsList, std::rc::Rc<RegisteredOptions>) {
973 let reg = registry();
974 (OptionsList::with_registered(std::rc::Rc::clone(®)), reg)
975 }
976
977 #[test]
981 fn every_listed_option_is_registered() {
982 let (_, reg) = fixture();
983 for group in UNIMPLEMENTED_FEATURES {
984 for name in group.options {
985 assert!(
986 reg.get_option(name).is_some(),
987 "`{name}` is in the refusal table but is not registered",
988 );
989 }
990 }
991 for name in UNEXPLOITED_HINTS.iter().chain(CONVEX_UNEXPLOITED_HINTS) {
992 assert!(
993 reg.get_option(name).is_some(),
994 "`{name}` is in a hint table but is not registered",
995 );
996 }
997 for group in UNIMPLEMENTED_BACKENDS {
998 for name in group.options {
999 assert!(
1000 reg.get_option(name).is_some(),
1001 "`{name}` is in the backend table but is not registered",
1002 );
1003 }
1004 }
1005 }
1006
1007 #[test]
1013 fn backend_families_are_complete() {
1014 let (_, reg) = fixture();
1015 for group in UNIMPLEMENTED_BACKENDS {
1016 let prefix = group
1017 .family
1018 .strip_suffix('*')
1019 .expect("family is a prefix glob, e.g. `ma97_*`");
1020 for opt in reg.registered_options_in_order() {
1021 if !opt.name.starts_with(prefix) {
1022 continue;
1023 }
1024 assert!(
1025 group.options.contains(&opt.name.as_str()),
1026 "`{}` is registered and matches `{}` but is missing from \
1027 the {} group, so setting it would still be silent",
1028 opt.name,
1029 group.family,
1030 group.backend,
1031 );
1032 }
1033 }
1034 }
1035
1036 #[test]
1042 fn the_convex_hint_table_does_not_collide_with_a_refusal() {
1043 let refused: BTreeSet<&str> = UNIMPLEMENTED_FEATURES
1044 .iter()
1045 .flat_map(|g| g.options.iter().copied())
1046 .collect();
1047 for name in CONVEX_UNEXPLOITED_HINTS {
1048 assert!(
1049 !refused.contains(name),
1050 "`{name}` both warns on the convex route and is refused",
1051 );
1052 }
1053 }
1054
1055 #[test]
1060 fn the_convex_route_still_says_the_hints_do_nothing_there() {
1061 let (mut opts, reg) = fixture();
1062 assert!(
1063 convex_hint_warnings(&opts, ®).is_empty(),
1064 "an unset hint must not warn"
1065 );
1066 opts.set_string_value("hessian_constant", "yes", true, false)
1067 .unwrap();
1068 let w = convex_hint_warnings(&opts, ®);
1069 assert_eq!(w.len(), 1, "{w:?}");
1070 assert!(w[0].contains("hessian_constant"), "{}", w[0]);
1071 assert!(w[0].contains("pounce-convex"), "{}", w[0]);
1072 assert_eq!(
1073 refusal(&opts, ®),
1074 None,
1075 "a hint must not block a convex solve either"
1076 );
1077 }
1078
1079 #[test]
1083 fn the_tables_do_not_overlap() {
1084 let mut seen = BTreeSet::new();
1085 for name in UNIMPLEMENTED_FEATURES
1086 .iter()
1087 .flat_map(|g| g.options.iter())
1088 .chain(UNEXPLOITED_HINTS.iter())
1089 .chain(UNIMPLEMENTED_BACKENDS.iter().flat_map(|g| g.options.iter()))
1090 {
1091 assert!(seen.insert(*name), "`{name}` is listed twice");
1092 }
1093 }
1094
1095 #[test]
1097 fn defaults_are_not_refused() {
1098 let (opts, reg) = fixture();
1099 assert_eq!(refusal(&opts, ®), None);
1100 assert!(hint_warnings(&opts, ®).is_empty());
1101 assert!(
1102 backend_warnings(&opts, ®).is_empty(),
1103 "a default run must stay silent",
1104 );
1105 assert_eq!(
1106 backend_only_refusal(&opts, ®),
1107 None,
1108 "an empty list sets no backend knob, so there is nothing to refuse",
1109 );
1110 }
1111
1112 #[test]
1116 fn backend_only_is_refused_but_a_mixed_list_is_not() {
1117 let (mut opts, reg) = fixture();
1118 opts.set_string_value("ma97_order", "metis", true, false)
1119 .unwrap();
1120 let refused = backend_only_refusal(&opts, ®)
1121 .expect("a list of nothing but backend knobs must be refused");
1122 assert!(
1123 refused.contains("every option this run sets"),
1124 "the message is about the whole list, not one knob: {refused}",
1125 );
1126 assert!(
1127 !backend_warnings(&opts, ®).is_empty(),
1128 "the per-family warning still describes the knob; the refusal is \
1129 what stops the run",
1130 );
1131
1132 opts.set_numeric_value("tol", 1e-8, true, false).unwrap();
1133 assert_eq!(
1134 backend_only_refusal(&opts, ®),
1135 None,
1136 "one option pounce reads is enough content to protect",
1137 );
1138 }
1139
1140 #[test]
1143 fn a_backend_knob_at_its_default_is_not_refused() {
1144 let (mut opts, reg) = fixture();
1145 opts.set_string_value("ma97_order", "auto", true, false)
1146 .unwrap();
1147 assert_eq!(backend_only_refusal(&opts, ®), None);
1148 }
1149
1150 #[test]
1155 fn the_delivery_mechanism_is_not_content() {
1156 let (mut opts, reg) = fixture();
1157 opts.set_string_value("ma97_order", "metis", true, false)
1158 .unwrap();
1159 opts.set_string_value("option_file_name", "ipopt.opt", true, false)
1160 .unwrap();
1161 assert!(
1162 backend_only_refusal(&opts, ®).is_some(),
1163 "naming the file that carried the knobs is not a reason to spare it",
1164 );
1165 }
1166
1167 #[test]
1171 fn the_delivery_mechanism_names_are_registered() {
1172 let (_, reg) = fixture();
1173 for name in DELIVERY_MECHANISM {
1174 assert!(
1175 reg.get_option(name).is_some(),
1176 "`{name}` is not a registered option",
1177 );
1178 assert!(
1179 !is_backend_knob(name),
1180 "`{name}` is a backend knob; exempting it is meaningless",
1181 );
1182 }
1183 }
1184
1185 #[test]
1189 fn a_backend_knob_warns_but_does_not_refuse() {
1190 let (mut opts, reg) = fixture();
1191 opts.set_string_value("ma97_order", "metis", true, false)
1192 .unwrap();
1193 assert_eq!(
1194 refusal(&opts, ®),
1195 None,
1196 "a backend knob must not block a solve",
1197 );
1198 let warnings = backend_warnings(&opts, ®);
1199 assert_eq!(warnings.len(), 1, "{warnings:?}");
1200 let w = &warnings[0];
1201 assert!(w.contains("warning:"), "{w}");
1202 assert!(w.contains("`ma97_order`"), "{w}");
1203 assert!(w.contains("MA97"), "the backend must be named: {w}");
1204 assert!(w.contains("`ma97_*`"), "the family must be named: {w}");
1205 assert!(w.contains("result is unaffected"), "{w}");
1209 assert!(w.contains("551"), "{w}");
1210 }
1211
1212 #[test]
1216 fn a_backend_knob_at_its_default_is_silent() {
1217 let (mut opts, reg) = fixture();
1218 opts.set_string_value("ma97_order", "auto", true, false)
1220 .unwrap();
1221 opts.set_integer_value("pardiso_msglvl", 0, true, false)
1222 .unwrap();
1223 assert!(backend_warnings(&opts, ®).is_empty());
1224 }
1225
1226 #[test]
1231 fn the_warning_is_grouped_by_backend_family() {
1232 let (mut opts, reg) = fixture();
1233 opts.set_string_value("ma97_order", "metis", true, false)
1234 .unwrap();
1235 opts.set_numeric_value("ma97_u", 1e-4, true, false).unwrap();
1236 opts.set_string_value("ma97_scaling", "mc64", true, false)
1237 .unwrap();
1238 opts.set_integer_value("pardiso_msglvl", 1, true, false)
1239 .unwrap();
1240
1241 let warnings = backend_warnings(&opts, ®);
1242 assert_eq!(
1243 warnings.len(),
1244 2,
1245 "one per family, not per option: {warnings:?}"
1246 );
1247 let ma97 = warnings.iter().find(|w| w.contains("MA97")).expect("MA97");
1248 for name in ["`ma97_order`", "`ma97_u`", "`ma97_scaling`"] {
1249 assert!(ma97.contains(name), "{ma97}");
1250 }
1251 assert!(ma97.contains("those 3 are ignored"), "{ma97}");
1252 assert!(
1253 warnings.iter().any(|w| w.contains("Pardiso")),
1254 "{warnings:?}",
1255 );
1256 }
1257
1258 #[test]
1263 fn pardisolib_warns_with_the_pardiso_family() {
1264 let (mut opts, reg) = fixture();
1265 opts.set_string_value("pardisolib", "libpardiso600.so", true, false)
1266 .unwrap();
1267 assert_eq!(refusal(&opts, ®), None);
1268 let warnings = backend_warnings(&opts, ®);
1269 assert_eq!(warnings.len(), 1, "{warnings:?}");
1270 assert!(warnings[0].contains("`pardisolib`"), "{:?}", warnings[0]);
1271 assert!(warnings[0].contains("Pardiso"), "{:?}", warnings[0]);
1272
1273 let (mut opts, reg) = fixture();
1275 opts.set_string_value("hsllib", "libcoinhsl.so", true, false)
1276 .unwrap();
1277 assert!(refusal(&opts, ®).is_some());
1278 }
1279
1280 #[test]
1283 fn explicitly_setting_the_default_is_not_refused() {
1284 let (mut opts, reg) = fixture();
1285 opts.set_string_value("dependency_detector", "none", true, false)
1287 .unwrap();
1288 opts.set_string_value("magic_steps", "no", true, false)
1289 .unwrap();
1290 assert_eq!(refusal(&opts, ®), None);
1291 }
1292
1293 #[test]
1295 fn requesting_an_unimplemented_feature_is_refused() {
1296 let (mut opts, reg) = fixture();
1297 opts.set_string_value("dependency_detector", "mumps", true, false)
1298 .unwrap();
1299 let msg = refusal(&opts, ®).expect("must refuse");
1300 assert!(msg.contains("dependency_detector"), "{msg}");
1301 assert!(msg.contains("linear-dependency detection"), "{msg}");
1302 assert!(msg.contains("483"), "{msg}");
1303 }
1304
1305 #[test]
1307 fn a_numeric_knob_of_an_absent_feature_is_refused() {
1308 let (mut opts, reg) = fixture();
1309 opts.set_numeric_value("penalty_init_max", 42.0, true, false)
1310 .unwrap();
1311 let msg = refusal(&opts, ®).expect("must refuse");
1312 assert!(msg.contains("CG-penalty"), "{msg}");
1313 }
1314
1315 #[test]
1323 fn the_constant_derivative_hints_are_no_longer_unexploited() {
1324 let (mut opts, reg) = fixture();
1325 opts.set_string_value("hessian_constant", "yes", true, false)
1326 .unwrap();
1327 assert_eq!(refusal(&opts, ®), None, "a hint must not block a solve");
1328 assert!(
1329 hint_warnings(&opts, ®).is_empty(),
1330 "`hessian_constant` is exploited now; the unexploited-hint \
1331 warning would contradict the reuse the solver actually does",
1332 );
1333 assert!(
1334 UNEXPLOITED_HINTS.is_empty(),
1335 "gh #588 Q6 emptied this table; an entry added back needs its \
1336 own warning text and a test that the option is really unused",
1337 );
1338 }
1339
1340 #[test]
1346 fn fast_step_computation_is_wired_not_refused() {
1347 let (mut opts, reg) = fixture();
1348 opts.set_string_value("fast_step_computation", "yes", true, false)
1349 .unwrap();
1350 assert_eq!(refusal(&opts, ®), None);
1351
1352 let mut app = crate::application::IpoptApplication::new();
1353 app.initialize().unwrap();
1354 app.initialize_with_options_str("fast_step_computation yes\n")
1355 .unwrap();
1356 assert!(
1357 app.algorithm_builder_from_options().fast_step_computation,
1358 "the option must reach the builder, or wiring it changed nothing",
1359 );
1360 let mut app = crate::application::IpoptApplication::new();
1362 app.initialize().unwrap();
1363 assert!(!app.algorithm_builder_from_options().fast_step_computation);
1364 }
1365
1366 #[test]
1371 fn option_file_name_is_implemented_not_in_the_table() {
1372 let (mut opts, reg) = fixture();
1373 opts.set_string_value("option_file_name", "tiny.opt", true, false)
1374 .unwrap();
1375 assert_eq!(refusal(&opts, ®), None);
1376 }
1377
1378 #[test]
1384 fn option_file_name_is_refused_where_nothing_resolves_it() {
1385 let mut app = crate::application::IpoptApplication::new();
1386 app.initialize().unwrap();
1387 assert_eq!(app.unhonored_option_file_name(), None, "unset asks nothing");
1388
1389 app.initialize_with_options_str("option_file_name tiny.opt\n")
1390 .unwrap();
1391 let msg = app
1392 .unhonored_option_file_name()
1393 .expect("a library caller cannot honor it");
1394 assert!(msg.contains("tiny.opt"), "{msg}");
1395 assert!(msg.contains("does not read options files"), "{msg}");
1396 assert!(msg.contains("518"), "{msg}");
1397 }
1398
1399 #[test]
1405 fn option_file_name_at_its_default_asks_nothing_of_a_library_caller() {
1406 let mut app = crate::application::IpoptApplication::new();
1407 app.initialize_with_options_str("option_file_name ipopt.opt\n")
1408 .unwrap();
1409 assert_eq!(app.unhonored_option_file_name(), None);
1410 }
1411
1412 #[test]
1416 fn the_guard_is_quiet_once_the_option_file_path_has_run() {
1417 let dir = std::env::temp_dir().join(format!("pounce_gh518_lib_{}", std::process::id()));
1418 std::fs::create_dir_all(&dir).unwrap();
1419 let path = dir.join("tiny.opt");
1420 std::fs::write(&path, "max_iter 5\n").unwrap();
1421
1422 let mut app = crate::application::IpoptApplication::new();
1423 app.initialize_with_option_file(Some(&path)).unwrap();
1424 assert_eq!(app.unhonored_option_file_name(), None);
1425 assert_eq!(
1426 app.options().get_integer_value("max_iter", "").unwrap(),
1427 (5, true),
1428 "the file must actually have been read",
1429 );
1430
1431 let _ = std::fs::remove_dir_all(&dir);
1432 }
1433
1434 #[test]
1441 fn the_restoration_switches_reach_the_builder() {
1442 for (key, default_on) in [
1443 ("evaluate_orig_obj_at_resto_trial", true),
1444 ("expect_infeasible_problem", false),
1445 ("start_with_resto", false),
1446 ] {
1447 let mut app = crate::application::IpoptApplication::new();
1448 app.initialize().unwrap();
1449 let resto = app.algorithm_builder_from_options().resto;
1450 let got = match key {
1451 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
1452 "expect_infeasible_problem" => resto.expect_infeasible_problem,
1453 _ => resto.start_with_resto,
1454 };
1455 assert_eq!(got, default_on, "{key}: default changed");
1456
1457 let flipped = if default_on { "no" } else { "yes" };
1459 let mut app = crate::application::IpoptApplication::new();
1460 app.initialize().unwrap();
1461 app.initialize_with_options_str(&format!("{key} {flipped}\n"))
1462 .unwrap();
1463 let resto = app.algorithm_builder_from_options().resto;
1464 let got = match key {
1465 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
1466 "expect_infeasible_problem" => resto.expect_infeasible_problem,
1467 _ => resto.start_with_resto,
1468 };
1469 assert_eq!(
1470 got, !default_on,
1471 "{key}={flipped} never reached the builder"
1472 );
1473 }
1474 }
1475
1476 #[test]
1481 fn max_resto_iter_reaches_the_builder() {
1482 let mut app = crate::application::IpoptApplication::new();
1483 app.initialize().unwrap();
1484 let b = app.algorithm_builder_from_options();
1485 assert_eq!(b.resto.max_resto_iter, 3000);
1490 let reg = registry();
1491 let registered = reg.get_option("max_resto_iter").expect("registered");
1492 assert!(
1493 matches!(registered.default, DefaultValue::Integer(3_000_000)),
1494 "the registry no longer declares 3000000 — if upstream's number \
1495 was adopted as the effective default, that is a trajectory \
1496 change and this test should be the one that says so",
1497 );
1498
1499 let mut app = crate::application::IpoptApplication::new();
1500 app.initialize().unwrap();
1501 app.initialize_with_options_str("max_resto_iter 17\n")
1502 .unwrap();
1503 assert_eq!(
1504 app.algorithm_builder_from_options().resto.max_resto_iter,
1505 17,
1506 "never reached the builder",
1507 );
1508 }
1509
1510 #[test]
1517 fn the_corrector_knobs_are_refused() {
1518 let (mut opts, reg) = fixture();
1519 opts.set_string_value("corrector_type", "affine", true, false)
1520 .unwrap();
1521 let msg = refusal(&opts, ®).expect("must refuse");
1522 assert!(msg.contains("`corrector_type`"), "{msg}");
1523 assert!(msg.contains("TryCorrector"), "{msg}");
1524 assert!(msg.contains("mehrotra_algorithm"), "{msg}");
1525 assert!(msg.contains("551"), "{msg}");
1526
1527 for (name, value) in [
1528 ("skip_corr_if_neg_curv", "no"),
1529 ("skip_corr_in_monotone_mode", "no"),
1530 ] {
1531 let (mut opts, reg) = fixture();
1532 opts.set_string_value(name, value, true, false).unwrap();
1533 assert!(refusal(&opts, ®).is_some(), "`{name}` must refuse");
1534 }
1535 let (mut opts, reg) = fixture();
1536 opts.set_numeric_value("corrector_compl_avrg_red_fact", 2.0, true, false)
1537 .unwrap();
1538 assert!(refusal(&opts, ®).is_some());
1539
1540 let (mut opts, reg) = fixture();
1543 opts.set_string_value("corrector_type", "none", true, false)
1544 .unwrap();
1545 assert_eq!(refusal(&opts, ®), None);
1546 }
1547
1548 #[test]
1554 fn the_missing_restoration_sub_capabilities_are_refused() {
1555 let (mut opts, reg) = fixture();
1556 opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-4, true, false)
1557 .unwrap();
1558 let msg = refusal(&opts, ®).expect("must refuse");
1559 assert!(msg.contains("filter line search"), "{msg}");
1560 assert!(
1561 msg.contains("restoration phase itself runs"),
1562 "the message must say the parent feature is unaffected: {msg}",
1563 );
1564
1565 let (mut opts, reg) = fixture();
1566 opts.set_numeric_value("expect_infeasible_problem_ytol", 1e6, true, false)
1567 .unwrap();
1568 assert!(refusal(&opts, ®).is_some());
1569
1570 let (mut opts, reg) = fixture();
1571 opts.set_string_value("limited_memory_special_for_resto", "yes", true, false)
1572 .unwrap();
1573 let msg = refusal(&opts, ®).expect("must refuse");
1574 assert!(msg.contains("Nov 2010"), "{msg}");
1575 assert!(
1576 msg.contains("L-BFGS runs in the restoration sub-solve"),
1577 "{msg}",
1578 );
1579
1580 let (mut opts, reg) = fixture();
1581 opts.set_numeric_value("resto_failure_feasibility_threshold", 1e-6, true, false)
1582 .unwrap();
1583 let msg = refusal(&opts, ®).expect("must refuse");
1584 assert!(msg.contains("restoration runs"), "{msg}");
1585 assert!(msg.contains("max_resto_iter"), "{msg}");
1588
1589 let (mut opts, reg) = fixture();
1591 opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-3, true, false)
1592 .unwrap();
1593 opts.set_string_value("limited_memory_special_for_resto", "no", true, false)
1594 .unwrap();
1595 opts.set_numeric_value("resto_failure_feasibility_threshold", 0.0, true, false)
1596 .unwrap();
1597 assert_eq!(refusal(&opts, ®), None);
1598 }
1599
1600 #[test]
1607 fn the_lbfgs_sigma_clamp_reaches_the_builder() {
1608 let mut app = crate::application::IpoptApplication::new();
1609 app.initialize().unwrap();
1610 let b = app.algorithm_builder_from_options();
1611 assert_eq!(b.limited_memory_init_val_max, 1e8, "default changed");
1612 assert_eq!(b.limited_memory_init_val_min, 1e-8, "default changed");
1613
1614 let mut app = crate::application::IpoptApplication::new();
1615 app.initialize().unwrap();
1616 app.initialize_with_options_str(
1617 "limited_memory_init_val_max 5e5\nlimited_memory_init_val_min 1e-3\n",
1618 )
1619 .unwrap();
1620 let b = app.algorithm_builder_from_options();
1621 assert_eq!(
1622 b.limited_memory_init_val_max, 5e5,
1623 "never reached the builder"
1624 );
1625 assert_eq!(
1626 b.limited_memory_init_val_min, 1e-3,
1627 "never reached the builder"
1628 );
1629 }
1630
1631 #[test]
1635 fn options_on_implemented_features_are_not_refused() {
1636 for (name, value) in [
1637 ("max_resto_iter", "17"),
1640 ("accept_after_max_steps", "3"),
1642 ("limited_memory_max_skipping", "4"),
1644 ("fast_step_computation", "yes"),
1648 ] {
1649 let (mut opts, reg) = fixture();
1650 let set = opts.set_string_value(name, value, true, false).is_ok()
1653 || value
1654 .parse::<i32>()
1655 .ok()
1656 .is_some_and(|v| opts.set_integer_value(name, v, true, false).is_ok())
1657 || value
1658 .parse::<f64>()
1659 .ok()
1660 .is_some_and(|v| opts.set_numeric_value(name, v, true, false).is_ok());
1661 assert!(set, "could not set `{name}` to `{value}`");
1662 assert_eq!(
1663 refusal(&opts, ®),
1664 None,
1665 "`{name}` configures a feature pounce implements; it needs a \
1666 read site, not a refusal",
1667 );
1668 }
1669 }
1670}