use crate::ConstraintSolveError;
use crate::core::barrier::{LogBarrier, strict_feasibility};
use crate::core::constraint::evaluation::{
LinearInequalityGradient, LinearInequalityProblem, invalid,
};
use crate::core::executor::run_loop_with_control;
use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{
NegInPlace, NormInfinity, NormSquared, Scalar, ScaledAdd, VectorIndex,
VectorLen,
};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::{CountsMirror, PointState, State};
use crate::core::termination::Termination;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct BarrierInnerDiagnostics<F: Scalar = f64> {
pub phase_one: bool,
pub mu: F,
pub gap: F,
pub start: Vec<F>,
pub returned: Option<Vec<F>>,
pub report: Option<crate::TerminationReport<F>>,
pub counts: crate::EvalCounts,
pub hard_error: bool,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BarrierMethod<So, F: Scalar = f64> {
inner_solver: So,
inner_max_iter: u64,
mu0: F,
mu: F,
reduction: F,
tol: Option<F>,
stationarity_tol: Option<F>,
relative_stationarity_tol: Option<F>,
stationarity_initial_norm: F,
stationarity_norm: F,
centering_mu: Option<F>,
phase_one_tol: F,
gap: F,
phase: BarrierPhase,
accepted_iterate: bool,
#[cfg_attr(feature = "serde", serde(default))]
inner_diagnostics: bool,
last_inner: Option<BarrierInnerDiagnostics<F>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum BarrierPhase {
PhaseOne,
PhaseTwo,
Failed,
}
struct StopAtStrictFeasibility<'a, So> {
inner: &'a mut So,
}
impl<'a, 'p, P, V, S, So, F> Solver<LogBarrier<'p, P, F>, S>
for StopAtStrictFeasibility<'a, So>
where
F: Scalar,
P: LinearInequalityProblem<V, F>,
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<crate::SolverStep<S>, Self::Error> {
let crate::SolverStep {
state,
termination: reason,
completed,
} = self.inner.next_iter(problem, state)?;
let inner_failed = reason.as_ref().is_some_and(|reason| {
!reason.can_continue_as_inner(crate::PartialResultPolicy::Consume)
});
if !inner_failed
&& problem.inner().strict_feasibility(state.param()) == Some(true)
{
Ok(crate::SolverStep {
state,
completed,
termination: Some(Termination::custom(
"barrier.strict_feasibility",
"All inequality slacks are strictly positive.",
vec![],
)),
})
} else {
Ok(crate::SolverStep {
state,
termination: reason,
completed,
})
}
}
fn terminate(&self, state: &S) -> Option<Termination<S::Float>> {
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<Termination<S::Float>> {
self.inner.check_convergence(problem, state)
}
}
impl<So, F: Scalar> BarrierMethod<So, F> {
pub fn with_inner_solver(inner_solver: So) -> Self {
Self {
inner_solver,
inner_max_iter: 50,
mu0: F::one(),
mu: F::one(),
reduction: F::from_f64(10.0).unwrap(),
tol: Some(F::from_f64(1e-8).unwrap()),
stationarity_tol: Some(F::from_f64(1e-8).unwrap()),
relative_stationarity_tol: Some(F::from_f64(1e-3).unwrap()),
stationarity_initial_norm: F::infinity(),
stationarity_norm: F::infinity(),
centering_mu: None,
phase_one_tol: F::from_f64(1e-8).unwrap(),
gap: F::infinity(),
phase: BarrierPhase::PhaseTwo,
accepted_iterate: false,
inner_diagnostics: false,
last_inner: None,
}
}
}
impl<So, F: Scalar> BarrierMethod<So, F> {
fn stationarity_satisfied(&self) -> bool {
crate::core::convergence::stationarity_within_tolerance(
self.stationarity_norm,
self.stationarity_initial_norm,
self.stationarity_tol,
self.relative_stationarity_tol,
)
}
pub fn with_absolute_stationarity_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.stationarity_tol =
crate::core::convergence::optional_tolerance(value);
self
}
pub fn with_relative_stationarity_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.relative_stationarity_tol =
crate::core::convergence::optional_tolerance(value);
self
}
pub fn with_inner_diagnostics(mut self, enabled: bool) -> Self {
self.inner_diagnostics = enabled;
if !enabled {
self.last_inner = None;
}
self
}
pub fn inner_diagnostics(&self) -> Option<&BarrierInnerDiagnostics<F>> {
self.last_inner.as_ref()
}
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
}
pub fn with_absolute_duality_gap_tolerance(
mut self,
tol: impl Into<Option<F>>,
) -> Self {
self.tol = crate::core::convergence::optional_tolerance(tol);
self
}
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
}
}
impl<So, V, F> InitialState<V> for BarrierMethod<So, F>
where
F: Scalar,
V: Clone,
{
type State = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
impl<P, V, So, F> Solver<P, PointState<V, F>> for BarrierMethod<So, F>
where
F: Scalar,
P: LinearInequalityGradient<V, F> + Gradient<Gradient = V>,
V: ScaledAdd<F>
+ NegInPlace
+ VectorIndex<F>
+ VectorLen
+ NormSquared<F>
+ NormInfinity<F>
+ Clone,
So: WarmStart<V>
+ for<'a> Solver<
LogBarrier<'a, P, F>,
So::State,
Error = ConstraintSolveError<<P as CostFunction>::Error>,
>,
So::State: State<Param = V, Float = F> + CountsMirror,
{
type Error = ConstraintSolveError<<P as CostFunction>::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
problem
.inner()
.validate_linear(state.param().vec_len())
.map_err(invalid)?;
state.reset();
self.accepted_iterate = false;
self.last_inner = None;
self.stationarity_norm = F::infinity();
self.stationarity_initial_norm = F::infinity();
self.centering_mu = None;
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.replace(state.param().clone(), F::infinity());
return Ok(state);
}
if self.phase == BarrierPhase::PhaseOne {
state.replace(state.param().clone(), F::infinity());
return Ok(state);
}
let cost = problem.cost(state.param())?;
state.replace(state.param().clone(), cost);
self.accepted_iterate = true;
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, Self::Error> {
self.accepted_iterate = false;
self.stationarity_norm = F::infinity();
if self.phase == BarrierPhase::Failed {
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"The barrier method could not initialize a finite barrier model.",
)),
)));
}
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::RunControl::new()
.max_iter(self.inner_max_iter);
let diagnostic_start = self.inner_diagnostics.then(|| {
(0..state.param().vec_len())
.map(|i| state.param().get_scalar(i))
.collect::<Vec<_>>()
});
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);
self.last_inner =
diagnostic_start.map(|start| BarrierInnerDiagnostics {
phase_one: true,
mu: self.mu,
gap: F::from_usize(problem.inner().linear_count()).unwrap()
* self.mu,
start,
returned: result.as_ref().ok().map(|r| {
(0..r.state.param().vec_len())
.map(|i| r.state.param().get_scalar(i))
.collect()
}),
report: result.as_ref().ok().map(|r| r.report.clone()),
counts: inner_counts,
hard_error: result.is_err(),
});
let result = result?;
if !result
.report
.termination
.can_continue_as_inner(crate::PartialResultPolicy::Consume)
{
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::from((
state,
result.report.into_outer_termination(
crate::PartialResultPolicy::Consume,
),
)));
}
let centered = matches!(
&result.report.termination,
Termination::Converged(convergence) if convergence.criteria().iter().any(|criterion|
matches!(criterion.test, crate::ConvergenceTest::AbsoluteGradientSquared | crate::ConvergenceTest::RelativeGradientSquared)
)
);
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(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"Phase I produced non-finite constraint slacks.",
)),
)));
};
if is_strictly_feasible {
let cost = problem.cost(candidate)?;
if !cost.is_finite() {
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"Phase I returned a strict point with a non-finite original objective.",
),
));
}
state.replace(candidate.clone(), cost);
self.phase = BarrierPhase::PhaseTwo;
self.mu = self.mu0;
self.gap = F::infinity();
self.accepted_iterate = true;
return Ok(crate::SolverStep::from((state, None)));
}
state.replace(candidate.clone(), F::infinity());
if centered {
let phase_one_gap =
F::from_usize(problem.inner().linear_count()).unwrap()
* self.mu;
if phase_one_gap <= self.phase_one_tol {
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"A centered Phase I subproblem reached its gap threshold without strict feasibility.",
)),
)));
}
self.mu = self.mu / self.reduction;
}
return Ok(crate::SolverStep::from((state, None)));
}
let mut barrier_wrapper =
Problem::new(LogBarrier::new(problem.inner(), self.mu));
let mut control = crate::core::run_control::RunControl::new()
.max_iter(self.inner_max_iter);
let diagnostic_start = self.inner_diagnostics.then(|| {
(0..state.param().vec_len())
.map(|i| state.param().get_scalar(i))
.collect::<Vec<_>>()
});
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();
let stationarity = result
.as_ref()
.ok()
.filter(|r| {
(self.stationarity_tol.is_some()
|| self.relative_stationarity_tol.is_some())
&& r.report.termination.can_continue_as_inner(
crate::PartialResultPolicy::Consume,
)
&& strict_feasibility(problem.inner(), r.state.param())
== Some(true)
})
.map(|r| {
let initial = if self.centering_mu == Some(self.mu) {
self.stationarity_initial_norm
} else {
barrier_wrapper.gradient(state.param())?.norm_infinity()
};
let final_norm =
barrier_wrapper.gradient(r.state.param())?.norm_infinity();
Ok::<_, Self::Error>((initial, final_norm))
})
.transpose();
let total_counts = *barrier_wrapper.counts();
problem.counts_mut().add(&total_counts);
self.last_inner =
diagnostic_start.map(|start| BarrierInnerDiagnostics {
phase_one: false,
mu: self.mu,
gap: F::from_usize(problem.inner().linear_count()).unwrap()
* self.mu,
start,
returned: result.as_ref().ok().map(|r| {
(0..r.state.param().vec_len())
.map(|i| r.state.param().get_scalar(i))
.collect()
}),
report: result.as_ref().ok().map(|r| r.report.clone()),
counts: inner_counts,
hard_error: result.is_err(),
});
let result = result?;
if let Some((initial, final_norm)) = stationarity? {
self.stationarity_initial_norm = initial;
self.stationarity_norm = final_norm;
self.centering_mu = Some(self.mu);
}
if !result
.report
.termination
.can_continue_as_inner(crate::PartialResultPolicy::Consume)
{
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::from((
state,
result.report.into_outer_termination(
crate::PartialResultPolicy::Consume,
),
)));
}
let candidate = result.state.param();
if strict_feasibility(problem.inner(), candidate) != Some(true) {
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"The inner barrier solve returned a point outside the strict barrier domain.",
)),
)));
}
let cost = problem.cost(candidate)?;
if !cost.is_finite() {
self.phase = BarrierPhase::Failed;
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"The inner barrier solve returned a non-finite original objective.",
),
));
}
state.replace(candidate.clone(), cost);
self.accepted_iterate = true;
self.gap =
F::from_usize(problem.inner().linear_count()).unwrap() * self.mu;
if self.stationarity_satisfied() {
self.mu = self.mu / self.reduction;
}
Ok(crate::SolverStep::from((state, None)))
}
fn should_check_iterate_change(&self) -> bool {
self.accepted_iterate && self.stationarity_satisfied()
}
fn terminate(&self, _state: &PointState<V, F>) -> Option<Termination<F>> {
if self.phase == BarrierPhase::PhaseTwo
&& self.stationarity_satisfied()
&& self.tol.is_some_and(|tol| self.gap <= tol)
{
let mut measurements = vec![
crate::Measurement {
name: "gap".into(),
value: self.gap,
},
crate::Measurement {
name: "tolerance".into(),
value: self.tol.unwrap(),
},
];
if let Some(tolerance) = self.stationarity_tol {
measurements.extend([
crate::Measurement {
name: "stationarity_norm".into(),
value: self.stationarity_norm,
},
crate::Measurement {
name: "stationarity_tolerance".into(),
value: tolerance,
},
]);
}
if let Some(tolerance) = self.relative_stationarity_tol {
if self.stationarity_tol.is_none() {
measurements.push(crate::Measurement {
name: "stationarity_norm".into(),
value: self.stationarity_norm,
});
}
measurements.extend([
crate::Measurement {
name: "relative_stationarity_tolerance".into(),
value: tolerance,
},
crate::Measurement {
name: "initial_stationarity_norm".into(),
value: self.stationarity_initial_norm,
},
]);
}
Some(Termination::custom(
"barrier.duality_gap",
concat!(
"In Phase II, gap <= tolerance AND (the finite barrier-gradient infinity norm ",
"meets an enabled absolute stationarity bound OR its relative bound times ",
"the finite initial norm at this weight). The two stationarity tests combine ",
"with OR; disabling both bypasses their guard."
),
measurements,
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "mu0 must be > 0")]
fn rejects_nonpositive_mu0() {
let _ = BarrierMethod::with_inner_solver(()).mu0(0.0);
}
#[test]
#[should_panic(expected = "reduction must be > 1")]
fn rejects_reduction_not_greater_than_one() {
let _ = BarrierMethod::with_inner_solver(()).with_reduction(1.0);
}
#[test]
#[should_panic(expected = "phase_one_tol must be > 0")]
fn rejects_nonpositive_phase_one_tol() {
let _ = BarrierMethod::with_inner_solver(())
.with_absolute_phase_one_gap_tolerance(0.0);
}
#[test]
#[should_panic(expected = "inner_max_iter must be ≥ 1")]
fn rejects_zero_inner_max_iter() {
let _ = BarrierMethod::<_, f64>::with_inner_solver(())
.with_inner_max_iter(0);
}
}