#![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::evaluation::{CobylaSet, SetLayout, invalid};
use crate::core::constraint::{ConstraintError, ConstraintSolveError};
use crate::core::inner::InitialState;
use crate::core::math::{Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::solver::Solver;
use crate::core::state::{SelectedState, State};
use crate::core::termination::Termination;
use crate::{ViolationMeasure, ViolationReport, ViolationTransformation};
use driver::{CobylaWork, Transition};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cobyla<V, F: Scalar = f64> {
radius_tolerance: Option<F>,
rho_beg: F,
rho_end: F,
work: Option<CobylaWork<F>>,
evaluation_param: Option<V>,
layout: Option<SetLayout>,
}
impl<V, F: Scalar> Cobyla<V, F> {
pub fn rho(&self) -> Option<F> {
self.work.as_ref().map(|work| work.rho())
}
#[allow(clippy::type_complexity)]
pub fn geometry_diagnostics(
&self,
) -> Option<(F, F, Vec<Vec<F>>, Vec<F>, F)> {
self.work.as_ref().map(|work| work.geometry_diagnostics())
}
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,
evaluation_param: None,
layout: None,
}
}
pub fn with_initial_radius(mut self, rho_beg: F) -> Self {
self.rho_beg = rho_beg;
self
}
pub fn with_final_radius(mut self, rho_end: F) -> Self {
self.rho_end = rho_end;
self
}
}
impl<V, F: Scalar> Default for Cobyla<V, 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;
}
}
fn publish<V, F>(
work: &CobylaWork<F>,
param: &mut V,
state: &mut SelectedState<V, F>,
) where
V: Clone + VectorLen + std::ops::IndexMut<usize, Output = F>,
F: Scalar,
{
let (x, cost, violation) = work.best_record_ref();
let changed = state.current().is_none_or(|(previous, f, v)| {
f != cost
|| v.value != violation
|| (0..x.len()).any(|i| previous[i] != x[i])
});
fill_into(param, x);
state.replace_from(
param,
cost,
ViolationReport::new(violation, ViolationMeasure::LInf)
.with_transformation(ViolationTransformation::PrimaModeration),
);
if changed {
state.select_current();
}
}
impl<V, F> InitialState<V> for Cobyla<V, F>
where
F: Scalar,
V: Clone,
{
type State = SelectedState<V, F>;
fn seed(&self, x: &V) -> Self::State {
SelectedState::new(x.clone())
}
}
type CobylaStep<V, F, E> = Result<crate::SolverStep<SelectedState<V, F>>, E>;
impl<V, F: Scalar> Cobyla<V, F> {
fn init_with<P>(
&mut self,
problem: &mut Problem<P>,
mut state: SelectedState<V, F>,
m: usize,
mut constraints: impl FnMut(
&mut Problem<P>,
&V,
) -> Result<
Vec<F>,
ConstraintSolveError<P::Error>,
>,
) -> Result<SelectedState<V, F>, ConstraintSolveError<P::Error>>
where
P: CostFunction<Param = V, Output = F>,
V: Clone
+ VectorLen
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
{
state.reset();
self.work = None;
self.evaluation_param = None;
let n = state.param().vec_len();
let mut param = state.param().clone();
assert!(n >= 1, "Cobyla requires a non-empty start point");
let x0: Vec<F> = (0..n).map(|i| state.param()[i]).collect();
let (work, _, _) = {
let mut eval = |slice: &[F]| -> Result<
(F, Vec<F>),
ConstraintSolveError<P::Error>,
> {
fill_into(&mut param, slice);
let f = problem.cost(¶m)?;
let c = constraints(problem, ¶m)?;
if c.len() != m {
return Err(invalid(ConstraintError::LayoutChanged));
}
Ok((f, c))
};
CobylaWork::try_init(x0, m, self.rho_beg, self.rho_end, &mut eval)?
};
publish(&work, &mut param, &mut state);
self.evaluation_param = Some(param);
self.work = Some(work);
Ok(state)
}
fn next_iter_with<P>(
&mut self,
problem: &mut Problem<P>,
mut state: SelectedState<V, F>,
mut constraints: impl FnMut(
&mut Problem<P>,
&V,
) -> Result<
Vec<F>,
ConstraintSolveError<P::Error>,
>,
) -> CobylaStep<V, F, ConstraintSolveError<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 param = self
.evaluation_param
.as_mut()
.expect("Cobyla::init seeds the evaluation buffer");
let transition = {
let mut eval = |slice: &[F]| -> Result<
(F, Vec<F>),
ConstraintSolveError<P::Error>,
> {
fill_into(param, slice);
let f = problem.cost(param)?;
let c = constraints(problem, param)?;
if c.len() != m {
return Err(invalid(ConstraintError::LayoutChanged));
}
Ok((f, c))
};
work.step(&mut eval)?
};
publish(work, param, &mut state);
let reason = match transition {
Transition::Converged => Some(Termination::custom(
"cobyla.final_radius",
"The algorithm completed its final trust-radius stage; this does not certify feasibility.",
vec![crate::Measurement {
name: "final_radius".into(),
value: self.rho_end,
}],
)),
Transition::Failed => Some(Termination::numerical_failure(
"COBYLA could not produce a valid interpolation step.",
)),
Transition::Continue | Transition::RhoReduced => None,
};
Ok(crate::SolverStep::from((state, reason)))
}
fn radius_termination(&self) -> Option<Termination<F>> {
let tolerance = self.radius_tolerance?;
let metric = self.rho()?;
(metric.is_finite() && metric <= tolerance).then(|| {
Termination::upper_bound(
crate::ConvergenceTest::Radius,
metric,
tolerance,
tolerance,
None,
)
})
}
}
impl<P, V, F> Solver<P, SelectedState<V, F>> for Cobyla<V, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
P: crate::Constraints,
P::Set: CobylaSet<P, V, F>,
V: Clone + VectorLen + std::ops::IndexMut<usize, Output = F>,
{
type Error = ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
state: SelectedState<V, F>,
) -> Result<SelectedState<V, F>, Self::Error> {
self.layout = None;
let layout = P::Set::layout(
problem.inner().constraints(),
state.param().vec_len(),
)
.map_err(invalid)?;
let count = layout.folded_count().map_err(invalid)?;
let result = self.init_with(problem, state, count, |p, x| {
P::Set::values(p, x, &layout)
});
self.layout = Some(layout);
result
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
state: SelectedState<V, F>,
) -> Result<crate::SolverStep<SelectedState<V, F>>, Self::Error> {
let layout = self
.layout
.take()
.expect("Cobyla::init prepares the constraint layout");
let result = self.next_iter_with(problem, state, |p, x| {
P::Set::values(p, x, &layout)
});
self.layout = Some(layout);
result
}
fn terminate(
&self,
_state: &SelectedState<V, F>,
) -> Option<Termination<F>> {
self.radius_termination()
}
}