use pounce_common::types::Index;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DerivativeProof {
#[default]
Unknown,
Constant,
Varying,
}
impl DerivativeProof {
pub fn all(pieces: impl IntoIterator<Item = DerivativeProof>) -> DerivativeProof {
let mut out = DerivativeProof::Constant;
for p in pieces {
match p {
DerivativeProof::Varying => return DerivativeProof::Varying,
DerivativeProof::Unknown => out = DerivativeProof::Unknown,
DerivativeProof::Constant => {}
}
}
out
}
pub fn forget_variation(self) -> DerivativeProof {
match self {
DerivativeProof::Varying => DerivativeProof::Unknown,
other => other,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DerivativeProofs {
pub grad_f: DerivativeProof,
pub hessian: DerivativeProof,
pub jac: Vec<DerivativeProof>,
}
impl DerivativeProofs {
pub fn row(&self, i: usize) -> DerivativeProof {
self.jac.get(i).copied().unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ConstantDerivatives {
pub grad_f: bool,
pub hessian: bool,
pub jac_c: bool,
pub jac_d: bool,
}
impl ConstantDerivatives {
pub fn is_empty(&self) -> bool {
!(self.grad_f || self.hessian || self.jac_c || self.jac_d)
}
}
pub const HINT_OPTIONS: [&str; 4] = [
"grad_f_constant",
"hessian_constant",
"jac_c_constant",
"jac_d_constant",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HintOutcome {
pub name: &'static str,
pub asserted: bool,
pub proof: DerivativeProof,
pub honoured: bool,
}
impl HintOutcome {
pub fn contradicted(&self) -> bool {
self.asserted && self.proof == DerivativeProof::Varying
}
pub fn trusted(&self) -> bool {
self.asserted && self.proof == DerivativeProof::Unknown
}
pub fn auto_detected(&self) -> bool {
!self.asserted && self.proof == DerivativeProof::Constant
}
pub fn warning(&self) -> Option<String> {
if !self.contradicted() {
return None;
}
Some(format!(
"pounce: warning: ignoring `{}=yes` — pounce proved from the \
model's own algebra that this derivative is not constant, so \
reusing it would return a wrong answer rather than a slow one. \
Ipopt honours this hint without checking it. Remove the option; \
pounce detects a genuinely constant derivative on its own and \
needs no hint to reuse it. (gh#588)",
self.name
))
}
}
pub fn reconcile(
proofs: [DerivativeProof; 4],
asserted: [bool; 4],
) -> ([HintOutcome; 4], ConstantDerivatives) {
let mut outcomes = [HintOutcome {
name: "",
asserted: false,
proof: DerivativeProof::Unknown,
honoured: false,
}; 4];
for k in 0..4 {
let honoured = match proofs[k] {
DerivativeProof::Constant => true,
DerivativeProof::Varying => false,
DerivativeProof::Unknown => asserted[k],
};
outcomes[k] = HintOutcome {
name: HINT_OPTIONS[k],
asserted: asserted[k],
proof: proofs[k],
honoured,
};
}
let enabled = ConstantDerivatives {
grad_f: outcomes[0].honoured,
hessian: outcomes[1].honoured,
jac_c: outcomes[2].honoured,
jac_d: outcomes[3].honoured,
};
(outcomes, enabled)
}
pub fn subsystem_proof(proofs: &DerivativeProofs, rows: &[Index]) -> DerivativeProof {
DerivativeProof::all(rows.iter().map(|&i| proofs.row(i as usize)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_is_dominated_by_a_proof_of_variation() {
use DerivativeProof::*;
assert_eq!(DerivativeProof::all([Constant, Constant]), Constant);
assert_eq!(DerivativeProof::all([Constant, Unknown]), Unknown);
assert_eq!(DerivativeProof::all([Unknown, Varying]), Varying);
assert_eq!(DerivativeProof::all([Varying, Unknown]), Varying);
assert_eq!(DerivativeProof::all([]), Constant);
}
#[test]
fn forgetting_variation_keeps_the_other_two() {
use DerivativeProof::*;
assert_eq!(Varying.forget_variation(), Unknown);
assert_eq!(Constant.forget_variation(), Constant);
assert_eq!(Unknown.forget_variation(), Unknown);
}
#[test]
fn proof_beats_the_user_in_both_directions() {
use DerivativeProof::*;
let (out, en) = reconcile(
[Constant, Varying, Unknown, Unknown],
[false, true, true, false],
);
assert!(out[0].honoured && out[0].auto_detected());
assert!(!out[1].honoured);
assert!(out[1].contradicted());
assert!(
out[1]
.warning()
.is_some_and(|w| w.contains("hessian_constant"))
);
assert!(out[2].honoured && out[2].trusted());
assert!(out[2].warning().is_none());
assert!(!out[3].honoured);
assert_eq!(
en,
ConstantDerivatives {
grad_f: true,
hessian: false,
jac_c: true,
jac_d: false,
}
);
assert!(!en.is_empty());
}
#[test]
fn a_default_options_list_over_a_silent_model_reuses_nothing() {
let (out, en) = reconcile([DerivativeProof::Unknown; 4], [false; 4]);
assert!(en.is_empty());
assert!(out.iter().all(|o| o.warning().is_none()));
}
#[test]
fn an_empty_subsystem_is_vacuously_constant() {
let p = DerivativeProofs {
jac: vec![DerivativeProof::Varying, DerivativeProof::Constant],
..Default::default()
};
assert_eq!(subsystem_proof(&p, &[]), DerivativeProof::Constant);
assert_eq!(subsystem_proof(&p, &[1]), DerivativeProof::Constant);
assert_eq!(subsystem_proof(&p, &[0, 1]), DerivativeProof::Varying);
assert_eq!(subsystem_proof(&p, &[5]), DerivativeProof::Unknown);
}
}