#![allow(clippy::needless_range_loop)]
pub(crate) mod driver;
pub(crate) mod filter;
pub(crate) mod geometry;
pub(crate) mod init;
pub(crate) mod linalg;
pub(crate) mod model;
pub(crate) mod trstlp;
pub(crate) mod update;
#[cfg(test)]
mod parity;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod regression;
use crate::core::constraint::{
FoldedConstraints, NonlinearConstraints, NonlinearInequalityConstraints,
};
use crate::core::inner::InitialState;
use crate::core::math::{MatVec, Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::CobylaState;
use crate::core::termination::TerminationReason;
use driver::{CobylaWork, Transition};
pub struct Cobyla<F = f64> {
radius_tolerance: Option<F>,
rho_beg: F,
rho_end: F,
work: Option<CobylaWork<F>>,
}
impl<F: Scalar> Cobyla<F> {
pub fn with_absolute_radius_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.radius_tolerance =
crate::core::convergence::optional_tolerance(value);
self
}
pub fn new() -> Self {
Self {
rho_beg: F::from_f64(1.0).expect("1.0 representable"),
radius_tolerance: None,
rho_end: F::from_f64(1e-6).expect("1e-6 representable"),
work: None,
}
}
#[deprecated(
note = "use `with_initial_radius`; removal scheduled for Basin 2.0"
)]
pub fn with_rho_beg(self, rho_beg: F) -> Self {
self.with_initial_radius(rho_beg)
}
pub fn with_initial_radius(mut self, rho_beg: F) -> Self {
self.rho_beg = rho_beg;
self
}
#[deprecated(
note = "use `with_final_radius`; removal scheduled for Basin 2.0"
)]
pub fn with_rho_end(self, rho_end: F) -> Self {
self.with_final_radius(rho_end)
}
pub fn with_final_radius(mut self, rho_end: F) -> Self {
self.rho_end = rho_end;
self
}
}
impl<F: Scalar> Default for Cobyla<F> {
fn default() -> Self {
Self::new()
}
}
fn fill_into<V, F>(v: &mut V, slice: &[F])
where
V: std::ops::IndexMut<usize, Output = F>,
F: Copy,
{
for (i, &x) in slice.iter().enumerate() {
v[i] = x;
}
}
impl<V, F> InitialState<V> for Cobyla<F>
where
F: Scalar,
V: Clone,
{
type State = CobylaState<V, F>;
fn seed(&self, x: &V) -> Self::State {
CobylaState::new(x.clone())
}
}
type CobylaStep<V, F, E> =
Result<(CobylaState<V, F>, Option<TerminationReason>), E>;
impl<F: Scalar> Cobyla<F> {
fn init_with<P, V>(
&mut self,
problem: &mut Problem<P>,
mut state: CobylaState<V, F>,
m: usize,
mut constraints: impl FnMut(&P, &V) -> Result<Vec<F>, P::Error>,
) -> Result<CobylaState<V, F>, P::Error>
where
P: CostFunction<Param = V, Output = F>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
let n = state.param.vec_len();
assert!(n >= 1, "Cobyla requires a non-empty start point");
let x0: Vec<F> = (0..n).map(|i| state.param[i]).collect();
let (work, best_x, best_f) = {
let mut eval = |slice: &[F]| -> Result<(F, Vec<F>), P::Error> {
fill_into(&mut state.param, slice);
let f = problem.cost(&state.param)?;
let c = constraints(problem.inner(), &state.param)?;
assert_eq!(
c.len(),
m,
"Cobyla constraint count must remain fixed during a solve",
);
Ok((f, c))
};
CobylaWork::try_init(x0, m, self.rho_beg, self.rho_end, &mut eval)?
};
fill_into(&mut state.param, &best_x);
state.cost = Some(best_f);
state.rho = work.rho();
self.work = Some(work);
Ok(state)
}
fn next_iter_with<P, V>(
&mut self,
problem: &mut Problem<P>,
mut state: CobylaState<V, F>,
mut constraints: impl FnMut(&P, &V) -> Result<Vec<F>, P::Error>,
) -> CobylaStep<V, F, P::Error>
where
P: CostFunction<Param = V, Output = F>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
let work = self
.work
.as_mut()
.expect("Cobyla::init must run before next_iter");
let m = work.num_constraints();
let transition = {
let mut eval = |slice: &[F]| -> Result<(F, Vec<F>), P::Error> {
fill_into(&mut state.param, slice);
let f = problem.cost(&state.param)?;
let c = constraints(problem.inner(), &state.param)?;
assert_eq!(
c.len(),
m,
"Cobyla constraint count must remain fixed during a solve",
);
Ok((f, c))
};
work.step(&mut eval)?
};
state.rho = work.rho();
let (best_x, best_f) = work.best_ref();
fill_into(&mut state.param, best_x);
state.cost = Some(best_f);
let reason = match transition {
Transition::Converged => Some(TerminationReason::SolverConverged),
Transition::Failed => Some(TerminationReason::SolverFailed),
Transition::Continue | Transition::RhoReduced => None,
};
Ok((state, reason))
}
fn radius_termination<V: Clone>(
&self,
state: &CobylaState<V, F>,
) -> Option<TerminationReason> {
let tolerance = self.radius_tolerance?;
let metric = crate::RhoState::rho(state);
(metric.is_finite() && metric <= tolerance)
.then_some(TerminationReason::RhoTolerance)
}
}
fn inequality_values<P, V, F>(
problem: &P,
x: &V,
m: usize,
) -> Result<Vec<F>, P::Error>
where
P: NonlinearInequalityConstraints<Param = V, Output = F>,
V: VectorLen + std::ops::Index<usize, Output = F>,
F: Scalar,
{
let cv = problem.constraints(x)?;
debug_assert_eq!(
cv.vec_len(),
m,
"constraints() returned {} values but num_constraints() = {m}",
cv.vec_len(),
);
Ok((0..m).map(|i| cv[i]).collect())
}
impl<P, V, F> Solver<P, CobylaState<V, F>> for Cobyla<F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + NonlinearInequalityConstraints,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
state: CobylaState<V, F>,
) -> Result<CobylaState<V, F>, Self::Error> {
let m = problem.inner().num_constraints();
self.init_with(problem, state, m, |p, x| inequality_values(p, x, m))
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: CobylaState<V, F>,
) -> Result<(CobylaState<V, F>, Option<TerminationReason>), Self::Error>
{
let m = problem.inner().num_constraints();
self.next_iter_with(problem, state, |p, x| inequality_values(p, x, m))
}
fn terminate(
&self,
state: &CobylaState<V, F>,
) -> Option<TerminationReason> {
self.radius_termination(state)
}
}
impl<P, V, F> Solver<FoldedConstraints<P>, CobylaState<V, F>> for Cobyla<F>
where
F: Scalar,
P: NonlinearConstraints<Param = V, Output = F>,
P::Matrix: MatVec<V>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<FoldedConstraints<P>>,
state: CobylaState<V, F>,
) -> Result<CobylaState<V, F>, Self::Error> {
let m = problem.inner().constraint_count(state.param.vec_len());
self.init_with(
problem,
state,
m,
FoldedConstraints::evaluate_constraints,
)
}
fn next_iter(
&mut self,
problem: &mut Problem<FoldedConstraints<P>>,
state: CobylaState<V, F>,
) -> Result<(CobylaState<V, F>, Option<TerminationReason>), Self::Error>
{
self.next_iter_with(
problem,
state,
FoldedConstraints::evaluate_constraints,
)
}
fn terminate(
&self,
state: &CobylaState<V, F>,
) -> Option<TerminationReason> {
self.radius_termination(state)
}
}