use pounce_algorithm::alg_builder::AlgorithmBuilder;
use pounce_algorithm::application::IpoptApplication;
use std::collections::BTreeSet;
fn builder_from(setup: impl FnOnce(&mut IpoptApplication)) -> AlgorithmBuilder {
let mut app = IpoptApplication::new();
setup(&mut app);
app.algorithm_builder_from_options()
}
const COLD_START_OPTIONS: &[&str] = &[
"bound_push",
"bound_frac",
"slack_bound_push",
"slack_bound_frac",
"constr_mult_init_max",
"bound_mult_init_val",
"bound_mult_init_method",
"least_square_init_primal",
];
const START_CONDITIONING_OPTIONS: &[&str] = &[
"infeasibility_perturbed_start_retry",
"start_point_perturbation",
"start_point_perturbation_seed",
"start_point_conditioner",
"adam_warmup_iters",
"adam_warmup_learning_rate",
"adam_warmup_penalty",
];
#[test]
fn every_start_conditioning_option_is_registered() {
let app = IpoptApplication::new();
let reg = app.registered_options();
for name in START_CONDITIONING_OPTIONS {
let opt = reg
.get_option(name)
.unwrap_or_else(|| panic!("`{name}` is read but not registered"));
assert_eq!(
opt.category, "Initialization",
"`{name}` should register under upstream's Initialization category"
);
}
}
#[test]
fn every_cold_start_option_is_registered() {
let app = IpoptApplication::new();
let reg = app.registered_options();
for name in COLD_START_OPTIONS {
let opt = reg
.get_option(name)
.unwrap_or_else(|| panic!("`{name}` is read by the builder but not registered"));
assert_eq!(
opt.category, "Initialization",
"`{name}` should register under upstream's Initialization category"
);
}
}
#[test]
fn cold_start_defaults_match_the_hard_coded_builder_values() {
let b = builder_from(|_| {}).init;
assert_eq!(b.bound_push, 1e-2);
assert_eq!(b.bound_frac, 1e-2);
assert_eq!(b.slack_bound_push, 1e-2);
assert_eq!(b.slack_bound_frac, 1e-2);
assert_eq!(b.constr_mult_init_max, 1e3);
assert_eq!(b.bound_mult_init_val, 1.0);
assert_eq!(b.bound_mult_init_method, "constant");
assert!(!b.least_square_init_primal);
let app = IpoptApplication::new();
let reg = app.registered_options();
for (name, expected) in [
("bound_push", 1e-2),
("bound_frac", 1e-2),
("slack_bound_push", 1e-2),
("slack_bound_frac", 1e-2),
("constr_mult_init_max", 1e3),
("bound_mult_init_val", 1.0),
] {
let opt = reg.get_option(name).expect("registered");
match opt.default {
pounce_common::reg_options::DefaultValue::Number(d) => assert_eq!(
d, expected,
"registered default for `{name}` must equal the builder's"
),
ref other => panic!("`{name}` should be a number option, got {other:?}"),
}
}
}
#[test]
fn numeric_cold_start_overrides_flow_through() {
let b = builder_from(|app| {
let o = app.options_mut();
o.set_numeric_value("bound_push", 0.25, true, false)
.unwrap();
o.set_numeric_value("bound_frac", 0.3, true, false).unwrap();
o.set_numeric_value("slack_bound_push", 0.4, true, false)
.unwrap();
o.set_numeric_value("slack_bound_frac", 0.45, true, false)
.unwrap();
o.set_numeric_value("constr_mult_init_max", 0.0, true, false)
.unwrap();
o.set_numeric_value("bound_mult_init_val", 7.5, true, false)
.unwrap();
})
.init;
assert_eq!(b.bound_push, 0.25);
assert_eq!(b.bound_frac, 0.3);
assert_eq!(b.slack_bound_push, 0.4);
assert_eq!(b.slack_bound_frac, 0.45);
assert_eq!(b.constr_mult_init_max, 0.0);
assert_eq!(b.bound_mult_init_val, 7.5);
}
#[test]
fn string_cold_start_overrides_flow_through() {
let b = builder_from(|app| {
let o = app.options_mut();
o.set_string_value("least_square_init_primal", "yes", true, false)
.unwrap();
o.set_string_value("bound_mult_init_method", "constant", true, false)
.unwrap();
})
.init;
assert!(b.least_square_init_primal);
assert_eq!(b.bound_mult_init_method, "constant");
}
#[test]
fn an_explicit_value_beats_the_mehrotra_cascade() {
let cascade = builder_from(|app| {
app.options_mut()
.set_string_value("mehrotra_algorithm", "yes", true, false)
.unwrap();
})
.init;
assert_eq!(cascade.bound_push, 10.0, "cascade default");
assert_eq!(cascade.bound_frac, 0.2);
assert_eq!(cascade.bound_mult_init_val, 10.0);
assert_eq!(cascade.constr_mult_init_max, 0.0);
let overridden = builder_from(|app| {
let o = app.options_mut();
o.set_string_value("mehrotra_algorithm", "yes", true, false)
.unwrap();
o.set_numeric_value("bound_push", 1e-3, true, false)
.unwrap();
o.set_numeric_value("bound_mult_init_val", 2.0, true, false)
.unwrap();
})
.init;
assert_eq!(overridden.bound_push, 1e-3, "explicit user value wins");
assert_eq!(overridden.bound_mult_init_val, 2.0);
assert_eq!(overridden.bound_frac, 0.2);
}
#[test]
fn out_of_range_values_are_refused_at_the_set_call() {
let mut app = IpoptApplication::new();
let o = app.options_mut();
for name in ["bound_push", "slack_bound_push", "bound_mult_init_val"] {
assert!(
o.set_numeric_value(name, 0.0, true, false).is_err(),
"`{name}` must refuse 0 (strict lower bound)"
);
assert!(
o.set_numeric_value(name, -1.0, true, false).is_err(),
"`{name}` must refuse a negative value"
);
}
for name in ["bound_frac", "slack_bound_frac"] {
assert!(
o.set_numeric_value(name, 0.0, true, false).is_err(),
"`{name}` must refuse 0"
);
assert!(
o.set_numeric_value(name, 0.51, true, false).is_err(),
"`{name}` must refuse a value above 0.5"
);
assert!(
o.set_numeric_value(name, 0.5, true, false).is_ok(),
"`{name}` must accept exactly 0.5 (upper bound is non-strict)"
);
}
assert!(
o.set_numeric_value("constr_mult_init_max", 0.0, true, false)
.is_ok()
);
assert!(
o.set_numeric_value("constr_mult_init_max", -1.0, true, false)
.is_err()
);
assert!(
o.set_string_value("bound_mult_init_method", "least-square", true, false)
.is_err(),
"an unregistered method name must fail at the set call"
);
assert!(
o.set_string_value("least_square_init_primal", "maybe", true, false)
.is_err()
);
}
#[test]
fn the_unimplemented_bound_mult_init_method_is_refused_not_served() {
let mut app = IpoptApplication::new();
assert_eq!(app.unimplemented_option_value_refusal(), None, "unset");
app.options_mut()
.set_string_value("bound_mult_init_method", "constant", true, false)
.expect("the implemented mode parses");
assert_eq!(
app.unimplemented_option_value_refusal(),
None,
"the implemented mode is not refused"
);
app.options_mut()
.set_string_value("bound_mult_init_method", "mu-based", true, false)
.expect("upstream's value must still parse");
let msg = app
.unimplemented_option_value_refusal()
.expect("`mu-based` must be refused");
assert!(msg.contains("bound_mult_init_method"), "{msg}");
assert!(msg.contains("mu-based"), "{msg}");
assert!(msg.contains("604"), "message should name the issue: {msg}");
}
#[test]
fn least_square_init_duals_parses_and_is_refused_when_requested() {
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("least_square_init_duals", "no", true, false)
.expect("parses");
assert_eq!(
app.unimplemented_option_refusal(),
None,
"`no` is exactly what pounce does — it asks for nothing"
);
app.options_mut()
.set_string_value("least_square_init_duals", "yes", true, false)
.expect("parses");
let msg = app
.unimplemented_option_refusal()
.expect("`yes` names a feature pounce does not have");
assert!(msg.contains("least_square_init_duals"), "{msg}");
}
fn option_names_read_by_the_solver_crates() -> BTreeSet<String> {
const READERS: &[&str] = &[
"read_num(",
"read_int(",
"read_yes(",
"get_string_value(",
"get_bool_value(",
"get_numeric_value(",
"get_integer_value(",
"get_enum_value(",
];
const CRATES: &[&str] = &["pounce-algorithm", "pounce-cli", "pounce-presolve"];
fn collect(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let crates_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/ dir")
.to_path_buf();
let mut files = Vec::new();
for name in CRATES {
let src_dir = crates_dir.join(name).join("src");
assert!(
src_dir.is_dir(),
"{} must be readable, or this test silently checks nothing",
src_dir.display()
);
collect(&src_dir, &mut files);
}
let mut found = BTreeSet::new();
for file in files {
let Ok(src) = std::fs::read_to_string(&file) else {
continue;
};
for reader in READERS {
for (idx, _) in src.match_indices(reader) {
let rest = src[idx + reader.len()..].trim_start();
let Some(rest) = rest.strip_prefix('"') else {
continue;
};
let Some(end) = rest.find('"') else { continue };
let tag = &rest[..end];
if tag.is_empty() {
continue;
}
found.insert(tag.to_string());
}
}
}
assert!(
found.len() > 150,
"the source scan found only {} tags — the reader patterns have \
drifted and this test is no longer checking anything",
found.len()
);
found
}
#[test]
fn every_option_read_by_the_solver_crates_is_registered() {
let app = IpoptApplication::new();
let reg = app.registered_options();
let mut unregistered: Vec<String> = option_names_read_by_the_solver_crates()
.into_iter()
.filter(|tag| reg.get_option(tag).is_none())
.collect();
unregistered.sort();
assert!(
unregistered.is_empty(),
"these options are read but never registered, so setting them \
raises `Unknown option` and the code reading them can never fire: \
{unregistered:?}"
);
}
#[test]
fn every_registered_initialization_option_is_consumed_or_refused() {
use pounce_algorithm::unimplemented_options::{UNIMPLEMENTED_FEATURES, UNIMPLEMENTED_VALUES};
let app = IpoptApplication::new();
let read = option_names_read_by_the_solver_crates();
let refused: BTreeSet<&str> = UNIMPLEMENTED_FEATURES
.iter()
.flat_map(|g| g.options.iter().copied())
.chain(UNIMPLEMENTED_VALUES.iter().map(|v| v.option))
.collect();
let mut dangling = Vec::new();
for opt in app.registered_options().registered_options_in_order() {
if opt.category != "Initialization" {
continue;
}
if read.contains(&opt.name) || refused.contains(opt.name.as_str()) {
continue;
}
dangling.push(opt.name.clone());
}
assert!(
dangling.is_empty(),
"these Initialization options are registered but neither read nor \
refused — setting one does nothing, silently: {dangling:?}"
);
let n = app
.registered_options()
.registered_options_in_order()
.iter()
.filter(|o| o.category == "Initialization")
.count();
assert_eq!(
n,
COLD_START_OPTIONS.len() + START_CONDITIONING_OPTIONS.len() + 1,
"expected the eight cold-start options, the seven starting-point \
conditioning options, and least_square_init_duals — if you added \
an Initialization option, add it to one of those lists so this \
count keeps meaning something"
);
}