1use pounce_common::options_list::OptionsList;
64use pounce_common::reg_options::{DefaultValue, RegisteredOptions};
65
66pub struct UnimplementedFeature {
68 pub feature: &'static str,
70 pub advice: &'static str,
72 pub options: &'static [&'static str],
74}
75
76pub const UNIMPLEMENTED_FEATURES: &[UnimplementedFeature] = &[
78 UnimplementedFeature {
79 feature: "the Chen-Goldfarb (CG-penalty) / inexact-Newton line search \
80 — Ipopt's `CGPenaltyLSAcceptor`",
81 advice: "pounce implements the filter line search (the default) and \
82 `line_search_method=penalty` (`IpPenaltyLSAcceptor`); tune \
83 those instead",
84 options: &[
85 "chi_cup",
86 "chi_hat",
87 "chi_tilde",
88 "delta_y_max",
89 "epsilon_c",
90 "eta_min",
91 "fast_des_fact",
92 "gamma_hat",
93 "gamma_tilde",
94 "kappa_x_dis",
95 "kappa_y_dis",
96 "min_alpha_primal",
97 "mult_diverg_feasibility_tol",
98 "mult_diverg_y_tol",
99 "never_use_fact_cgpen_direction",
100 "never_use_piecewise_penalty_ls",
101 "pen_des_fact",
102 "pen_init_fac",
103 "pen_theta_max_fact",
104 "penalty_init_max",
105 "penalty_init_min",
106 "penalty_max",
107 "penalty_update_compl_tol",
108 "penalty_update_infeasibility_tol",
109 "piecewisepenalty_gamma_infeasi",
110 "piecewisepenalty_gamma_obj",
111 "vartheta",
112 "inexact_algorithm",
113 ],
114 },
115 UnimplementedFeature {
116 feature: "derivative approximation by finite differences",
117 advice: "supply `eval_grad_f` / `eval_jac_g` / `eval_h`, and check them \
118 with `derivative_test=first-order`",
119 options: &[
120 "gradient_approximation",
121 "jacobian_approximation",
122 "findiff_perturbation",
123 ],
124 },
125 UnimplementedFeature {
126 feature: "linear-dependency detection on the equality constraints",
127 advice: "pounce's presolve removes structurally redundant rows; see \
128 `presolve`",
129 options: &[
130 "dependency_detector",
131 "dependency_detection_with_rhs",
132 "ma28_pivtol",
133 ],
134 },
135 UnimplementedFeature {
136 feature: "the per-iteration NaN/Inf check on derivative matrices",
137 advice: "`derivative_test=first-order` checks the derivatives once, at \
138 the starting point",
139 options: &["check_derivatives_for_naninf"],
140 },
141 UnimplementedFeature {
142 feature: "multiplier recalculation by least squares",
143 advice: "",
144 options: &["recalc_y", "recalc_y_feas_tol"],
145 },
146 UnimplementedFeature {
147 feature: "a selectable constraint-violation norm",
148 advice: "pounce measures the violation in the 2-norm throughout",
149 options: &["constraint_violation_norm_type"],
150 },
151 UnimplementedFeature {
152 feature: "magic steps",
153 advice: "",
154 options: &["magic_steps"],
155 },
156 UnimplementedFeature {
157 feature: "bound replacement on the original problem",
158 advice: "",
159 options: &["replace_bounds"],
160 },
161 UnimplementedFeature {
162 feature: "the L-BFGS augmented-system and space variants",
163 advice: "`hessian_approximation=limited-memory` uses the low-rank \
164 augmented system unconditionally",
165 options: &["hessian_approximation_space", "limited_memory_aug_solver"],
166 },
167 UnimplementedFeature {
168 feature: "the linear-variable count hint for L-BFGS",
169 advice: "",
170 options: &["num_linear_variables"],
171 },
172 UnimplementedFeature {
173 feature: "skipping the finalize-solution callback",
174 advice: "",
175 options: &["skip_finalize_solution_call"],
176 },
177 UnimplementedFeature {
178 feature: "the dynamic HSL loader",
179 advice: "MA57 is linked at build time with `--features ma57`",
180 options: &["hsllib"],
181 },
182 UnimplementedFeature {
183 feature: "these output controls",
184 advice: "use `print_level` (0 silences the solver) and `sb=yes` to \
185 suppress the banner",
186 options: &["suppress_all_output", "debug_print_level"],
187 },
188 UnimplementedFeature {
189 feature: "a randomly perturbed evaluation point for the derivative \
190 checker",
191 advice: "pounce's checker tests at the (bound-projected) starting point, \
192 which is where the solve actually begins",
193 options: &["point_perturbation_radius"],
194 },
195];
196
197pub const UNEXPLOITED_HINTS: &[&str] = &[
202 "grad_f_constant",
203 "hessian_constant",
204 "jac_c_constant",
205 "jac_d_constant",
206];
207
208pub(crate) fn set_to_a_non_default(
214 options: &OptionsList,
215 reg: &RegisteredOptions,
216 name: &str,
217) -> bool {
218 let Some(opt) = reg.get_option(name) else {
219 return false;
220 };
221 match &opt.default {
222 DefaultValue::String(d) => {
225 matches!(options.get_string_value(name, ""), Ok((v, true)) if !v.eq_ignore_ascii_case(d))
226 }
227 DefaultValue::Number(d) => {
228 matches!(options.get_numeric_value(name, ""), Ok((v, true)) if v != *d)
229 }
230 DefaultValue::Integer(d) => {
231 matches!(options.get_integer_value(name, ""), Ok((v, true)) if v != *d)
232 }
233 DefaultValue::None => false,
234 }
235}
236
237pub fn refusal(options: &OptionsList, reg: &RegisteredOptions) -> Option<String> {
240 for group in UNIMPLEMENTED_FEATURES {
241 for name in group.options {
242 if set_to_a_non_default(options, reg, name) {
243 let advice = if group.advice.is_empty() {
244 String::new()
245 } else {
246 format!(" Instead: {}.", group.advice)
247 };
248 return Some(format!(
249 "pounce: `{name}` configures {}, which pounce does not \
250 implement. It is registered so an ipopt.opt written for \
251 Ipopt still parses, but setting it used to do nothing at \
252 all — silently — so it is refused instead.{advice} \
253 Remove it to run. Tracking issue: \
254 https://github.com/jkitchin/pounce/issues/483",
255 group.feature
256 ));
257 }
258 }
259 }
260 None
261}
262
263pub fn hint_warnings(options: &OptionsList, reg: &RegisteredOptions) -> Vec<String> {
265 UNEXPLOITED_HINTS
266 .iter()
267 .filter(|name| set_to_a_non_default(options, reg, name))
268 .map(|name| {
269 format!(
270 "pounce: warning: `{name}` is a caching hint pounce does not \
271 exploit — it re-evaluates each iteration regardless. Your \
272 answer is unaffected; only the evaluation count is. \
273 (gh#483)"
274 )
275 })
276 .collect()
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use std::collections::BTreeSet;
283
284 fn registry() -> std::rc::Rc<RegisteredOptions> {
285 let r = RegisteredOptions::new();
286 crate::upstream_options::register_all_upstream_options(&r).expect("register");
287 r
288 }
289
290 fn fixture() -> (OptionsList, std::rc::Rc<RegisteredOptions>) {
293 let reg = registry();
294 (OptionsList::with_registered(std::rc::Rc::clone(®)), reg)
295 }
296
297 #[test]
301 fn every_listed_option_is_registered() {
302 let (_, reg) = fixture();
303 for group in UNIMPLEMENTED_FEATURES {
304 for name in group.options {
305 assert!(
306 reg.get_option(name).is_some(),
307 "`{name}` is in the refusal table but is not registered",
308 );
309 }
310 }
311 for name in UNEXPLOITED_HINTS {
312 assert!(
313 reg.get_option(name).is_some(),
314 "`{name}` is in the hint table but is not registered",
315 );
316 }
317 }
318
319 #[test]
323 fn the_tables_do_not_overlap() {
324 let mut seen = BTreeSet::new();
325 for name in UNIMPLEMENTED_FEATURES
326 .iter()
327 .flat_map(|g| g.options.iter())
328 .chain(UNEXPLOITED_HINTS.iter())
329 {
330 assert!(seen.insert(*name), "`{name}` is listed twice");
331 }
332 }
333
334 #[test]
336 fn defaults_are_not_refused() {
337 let (opts, reg) = fixture();
338 assert_eq!(refusal(&opts, ®), None);
339 assert!(hint_warnings(&opts, ®).is_empty());
340 }
341
342 #[test]
345 fn explicitly_setting_the_default_is_not_refused() {
346 let (mut opts, reg) = fixture();
347 opts.set_string_value("dependency_detector", "none", true, false)
349 .unwrap();
350 opts.set_string_value("magic_steps", "no", true, false)
351 .unwrap();
352 assert_eq!(refusal(&opts, ®), None);
353 }
354
355 #[test]
357 fn requesting_an_unimplemented_feature_is_refused() {
358 let (mut opts, reg) = fixture();
359 opts.set_string_value("dependency_detector", "mumps", true, false)
360 .unwrap();
361 let msg = refusal(&opts, ®).expect("must refuse");
362 assert!(msg.contains("dependency_detector"), "{msg}");
363 assert!(msg.contains("linear-dependency detection"), "{msg}");
364 assert!(msg.contains("483"), "{msg}");
365 }
366
367 #[test]
369 fn a_numeric_knob_of_an_absent_feature_is_refused() {
370 let (mut opts, reg) = fixture();
371 opts.set_numeric_value("penalty_init_max", 42.0, true, false)
372 .unwrap();
373 let msg = refusal(&opts, ®).expect("must refuse");
374 assert!(msg.contains("CG-penalty"), "{msg}");
375 }
376
377 #[test]
381 fn caching_hints_warn_but_do_not_refuse() {
382 let (mut opts, reg) = fixture();
383 opts.set_string_value("hessian_constant", "yes", true, false)
384 .unwrap();
385 assert_eq!(refusal(&opts, ®), None, "a hint must not block a solve");
386 let warnings = hint_warnings(&opts, ®);
387 assert_eq!(warnings.len(), 1, "{warnings:?}");
388 assert!(warnings[0].contains("hessian_constant"));
389 }
390
391 #[test]
397 fn fast_step_computation_is_wired_not_refused() {
398 let (mut opts, reg) = fixture();
399 opts.set_string_value("fast_step_computation", "yes", true, false)
400 .unwrap();
401 assert_eq!(refusal(&opts, ®), None);
402
403 let mut app = crate::application::IpoptApplication::new();
404 app.initialize().unwrap();
405 app.initialize_with_options_str("fast_step_computation yes\n")
406 .unwrap();
407 assert!(
408 app.algorithm_builder_from_options().fast_step_computation,
409 "the option must reach the builder, or wiring it changed nothing",
410 );
411 let mut app = crate::application::IpoptApplication::new();
413 app.initialize().unwrap();
414 assert!(!app.algorithm_builder_from_options().fast_step_computation);
415 }
416
417 #[test]
422 fn option_file_name_is_implemented_not_in_the_table() {
423 let (mut opts, reg) = fixture();
424 opts.set_string_value("option_file_name", "tiny.opt", true, false)
425 .unwrap();
426 assert_eq!(refusal(&opts, ®), None);
427 }
428
429 #[test]
435 fn option_file_name_is_refused_where_nothing_resolves_it() {
436 let mut app = crate::application::IpoptApplication::new();
437 app.initialize().unwrap();
438 assert_eq!(app.unhonored_option_file_name(), None, "unset asks nothing");
439
440 app.initialize_with_options_str("option_file_name tiny.opt\n")
441 .unwrap();
442 let msg = app
443 .unhonored_option_file_name()
444 .expect("a library caller cannot honor it");
445 assert!(msg.contains("tiny.opt"), "{msg}");
446 assert!(msg.contains("does not read options files"), "{msg}");
447 assert!(msg.contains("518"), "{msg}");
448 }
449
450 #[test]
456 fn option_file_name_at_its_default_asks_nothing_of_a_library_caller() {
457 let mut app = crate::application::IpoptApplication::new();
458 app.initialize_with_options_str("option_file_name ipopt.opt\n")
459 .unwrap();
460 assert_eq!(app.unhonored_option_file_name(), None);
461 }
462
463 #[test]
467 fn the_guard_is_quiet_once_the_option_file_path_has_run() {
468 let dir = std::env::temp_dir().join(format!("pounce_gh518_lib_{}", std::process::id()));
469 std::fs::create_dir_all(&dir).unwrap();
470 let path = dir.join("tiny.opt");
471 std::fs::write(&path, "max_iter 5\n").unwrap();
472
473 let mut app = crate::application::IpoptApplication::new();
474 app.initialize_with_option_file(Some(&path)).unwrap();
475 assert_eq!(app.unhonored_option_file_name(), None);
476 assert_eq!(
477 app.options().get_integer_value("max_iter", "").unwrap(),
478 (5, true),
479 "the file must actually have been read",
480 );
481
482 let _ = std::fs::remove_dir_all(&dir);
483 }
484
485 #[test]
492 fn the_restoration_switches_reach_the_builder() {
493 for (key, default_on) in [
494 ("evaluate_orig_obj_at_resto_trial", true),
495 ("expect_infeasible_problem", false),
496 ("start_with_resto", false),
497 ] {
498 let mut app = crate::application::IpoptApplication::new();
499 app.initialize().unwrap();
500 let resto = app.algorithm_builder_from_options().resto;
501 let got = match key {
502 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
503 "expect_infeasible_problem" => resto.expect_infeasible_problem,
504 _ => resto.start_with_resto,
505 };
506 assert_eq!(got, default_on, "{key}: default changed");
507
508 let flipped = if default_on { "no" } else { "yes" };
510 let mut app = crate::application::IpoptApplication::new();
511 app.initialize().unwrap();
512 app.initialize_with_options_str(&format!("{key} {flipped}\n"))
513 .unwrap();
514 let resto = app.algorithm_builder_from_options().resto;
515 let got = match key {
516 "evaluate_orig_obj_at_resto_trial" => resto.evaluate_orig_obj_at_resto_trial,
517 "expect_infeasible_problem" => resto.expect_infeasible_problem,
518 _ => resto.start_with_resto,
519 };
520 assert_eq!(
521 got, !default_on,
522 "{key}={flipped} never reached the builder"
523 );
524 }
525 }
526
527 #[test]
534 fn the_lbfgs_sigma_clamp_reaches_the_builder() {
535 let mut app = crate::application::IpoptApplication::new();
536 app.initialize().unwrap();
537 let b = app.algorithm_builder_from_options();
538 assert_eq!(b.limited_memory_init_val_max, 1e8, "default changed");
539 assert_eq!(b.limited_memory_init_val_min, 1e-8, "default changed");
540
541 let mut app = crate::application::IpoptApplication::new();
542 app.initialize().unwrap();
543 app.initialize_with_options_str(
544 "limited_memory_init_val_max 5e5\nlimited_memory_init_val_min 1e-3\n",
545 )
546 .unwrap();
547 let b = app.algorithm_builder_from_options();
548 assert_eq!(
549 b.limited_memory_init_val_max, 5e5,
550 "never reached the builder"
551 );
552 assert_eq!(
553 b.limited_memory_init_val_min, 1e-3,
554 "never reached the builder"
555 );
556 }
557
558 #[test]
562 fn options_on_implemented_features_are_not_refused() {
563 for (name, value) in [
564 ("max_resto_iter", "17"),
566 ("accept_after_max_steps", "3"),
568 ("limited_memory_max_skipping", "4"),
570 ("corrector_type", "affine"),
572 ("fast_step_computation", "yes"),
576 ] {
577 let (mut opts, reg) = fixture();
578 let set = opts.set_string_value(name, value, true, false).is_ok()
581 || value
582 .parse::<i32>()
583 .ok()
584 .is_some_and(|v| opts.set_integer_value(name, v, true, false).is_ok())
585 || value
586 .parse::<f64>()
587 .ok()
588 .is_some_and(|v| opts.set_numeric_value(name, v, true, false).is_ok());
589 assert!(set, "could not set `{name}` to `{value}`");
590 assert_eq!(
591 refusal(&opts, ®),
592 None,
593 "`{name}` configures a feature pounce implements; it needs a \
594 read site, not a refusal",
595 );
596 }
597 }
598}