use crate::core::barrier::{LogBarrier, strict_feasibility};
use crate::core::constraint::LinearInequalityConstraints;
use crate::core::executor::run_loop_with_control;
use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{
MatTransposeVec, MatVec, NegInPlace, NormSquared, Scalar, ScaledAdd,
VectorIndex, VectorLen,
};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::{BasicState, CountsMirror, GradientState, State};
use crate::core::termination::TerminationReason;
pub struct BarrierMethod<So, F = f64> {
inner_solver: So,
inner_max_iter: u64,
inner_grad_tol: Option<F>,
mu0: F,
mu: F,
reduction: F,
tol: Option<F>,
phase_one_tol: F,
gap: F,
phase: BarrierPhase,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BarrierPhase {
PhaseOne,
PhaseTwo,
Failed,
}
struct StopAtStrictFeasibility<'a, So> {
inner: &'a mut So,
}
impl<'a, 'p, P, V, M, S, So, F> Solver<LogBarrier<'p, P, F>, S>
for StopAtStrictFeasibility<'a, So>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>
+ LinearInequalityConstraints<Param = V, Matrix = M>,
M: MatVec<V>,
V: ScaledAdd<F> + VectorIndex<F> + VectorLen,
S: State<Param = V>,
So: Solver<LogBarrier<'p, P, F>, S>,
{
type Error = So::Error;
fn init(
&mut self,
problem: &mut Problem<LogBarrier<'p, P, F>>,
state: S,
) -> Result<S, Self::Error> {
self.inner.init(problem, state)
}
fn next_iter(
&mut self,
problem: &mut Problem<LogBarrier<'p, P, F>>,
state: S,
) -> Result<(S, Option<TerminationReason>), Self::Error> {
let (state, reason) = self.inner.next_iter(problem, state)?;
let inner_failed = reason.is_some_and(|reason| reason.is_failure());
if !inner_failed
&& problem.inner().strict_feasibility(state.param()) == Some(true)
{
Ok((state, Some(TerminationReason::SolverConverged)))
} else {
Ok((state, reason))
}
}
fn terminate(&self, state: &S) -> Option<TerminationReason> {
self.inner.terminate(state)
}
fn reset_convergence(&mut self) {
self.inner.reset_convergence();
}
fn check_convergence(
&mut self,
problem: &Problem<LogBarrier<'p, P, F>>,
state: &S,
) -> Option<TerminationReason> {
self.inner.check_convergence(problem, state)
}
}
impl<So> BarrierMethod<So> {
#[deprecated(
note = "use `with_inner_solver` with convergence configured on the inner solver; removal scheduled for Basin 2.0"
)]
pub fn new(inner_solver: So) -> Self {
Self::legacy_defaults(inner_solver)
}
pub fn with_inner_solver(inner_solver: So) -> Self {
let mut solver = Self::legacy_defaults(inner_solver);
solver.inner_grad_tol = None;
solver
}
fn legacy_defaults(inner_solver: So) -> Self {
Self {
inner_solver,
inner_max_iter: 50,
inner_grad_tol: Some(1e-8),
mu0: 1.0,
mu: 1.0,
reduction: 10.0,
tol: Some(1e-8),
phase_one_tol: 1e-8,
gap: f64::INFINITY,
phase: BarrierPhase::PhaseTwo,
}
}
}
impl<So, F: Scalar> BarrierMethod<So, F> {
pub fn mu0(mut self, mu0: F) -> Self {
assert!(mu0 > F::zero(), "mu0 must be > 0");
self.mu0 = mu0;
self
}
pub fn with_reduction(mut self, reduction: F) -> Self {
assert!(reduction > F::one(), "reduction must be > 1");
self.reduction = reduction;
self
}
#[deprecated(
note = "use `with_absolute_duality_gap_tolerance`; removal scheduled for Basin 2.0"
)]
pub fn with_tol(mut self, tol: F) -> Self {
assert!(tol > F::zero(), "tol must be > 0");
self.tol = Some(tol);
self
}
pub fn with_absolute_duality_gap_tolerance(
mut self,
tol: impl Into<Option<F>>,
) -> Self {
self.tol = crate::core::convergence::optional_tolerance(tol);
self
}
#[deprecated(
note = "use `with_absolute_phase_one_gap_tolerance`; removal scheduled for Basin 2.0"
)]
pub fn with_phase_one_tol(self, phase_one_tol: F) -> Self {
self.with_absolute_phase_one_gap_tolerance(phase_one_tol)
}
pub fn with_absolute_phase_one_gap_tolerance(
mut self,
phase_one_tol: F,
) -> Self {
assert!(phase_one_tol > F::zero(), "phase_one_tol must be > 0");
self.phase_one_tol = phase_one_tol;
self
}
pub fn with_inner_max_iter(mut self, inner_max_iter: u64) -> Self {
assert!(inner_max_iter >= 1, "inner_max_iter must be ≥ 1");
self.inner_max_iter = inner_max_iter;
self
}
#[deprecated(
note = "configure the supplied inner solver and use `with_inner_solver`; removal scheduled for Basin 2.0"
)]
pub fn with_inner_grad_tol(mut self, inner_grad_tol: F) -> Self {
assert!(inner_grad_tol >= F::zero(), "inner_grad_tol must be ≥ 0");
self.inner_grad_tol = Some(inner_grad_tol);
self
}
}
impl<So, V, F> InitialState<V> for BarrierMethod<So, F>
where
F: Scalar,
V: Clone,
{
type State = BasicState<V, F>;
fn seed(&self, x: &V) -> Self::State {
BasicState::new(x.clone())
}
}
impl<P, V, M, So, F> Solver<P, BasicState<V, F>> for BarrierMethod<So, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>
+ Gradient<Gradient = V>
+ LinearInequalityConstraints<Param = V, Matrix = M>,
M: MatVec<V> + MatTransposeVec<V>,
V: ScaledAdd<F>
+ NegInPlace
+ VectorIndex<F>
+ VectorLen
+ NormSquared<F>
+ Clone,
So: WarmStart<V>
+ for<'a> Solver<
LogBarrier<'a, P, F>,
So::State,
Error = <P as CostFunction>::Error,
>,
So::State: GradientState<Param = V, Float = F> + CountsMirror,
{
type Error = <P as CostFunction>::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
) -> Result<BasicState<V, F>, Self::Error> {
self.mu = self.mu0;
self.gap = F::infinity();
self.phase = match strict_feasibility(problem.inner(), state.param()) {
Some(true) => BarrierPhase::PhaseTwo,
Some(false) => BarrierPhase::PhaseOne,
None => BarrierPhase::Failed,
};
if self.phase == BarrierPhase::Failed {
state.cost = Some(F::infinity());
state.gradient = None;
return Ok(state);
}
if self.phase == BarrierPhase::PhaseOne {
state.cost = Some(F::infinity());
state.gradient = None;
return Ok(state);
}
let (cost, grad) = problem.cost_and_gradient(state.param())?;
state.cost = Some(cost);
state.gradient = Some(grad);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
) -> Result<(BasicState<V, F>, Option<TerminationReason>), Self::Error>
{
if self.phase == BarrierPhase::Failed {
return Ok((state, Some(TerminationReason::SolverFailed)));
}
if self.phase == BarrierPhase::PhaseOne {
let mut barrier_wrapper =
Problem::new(LogBarrier::phase_one(problem.inner(), self.mu));
let mut control =
crate::core::run_control::legacy_inner_control::<So::State, F>(
self.inner_max_iter,
self.inner_grad_tol,
);
let inner_state = self.inner_solver.seed(state.param());
let mut phase_one_solver = StopAtStrictFeasibility {
inner: &mut self.inner_solver,
};
let result = run_loop_with_control(
&mut barrier_wrapper,
inner_state,
&mut phase_one_solver,
&mut control,
)?;
let inner_counts = *barrier_wrapper.counts();
problem.counts_mut().add(&inner_counts);
if result.reason.is_failure() {
self.phase = BarrierPhase::Failed;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
let centered = if let Some(tol) = self.inner_grad_tol {
result
.state
.gradient()
.is_some_and(|g| g.norm_squared() <= tol * tol)
} else {
matches!(
result.reason,
TerminationReason::GradientTolerance
| TerminationReason::RelativeGradientTolerance
)
};
let candidate = result.state.param();
let feasibility = strict_feasibility(problem.inner(), candidate);
let Some(is_strictly_feasible) = feasibility else {
self.phase = BarrierPhase::Failed;
return Ok((state, Some(TerminationReason::SolverFailed)));
};
state.param = candidate.clone();
if is_strictly_feasible {
let (cost, grad) = problem.cost_and_gradient(&state.param)?;
state.cost = Some(cost);
state.gradient = Some(grad);
self.phase = BarrierPhase::PhaseTwo;
self.mu = self.mu0;
self.gap = F::infinity();
return Ok((state, None));
}
if centered {
let phase_one_gap =
F::from_usize(problem.inner().b().vec_len()).unwrap()
* self.mu;
if phase_one_gap <= self.phase_one_tol {
self.phase = BarrierPhase::Failed;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
self.mu = self.mu / self.reduction;
}
state.cost = Some(F::infinity());
state.gradient = None;
return Ok((state, None));
}
let mut barrier_wrapper =
Problem::new(LogBarrier::new(problem.inner(), self.mu));
let mut control = crate::core::run_control::legacy_inner_control::<
So::State,
F,
>(self.inner_max_iter, self.inner_grad_tol);
let inner_state = self.inner_solver.seed(state.param());
let result = run_loop_with_control(
&mut barrier_wrapper,
inner_state,
&mut self.inner_solver,
&mut control,
)?;
let inner_counts = *barrier_wrapper.counts();
problem.counts_mut().add(&inner_counts);
if result.reason.is_failure() {
self.phase = BarrierPhase::Failed;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
let candidate = result.state.param();
if strict_feasibility(problem.inner(), candidate) != Some(true) {
self.phase = BarrierPhase::Failed;
return Ok((state, Some(TerminationReason::SolverFailed)));
}
state.param = candidate.clone();
let (cost, grad) = problem.cost_and_gradient(&state.param)?;
state.cost = Some(cost);
state.gradient = Some(grad);
self.gap =
F::from_usize(problem.inner().b().vec_len()).unwrap() * self.mu;
self.mu = self.mu / self.reduction;
Ok((state, None))
}
fn terminate(
&self,
_state: &BasicState<V, F>,
) -> Option<TerminationReason> {
if self.phase == BarrierPhase::PhaseTwo
&& self.tol.is_some_and(|tol| self.gap <= tol)
{
Some(TerminationReason::SolverConverged)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
#![allow(deprecated)]
use super::*;
#[test]
#[should_panic(expected = "mu0 must be > 0")]
fn rejects_nonpositive_mu0() {
let _ = BarrierMethod::new(()).mu0(0.0);
}
#[test]
#[should_panic(expected = "reduction must be > 1")]
fn rejects_reduction_not_greater_than_one() {
let _ = BarrierMethod::new(()).with_reduction(1.0);
}
#[test]
#[should_panic(expected = "tol must be > 0")]
fn rejects_nonpositive_tol() {
let _ = BarrierMethod::new(()).with_tol(0.0);
}
#[test]
#[should_panic(expected = "phase_one_tol must be > 0")]
fn rejects_nonpositive_phase_one_tol() {
let _ = BarrierMethod::new(()).with_phase_one_tol(0.0);
}
#[test]
#[should_panic(expected = "inner_max_iter must be ≥ 1")]
fn rejects_zero_inner_max_iter() {
let _ = BarrierMethod::new(()).with_inner_max_iter(0);
}
#[test]
#[should_panic(expected = "inner_grad_tol must be ≥ 0")]
fn rejects_negative_inner_grad_tol() {
let _ = BarrierMethod::new(()).with_inner_grad_tol(-1.0);
}
}