use crate::core::constraint::evaluation::BoxProblem;
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::Termination;
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>>,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct RectangleRef<'a, F: Scalar = f64> {
pub free_coordinates: &'a [usize],
pub center: &'a [F],
pub depths: &'a [usize],
pub radius: F,
pub level: u64,
pub rectangle_count: usize,
}
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 best_rectangle(&self) -> Option<RectangleRef<'_, F>> {
let work = self.work.as_ref()?;
let rectangle = &work.rectangles[work.best?];
Some(RectangleRef {
free_coordinates: &work.active,
center: &rectangle.center,
depths: &rectangle.depths,
radius: work.radius(rectangle),
level: rectangle.level(),
rectangle_count: work.rectangles.len(),
})
}
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> + BoxProblem<Param = V> + MaybeSync,
P::Error: MaybeSend,
V: Clone + VectorLen + VectorIndex<F> + MaybeSync,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_coordinate_box_problem(
problem.inner(),
state.param().vec_len(),
true,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
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<crate::SolverStep<PointState<V, F>>, 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(crate::SolverStep::from((
state,
Some(Termination::numerical_stall(
"direct: no numerical progress",
)),
)));
}
for id in selected {
let Some(probes) = work.probes(state.param(), id) else {
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_stall(
"direct: no numerical progress",
)),
)));
};
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(crate::SolverStep::from((
state,
Some(Termination::custom(
"direct.negative_infinity",
"The selected rectangle has objective negative infinity.",
vec![crate::Measurement {
name: "cost".into(),
value: rectangle.cost,
}],
)),
)));
}
}
}
Ok(crate::SolverStep::from((state, None)))
}
fn terminate(&self, _state: &PointState<V, F>) -> Option<Termination<F>> {
let work = self.work.as_ref()?;
let Some(best) = work.best else {
return work.active.is_empty().then(|| Termination::numerical_failure("DIRECT exhausted its rectangles without a valid incumbent."));
};
let rectangle = &work.rectangles[best];
if rectangle.cost == F::neg_infinity() || work.active.is_empty() {
return Some(Termination::custom(
"direct.exhausted_or_unbounded",
"The selected objective is negative infinity or every splittable rectangle is exhausted.",
vec![
crate::Measurement {
name: "cost".into(),
value: rectangle.cost,
},
crate::Measurement {
name: "active_rectangles".into(),
value: F::from_usize(work.active.len()).unwrap(),
},
],
));
}
let radius = work.radius(rectangle);
let radius_small = self.radius_tolerance.is_some_and(|t| radius <= t);
let volume_small = self.volume_tolerance.is_some_and(|t| {
F::from_u64(rectangle.level()).unwrap()
* F::from_f64(3.0).unwrap().ln()
>= -t.ln()
});
let radius_report = radius_small.then(|| {
Termination::custom(
"direct.radius",
"The scaled best-rectangle radius is <= the configured tolerance, including exact zero.",
vec![
crate::Measurement {
name: "radius".into(),
value: radius,
},
crate::Measurement {
name: "tolerance".into(),
value: self.radius_tolerance.unwrap(),
},
],
)
});
let volume_report = volume_small.then(|| {
Termination::custom(
"direct.volume",
"level * ln(3) >= -ln(volume_tolerance); a zero threshold requires a truly zero volume, not underflow.",
vec![
crate::Measurement {
name: "level".into(),
value: F::from_u64(rectangle.level()).unwrap(),
},
crate::Measurement {
name: "tolerance".into(),
value: self.volume_tolerance.unwrap(),
},
],
)
});
radius_report
.into_iter()
.chain(volume_report)
.reduce(Termination::merge)
}
}