use rand_distr::uniform::SampleUniform;
use crate::core::constraint::evaluation::BoxProblem;
use crate::core::inner::InitialState;
use crate::core::math::{
ClampInPlace, SampleUniformBox, Scalar, ScaleInPlace, ScaledAdd,
VectorIndex, VectorLen,
};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, RngExt, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::{PointState, SimplexProgress, State};
use crate::core::termination::Termination;
use crate::solver::{NelderMead, Projected, Unbounded};
mod geometry;
mod restart;
use geometry::{
free_coordinates, log_parzen_density, normalized_l1_distance,
point_on_bounds, regular_simplex, scaled_minimum_width,
simplex_is_degenerate, simplex_is_small,
};
use restart::{OptimumAction, select_restart};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum GbnmRestart {
Probabilistic,
SmallTest,
LargeTest,
}
struct BoxGeometry<'a, V> {
lower: &'a V,
upper: &'a V,
free: &'a [usize],
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Gbnm<V, F: Scalar = f64> {
local: NelderMead<V, F, Projected>,
rng: ChaCha8Rng,
seed: u64,
simplex: Option<SimplexProgress<V, F>>,
fixed_box: bool,
search_starts: Vec<V>,
local_optima: Vec<(V, F)>,
restart: GbnmRestart,
restart_center: Option<V>,
restart_count: u64,
restart_candidates: usize,
gaussian_variance_factor: F,
initial_simplex_fraction: F,
probabilistic_min_fraction: F,
probabilistic_max_fraction: F,
small_test_fraction: F,
large_test_fraction: F,
small_tolerance: F,
flat_tolerance: F,
degeneracy_edge_ratio: F,
degeneracy_shape: F,
}
impl<V, F: Scalar> Gbnm<V, F> {
pub fn new(seed: u64) -> Self {
Self {
local: NelderMead::new().projected(),
rng: ChaCha8Rng::seed_from_u64(seed),
seed,
simplex: None,
fixed_box: false,
search_starts: Vec::new(),
local_optima: Vec::new(),
restart: GbnmRestart::Probabilistic,
restart_center: None,
restart_count: 0,
restart_candidates: 10,
gaussian_variance_factor: F::from_f64(0.01).unwrap(),
initial_simplex_fraction: F::from_f64(0.05).unwrap(),
probabilistic_min_fraction: F::from_f64(0.02).unwrap(),
probabilistic_max_fraction: F::from_f64(0.10).unwrap(),
small_test_fraction: F::from_f64(1e-5).unwrap(),
large_test_fraction: F::from_f64(0.05).unwrap(),
small_tolerance: F::from_f64(1e-6).unwrap(),
flat_tolerance: F::from_f64(1e-20).unwrap(),
degeneracy_edge_ratio: F::from_f64(1e-7).unwrap(),
degeneracy_shape: F::from_f64(1e-7).unwrap(),
}
}
pub fn vertices(&self) -> &[V] {
self.simplex
.as_ref()
.map_or(&[], |simplex| simplex.vertices())
}
pub fn costs(&self) -> &[F] {
self.simplex.as_ref().map_or(&[], |simplex| simplex.costs())
}
pub fn search_starts(&self) -> &[V] {
&self.search_starts
}
pub fn local_optima(&self) -> &[(V, F)] {
&self.local_optima
}
pub fn restart_count(&self) -> u64 {
self.restart_count
}
pub fn with_nm_params(
mut self,
alpha: F,
beta: F,
gamma: F,
delta: F,
) -> Self {
self.local = NelderMead::<V, F, Unbounded>::with_params(
alpha, beta, gamma, delta,
)
.projected();
self
}
pub fn with_restart_candidates(mut self, count: usize) -> Self {
assert!(count >= 1, "GBNM requires at least one restart candidate");
self.restart_candidates = count;
self
}
pub fn with_gaussian_variance_factor(mut self, factor: F) -> Self {
assert_positive_finite(factor, "Gaussian variance factor");
self.gaussian_variance_factor = factor;
self
}
pub fn with_initial_simplex_fraction(mut self, fraction: F) -> Self {
assert_fraction(fraction, "initial simplex fraction");
self.initial_simplex_fraction = fraction;
self
}
pub fn with_probabilistic_simplex_fractions(
mut self,
min: F,
max: F,
) -> Self {
assert_fraction(min, "minimum probabilistic simplex fraction");
assert_fraction(max, "maximum probabilistic simplex fraction");
assert!(
min <= max,
"minimum probabilistic simplex fraction must not exceed maximum"
);
self.probabilistic_min_fraction = min;
self.probabilistic_max_fraction = max;
self
}
pub fn with_test_simplex_fractions(mut self, small: F, large: F) -> Self {
assert_fraction(small, "small-test simplex fraction");
assert_fraction(large, "large-test simplex fraction");
assert!(
small < large,
"small-test simplex fraction must be smaller than large-test fraction"
);
self.small_test_fraction = small;
self.large_test_fraction = large;
self
}
pub fn with_normalized_simplex_size_tolerance(
mut self,
tolerance: F,
) -> Self {
assert_positive_finite(tolerance, "small-simplex tolerance");
self.small_tolerance = tolerance;
self
}
pub fn with_absolute_simplex_cost_tolerance(
mut self,
tolerance: F,
) -> Self {
assert_positive_finite(tolerance, "flat-simplex tolerance");
self.flat_tolerance = tolerance;
self
}
pub fn with_edge_ratio_tolerance(mut self, value: F) -> Self {
assert_unit_tolerance(value, "degeneracy edge-ratio tolerance");
self.degeneracy_edge_ratio = value;
self
}
pub fn with_normalized_determinant_tolerance(mut self, value: F) -> Self {
assert_unit_tolerance(value, "degeneracy shape tolerance");
self.degeneracy_shape = value;
self
}
}
fn assert_positive_finite<F: Scalar>(value: F, name: &str) {
assert!(
value.is_finite() && value > F::zero(),
"GBNM {name} must be finite and > 0, got {value:?}"
);
}
fn assert_fraction<F: Scalar>(value: F, name: &str) {
assert!(
value.is_finite() && value > F::zero() && value <= F::one(),
"GBNM {name} must be finite and in (0, 1], got {value:?}"
);
}
fn assert_unit_tolerance<F: Scalar>(value: F, name: &str) {
assert!(
value.is_finite() && value > F::zero() && value < F::one(),
"GBNM {name} must be finite and in (0, 1), got {value:?}"
);
}
impl<V, F> Gbnm<V, F>
where
F: Scalar + SampleUniform,
{
fn sample_restart_center(
&mut self,
lower: &V,
upper: &V,
free: &[usize],
) -> V
where
V: Clone + SampleUniformBox + VectorIndex<F>,
{
if free
.iter()
.all(|&i| (upper.get_scalar(i) - lower.get_scalar(i)).is_finite())
{
return V::sample_uniform_box(lower, upper, &mut self.rng);
}
let mut sample = lower.clone();
for &i in free {
let lower = lower.get_scalar(i);
let upper = upper.get_scalar(i);
let value = if (upper - lower).is_finite() {
self.rng.random_range(lower..=upper)
} else {
let weight = self.rng.random_range(F::zero()..=F::one());
lower * (F::one() - weight) + upper * weight
};
sample.set_scalar(i, value);
}
sample
}
fn probabilistic_center(
&mut self,
lower: &V,
upper: &V,
free: &[usize],
) -> V
where
V: Clone + SampleUniformBox + VectorIndex<F>,
{
let mut selected = self.sample_restart_center(lower, upper, free);
let mut selected_density = log_parzen_density(
&selected,
&self.search_starts,
&self.local_optima,
lower,
upper,
free,
self.gaussian_variance_factor,
);
for _ in 1..self.restart_candidates {
let candidate = self.sample_restart_center(lower, upper, free);
let density = log_parzen_density(
&candidate,
&self.search_starts,
&self.local_optima,
lower,
upper,
free,
self.gaussian_variance_factor,
);
if density < selected_density {
selected = candidate;
selected_density = density;
}
}
selected
}
fn install_restart<P>(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
restart: GbnmRestart,
center: V,
edge_fraction: F,
geometry: BoxGeometry<'_, V>,
) -> Result<PointState<V, F>, crate::ConstraintSolveError<P::Error>>
where
P: CostFunction<Param = V, Output = F> + BoxProblem<Param = V>,
V: Clone
+ VectorLen
+ ClampInPlace
+ ScaleInPlace<F>
+ ScaledAdd<F>
+ VectorIndex<F>,
{
let edge = scaled_minimum_width(
geometry.lower,
geometry.upper,
geometry.free,
edge_fraction,
);
let simplex = SimplexProgress::from_simplex(regular_simplex(
¢er,
edge,
geometry.lower,
geometry.upper,
geometry.free,
));
let simplex = <NelderMead<V, F, Projected> as Solver<
P,
SimplexProgress<V, F>,
>>::init(&mut self.local, problem, simplex)?;
self.search_starts.push(center.clone());
self.restart = restart;
self.restart_center = Some(center);
state.replace_from(&simplex.vertices[0], simplex.costs[0]);
self.simplex = Some(simplex);
Ok(state)
}
fn is_known_optimum(
&self,
point: &V,
lower: &V,
upper: &V,
free: &[usize],
) -> bool
where
V: VectorIndex<F>,
{
self.local_optima.iter().any(|(known, _)| {
normalized_l1_distance(point, known, lower, upper, free)
< self.small_tolerance
})
}
fn save_optimum(
&mut self,
point: V,
cost: F,
lower: &V,
upper: &V,
free: &[usize],
) where
V: VectorIndex<F>,
{
if !cost.is_finite()
|| self.is_known_optimum(&point, lower, upper, free)
{
return;
}
self.local_optima.push((point, cost));
}
}
impl<P, V, F> Solver<P, PointState<V, F>> for Gbnm<V, F>
where
F: Scalar + SampleUniform,
P: CostFunction<Param = V, Output = F> + BoxProblem<Param = V>,
V: Clone
+ ClampInPlace
+ SampleUniformBox
+ ScaleInPlace<F>
+ ScaledAdd<F>
+ VectorIndex<F>
+ VectorLen,
{
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)?;
let lower = problem.inner().lower().clone();
let upper = problem.inner().upper().clone();
assert_eq!(
state.param().vec_len(),
lower.vec_len(),
"GBNM initial point and bounds length mismatch"
);
let free = free_coordinates(&lower, &upper);
let mut center = state.param().clone();
center.clamp_in_place(&lower, &upper);
state.reset();
self.rng = ChaCha8Rng::seed_from_u64(self.seed);
self.simplex = None;
self.fixed_box = free.is_empty();
self.search_starts.clear();
self.local_optima.clear();
self.restart_count = 0;
self.restart = GbnmRestart::Probabilistic;
self.restart_center = None;
if self.fixed_box {
let cost = problem.cost(¢er)?;
state.replace(center, cost);
return Ok(state);
}
self.install_restart(
problem,
state,
GbnmRestart::Probabilistic,
center,
self.initial_simplex_fraction,
BoxGeometry {
lower: &lower,
upper: &upper,
free: &free,
},
)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, Self::Error> {
if self.fixed_box {
let termination =
<Self as Solver<P, PointState<V, F>>>::terminate(self, &state)
.expect("an initialized fixed box has a terminal outcome");
return Ok(crate::SolverStep::stopped(state, termination));
}
let lower = problem.inner().lower().clone();
let upper = problem.inner().upper().clone();
let free = free_coordinates(&lower, &upper);
let simplex = self
.simplex
.take()
.expect("Gbnm::init must run before next_iter");
let small = simplex_is_small(
&simplex.vertices,
&lower,
&upper,
&free,
self.small_tolerance,
);
let spread = simplex.costs[simplex.costs.len() - 1] - simplex.costs[0];
let flat = spread.is_finite() && spread.abs() < self.flat_tolerance;
let degenerate = simplex_is_degenerate(
&simplex.vertices,
&lower,
&upper,
&free,
small,
self.degeneracy_edge_ratio,
self.degeneracy_shape,
);
if !(small || flat || degenerate) {
let crate::SolverStep {
state: simplex,
termination: reason,
completed,
} = <NelderMead<V, F, Projected> as Solver<
P,
SimplexProgress<V, F>,
>>::next_iter(&mut self.local, problem, simplex)?;
state.replace_from(&simplex.vertices[0], simplex.costs[0]);
self.simplex = Some(simplex);
return Ok(crate::SolverStep {
state,
completed,
termination: reason,
});
}
let point = simplex.vertices[0].clone();
let cost = simplex.costs[0];
let center = self
.restart_center
.as_ref()
.expect("GBNM restart center missing after initialization");
let returned_to_center =
normalized_l1_distance(&point, center, &lower, &upper, &free)
< self.small_tolerance;
let on_bounds = point_on_bounds(&point, &lower, &upper, &free);
let (next_restart, optimum_action) = select_restart(
self.restart,
self.is_known_optimum(&point, &lower, &upper, &free),
flat,
small,
degenerate,
returned_to_center,
on_bounds,
);
if optimum_action == OptimumAction::Save {
self.save_optimum(point.clone(), cost, &lower, &upper, &free);
}
let (next_center, edge_fraction) = match next_restart {
GbnmRestart::Probabilistic => {
let center = self.probabilistic_center(&lower, &upper, &free);
let fraction = self.rng.random_range(
self.probabilistic_min_fraction
..=self.probabilistic_max_fraction,
);
(center, fraction)
}
GbnmRestart::SmallTest => (point, self.small_test_fraction),
GbnmRestart::LargeTest => (point, self.large_test_fraction),
};
self.restart_count += 1;
let state = self.install_restart(
problem,
state,
next_restart,
next_center,
edge_fraction,
BoxGeometry {
lower: &lower,
upper: &upper,
free: &free,
},
)?;
Ok(crate::SolverStep::from((state, None)))
}
fn terminate(&self, state: &PointState<V, F>) -> Option<Termination<F>> {
self.fixed_box.then(|| {
if state.cost().is_finite() {
Termination::custom(
"gbnm.fixed_box",
"The box has exactly one feasible point with a finite objective.",
vec![crate::Measurement {
name: "cost".into(),
value: state.cost(),
}],
)
} else {
Termination::numerical_failure(
"GBNM's sole feasible point has a non-finite objective.",
)
}
})
}
}
impl<V, F> InitialState<V> for Gbnm<V, F>
where
F: Scalar,
V: Clone,
{
type State = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
#[cfg(test)]
mod tests {
use super::Gbnm;
#[test]
fn possible_local_optima_are_finite_and_deduplicated() {
let mut solver = Gbnm::new(1);
let lower = vec![-5.0, -5.0];
let upper = vec![5.0, 5.0];
let free = [0, 1];
solver.save_optimum(vec![1.0, 1.0], 2.0, &lower, &upper, &free);
solver.save_optimum(vec![1.0 + 1e-7, 1.0], 1.9, &lower, &upper, &free);
solver.save_optimum(
vec![3.0, 3.0],
f64::INFINITY,
&lower,
&upper,
&free,
);
assert_eq!(solver.local_optima().len(), 1);
}
}