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 pub issue: u32,
455}
456
457pub const UNIMPLEMENTED_VALUES: &[UnimplementedValue] = &[
459 UnimplementedValue {
460 option: "bound_mult_init_method",
461 value: "mu-based",
462 feature: "initializing each bound multiplier to mu_init divided by its \
463 own slack",
464 advice: "`bound_mult_init_method=constant` (the default) initializes \
465 them all to `bound_mult_init_val`, which you can set",
466 issue: 604,
467 },
468 UnimplementedValue {
475 option: "line_search_method",
476 value: "cg-penalty",
477 feature: "the Chen-Goldfarb (CG-penalty) line search — Ipopt's \
478 `CGPenaltyLSAcceptor`",
479 advice: "`line_search_method=filter` (the default) or \
480 `line_search_method=penalty` (`IpPenaltyLSAcceptor`), which \
481 is what `cg-penalty` actually ran",
482 issue: 981,
483 },
484];
485
486pub const UNEXPLOITED_HINTS: &[&str] = &[];
506
507pub const CONVEX_UNEXPLOITED_HINTS: &[&str] = &pounce_nlp::constant_derivatives::HINT_OPTIONS;
525
526pub struct UnimplementedBackend {
533 pub backend: &'static str,
536 pub family: &'static str,
540 pub options: &'static [&'static str],
544}
545
546pub const UNIMPLEMENTED_BACKENDS: &[UnimplementedBackend] = &[
549 UnimplementedBackend {
550 backend: "the HSL MA27 sparse symmetric linear solver",
551 family: "ma27_*",
552 options: &[
553 "ma27_ignore_singularity",
554 "ma27_la_init_factor",
555 "ma27_liw_init_factor",
556 "ma27_meminc_factor",
557 "ma27_pivtol",
558 "ma27_pivtolmax",
559 "ma27_print_level",
560 "ma27_skip_inertia_check",
561 ],
562 },
563 UnimplementedBackend {
564 backend: "the HSL MA77 out-of-core sparse symmetric linear solver",
565 family: "ma77_*",
566 options: &[
567 "ma77_buffer_lpage",
568 "ma77_buffer_npage",
569 "ma77_file_size",
570 "ma77_maxstore",
571 "ma77_nemin",
572 "ma77_order",
573 "ma77_print_level",
574 "ma77_small",
575 "ma77_static",
576 "ma77_u",
577 "ma77_umax",
578 ],
579 },
580 UnimplementedBackend {
581 backend: "the HSL MA86 parallel sparse symmetric linear solver",
582 family: "ma86_*",
583 options: &[
584 "ma86_nemin",
585 "ma86_order",
586 "ma86_print_level",
587 "ma86_scaling",
588 "ma86_small",
589 "ma86_static",
590 "ma86_u",
591 "ma86_umax",
592 ],
593 },
594 UnimplementedBackend {
595 backend: "the HSL MA97 sparse symmetric linear solver",
596 family: "ma97_*",
597 options: &[
598 "ma97_dump_matrix",
599 "ma97_nemin",
600 "ma97_order",
601 "ma97_print_level",
602 "ma97_scaling",
603 "ma97_scaling1",
604 "ma97_scaling2",
605 "ma97_scaling3",
606 "ma97_small",
607 "ma97_solve_blas3",
608 "ma97_switch1",
609 "ma97_switch2",
610 "ma97_switch3",
611 "ma97_u",
612 "ma97_umax",
613 ],
614 },
615 UnimplementedBackend {
616 backend: "the MUMPS sparse symmetric linear solver",
617 family: "mumps_*",
618 options: &[
619 "mumps_dep_tol",
620 "mumps_mem_percent",
621 "mumps_mpi_communicator",
622 "mumps_permuting_scaling",
623 "mumps_pivot_order",
624 "mumps_pivtol",
625 "mumps_pivtolmax",
626 "mumps_print_level",
627 "mumps_scaling",
628 ],
629 },
630 UnimplementedBackend {
631 backend: "the Pardiso linear solver (pardiso-project.org)",
632 family: "pardiso_*",
633 options: &[
634 "pardiso_iter_coarse_size",
635 "pardiso_iter_dropping_factor",
636 "pardiso_iter_dropping_schur",
637 "pardiso_iter_inverse_norm_factor",
638 "pardiso_iter_max_levels",
639 "pardiso_iter_max_row_fill",
640 "pardiso_iter_relative_tol",
641 "pardiso_iterative",
642 "pardiso_matching_strategy",
643 "pardiso_max_droptol_corrections",
644 "pardiso_max_iter",
645 "pardiso_max_iterative_refinement_steps",
646 "pardiso_msglvl",
647 "pardiso_order",
648 "pardiso_redo_symbolic_fact_only_if_inertia_wrong",
649 "pardiso_repeated_perturbation_means_singular",
650 "pardiso_skip_inertia_check",
651 "pardisolib",
652 ],
653 },
654 UnimplementedBackend {
655 backend: "the Pardiso linear solver bundled with Intel MKL",
656 family: "pardisomkl_*",
657 options: &[
658 "pardisomkl_matching_strategy",
659 "pardisomkl_max_iterative_refinement_steps",
660 "pardisomkl_msglvl",
661 "pardisomkl_order",
662 "pardisomkl_redo_symbolic_fact_only_if_inertia_wrong",
663 "pardisomkl_repeated_perturbation_means_singular",
664 "pardisomkl_skip_inertia_check",
665 ],
666 },
667 UnimplementedBackend {
668 backend: "the SPRAL SSIDS sparse symmetric linear solver",
669 family: "spral_*",
670 options: &[
671 "spral_cpu_block_size",
672 "spral_gpu_perf_coeff",
673 "spral_ignore_numa",
674 "spral_max_load_inbalance",
675 "spral_min_gpu_work",
676 "spral_nemin",
677 "spral_order",
678 "spral_pivot_method",
679 "spral_print_level",
680 "spral_scaling",
681 "spral_scaling_1",
682 "spral_scaling_2",
683 "spral_scaling_3",
684 "spral_small",
685 "spral_small_subtree_threshold",
686 "spral_switch_1",
687 "spral_switch_2",
688 "spral_switch_3",
689 "spral_u",
690 "spral_umax",
691 "spral_use_gpu",
692 ],
693 },
694 UnimplementedBackend {
695 backend: "the WSMP sparse symmetric linear solver",
696 family: "wsmp_*",
697 options: &[
698 "wsmp_inexact_droptol",
699 "wsmp_inexact_fillin_limit",
700 "wsmp_iterative",
701 "wsmp_max_iter",
702 "wsmp_no_pivoting",
703 "wsmp_num_threads",
704 "wsmp_ordering_option",
705 "wsmp_ordering_option2",
706 "wsmp_pivtol",
707 "wsmp_pivtolmax",
708 "wsmp_scaling",
709 "wsmp_singularity_threshold",
710 "wsmp_skip_inertia_check",
711 "wsmp_write_matrix_iteration",
712 ],
713 },
714];
715
716pub fn backend_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
725 UNIMPLEMENTED_BACKENDS
726 .iter()
727 .filter_map(|group| {
728 let set: Vec<&str> = group
729 .options
730 .iter()
731 .copied()
732 .filter(|name| set_to_a_non_default(options, reg, name))
733 .collect();
734 if set.is_empty() {
735 return None;
736 }
737 let named = set
738 .iter()
739 .map(|n| format!("`{n}`"))
740 .collect::<Vec<_>>()
741 .join(", ");
742 let (verb, ignored, registered) = if set.len() == 1 {
743 ("configures", "it is ignored".to_string(), "The name is")
744 } else {
745 (
746 "configure",
747 format!("those {} are ignored", set.len()),
748 "The names are",
749 )
750 };
751 Some(format!(
752 "pounce: warning: {named} {verb} {}, which pounce does not \
753 implement, so {ignored} — as is every other `{}` option. \
754 pounce factors the KKT system with `feral` (pure Rust, the \
755 default) or MA57 (`linear_solver=ma57`, in a `--features \
756 ma57` build); no setting written for another backend \
757 transfers to either. {registered} registered so an \
758 `ipopt.opt` written for Ipopt still parses unchanged — which \
759 is why this is a warning and not an error: the solve runs, \
760 and its result is unaffected. Tracking issue: \
761 https://github.com/jkitchin/pounce/issues/551",
762 group.backend, group.family,
763 ))
764 })
765 .collect()
766}
767
768const DELIVERY_MECHANISM: &[&str] = &["option_file_name"];
777
778fn is_backend_knob(name: &str) -> bool {
780 UNIMPLEMENTED_BACKENDS
781 .iter()
782 .any(|group| group.options.contains(&name))
783}
784
785pub fn backend_only_refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
821 let asked_for_something = UNIMPLEMENTED_BACKENDS.iter().any(|group| {
822 group
823 .options
824 .iter()
825 .any(|name| set_to_a_non_default(options, reg, name))
826 });
827 if !asked_for_something {
828 return None;
829 }
830 let mentions_something_real = options
831 .names()
832 .any(|name| !is_backend_knob(name) && !DELIVERY_MECHANISM.contains(&name));
833 if mentions_something_real {
834 return None;
835 }
836 Some(
837 "pounce: error: every option this run sets configures a linear-solver \
838 backend pounce does not implement, so there is nothing left for it to \
839 act on. pounce factors the KKT system with `feral` (pure Rust, the \
840 default) or MA57 (`linear_solver=ma57`, in a `--features ma57` \
841 build); no setting written for another backend transfers to either. \
842 These names are registered so an `ipopt.opt` written for Ipopt still \
843 parses unchanged, and a file that also carries options pounce reads \
844 is warned about rather than refused — but a file that carries only \
845 these would run as if it had configured the solver when it \
846 configured nothing. Set `linear_solver=feral` (or `ma57`) if the \
847 defaults are what you want. Tracking issue: \
848 https://github.com/jkitchin/pounce/issues/551"
849 .to_string(),
850 )
851}
852
853pub(crate) fn set_to_a_non_default(
859 options: &OptionsList,
860 reg: &RegisteredOptions,
861 name: &str,
862) -> bool {
863 let Some(opt) = reg.get_option(name) else {
864 return false;
865 };
866 match &opt.default {
867 DefaultValue::String(d) => {
870 matches!(options.get_string_value(name, ""), Ok((v, true)) if !v.eq_ignore_ascii_case(d))
871 }
872 DefaultValue::Number(d) => {
873 matches!(options.get_numeric_value(name, ""), Ok((v, true)) if v != *d)
874 }
875 DefaultValue::Integer(d) => {
876 matches!(options.get_integer_value(name, ""), Ok((v, true)) if v != *d)
877 }
878 DefaultValue::None => false,
879 }
880}
881
882pub fn refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
885 for group in UNIMPLEMENTED_FEATURES {
886 for name in group.options {
887 if set_to_a_non_default(options, reg, name) {
888 let advice = if group.advice.is_empty() {
889 String::new()
890 } else {
891 format!(" Instead: {}.", group.advice)
892 };
893 return Some(format!(
894 "pounce: `{name}` configures {}, which pounce does not \
895 implement. It is registered so an ipopt.opt written for \
896 Ipopt still parses, but setting it used to do nothing at \
897 all — silently — so it is refused instead.{advice} \
898 Remove it to run. Tracking issue: \
899 https://github.com/jkitchin/pounce/issues/{}",
900 group.feature, group.issue
901 ));
902 }
903 }
904 }
905 None
906}
907
908pub fn value_refusal(options: &OptionsList) -> Option<String> {
915 for entry in UNIMPLEMENTED_VALUES {
916 let selected = matches!(
917 options.get_string_value(entry.option, ""),
918 Ok((v, true)) if v.eq_ignore_ascii_case(entry.value)
919 );
920 if !selected {
921 continue;
922 }
923 let advice = if entry.advice.is_empty() {
924 String::new()
925 } else {
926 format!(" Instead: {}.", entry.advice)
927 };
928 return Some(format!(
929 "pounce: `{}={}` selects {}, which pounce does not implement. The \
930 value is registered so an ipopt.opt written for Ipopt still \
931 parses; falling back to another mode would silently run a \
932 different algorithm than the one you asked for, so it is \
933 refused instead.{advice} Tracking issue: \
934 https://github.com/jkitchin/pounce/issues/{}",
935 entry.option, entry.value, entry.feature, entry.issue,
936 ));
937 }
938 None
939}
940
941pub fn hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
943 UNEXPLOITED_HINTS
944 .iter()
945 .filter(|name| set_to_a_non_default(options, reg, name))
946 .map(|name| {
947 format!(
948 "pounce: warning: `{name}` is a caching hint pounce does not \
949 exploit — it re-evaluates each iteration regardless. Your \
950 answer is unaffected; only the evaluation count is. \
951 (gh#483)"
952 )
953 })
954 .collect()
955}
956
957pub fn convex_hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
963 CONVEX_UNEXPLOITED_HINTS
964 .iter()
965 .filter(|name| set_to_a_non_default(options, reg, name))
966 .map(|name| {
967 format!(
968 "pounce: warning: `{name}` asserts a derivative is constant \
969 across iterates, which the NLP path reconciles against the \
970 model and exploits — but this problem routed to \
971 pounce-convex, whose LP/QP/SOCP engines are handed constant \
972 matrices to begin with and do not read the option at all. \
973 Your answer is unaffected. Use `solver_selection=nlp` if you \
974 wanted the path that acts on it. (gh#483, gh#588)"
975 )
976 })
977 .collect()
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use std::collections::BTreeSet;
984
985 fn registry() -> std::rc::Rc<RegisteredOptions> {
986 let r = RegisteredOptions::new();
987 crate::upstream_options::register_all_upstream_options(&r).expect("register");
988 r
989 }
990
991 fn fixture() -> (OptionsList, std::rc::Rc<RegisteredOptions>) {
994 let reg = registry();
995 (OptionsList::with_registered(std::rc::Rc::clone(®)), reg)
996 }
997
998 #[test]
1002 fn every_listed_option_is_registered() {
1003 let (_, reg) = fixture();
1004 for group in UNIMPLEMENTED_FEATURES {
1005 for name in group.options {
1006 assert!(
1007 reg.get_option(name).is_some(),
1008 "`{name}` is in the refusal table but is not registered",
1009 );
1010 }
1011 }
1012 for name in UNEXPLOITED_HINTS.iter().chain(CONVEX_UNEXPLOITED_HINTS) {
1013 assert!(
1014 reg.get_option(name).is_some(),
1015 "`{name}` is in a hint table but is not registered",
1016 );
1017 }
1018 for group in UNIMPLEMENTED_BACKENDS {
1019 for name in group.options {
1020 assert!(
1021 reg.get_option(name).is_some(),
1022 "`{name}` is in the backend table but is not registered",
1023 );
1024 }
1025 }
1026 }
1027
1028 #[test]
1034 fn backend_families_are_complete() {
1035 let (_, reg) = fixture();
1036 for group in UNIMPLEMENTED_BACKENDS {
1037 let prefix = group
1038 .family
1039 .strip_suffix('*')
1040 .expect("family is a prefix glob, e.g. `ma97_*`");
1041 for opt in reg.registered_options_in_order() {
1042 if !opt.name.starts_with(prefix) {
1043 continue;
1044 }
1045 assert!(
1046 group.options.contains(&opt.name.as_str()),
1047 "`{}` is registered and matches `{}` but is missing from \
1048 the {} group, so setting it would still be silent",
1049 opt.name,
1050 group.family,
1051 group.backend,
1052 );
1053 }
1054 }
1055 }
1056
1057 #[test]
1063 fn the_convex_hint_table_does_not_collide_with_a_refusal() {
1064 let refused: BTreeSet<&str> = UNIMPLEMENTED_FEATURES
1065 .iter()
1066 .flat_map(|g| g.options.iter().copied())
1067 .collect();
1068 for name in CONVEX_UNEXPLOITED_HINTS {
1069 assert!(
1070 !refused.contains(name),
1071 "`{name}` both warns on the convex route and is refused",
1072 );
1073 }
1074 }
1075
1076 #[test]
1081 fn the_convex_route_still_says_the_hints_do_nothing_there() {
1082 let (mut opts, reg) = fixture();
1083 assert!(
1084 convex_hint_warnings(&opts, ®).is_empty(),
1085 "an unset hint must not warn"
1086 );
1087 opts.set_string_value("hessian_constant", "yes", true, false)
1088 .unwrap();
1089 let w = convex_hint_warnings(&opts, ®);
1090 assert_eq!(w.len(), 1, "{w:?}");
1091 assert!(w[0].contains("hessian_constant"), "{}", w[0]);
1092 assert!(w[0].contains("pounce-convex"), "{}", w[0]);
1093 assert_eq!(
1094 refusal(&opts, ®),
1095 None,
1096 "a hint must not block a convex solve either"
1097 );
1098 }
1099
1100 #[test]
1104 fn the_tables_do_not_overlap() {
1105 let mut seen = BTreeSet::new();
1106 for name in UNIMPLEMENTED_FEATURES
1107 .iter()
1108 .flat_map(|g| g.options.iter())
1109 .chain(UNEXPLOITED_HINTS.iter())
1110 .chain(UNIMPLEMENTED_BACKENDS.iter().flat_map(|g| g.options.iter()))
1111 {
1112 assert!(seen.insert(*name), "`{name}` is listed twice");
1113 }
1114 }
1115
1116 #[test]
1118 fn defaults_are_not_refused() {
1119 let (opts, reg) = fixture();
1120 assert_eq!(refusal(&opts, ®), None);
1121 assert!(hint_warnings(&opts, ®).is_empty());
1122 assert!(
1123 backend_warnings(&opts, ®).is_empty(),
1124 "a default run must stay silent",
1125 );
1126 assert_eq!(
1127 backend_only_refusal(&opts, ®),
1128 None,
1129 "an empty list sets no backend knob, so there is nothing to refuse",
1130 );
1131 }
1132
1133 #[test]
1137 fn backend_only_is_refused_but_a_mixed_list_is_not() {
1138 let (mut opts, reg) = fixture();
1139 opts.set_string_value("ma97_order", "metis", true, false)
1140 .unwrap();
1141 let refused = backend_only_refusal(&opts, ®)
1142 .expect("a list of nothing but backend knobs must be refused");
1143 assert!(
1144 refused.contains("every option this run sets"),
1145 "the message is about the whole list, not one knob: {refused}",
1146 );
1147 assert!(
1148 !backend_warnings(&opts, ®).is_empty(),
1149 "the per-family warning still describes the knob; the refusal is \
1150 what stops the run",
1151 );
1152
1153 opts.set_numeric_value("tol", 1e-8, true, false).unwrap();
1154 assert_eq!(
1155 backend_only_refusal(&opts, ®),
1156 None,
1157 "one option pounce reads is enough content to protect",
1158 );
1159 }
1160
1161 #[test]
1164 fn a_backend_knob_at_its_default_is_not_refused() {
1165 let (mut opts, reg) = fixture();
1166 opts.set_string_value("ma97_order", "auto", true, false)
1167 .unwrap();
1168 assert_eq!(backend_only_refusal(&opts, ®), None);
1169 }
1170
1171 #[test]
1176 fn the_delivery_mechanism_is_not_content() {
1177 let (mut opts, reg) = fixture();
1178 opts.set_string_value("ma97_order", "metis", true, false)
1179 .unwrap();
1180 opts.set_string_value("option_file_name", "ipopt.opt", true, false)
1181 .unwrap();
1182 assert!(
1183 backend_only_refusal(&opts, ®).is_some(),
1184 "naming the file that carried the knobs is not a reason to spare it",
1185 );
1186 }
1187
1188 #[test]
1192 fn the_delivery_mechanism_names_are_registered() {
1193 let (_, reg) = fixture();
1194 for name in DELIVERY_MECHANISM {
1195 assert!(
1196 reg.get_option(name).is_some(),
1197 "`{name}` is not a registered option",
1198 );
1199 assert!(
1200 !is_backend_knob(name),
1201 "`{name}` is a backend knob; exempting it is meaningless",
1202 );
1203 }
1204 }
1205
1206 #[test]
1210 fn a_backend_knob_warns_but_does_not_refuse() {
1211 let (mut opts, reg) = fixture();
1212 opts.set_string_value("ma97_order", "metis", true, false)
1213 .unwrap();
1214 assert_eq!(
1215 refusal(&opts, ®),
1216 None,
1217 "a backend knob must not block a solve",
1218 );
1219 let warnings = backend_warnings(&opts, ®);
1220 assert_eq!(warnings.len(), 1, "{warnings:?}");
1221 let w = &warnings[0];
1222 assert!(w.contains("warning:"), "{w}");
1223 assert!(w.contains("`ma97_order`"), "{w}");
1224 assert!(w.contains("MA97"), "the backend must be named: {w}");
1225 assert!(w.contains("`ma97_*`"), "the family must be named: {w}");
1226 assert!(w.contains("result is unaffected"), "{w}");
1230 assert!(w.contains("551"), "{w}");
1231 }
1232
1233 #[test]
1237 fn a_backend_knob_at_its_default_is_silent() {
1238 let (mut opts, reg) = fixture();
1239 opts.set_string_value("ma97_order", "auto", true, false)
1241 .unwrap();
1242 opts.set_integer_value("pardiso_msglvl", 0, true, false)
1243 .unwrap();
1244 assert!(backend_warnings(&opts, ®).is_empty());
1245 }
1246
1247 #[test]
1252 fn the_warning_is_grouped_by_backend_family() {
1253 let (mut opts, reg) = fixture();
1254 opts.set_string_value("ma97_order", "metis", true, false)
1255 .unwrap();
1256 opts.set_numeric_value("ma97_u", 1e-4, true, false).unwrap();
1257 opts.set_string_value("ma97_scaling", "mc64", true, false)
1258 .unwrap();
1259 opts.set_integer_value("pardiso_msglvl", 1, true, false)
1260 .unwrap();
1261
1262 let warnings = backend_warnings(&opts, ®);
1263 assert_eq!(
1264 warnings.len(),
1265 2,
1266 "one per family, not per option: {warnings:?}"
1267 );
1268 let ma97 = warnings.iter().find(|w| w.contains("MA97")).expect("MA97");
1269 for name in ["`ma97_order`", "`ma97_u`", "`ma97_scaling`"] {
1270 assert!(ma97.contains(name), "{ma97}");
1271 }
1272 assert!(ma97.contains("those 3 are ignored"), "{ma97}");
1273 assert!(
1274 warnings.iter().any(|w| w.contains("Pardiso")),
1275 "{warnings:?}",
1276 );
1277 }
1278
1279 #[test]
1284 fn pardisolib_warns_with_the_pardiso_family() {
1285 let (mut opts, reg) = fixture();
1286 opts.set_string_value("pardisolib", "libpardiso600.so", true, false)
1287 .unwrap();
1288 assert_eq!(refusal(&opts, ®), None);
1289 let warnings = backend_warnings(&opts, ®);
1290 assert_eq!(warnings.len(), 1, "{warnings:?}");
1291 assert!(warnings[0].contains("`pardisolib`"), "{:?}", warnings[0]);
1292 assert!(warnings[0].contains("Pardiso"), "{:?}", warnings[0]);
1293
1294 let (mut opts, reg) = fixture();
1296 opts.set_string_value("hsllib", "libcoinhsl.so", true, false)
1297 .unwrap();
1298 assert!(refusal(&opts, ®).is_some());
1299 }
1300
1301 #[test]
1304 fn explicitly_setting_the_default_is_not_refused() {
1305 let (mut opts, reg) = fixture();
1306 opts.set_string_value("dependency_detector", "none", true, false)
1308 .unwrap();
1309 opts.set_string_value("magic_steps", "no", true, false)
1310 .unwrap();
1311 assert_eq!(refusal(&opts, ®), None);
1312 }
1313
1314 #[test]
1316 fn requesting_an_unimplemented_feature_is_refused() {
1317 let (mut opts, reg) = fixture();
1318 opts.set_string_value("dependency_detector", "mumps", true, false)
1319 .unwrap();
1320 let msg = refusal(&opts, ®).expect("must refuse");
1321 assert!(msg.contains("dependency_detector"), "{msg}");
1322 assert!(msg.contains("linear-dependency detection"), "{msg}");
1323 assert!(msg.contains("483"), "{msg}");
1324 }
1325
1326 #[test]
1328 fn a_numeric_knob_of_an_absent_feature_is_refused() {
1329 let (mut opts, reg) = fixture();
1330 opts.set_numeric_value("penalty_init_max", 42.0, true, false)
1331 .unwrap();
1332 let msg = refusal(&opts, ®).expect("must refuse");
1333 assert!(msg.contains("CG-penalty"), "{msg}");
1334 }
1335
1336 #[test]
1340 fn line_search_method_cg_penalty_is_refused_but_penalty_is_not() {
1341 for ok in ["filter", "penalty"] {
1342 let (mut opts, _) = fixture();
1343 opts.set_string_value("line_search_method", ok, true, false)
1344 .unwrap();
1345 assert_eq!(value_refusal(&opts), None, "{ok} is implemented");
1346 }
1347 let (mut opts, _) = fixture();
1348 opts.set_string_value("line_search_method", "cg-penalty", true, false)
1349 .expect("upstream's value must still parse");
1350 let msg = value_refusal(&opts).expect("cg-penalty must be refused");
1351 assert!(msg.contains("line_search_method=cg-penalty"), "{msg}");
1352 assert!(msg.contains("Chen-Goldfarb"), "{msg}");
1353 assert!(msg.contains("issues/981"), "{msg}");
1354 }
1355
1356 #[test]
1364 fn the_constant_derivative_hints_are_no_longer_unexploited() {
1365 let (mut opts, reg) = fixture();
1366 opts.set_string_value("hessian_constant", "yes", true, false)
1367 .unwrap();
1368 assert_eq!(refusal(&opts, ®), None, "a hint must not block a solve");
1369 assert!(
1370 hint_warnings(&opts, ®).is_empty(),
1371 "`hessian_constant` is exploited now; the unexploited-hint \
1372 warning would contradict the reuse the solver actually does",
1373 );
1374 assert!(
1375 UNEXPLOITED_HINTS.is_empty(),
1376 "gh #588 Q6 emptied this table; an entry added back needs its \
1377 own warning text and a test that the option is really unused",
1378 );
1379 }
1380
1381 #[test]
1387 fn fast_step_computation_is_wired_not_refused() {
1388 let (mut opts, reg) = fixture();
1389 opts.set_string_value("fast_step_computation", "yes", true, false)
1390 .unwrap();
1391 assert_eq!(refusal(&opts, ®), None);
1392
1393 let mut app = crate::application::IpoptApplication::new();
1394 app.initialize().unwrap();
1395 app.initialize_with_options_str("fast_step_computation yes\n")
1396 .unwrap();
1397 assert!(
1398 app.algorithm_builder_from_options().fast_step_computation,
1399 "the option must reach the builder, or wiring it changed nothing",
1400 );
1401 let mut app = crate::application::IpoptApplication::new();
1403 app.initialize().unwrap();
1404 assert!(!app.algorithm_builder_from_options().fast_step_computation);
1405 }
1406
1407 #[test]
1412 fn option_file_name_is_implemented_not_in_the_table() {
1413 let (mut opts, reg) = fixture();
1414 opts.set_string_value("option_file_name", "tiny.opt", true, false)
1415 .unwrap();
1416 assert_eq!(refusal(&opts, ®), None);
1417 }
1418
1419 #[test]
1425 fn option_file_name_is_refused_where_nothing_resolves_it() {
1426 let mut app = crate::application::IpoptApplication::new();
1427 app.initialize().unwrap();
1428 assert_eq!(app.unhonored_option_file_name(), None, "unset asks nothing");
1429
1430 app.initialize_with_options_str("option_file_name tiny.opt\n")
1431 .unwrap();
1432 let msg = app
1433 .unhonored_option_file_name()
1434 .expect("a library caller cannot honor it");
1435 assert!(msg.contains("tiny.opt"), "{msg}");
1436 assert!(msg.contains("does not read options files"), "{msg}");
1437 assert!(msg.contains("518"), "{msg}");
1438 }
1439
1440 #[test]
1446 fn option_file_name_at_its_default_asks_nothing_of_a_library_caller() {
1447 let mut app = crate::application::IpoptApplication::new();
1448 app.initialize_with_options_str("option_file_name ipopt.opt\n")
1449 .unwrap();
1450 assert_eq!(app.unhonored_option_file_name(), None);
1451 }
1452
1453 #[test]
1457 fn the_guard_is_quiet_once_the_option_file_path_has_run() {
1458 let dir = std::env::temp_dir().join(format!("pounce_gh518_lib_{}", std::process::id()));
1459 std::fs::create_dir_all(&dir).unwrap();
1460 let path = dir.join("tiny.opt");
1461 std::fs::write(&path, "max_iter 5\n").unwrap();
1462
1463 let mut app = crate::application::IpoptApplication::new();
1464 app.initialize_with_option_file(Some(&path)).unwrap();
1465 assert_eq!(app.unhonored_option_file_name(), None);
1466 assert_eq!(
1467 app.options().get_integer_value("max_iter", "").unwrap(),
1468 (5, true),
1469 "the file must actually have been read",
1470 );
1471
1472 let _ = std::fs::remove_dir_all(&dir);
1473 }
1474
1475 #[test]
1482 fn the_restoration_switches_reach_the_builder() {
1483 for (key, default_on) in [
1484 ("evaluate_orig_obj_at_resto_trial", true),
1485 ("expect_infeasible_problem", false),
1486 ("start_with_resto", false),
1487 ] {
1488 let mut app = crate::application::IpoptApplication::new();
1489 app.initialize().unwrap();
1490 let resto = app.algorithm_builder_from_options().resto;
1491 let got = match key {
1492 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
1493 "expect_infeasible_problem" => resto.expect_infeasible_problem,
1494 _ => resto.start_with_resto,
1495 };
1496 assert_eq!(got, default_on, "{key}: default changed");
1497
1498 let flipped = if default_on { "no" } else { "yes" };
1500 let mut app = crate::application::IpoptApplication::new();
1501 app.initialize().unwrap();
1502 app.initialize_with_options_str(&format!("{key} {flipped}\n"))
1503 .unwrap();
1504 let resto = app.algorithm_builder_from_options().resto;
1505 let got = match key {
1506 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
1507 "expect_infeasible_problem" => resto.expect_infeasible_problem,
1508 _ => resto.start_with_resto,
1509 };
1510 assert_eq!(
1511 got, !default_on,
1512 "{key}={flipped} never reached the builder"
1513 );
1514 }
1515 }
1516
1517 #[test]
1522 fn max_resto_iter_reaches_the_builder() {
1523 let mut app = crate::application::IpoptApplication::new();
1524 app.initialize().unwrap();
1525 let b = app.algorithm_builder_from_options();
1526 assert_eq!(b.resto.max_resto_iter, 3000);
1531 let reg = registry();
1532 let registered = reg.get_option("max_resto_iter").expect("registered");
1533 assert!(
1534 matches!(registered.default, DefaultValue::Integer(3_000_000)),
1535 "the registry no longer declares 3000000 — if upstream's number \
1536 was adopted as the effective default, that is a trajectory \
1537 change and this test should be the one that says so",
1538 );
1539
1540 let mut app = crate::application::IpoptApplication::new();
1541 app.initialize().unwrap();
1542 app.initialize_with_options_str("max_resto_iter 17\n")
1543 .unwrap();
1544 assert_eq!(
1545 app.algorithm_builder_from_options().resto.max_resto_iter,
1546 17,
1547 "never reached the builder",
1548 );
1549 }
1550
1551 #[test]
1558 fn the_corrector_knobs_are_refused() {
1559 let (mut opts, reg) = fixture();
1560 opts.set_string_value("corrector_type", "affine", true, false)
1561 .unwrap();
1562 let msg = refusal(&opts, ®).expect("must refuse");
1563 assert!(msg.contains("`corrector_type`"), "{msg}");
1564 assert!(msg.contains("TryCorrector"), "{msg}");
1565 assert!(msg.contains("mehrotra_algorithm"), "{msg}");
1566 assert!(msg.contains("551"), "{msg}");
1567
1568 for (name, value) in [
1569 ("skip_corr_if_neg_curv", "no"),
1570 ("skip_corr_in_monotone_mode", "no"),
1571 ] {
1572 let (mut opts, reg) = fixture();
1573 opts.set_string_value(name, value, true, false).unwrap();
1574 assert!(refusal(&opts, ®).is_some(), "`{name}` must refuse");
1575 }
1576 let (mut opts, reg) = fixture();
1577 opts.set_numeric_value("corrector_compl_avrg_red_fact", 2.0, true, false)
1578 .unwrap();
1579 assert!(refusal(&opts, ®).is_some());
1580
1581 let (mut opts, reg) = fixture();
1584 opts.set_string_value("corrector_type", "none", true, false)
1585 .unwrap();
1586 assert_eq!(refusal(&opts, ®), None);
1587 }
1588
1589 #[test]
1595 fn the_missing_restoration_sub_capabilities_are_refused() {
1596 let (mut opts, reg) = fixture();
1597 opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-4, true, false)
1598 .unwrap();
1599 let msg = refusal(&opts, ®).expect("must refuse");
1600 assert!(msg.contains("filter line search"), "{msg}");
1601 assert!(
1602 msg.contains("restoration phase itself runs"),
1603 "the message must say the parent feature is unaffected: {msg}",
1604 );
1605
1606 let (mut opts, reg) = fixture();
1607 opts.set_numeric_value("expect_infeasible_problem_ytol", 1e6, true, false)
1608 .unwrap();
1609 assert!(refusal(&opts, ®).is_some());
1610
1611 let (mut opts, reg) = fixture();
1612 opts.set_string_value("limited_memory_special_for_resto", "yes", true, false)
1613 .unwrap();
1614 let msg = refusal(&opts, ®).expect("must refuse");
1615 assert!(msg.contains("Nov 2010"), "{msg}");
1616 assert!(
1617 msg.contains("L-BFGS runs in the restoration sub-solve"),
1618 "{msg}",
1619 );
1620
1621 let (mut opts, reg) = fixture();
1622 opts.set_numeric_value("resto_failure_feasibility_threshold", 1e-6, true, false)
1623 .unwrap();
1624 let msg = refusal(&opts, ®).expect("must refuse");
1625 assert!(msg.contains("restoration runs"), "{msg}");
1626 assert!(msg.contains("max_resto_iter"), "{msg}");
1629
1630 let (mut opts, reg) = fixture();
1632 opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-3, true, false)
1633 .unwrap();
1634 opts.set_string_value("limited_memory_special_for_resto", "no", true, false)
1635 .unwrap();
1636 opts.set_numeric_value("resto_failure_feasibility_threshold", 0.0, true, false)
1637 .unwrap();
1638 assert_eq!(refusal(&opts, ®), None);
1639 }
1640
1641 #[test]
1648 fn the_lbfgs_sigma_clamp_reaches_the_builder() {
1649 let mut app = crate::application::IpoptApplication::new();
1650 app.initialize().unwrap();
1651 let b = app.algorithm_builder_from_options();
1652 assert_eq!(b.limited_memory_init_val_max, 1e8, "default changed");
1653 assert_eq!(b.limited_memory_init_val_min, 1e-8, "default changed");
1654
1655 let mut app = crate::application::IpoptApplication::new();
1656 app.initialize().unwrap();
1657 app.initialize_with_options_str(
1658 "limited_memory_init_val_max 5e5\nlimited_memory_init_val_min 1e-3\n",
1659 )
1660 .unwrap();
1661 let b = app.algorithm_builder_from_options();
1662 assert_eq!(
1663 b.limited_memory_init_val_max, 5e5,
1664 "never reached the builder"
1665 );
1666 assert_eq!(
1667 b.limited_memory_init_val_min, 1e-3,
1668 "never reached the builder"
1669 );
1670 }
1671
1672 #[test]
1676 fn options_on_implemented_features_are_not_refused() {
1677 for (name, value) in [
1678 ("max_resto_iter", "17"),
1681 ("accept_after_max_steps", "3"),
1683 ("limited_memory_max_skipping", "4"),
1685 ("fast_step_computation", "yes"),
1689 ] {
1690 let (mut opts, reg) = fixture();
1691 let set = opts.set_string_value(name, value, true, false).is_ok()
1694 || value
1695 .parse::<i32>()
1696 .ok()
1697 .is_some_and(|v| opts.set_integer_value(name, v, true, false).is_ok())
1698 || value
1699 .parse::<f64>()
1700 .ok()
1701 .is_some_and(|v| opts.set_numeric_value(name, v, true, false).is_ok());
1702 assert!(set, "could not set `{name}` to `{value}`");
1703 assert_eq!(
1704 refusal(&opts, ®),
1705 None,
1706 "`{name}` configures a feature pounce implements; it needs a \
1707 read site, not a refusal",
1708 );
1709 }
1710 }
1711}