use pounce_common::options_list::OptionsList;
use pounce_common::types::Number;
pub const DEFAULT_SENS_BOUND_EPS: Number = 1.0e-3;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct SensOptionOverrides {
pub run_sens: Option<bool>,
pub compute_red_hessian: Option<bool>,
pub rh_eigendecomp: Option<bool>,
pub sens_boundcheck: Option<bool>,
pub sens_bound_eps: Option<Number>,
pub sens_max_pdpert: Option<Number>,
}
fn read_yes(key: &str, options: &OptionsList) -> Option<bool> {
match options.get_bool_value(key, "") {
Ok((v, true)) => Some(v),
_ => None,
}
}
fn read_num(key: &str, options: &OptionsList) -> Option<Number> {
match options.get_numeric_value(key, "") {
Ok((v, true)) => Some(v),
_ => None,
}
}
impl SensOptionOverrides {
pub fn from_options_list(options: &OptionsList) -> Self {
Self {
run_sens: read_yes("run_sens", options),
compute_red_hessian: read_yes("compute_red_hessian", options),
rh_eigendecomp: read_yes("rh_eigendecomp", options),
sens_boundcheck: read_yes("sens_boundcheck", options),
sens_bound_eps: read_num("sens_bound_eps", options),
sens_max_pdpert: read_num("sens_max_pdpert", options),
}
}
pub fn wants_reduced_hessian(&self) -> bool {
self.compute_red_hessian == Some(true) || self.rh_eigendecomp == Some(true)
}
pub fn wants_eigendecomp(&self) -> bool {
self.rh_eigendecomp == Some(true)
}
pub fn suppresses_sens_step(&self) -> bool {
self.run_sens == Some(false)
}
pub fn boundcheck_eps(&self) -> Option<Number> {
(self.sens_boundcheck == Some(true))
.then(|| self.sens_bound_eps.unwrap_or(DEFAULT_SENS_BOUND_EPS))
}
pub fn pdpert_refusal(&self, perturbations: &[Number; 4]) -> Option<String> {
let limit = self.sens_max_pdpert?;
let (refuse, worst) = pdpert_verdict(perturbations, limit);
refuse.then(|| {
format!(
"sensitivity skipped: the converged KKT factor carries a \
perturbation of {worst:.3e} (δ_x={:.3e}, δ_s={:.3e}, \
δ_c={:.3e}, δ_d={:.3e}), above the requested \
`sens_max_pdpert={limit:.3e}` — the factor the step would \
invert is not this problem's KKT matrix. Raise \
`sens_max_pdpert` to accept it anyway.",
perturbations[0], perturbations[1], perturbations[2], perturbations[3],
)
})
}
}
pub fn release_floor_from_options(options: &OptionsList) -> Number {
let brf = options
.get_numeric_value("bound_relax_factor", "")
.map_or(0.0, |(v, _)| v);
crate::boundcheck::release_floor(brf)
}
pub fn pdpert_verdict(perturbations: &[Number; 4], limit: Number) -> (bool, Number) {
let worst = perturbations
.iter()
.fold(0.0_f64, |acc, v| acc.max(v.abs()));
(worst > limit, worst)
}
#[cfg(test)]
mod tests {
use super::*;
use pounce_common::reg_options::RegisteredOptions;
fn list() -> OptionsList {
let reg = RegisteredOptions::new();
crate::sens_app::register_options(®).expect("register");
OptionsList::with_registered(reg)
}
#[test]
fn nothing_set_asks_for_nothing() {
let o = SensOptionOverrides::from_options_list(&list());
assert_eq!(o, SensOptionOverrides::default());
assert!(!o.wants_reduced_hessian());
assert!(!o.suppresses_sens_step());
assert!(o.boundcheck_eps().is_none());
assert!(o.pdpert_refusal(&[1e9, 0.0, 0.0, 0.0]).is_none());
}
#[test]
fn explicit_values_come_back() {
let mut l = list();
l.set_string_value("compute_red_hessian", "yes", true, false)
.unwrap();
l.set_string_value("rh_eigendecomp", "yes", true, false)
.unwrap();
l.set_string_value("run_sens", "no", true, false).unwrap();
l.set_string_value("sens_boundcheck", "yes", true, false)
.unwrap();
l.set_numeric_value("sens_bound_eps", 1e-6, true, false)
.unwrap();
l.set_numeric_value("sens_max_pdpert", 1e-9, true, false)
.unwrap();
let o = SensOptionOverrides::from_options_list(&l);
assert_eq!(o.compute_red_hessian, Some(true));
assert!(o.wants_reduced_hessian() && o.wants_eigendecomp());
assert!(o.suppresses_sens_step());
assert_eq!(o.boundcheck_eps(), Some(1e-6));
assert_eq!(o.sens_max_pdpert, Some(1e-9));
}
#[test]
fn eigendecomp_implies_the_reduced_hessian() {
let mut l = list();
l.set_string_value("rh_eigendecomp", "yes", true, false)
.unwrap();
let o = SensOptionOverrides::from_options_list(&l);
assert!(o.wants_reduced_hessian());
}
#[test]
fn the_margin_alone_does_not_enable_the_refinement() {
let mut l = list();
l.set_numeric_value("sens_bound_eps", 1e-6, true, false)
.unwrap();
let o = SensOptionOverrides::from_options_list(&l);
assert_eq!(o.sens_bound_eps, Some(1e-6));
assert_eq!(o.boundcheck_eps(), None);
}
#[test]
fn the_perturbation_cap_fires_only_above_the_limit() {
let mut l = list();
l.set_numeric_value("sens_max_pdpert", 1e-6, true, false)
.unwrap();
let o = SensOptionOverrides::from_options_list(&l);
assert!(o.pdpert_refusal(&[0.0, 0.0, 0.0, 0.0]).is_none());
assert!(o.pdpert_refusal(&[1e-7, 1e-9, 0.0, 0.0]).is_none());
let msg = o
.pdpert_refusal(&[0.0, 0.0, 1e-4, 0.0])
.expect("1e-4 is above the 1e-6 cap");
assert!(msg.contains("sens_max_pdpert"), "{msg}");
}
#[test]
fn the_verdict_reads_the_largest_correction_by_magnitude() {
assert_eq!(pdpert_verdict(&[0.0, 0.0, 0.0, 0.0], 1e-6), (false, 0.0));
assert_eq!(pdpert_verdict(&[1e-9, 1e-4, 0.0, 1e-7], 1e-6).1, 1e-4);
assert!(pdpert_verdict(&[0.0, 0.0, -1e-4, 0.0], 1e-6).0);
assert!(!pdpert_verdict(&[1e-6, 0.0, 0.0, 0.0], 1e-6).0);
assert!(pdpert_verdict(&[1.0000001e-6, 0.0, 0.0, 0.0], 1e-6).0);
}
#[test]
fn the_release_floor_is_the_solves_margin_never_below_the_floor() {
assert_eq!(crate::boundcheck::release_floor(0.0), 1e-9);
assert_eq!(crate::boundcheck::release_floor(1e-12), 1e-9);
assert_eq!(crate::boundcheck::release_floor(1e-8), 1e-8);
assert_eq!(crate::boundcheck::release_floor(-1e-8), 1e-8);
let sens_only = list();
assert_eq!(release_floor_from_options(&sens_only), 1e-9);
let reg = RegisteredOptions::new();
crate::sens_app::register_options(®).expect("register");
reg.add_lower_bounded_number_option(
"bound_relax_factor",
"the algorithm's own, as the two option-reading callers see it",
0.0,
false,
1e-8,
"",
)
.expect("register bound_relax_factor");
let mut l = OptionsList::with_registered(reg);
assert_eq!(release_floor_from_options(&l), 1e-8);
l.set_numeric_value("bound_relax_factor", 0.0, true, false)
.unwrap();
assert_eq!(release_floor_from_options(&l), 1e-9);
}
}