use crate::core::constraint::BoxConstraints;
use crate::core::convergence::optional_tolerance;
use crate::core::math::{Scalar, VectorIndex, VectorLen};
use crate::core::parallel::{MaybeSend, MaybeSync};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::{PointState, State};
use crate::core::termination::TerminationReason;
mod rectangle;
use rectangle::{Rectangle, Work};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct Direct<F: Scalar = f64> {
epsilon: F,
radius_tolerance: Option<F>,
volume_tolerance: Option<F>,
work: Option<Work<F>>,
}
impl<F: Scalar> Default for Direct<F> {
fn default() -> Self {
Self {
epsilon: F::from_f64(1e-4).unwrap(),
radius_tolerance: Some(F::from_f64(1e-6).unwrap()),
volume_tolerance: None,
work: None,
}
}
}
impl<F: Scalar> Direct<F> {
pub fn new() -> Self {
Self::default()
}
pub fn with_epsilon(mut self, epsilon: F) -> Self {
assert!(
epsilon.is_finite() && epsilon >= F::zero(),
"DIRECT epsilon must be finite and nonnegative"
);
self.epsilon = epsilon;
self
}
pub fn with_absolute_radius_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.radius_tolerance = optional_tolerance(value);
self
}
pub fn with_relative_volume_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.volume_tolerance = optional_tolerance(value);
self
}
}
impl<P, V, F> Solver<P, PointState<V, F>> for Direct<F>
where
F: Scalar + MaybeSend,
P: CostFunction<Param = V, Output = F> + BoxConstraints + MaybeSync,
P::Error: MaybeSend,
V: Clone + VectorLen + VectorIndex<F> + MaybeSync,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
self.work = None;
let lower = problem.inner().lower();
let upper = problem.inner().upper();
let n = lower.vec_len();
assert!(n > 0, "DIRECT requires a nonempty box");
assert_eq!(upper.vec_len(), n, "DIRECT bound dimensions differ");
assert_eq!(
state.param().vec_len(),
n,
"DIRECT state and bound dimensions differ"
);
let mut bounds = Vec::with_capacity(n);
let mut active = Vec::new();
for i in 0..n {
let lo = lower.get_scalar(i);
let hi = upper.get_scalar(i);
assert!(
lo.is_finite() && hi.is_finite() && lo <= hi,
"DIRECT requires finite ordered bounds at coordinate {i}"
);
bounds.push((lo, hi));
if lo < hi {
active.push(i);
}
}
let root = Rectangle::new(
vec![F::from_f64(0.5).unwrap(); active.len()],
F::infinity(),
);
let mut work = Work::new(bounds, active, root);
let point = work.point(state.param(), &work.rectangles[0].center);
let cost = problem.cost(&point)?;
work.rectangles[0].cost = cost;
work.consider(0);
state.reset();
state.replace(point, cost);
self.work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<(PointState<V, F>, Option<TerminationReason>), Self::Error>
{
let work = self.work.as_mut().expect("DIRECT must be initialized");
let selected = work.select(self.epsilon);
if selected.is_empty() {
return Ok((state, Some(TerminationReason::NumericalNoProgress)));
}
for id in selected {
let Some(probes) = work.probes(state.param(), id) else {
return Ok((
state,
Some(TerminationReason::NumericalNoProgress),
));
};
let costs = problem.cost_batch(&probes.params)?;
work.divide(id, probes.axes, probes.centers, costs);
if let Some(best) = work.best {
let rectangle = &work.rectangles[best];
state.replace(
work.point(state.param(), &rectangle.center),
rectangle.cost,
);
if rectangle.cost == F::neg_infinity() {
return Ok((
state,
Some(TerminationReason::SolverConverged),
));
}
}
}
Ok((state, None))
}
fn terminate(
&self,
_state: &PointState<V, F>,
) -> Option<TerminationReason> {
let work = self.work.as_ref()?;
let Some(best) = work.best else {
return work
.active
.is_empty()
.then_some(TerminationReason::SolverFailed);
};
let rectangle = &work.rectangles[best];
if rectangle.cost == F::neg_infinity() || work.active.is_empty() {
return Some(TerminationReason::SolverConverged);
}
let radius = work.radius(rectangle);
let radius_small = self.radius_tolerance.is_some_and(|t| {
t > F::zero() && radius > F::zero() && radius <= t
});
let volume_small = self.volume_tolerance.is_some_and(|t| {
t > F::zero()
&& F::from_u64(rectangle.level()).unwrap()
* F::from_f64(3.0).unwrap().ln()
>= -t.ln()
});
(radius_small || volume_small)
.then_some(TerminationReason::SolverConverged)
}
}