mod dinvr;
mod dzror;
use crate::error::SearchError;
use dinvr::{InvrAction, InvrConfig, InvrState};
use dzror::{ZrorAction, ZrorConfig, ZrorState};
pub(crate) const SEARCH_BOUND: f64 = 1.0e300;
const ABS_STEP: f64 = 0.5;
const REL_STEP: f64 = 0.5;
const STP_MUL: f64 = 5.0;
pub(crate) const ABS_TOL: f64 = 1.0e-10;
const REL_TOL: f64 = 1.0e-8;
#[inline]
pub(crate) fn search_monotone(
small: f64,
big: f64,
start: f64,
qleft_bound: f64,
qhi_bound: f64,
f: impl FnMut(f64) -> f64,
) -> Result<f64, SearchError> {
search_monotone_with_atol(small, big, start, qleft_bound, qhi_bound, ABS_TOL, f)
}
#[inline]
pub(crate) fn search_monotone_with_atol(
small: f64,
big: f64,
start: f64,
qleft_bound: f64,
qhi_bound: f64,
abs_tol: f64,
mut f: impl FnMut(f64) -> f64,
) -> Result<f64, SearchError> {
let big = big.min(SEARCH_BOUND);
let small = small.max(-SEARCH_BOUND);
if !(small <= start && start <= big) {
return Err(SearchError::StartOutOfRange { start, small, big });
}
let cfg = InvrConfig {
small,
big,
abs_step: ABS_STEP,
rel_step: REL_STEP,
stp_mul: STP_MUL,
abs_tol,
rel_tol: REL_TOL,
};
let mut state = InvrState::new(cfg, start);
let mut fx = 0.0;
loop {
match state.step(fx) {
InvrAction::NeedEval(x) => {
fx = f(x);
}
InvrAction::Converged(x) => return Ok(x),
InvrAction::Failed { qleft, .. } => {
return Err(if qleft {
SearchError::AnswerBelowLowerBound { bound: qleft_bound }
} else {
SearchError::AnswerAboveUpperBound { bound: qhi_bound }
});
}
}
}
}
#[inline]
pub(crate) fn search_bounded_zero(
xlo: f64,
xhi: f64,
mut f: impl FnMut(f64) -> f64,
) -> Result<f64, SearchError> {
search_bounded_zero_with_tol(xlo, xhi, ABS_TOL, REL_TOL, &mut f)
}
#[inline]
pub(crate) fn search_bounded_zero_with_tol(
xlo: f64,
xhi: f64,
abs_tol: f64,
rel_tol: f64,
f: &mut impl FnMut(f64) -> f64,
) -> Result<f64, SearchError> {
let cfg = ZrorConfig {
xlo,
xhi,
abstol: abs_tol,
reltol: rel_tol,
};
let mut state = ZrorState::new(cfg);
let mut fx = 0.0;
loop {
match state.step(fx) {
ZrorAction::NeedEval(x) => fx = f(x),
ZrorAction::Converged { xlo, .. } => return Ok(xlo),
ZrorAction::Failed { qleft, .. } => {
return Err(if qleft {
SearchError::AnswerBelowLowerBound { bound: cfg.xlo }
} else {
SearchError::AnswerAboveUpperBound { bound: cfg.xhi }
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solves_increasing_function() {
let r = search_monotone(0.0, 100.0, 1.0, 0.0, 100.0, |x| x.powi(3) - 8.0).unwrap();
assert!((r - 2.0).abs() < 1e-10, "r = {r}");
}
#[test]
fn solves_decreasing_function() {
let r = search_monotone(0.01, 1000.0, 10.0, 0.01, 1000.0, |x| 1.0 / x - 0.25).unwrap();
assert!((r - 4.0).abs() < 1e-10, "r = {r}");
}
#[test]
fn solves_root_at_moderate_value() {
let r = search_monotone(1e-10, 1000.0, 1.0, 1e-10, 1000.0, |x| x.ln() - 1.0).unwrap();
let e = std::f64::consts::E;
assert!((r - e).abs() / e < 1e-8, "r = {r}, e = {e}");
}
#[test]
fn increasing_fsmall_positive_fails_at_small() {
let r = search_monotone(1.0, 10.0, 5.0, 1.0, 10.0, |x| x + 1.0);
assert!(matches!(
r,
Err(SearchError::AnswerBelowLowerBound { bound }) if bound == 1.0
));
}
#[test]
fn increasing_fbig_negative_fails_at_big() {
let r = search_monotone(1.0, 10.0, 5.0, 1.0, 10.0, |x| x - 100.0);
assert!(matches!(
r,
Err(SearchError::AnswerAboveUpperBound { bound }) if bound == 10.0
));
}
#[test]
fn decreasing_fsmall_negative_fails_at_small() {
let r = search_monotone(1.0, 10.0, 5.0, 1.0, 10.0, |x| -x - 1.0);
assert!(matches!(
r,
Err(SearchError::AnswerBelowLowerBound { bound }) if bound == 1.0
));
}
#[test]
fn decreasing_fbig_positive_fails_at_big() {
let r = search_monotone(1.0, 10.0, 5.0, 1.0, 10.0, |x| 100.0 - x);
assert!(matches!(
r,
Err(SearchError::AnswerAboveUpperBound { bound }) if bound == 10.0
));
}
#[test]
fn converges_immediately_when_fstart_zero() {
let r = search_monotone(0.0, 10.0, 3.0, 0.0, 10.0, |x| x - 3.0).unwrap();
assert!((r - 3.0).abs() < 1e-15);
}
#[test]
fn qleft_qhi_bounds_are_passed_through_distinctly() {
let err = search_monotone(1.0, 10.0, 5.0, 99.0, 999.0, |x| x + 1.0).unwrap_err();
assert!(
matches!(err, SearchError::AnswerBelowLowerBound { bound } if bound == 99.0),
"expected AnswerBelowLowerBound {{ bound: 99.0 }}, got {err:?}"
);
let err = search_monotone(1.0, 10.0, 5.0, 99.0, 999.0, |x| x - 100.0).unwrap_err();
assert!(
matches!(err, SearchError::AnswerAboveUpperBound { bound } if bound == 999.0),
"expected AnswerAboveUpperBound {{ bound: 999.0 }}, got {err:?}"
);
}
#[test]
fn nan_objective_surfaces_as_search_failure() {
let err = search_monotone(0.0, 1.0, 0.5, 0.0, 1.0, |_| f64::NAN).unwrap_err();
assert!(matches!(
err,
SearchError::AnswerBelowLowerBound { bound: 0.0 }
));
}
}