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 const UNIMPLEMENTED_FEATURES: &[UnimplementedFeature] = &[
UnimplementedFeature {
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 {
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 {
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 {
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 {
feature: "multiplier recalculation by least squares",
advice: "",
options: &["recalc_y", "recalc_y_feas_tol"],
},
UnimplementedFeature {
feature: "a selectable constraint-violation norm",
advice: "pounce measures the violation in the 2-norm throughout",
options: &["constraint_violation_norm_type"],
},
UnimplementedFeature {
feature: "magic steps",
advice: "",
options: &["magic_steps"],
},
UnimplementedFeature {
feature: "bound replacement on the original problem",
advice: "",
options: &["replace_bounds"],
},
UnimplementedFeature {
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 {
feature: "the linear-variable count hint for L-BFGS",
advice: "",
options: &["num_linear_variables"],
},
UnimplementedFeature {
feature: "skipping the finalize-solution callback",
advice: "",
options: &["skip_finalize_solution_call"],
},
UnimplementedFeature {
feature: "the dynamic HSL loader",
advice: "MA57 is linked at build time with `--features ma57`",
options: &["hsllib"],
},
UnimplementedFeature {
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 {
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"],
},
];
pub const UNEXPLOITED_HINTS: &[&str] = &[
"grad_f_constant",
"hessian_constant",
"jac_c_constant",
"jac_d_constant",
];
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/483",
group.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()
}
#[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 {
assert!(
reg.get_option(name).is_some(),
"`{name}` is in the hint table but is not registered",
);
}
}
#[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())
{
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());
}
#[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 caching_hints_warn_but_do_not_refuse() {
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");
let warnings = hint_warnings(&opts, ®);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("hessian_constant"));
}
#[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 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"),
("corrector_type", "affine"),
("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",
);
}
}
}