use pounce_common::options_list::OptionsList;
use pounce_common::reg_options::{DefaultValue, RegisteredOptions};
pub struct UnimplementedFeature {
pub feature: &'static str,
pub advice: &'static str,
pub options: &'static [&'static str],
pub issue: u32,
}
pub const UNIMPLEMENTED_FEATURES: &[UnimplementedFeature] = &[
UnimplementedFeature {
issue: 483,
feature: "the Chen-Goldfarb (CG-penalty) / inexact-Newton line search \
— Ipopt's `CGPenaltyLSAcceptor`",
advice: "pounce implements the filter line search (the default) and \
`line_search_method=penalty` (`IpPenaltyLSAcceptor`); tune \
those instead",
options: &[
"chi_cup",
"chi_hat",
"chi_tilde",
"delta_y_max",
"epsilon_c",
"eta_min",
"fast_des_fact",
"gamma_hat",
"gamma_tilde",
"kappa_x_dis",
"kappa_y_dis",
"min_alpha_primal",
"mult_diverg_feasibility_tol",
"mult_diverg_y_tol",
"never_use_fact_cgpen_direction",
"never_use_piecewise_penalty_ls",
"pen_des_fact",
"pen_init_fac",
"pen_theta_max_fact",
"penalty_init_max",
"penalty_init_min",
"penalty_max",
"penalty_update_compl_tol",
"penalty_update_infeasibility_tol",
"piecewisepenalty_gamma_infeasi",
"piecewisepenalty_gamma_obj",
"vartheta",
"inexact_algorithm",
],
},
UnimplementedFeature {
issue: 551,
feature: "the CG-penalty acceptor's `theta_min` — the constraint-violation \
threshold its piecewise-penalty tests switch on. It is \
registered by `IpCGPenaltyLSAcceptor`, not by the filter \
acceptor, and pounce has no CG-penalty acceptor to point it at",
advice: "the filter line search has a theta_min of its own, but derives \
it the way upstream does — `theta_min_fact * max(1, theta_0)`, \
never set directly — so set `theta_min_fact` to move it",
options: &["theta_min"],
},
UnimplementedFeature {
issue: 551,
feature: "the `primal-and-full` / `dual-and-full` equality-multiplier \
step rules, which is all this tolerance configures — under \
them the multiplier step jumps to 1 once the max-norm of the \
primal step drops below it",
advice: "pounce implements `alpha_for_y` = `primal` (the default), \
`bound-mult`, `min`, `max` and `full`; `full` takes the unit \
multiplier step unconditionally",
options: &["alpha_for_y_tol"],
},
UnimplementedFeature {
issue: 483,
feature: "derivative approximation by finite differences",
advice: "supply `eval_grad_f` / `eval_jac_g` / `eval_h`, and check them \
with `derivative_test=first-order`",
options: &[
"gradient_approximation",
"jacobian_approximation",
"findiff_perturbation",
],
},
UnimplementedFeature {
issue: 483,
feature: "linear-dependency detection on the equality constraints",
advice: "pounce's presolve removes structurally redundant rows; see \
`presolve`",
options: &[
"dependency_detector",
"dependency_detection_with_rhs",
"ma28_pivtol",
],
},
UnimplementedFeature {
issue: 483,
feature: "the per-iteration NaN/Inf check on derivative matrices",
advice: "`derivative_test=first-order` checks the derivatives once, at \
the starting point",
options: &["check_derivatives_for_naninf"],
},
UnimplementedFeature {
issue: 483,
feature: "least-square initialization of *all* dual variables \
(the first-order-optimality fit)",
advice: "the equality multipliers are least-square initialized \
regardless (capped by `constr_mult_init_max`), and the bound \
multipliers take `bound_mult_init_val` — which is what \
`least_square_init_duals=no` asks for",
options: &["least_square_init_duals"],
},
UnimplementedFeature {
issue: 483,
feature: "a selectable constraint-violation norm",
advice: "pounce measures the violation in the 2-norm throughout",
options: &["constraint_violation_norm_type"],
},
UnimplementedFeature {
issue: 483,
feature: "magic steps",
advice: "",
options: &["magic_steps"],
},
UnimplementedFeature {
issue: 483,
feature: "bound replacement on the original problem",
advice: "",
options: &["replace_bounds"],
},
UnimplementedFeature {
issue: 483,
feature: "the L-BFGS augmented-system and space variants",
advice: "`hessian_approximation=limited-memory` uses the low-rank \
augmented system unconditionally",
options: &["hessian_approximation_space", "limited_memory_aug_solver"],
},
UnimplementedFeature {
issue: 483,
feature: "skipping the finalize-solution callback",
advice: "",
options: &["skip_finalize_solution_call"],
},
UnimplementedFeature {
issue: 483,
feature: "the dynamic HSL loader",
advice: "MA57 is linked at build time with `--features ma57`",
options: &["hsllib"],
},
UnimplementedFeature {
issue: 483,
feature: "these output controls",
advice: "use `print_level` (0 silences the solver) and `sb=yes` to \
suppress the banner",
options: &["suppress_all_output", "debug_print_level"],
},
UnimplementedFeature {
issue: 483,
feature: "a randomly perturbed evaluation point for the derivative \
checker",
advice: "pounce's checker tests at the (bound-projected) starting point, \
which is where the solve actually begins",
options: &["point_perturbation_radius"],
},
UnimplementedFeature {
issue: 551,
feature: "the corrector step tried inside the filter line search \
under the adaptive barrier strategy — Ipopt's \
`FilterLSAcceptor::TryCorrector`, which these four knobs \
select and safeguard",
advice: "pounce's line search takes no corrector step at all, so \
there is nothing for these to gate. The predictor-corrector \
pounce does implement is Mehrotra's, applied to the \
search-direction right-hand side rather than as a \
line-search trial: `mehrotra_algorithm=yes` is read and \
honoured (it also selects `mu_strategy=adaptive` and \
`mu_oracle=probing`)",
options: &[
"corrector_type",
"skip_corr_if_neg_curv",
"skip_corr_in_monotone_mode",
"corrector_compl_avrg_red_fact",
],
},
UnimplementedFeature {
issue: 551,
feature: "the `expect_infeasible_problem` heuristics inside the \
filter line search — switching them off once the \
constraint violation drops below a threshold (`_ctol`), \
and diverting to restoration once the constraint \
multipliers' max-norm rises above one (`_ytol`)",
advice: "the restoration phase itself runs and is unaffected; \
pounce enters it when the line search cannot make \
progress, and `required_infeasibility_reduction` sets how \
much infeasibility reduction it must deliver before \
handing back. `IpBacktrackingLineSearch`'s \
`count_successive_shortened_steps_` machinery, which is \
what these two thresholds steer, has no counterpart here",
options: &[
"expect_infeasible_problem_ctol",
"expect_infeasible_problem_ytol",
],
},
UnimplementedFeature {
issue: 551,
feature: "the special quasi-Newton update Ipopt used inside the \
restoration phase before Nov 2010",
advice: "L-BFGS runs in the restoration sub-solve; it uses the \
regular update procedure there, which is what \
`limited_memory_special_for_resto=no` — upstream's own \
default, and its recommendation — asks for. Only the \
revert to the old update is missing",
options: &["limited_memory_special_for_resto"],
},
UnimplementedFeature {
issue: 551,
feature: "declaring the restoration phase *failed* when it stops on \
the acceptable-point criteria at a primal infeasibility \
below a threshold",
advice: "restoration runs and reports failure on its own terms — \
the reduction guard (`required_infeasibility_reduction`), \
the successive-iteration cap (`max_resto_iter`), and the \
locally-infeasible verdicts, which measure a violation \
against `constr_viol_tol` relative to the offending row. \
There is no threshold below which a stopped restoration is \
reclassified as a failure, so this option has nothing to \
set",
options: &["resto_failure_feasibility_threshold"],
},
UnimplementedFeature {
issue: 551,
feature: "choosing the barrier parameter with an *oracle* when the \
adaptive strategy leaves free mode — Ipopt's \
`fix_mu_oracle_`",
advice: "pounce implements `fixed_mu_oracle=average_compl` (the \
default): the switch into fixed mode seeds μ with \
`adaptive_mu_monotone_init_factor · avrg_compl`, which \
that factor tunes. The probing / loqo / quality-function \
oracles are implemented, but only for `mu_oracle`, which \
drives μ in free mode",
options: &["fixed_mu_oracle"],
},
UnimplementedFeature {
issue: 606,
feature: "reuse of a previously-solved iterate or problem structure \
through Ipopt's `TNLP::GetWarmStartIterate` surface",
advice: "pounce's warm start goes through `TNLP::get_starting_point` \
with `warm_start_init_point=yes`, which carries the primal \
point and all three multiplier blocks; from Python, \
`pounce.WarmStart.from_info` packages it",
options: &["warm_start_entire_iterate", "warm_start_same_structure"],
},
UnimplementedFeature {
issue: 677,
feature: "sensitivity over more than one perturbation tier — upstream \
sIPOPT walks `sens_state_1`, `sens_state_2`, … one tier per \
step and reports a `sens_sol_state_k` for each",
advice: "pounce computes the single `sens_state_1` tier, which is what \
`n_sens_steps=1` (the default) asks for; for a multi-step \
parameter path, run one solve per perturbation, or drive \
`pounce_sensitivity::Solver::parametric_step` in a loop \
against the one converged factor",
options: &["n_sens_steps"],
},
];
pub struct UnimplementedValue {
pub option: &'static str,
pub value: &'static str,
pub feature: &'static str,
pub advice: &'static str,
}
pub const UNIMPLEMENTED_VALUES: &[UnimplementedValue] = &[UnimplementedValue {
option: "bound_mult_init_method",
value: "mu-based",
feature: "initializing each bound multiplier to mu_init divided by its \
own slack",
advice: "`bound_mult_init_method=constant` (the default) initializes them \
all to `bound_mult_init_val`, which you can set",
}];
pub const UNEXPLOITED_HINTS: &[&str] = &[];
pub const CONVEX_UNEXPLOITED_HINTS: &[&str] = &pounce_nlp::constant_derivatives::HINT_OPTIONS;
pub struct UnimplementedBackend {
pub backend: &'static str,
pub family: &'static str,
pub options: &'static [&'static str],
}
pub const UNIMPLEMENTED_BACKENDS: &[UnimplementedBackend] = &[
UnimplementedBackend {
backend: "the HSL MA27 sparse symmetric linear solver",
family: "ma27_*",
options: &[
"ma27_ignore_singularity",
"ma27_la_init_factor",
"ma27_liw_init_factor",
"ma27_meminc_factor",
"ma27_pivtol",
"ma27_pivtolmax",
"ma27_print_level",
"ma27_skip_inertia_check",
],
},
UnimplementedBackend {
backend: "the HSL MA77 out-of-core sparse symmetric linear solver",
family: "ma77_*",
options: &[
"ma77_buffer_lpage",
"ma77_buffer_npage",
"ma77_file_size",
"ma77_maxstore",
"ma77_nemin",
"ma77_order",
"ma77_print_level",
"ma77_small",
"ma77_static",
"ma77_u",
"ma77_umax",
],
},
UnimplementedBackend {
backend: "the HSL MA86 parallel sparse symmetric linear solver",
family: "ma86_*",
options: &[
"ma86_nemin",
"ma86_order",
"ma86_print_level",
"ma86_scaling",
"ma86_small",
"ma86_static",
"ma86_u",
"ma86_umax",
],
},
UnimplementedBackend {
backend: "the HSL MA97 sparse symmetric linear solver",
family: "ma97_*",
options: &[
"ma97_dump_matrix",
"ma97_nemin",
"ma97_order",
"ma97_print_level",
"ma97_scaling",
"ma97_scaling1",
"ma97_scaling2",
"ma97_scaling3",
"ma97_small",
"ma97_solve_blas3",
"ma97_switch1",
"ma97_switch2",
"ma97_switch3",
"ma97_u",
"ma97_umax",
],
},
UnimplementedBackend {
backend: "the MUMPS sparse symmetric linear solver",
family: "mumps_*",
options: &[
"mumps_dep_tol",
"mumps_mem_percent",
"mumps_mpi_communicator",
"mumps_permuting_scaling",
"mumps_pivot_order",
"mumps_pivtol",
"mumps_pivtolmax",
"mumps_print_level",
"mumps_scaling",
],
},
UnimplementedBackend {
backend: "the Pardiso linear solver (pardiso-project.org)",
family: "pardiso_*",
options: &[
"pardiso_iter_coarse_size",
"pardiso_iter_dropping_factor",
"pardiso_iter_dropping_schur",
"pardiso_iter_inverse_norm_factor",
"pardiso_iter_max_levels",
"pardiso_iter_max_row_fill",
"pardiso_iter_relative_tol",
"pardiso_iterative",
"pardiso_matching_strategy",
"pardiso_max_droptol_corrections",
"pardiso_max_iter",
"pardiso_max_iterative_refinement_steps",
"pardiso_msglvl",
"pardiso_order",
"pardiso_redo_symbolic_fact_only_if_inertia_wrong",
"pardiso_repeated_perturbation_means_singular",
"pardiso_skip_inertia_check",
"pardisolib",
],
},
UnimplementedBackend {
backend: "the Pardiso linear solver bundled with Intel MKL",
family: "pardisomkl_*",
options: &[
"pardisomkl_matching_strategy",
"pardisomkl_max_iterative_refinement_steps",
"pardisomkl_msglvl",
"pardisomkl_order",
"pardisomkl_redo_symbolic_fact_only_if_inertia_wrong",
"pardisomkl_repeated_perturbation_means_singular",
"pardisomkl_skip_inertia_check",
],
},
UnimplementedBackend {
backend: "the SPRAL SSIDS sparse symmetric linear solver",
family: "spral_*",
options: &[
"spral_cpu_block_size",
"spral_gpu_perf_coeff",
"spral_ignore_numa",
"spral_max_load_inbalance",
"spral_min_gpu_work",
"spral_nemin",
"spral_order",
"spral_pivot_method",
"spral_print_level",
"spral_scaling",
"spral_scaling_1",
"spral_scaling_2",
"spral_scaling_3",
"spral_small",
"spral_small_subtree_threshold",
"spral_switch_1",
"spral_switch_2",
"spral_switch_3",
"spral_u",
"spral_umax",
"spral_use_gpu",
],
},
UnimplementedBackend {
backend: "the WSMP sparse symmetric linear solver",
family: "wsmp_*",
options: &[
"wsmp_inexact_droptol",
"wsmp_inexact_fillin_limit",
"wsmp_iterative",
"wsmp_max_iter",
"wsmp_no_pivoting",
"wsmp_num_threads",
"wsmp_ordering_option",
"wsmp_ordering_option2",
"wsmp_pivtol",
"wsmp_pivtolmax",
"wsmp_scaling",
"wsmp_singularity_threshold",
"wsmp_skip_inertia_check",
"wsmp_write_matrix_iteration",
],
},
];
pub fn backend_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
UNIMPLEMENTED_BACKENDS
.iter()
.filter_map(|group| {
let set: Vec<&str> = group
.options
.iter()
.copied()
.filter(|name| set_to_a_non_default(options, reg, name))
.collect();
if set.is_empty() {
return None;
}
let named = set
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ");
let (verb, ignored, registered) = if set.len() == 1 {
("configures", "it is ignored".to_string(), "The name is")
} else {
(
"configure",
format!("those {} are ignored", set.len()),
"The names are",
)
};
Some(format!(
"pounce: warning: {named} {verb} {}, which pounce does not \
implement, so {ignored} — as is every other `{}` option. \
pounce factors the KKT system with `feral` (pure Rust, the \
default) or MA57 (`linear_solver=ma57`, in a `--features \
ma57` build); no setting written for another backend \
transfers to either. {registered} registered so an \
`ipopt.opt` written for Ipopt still parses unchanged — which \
is why this is a warning and not an error: the solve runs, \
and its result is unaffected. Tracking issue: \
https://github.com/jkitchin/pounce/issues/551",
group.backend, group.family,
))
})
.collect()
}
const DELIVERY_MECHANISM: &[&str] = &["option_file_name"];
fn is_backend_knob(name: &str) -> bool {
UNIMPLEMENTED_BACKENDS
.iter()
.any(|group| group.options.contains(&name))
}
pub fn backend_only_refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
let asked_for_something = UNIMPLEMENTED_BACKENDS.iter().any(|group| {
group
.options
.iter()
.any(|name| set_to_a_non_default(options, reg, name))
});
if !asked_for_something {
return None;
}
let mentions_something_real = options
.names()
.any(|name| !is_backend_knob(name) && !DELIVERY_MECHANISM.contains(&name));
if mentions_something_real {
return None;
}
Some(
"pounce: error: every option this run sets configures a linear-solver \
backend pounce does not implement, so there is nothing left for it to \
act on. pounce factors the KKT system with `feral` (pure Rust, the \
default) or MA57 (`linear_solver=ma57`, in a `--features ma57` \
build); no setting written for another backend transfers to either. \
These names are registered so an `ipopt.opt` written for Ipopt still \
parses unchanged, and a file that also carries options pounce reads \
is warned about rather than refused — but a file that carries only \
these would run as if it had configured the solver when it \
configured nothing. Set `linear_solver=feral` (or `ma57`) if the \
defaults are what you want. Tracking issue: \
https://github.com/jkitchin/pounce/issues/551"
.to_string(),
)
}
pub(crate) fn set_to_a_non_default(
options: &OptionsList,
reg: &RegisteredOptions,
name: &str,
) -> bool {
let Some(opt) = reg.get_option(name) else {
return false;
};
match &opt.default {
DefaultValue::String(d) => {
matches!(options.get_string_value(name, ""), Ok((v, true)) if !v.eq_ignore_ascii_case(d))
}
DefaultValue::Number(d) => {
matches!(options.get_numeric_value(name, ""), Ok((v, true)) if v != *d)
}
DefaultValue::Integer(d) => {
matches!(options.get_integer_value(name, ""), Ok((v, true)) if v != *d)
}
DefaultValue::None => false,
}
}
pub fn refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
for group in UNIMPLEMENTED_FEATURES {
for name in group.options {
if set_to_a_non_default(options, reg, name) {
let advice = if group.advice.is_empty() {
String::new()
} else {
format!(" Instead: {}.", group.advice)
};
return Some(format!(
"pounce: `{name}` configures {}, which pounce does not \
implement. It is registered so an ipopt.opt written for \
Ipopt still parses, but setting it used to do nothing at \
all — silently — so it is refused instead.{advice} \
Remove it to run. Tracking issue: \
https://github.com/jkitchin/pounce/issues/{}",
group.feature, group.issue
));
}
}
}
None
}
pub fn value_refusal(options: &OptionsList) -> Option<String> {
for entry in UNIMPLEMENTED_VALUES {
let selected = matches!(
options.get_string_value(entry.option, ""),
Ok((v, true)) if v.eq_ignore_ascii_case(entry.value)
);
if !selected {
continue;
}
let advice = if entry.advice.is_empty() {
String::new()
} else {
format!(" Instead: {}.", entry.advice)
};
return Some(format!(
"pounce: `{}={}` selects {}, which pounce does not implement. The \
value is registered so an ipopt.opt written for Ipopt still \
parses; falling back to another mode would silently run a \
different initialization than the one you asked for, so it is \
refused instead.{advice} Tracking issue: \
https://github.com/jkitchin/pounce/issues/604",
entry.option, entry.value, entry.feature,
));
}
None
}
pub fn hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
UNEXPLOITED_HINTS
.iter()
.filter(|name| set_to_a_non_default(options, reg, name))
.map(|name| {
format!(
"pounce: warning: `{name}` is a caching hint pounce does not \
exploit — it re-evaluates each iteration regardless. Your \
answer is unaffected; only the evaluation count is. \
(gh#483)"
)
})
.collect()
}
pub fn convex_hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
CONVEX_UNEXPLOITED_HINTS
.iter()
.filter(|name| set_to_a_non_default(options, reg, name))
.map(|name| {
format!(
"pounce: warning: `{name}` asserts a derivative is constant \
across iterates, which the NLP path reconciles against the \
model and exploits — but this problem routed to \
pounce-convex, whose LP/QP/SOCP engines are handed constant \
matrices to begin with and do not read the option at all. \
Your answer is unaffected. Use `solver_selection=nlp` if you \
wanted the path that acts on it. (gh#483, gh#588)"
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn registry() -> std::rc::Rc<RegisteredOptions> {
let r = RegisteredOptions::new();
crate::upstream_options::register_all_upstream_options(&r).expect("register");
r
}
fn fixture() -> (OptionsList, std::rc::Rc<RegisteredOptions>) {
let reg = registry();
(OptionsList::with_registered(std::rc::Rc::clone(®)), reg)
}
#[test]
fn every_listed_option_is_registered() {
let (_, reg) = fixture();
for group in UNIMPLEMENTED_FEATURES {
for name in group.options {
assert!(
reg.get_option(name).is_some(),
"`{name}` is in the refusal table but is not registered",
);
}
}
for name in UNEXPLOITED_HINTS.iter().chain(CONVEX_UNEXPLOITED_HINTS) {
assert!(
reg.get_option(name).is_some(),
"`{name}` is in a hint table but is not registered",
);
}
for group in UNIMPLEMENTED_BACKENDS {
for name in group.options {
assert!(
reg.get_option(name).is_some(),
"`{name}` is in the backend table but is not registered",
);
}
}
}
#[test]
fn backend_families_are_complete() {
let (_, reg) = fixture();
for group in UNIMPLEMENTED_BACKENDS {
let prefix = group
.family
.strip_suffix('*')
.expect("family is a prefix glob, e.g. `ma97_*`");
for opt in reg.registered_options_in_order() {
if !opt.name.starts_with(prefix) {
continue;
}
assert!(
group.options.contains(&opt.name.as_str()),
"`{}` is registered and matches `{}` but is missing from \
the {} group, so setting it would still be silent",
opt.name,
group.family,
group.backend,
);
}
}
}
#[test]
fn the_convex_hint_table_does_not_collide_with_a_refusal() {
let refused: BTreeSet<&str> = UNIMPLEMENTED_FEATURES
.iter()
.flat_map(|g| g.options.iter().copied())
.collect();
for name in CONVEX_UNEXPLOITED_HINTS {
assert!(
!refused.contains(name),
"`{name}` both warns on the convex route and is refused",
);
}
}
#[test]
fn the_convex_route_still_says_the_hints_do_nothing_there() {
let (mut opts, reg) = fixture();
assert!(
convex_hint_warnings(&opts, ®).is_empty(),
"an unset hint must not warn"
);
opts.set_string_value("hessian_constant", "yes", true, false)
.unwrap();
let w = convex_hint_warnings(&opts, ®);
assert_eq!(w.len(), 1, "{w:?}");
assert!(w[0].contains("hessian_constant"), "{}", w[0]);
assert!(w[0].contains("pounce-convex"), "{}", w[0]);
assert_eq!(
refusal(&opts, ®),
None,
"a hint must not block a convex solve either"
);
}
#[test]
fn the_tables_do_not_overlap() {
let mut seen = BTreeSet::new();
for name in UNIMPLEMENTED_FEATURES
.iter()
.flat_map(|g| g.options.iter())
.chain(UNEXPLOITED_HINTS.iter())
.chain(UNIMPLEMENTED_BACKENDS.iter().flat_map(|g| g.options.iter()))
{
assert!(seen.insert(*name), "`{name}` is listed twice");
}
}
#[test]
fn defaults_are_not_refused() {
let (opts, reg) = fixture();
assert_eq!(refusal(&opts, ®), None);
assert!(hint_warnings(&opts, ®).is_empty());
assert!(
backend_warnings(&opts, ®).is_empty(),
"a default run must stay silent",
);
assert_eq!(
backend_only_refusal(&opts, ®),
None,
"an empty list sets no backend knob, so there is nothing to refuse",
);
}
#[test]
fn backend_only_is_refused_but_a_mixed_list_is_not() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "metis", true, false)
.unwrap();
let refused = backend_only_refusal(&opts, ®)
.expect("a list of nothing but backend knobs must be refused");
assert!(
refused.contains("every option this run sets"),
"the message is about the whole list, not one knob: {refused}",
);
assert!(
!backend_warnings(&opts, ®).is_empty(),
"the per-family warning still describes the knob; the refusal is \
what stops the run",
);
opts.set_numeric_value("tol", 1e-8, true, false).unwrap();
assert_eq!(
backend_only_refusal(&opts, ®),
None,
"one option pounce reads is enough content to protect",
);
}
#[test]
fn a_backend_knob_at_its_default_is_not_refused() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "auto", true, false)
.unwrap();
assert_eq!(backend_only_refusal(&opts, ®), None);
}
#[test]
fn the_delivery_mechanism_is_not_content() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "metis", true, false)
.unwrap();
opts.set_string_value("option_file_name", "ipopt.opt", true, false)
.unwrap();
assert!(
backend_only_refusal(&opts, ®).is_some(),
"naming the file that carried the knobs is not a reason to spare it",
);
}
#[test]
fn the_delivery_mechanism_names_are_registered() {
let (_, reg) = fixture();
for name in DELIVERY_MECHANISM {
assert!(
reg.get_option(name).is_some(),
"`{name}` is not a registered option",
);
assert!(
!is_backend_knob(name),
"`{name}` is a backend knob; exempting it is meaningless",
);
}
}
#[test]
fn a_backend_knob_warns_but_does_not_refuse() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "metis", true, false)
.unwrap();
assert_eq!(
refusal(&opts, ®),
None,
"a backend knob must not block a solve",
);
let warnings = backend_warnings(&opts, ®);
assert_eq!(warnings.len(), 1, "{warnings:?}");
let w = &warnings[0];
assert!(w.contains("warning:"), "{w}");
assert!(w.contains("`ma97_order`"), "{w}");
assert!(w.contains("MA97"), "the backend must be named: {w}");
assert!(w.contains("`ma97_*`"), "the family must be named: {w}");
assert!(w.contains("result is unaffected"), "{w}");
assert!(w.contains("551"), "{w}");
}
#[test]
fn a_backend_knob_at_its_default_is_silent() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "auto", true, false)
.unwrap();
opts.set_integer_value("pardiso_msglvl", 0, true, false)
.unwrap();
assert!(backend_warnings(&opts, ®).is_empty());
}
#[test]
fn the_warning_is_grouped_by_backend_family() {
let (mut opts, reg) = fixture();
opts.set_string_value("ma97_order", "metis", true, false)
.unwrap();
opts.set_numeric_value("ma97_u", 1e-4, true, false).unwrap();
opts.set_string_value("ma97_scaling", "mc64", true, false)
.unwrap();
opts.set_integer_value("pardiso_msglvl", 1, true, false)
.unwrap();
let warnings = backend_warnings(&opts, ®);
assert_eq!(
warnings.len(),
2,
"one per family, not per option: {warnings:?}"
);
let ma97 = warnings.iter().find(|w| w.contains("MA97")).expect("MA97");
for name in ["`ma97_order`", "`ma97_u`", "`ma97_scaling`"] {
assert!(ma97.contains(name), "{ma97}");
}
assert!(ma97.contains("those 3 are ignored"), "{ma97}");
assert!(
warnings.iter().any(|w| w.contains("Pardiso")),
"{warnings:?}",
);
}
#[test]
fn pardisolib_warns_with_the_pardiso_family() {
let (mut opts, reg) = fixture();
opts.set_string_value("pardisolib", "libpardiso600.so", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
let warnings = backend_warnings(&opts, ®);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("`pardisolib`"), "{:?}", warnings[0]);
assert!(warnings[0].contains("Pardiso"), "{:?}", warnings[0]);
let (mut opts, reg) = fixture();
opts.set_string_value("hsllib", "libcoinhsl.so", true, false)
.unwrap();
assert!(refusal(&opts, ®).is_some());
}
#[test]
fn explicitly_setting_the_default_is_not_refused() {
let (mut opts, reg) = fixture();
opts.set_string_value("dependency_detector", "none", true, false)
.unwrap();
opts.set_string_value("magic_steps", "no", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
}
#[test]
fn requesting_an_unimplemented_feature_is_refused() {
let (mut opts, reg) = fixture();
opts.set_string_value("dependency_detector", "mumps", true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("dependency_detector"), "{msg}");
assert!(msg.contains("linear-dependency detection"), "{msg}");
assert!(msg.contains("483"), "{msg}");
}
#[test]
fn a_numeric_knob_of_an_absent_feature_is_refused() {
let (mut opts, reg) = fixture();
opts.set_numeric_value("penalty_init_max", 42.0, true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("CG-penalty"), "{msg}");
}
#[test]
fn the_constant_derivative_hints_are_no_longer_unexploited() {
let (mut opts, reg) = fixture();
opts.set_string_value("hessian_constant", "yes", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None, "a hint must not block a solve");
assert!(
hint_warnings(&opts, ®).is_empty(),
"`hessian_constant` is exploited now; the unexploited-hint \
warning would contradict the reuse the solver actually does",
);
assert!(
UNEXPLOITED_HINTS.is_empty(),
"gh #588 Q6 emptied this table; an entry added back needs its \
own warning text and a test that the option is really unused",
);
}
#[test]
fn fast_step_computation_is_wired_not_refused() {
let (mut opts, reg) = fixture();
opts.set_string_value("fast_step_computation", "yes", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("fast_step_computation yes\n")
.unwrap();
assert!(
app.algorithm_builder_from_options().fast_step_computation,
"the option must reach the builder, or wiring it changed nothing",
);
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
assert!(!app.algorithm_builder_from_options().fast_step_computation);
}
#[test]
fn option_file_name_is_implemented_not_in_the_table() {
let (mut opts, reg) = fixture();
opts.set_string_value("option_file_name", "tiny.opt", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
}
#[test]
fn option_file_name_is_refused_where_nothing_resolves_it() {
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(app.unhonored_option_file_name(), None, "unset asks nothing");
app.initialize_with_options_str("option_file_name tiny.opt\n")
.unwrap();
let msg = app
.unhonored_option_file_name()
.expect("a library caller cannot honor it");
assert!(msg.contains("tiny.opt"), "{msg}");
assert!(msg.contains("does not read options files"), "{msg}");
assert!(msg.contains("518"), "{msg}");
}
#[test]
fn option_file_name_at_its_default_asks_nothing_of_a_library_caller() {
let mut app = crate::application::IpoptApplication::new();
app.initialize_with_options_str("option_file_name ipopt.opt\n")
.unwrap();
assert_eq!(app.unhonored_option_file_name(), None);
}
#[test]
fn the_guard_is_quiet_once_the_option_file_path_has_run() {
let dir = std::env::temp_dir().join(format!("pounce_gh518_lib_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("tiny.opt");
std::fs::write(&path, "max_iter 5\n").unwrap();
let mut app = crate::application::IpoptApplication::new();
app.initialize_with_option_file(Some(&path)).unwrap();
assert_eq!(app.unhonored_option_file_name(), None);
assert_eq!(
app.options().get_integer_value("max_iter", "").unwrap(),
(5, true),
"the file must actually have been read",
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn the_restoration_switches_reach_the_builder() {
for (key, default_on) in [
("evaluate_orig_obj_at_resto_trial", true),
("expect_infeasible_problem", false),
("start_with_resto", false),
] {
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
let resto = app.algorithm_builder_from_options().resto;
let got = match key {
"evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
"expect_infeasible_problem" => resto.expect_infeasible_problem,
_ => resto.start_with_resto,
};
assert_eq!(got, default_on, "{key}: default changed");
let flipped = if default_on { "no" } else { "yes" };
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(&format!("{key} {flipped}\n"))
.unwrap();
let resto = app.algorithm_builder_from_options().resto;
let got = match key {
"evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
"expect_infeasible_problem" => resto.expect_infeasible_problem,
_ => resto.start_with_resto,
};
assert_eq!(
got, !default_on,
"{key}={flipped} never reached the builder"
);
}
}
#[test]
fn max_resto_iter_reaches_the_builder() {
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
let b = app.algorithm_builder_from_options();
assert_eq!(b.resto.max_resto_iter, 3000);
let reg = registry();
let registered = reg.get_option("max_resto_iter").expect("registered");
assert!(
matches!(registered.default, DefaultValue::Integer(3_000_000)),
"the registry no longer declares 3000000 — if upstream's number \
was adopted as the effective default, that is a trajectory \
change and this test should be the one that says so",
);
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("max_resto_iter 17\n")
.unwrap();
assert_eq!(
app.algorithm_builder_from_options().resto.max_resto_iter,
17,
"never reached the builder",
);
}
#[test]
fn the_corrector_knobs_are_refused() {
let (mut opts, reg) = fixture();
opts.set_string_value("corrector_type", "affine", true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("`corrector_type`"), "{msg}");
assert!(msg.contains("TryCorrector"), "{msg}");
assert!(msg.contains("mehrotra_algorithm"), "{msg}");
assert!(msg.contains("551"), "{msg}");
for (name, value) in [
("skip_corr_if_neg_curv", "no"),
("skip_corr_in_monotone_mode", "no"),
] {
let (mut opts, reg) = fixture();
opts.set_string_value(name, value, true, false).unwrap();
assert!(refusal(&opts, ®).is_some(), "`{name}` must refuse");
}
let (mut opts, reg) = fixture();
opts.set_numeric_value("corrector_compl_avrg_red_fact", 2.0, true, false)
.unwrap();
assert!(refusal(&opts, ®).is_some());
let (mut opts, reg) = fixture();
opts.set_string_value("corrector_type", "none", true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
}
#[test]
fn the_missing_restoration_sub_capabilities_are_refused() {
let (mut opts, reg) = fixture();
opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-4, true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("filter line search"), "{msg}");
assert!(
msg.contains("restoration phase itself runs"),
"the message must say the parent feature is unaffected: {msg}",
);
let (mut opts, reg) = fixture();
opts.set_numeric_value("expect_infeasible_problem_ytol", 1e6, true, false)
.unwrap();
assert!(refusal(&opts, ®).is_some());
let (mut opts, reg) = fixture();
opts.set_string_value("limited_memory_special_for_resto", "yes", true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("Nov 2010"), "{msg}");
assert!(
msg.contains("L-BFGS runs in the restoration sub-solve"),
"{msg}",
);
let (mut opts, reg) = fixture();
opts.set_numeric_value("resto_failure_feasibility_threshold", 1e-6, true, false)
.unwrap();
let msg = refusal(&opts, ®).expect("must refuse");
assert!(msg.contains("restoration runs"), "{msg}");
assert!(msg.contains("max_resto_iter"), "{msg}");
let (mut opts, reg) = fixture();
opts.set_numeric_value("expect_infeasible_problem_ctol", 1e-3, true, false)
.unwrap();
opts.set_string_value("limited_memory_special_for_resto", "no", true, false)
.unwrap();
opts.set_numeric_value("resto_failure_feasibility_threshold", 0.0, true, false)
.unwrap();
assert_eq!(refusal(&opts, ®), None);
}
#[test]
fn the_lbfgs_sigma_clamp_reaches_the_builder() {
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
let b = app.algorithm_builder_from_options();
assert_eq!(b.limited_memory_init_val_max, 1e8, "default changed");
assert_eq!(b.limited_memory_init_val_min, 1e-8, "default changed");
let mut app = crate::application::IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"limited_memory_init_val_max 5e5\nlimited_memory_init_val_min 1e-3\n",
)
.unwrap();
let b = app.algorithm_builder_from_options();
assert_eq!(
b.limited_memory_init_val_max, 5e5,
"never reached the builder"
);
assert_eq!(
b.limited_memory_init_val_min, 1e-3,
"never reached the builder"
);
}
#[test]
fn options_on_implemented_features_are_not_refused() {
for (name, value) in [
("max_resto_iter", "17"),
("accept_after_max_steps", "3"),
("limited_memory_max_skipping", "4"),
("fast_step_computation", "yes"),
] {
let (mut opts, reg) = fixture();
let set = opts.set_string_value(name, value, true, false).is_ok()
|| value
.parse::<i32>()
.ok()
.is_some_and(|v| opts.set_integer_value(name, v, true, false).is_ok())
|| value
.parse::<f64>()
.ok()
.is_some_and(|v| opts.set_numeric_value(name, v, true, false).is_ok());
assert!(set, "could not set `{name}` to `{value}`");
assert_eq!(
refusal(&opts, ®),
None,
"`{name}` configures a feature pounce implements; it needs a \
read site, not a refusal",
);
}
}
}