use crate::ConstraintSolveError;
use crate::core::augmented_lagrangian::AugmentedLagrangian;
use crate::core::constraint::evaluation::{LinearEqualityGradient, invalid};
use crate::core::executor::run_loop_with_control;
use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{
Dot, NormInfinity, NormSquared, Scalar, ScaleInPlace, ScaledAdd,
};
use crate::core::problem::{CostFunction, Gradient, Problem};
use crate::core::solver::Solver;
use crate::core::state::{CountsMirror, SelectedState, State};
use crate::core::termination::Termination;
use crate::{ViolationMeasure, ViolationReport};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct AugmentedLagrangianInnerDiagnostics<V, F: Scalar = f64> {
pub rho: F,
pub lambda: Vec<F>,
pub start: V,
pub returned: Option<V>,
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 AugmentedLagrangianMethod<So, V, F: Scalar = f64> {
inner_solver: So,
inner_max_iter: u64,
rho0: F,
rho: F,
rho_increase: F,
feasibility_decrease: F,
tol: Option<F>,
stationarity_tol: Option<F>,
relative_stationarity_tol: Option<F>,
stationarity_initial_norm: F,
stationarity_norm: F,
lambda: Option<Vec<F>>,
c_norm: F,
c_norm_prev: F,
#[cfg_attr(feature = "serde", serde(default))]
inner_diagnostics: bool,
last_inner: Option<AugmentedLagrangianInnerDiagnostics<V, F>>,
}
impl<So, V, F: Scalar> AugmentedLagrangianMethod<So, V, F> {
pub fn with_inner_solver(inner_solver: So) -> Self {
Self {
inner_solver,
inner_max_iter: 50,
rho0: F::from_f64(10.0).unwrap(),
rho: F::from_f64(10.0).unwrap(),
rho_increase: F::from_f64(10.0).unwrap(),
feasibility_decrease: F::from_f64(0.25).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(),
lambda: None,
c_norm: F::infinity(),
c_norm_prev: F::infinity(),
inner_diagnostics: false,
last_inner: None,
}
}
}
impl<So, V, F: Scalar> AugmentedLagrangianMethod<So, V, F> {
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<&AugmentedLagrangianInnerDiagnostics<V, F>> {
self.last_inner.as_ref()
}
pub fn rho0(mut self, rho0: F) -> Self {
assert!(rho0 > F::zero(), "rho0 must be > 0");
self.rho0 = rho0;
self
}
pub fn with_rho_increase(mut self, rho_increase: F) -> Self {
assert!(rho_increase > F::one(), "rho_increase must be > 1");
self.rho_increase = rho_increase;
self
}
pub fn with_feasibility_decrease(
mut self,
feasibility_decrease: F,
) -> Self {
assert!(
feasibility_decrease > F::zero() && feasibility_decrease < F::one(),
"feasibility_decrease must be in (0, 1)"
);
self.feasibility_decrease = feasibility_decrease;
self
}
pub fn with_absolute_feasibility_tolerance(
mut self,
tol: impl Into<Option<F>>,
) -> Self {
self.tol = crate::core::convergence::optional_tolerance(tol);
self
}
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_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: Clone, F: Scalar> AugmentedLagrangianMethod<So, V, 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,
)
}
fn select_incumbent(&self, state: &mut SelectedState<V, F>) {
let (_, cost, violation) =
state.current().expect("evaluated outer point");
let violation = violation.value;
if cost.is_nan() || cost == F::infinity() || !violation.is_finite() {
return;
}
let threshold = self.tol.unwrap_or(F::zero());
let feasible = violation <= threshold;
let improved =
state.best().is_none_or(|(_, best_cost, best_violation)| {
let best_violation = best_violation.value;
let best_feasible = best_violation <= threshold;
if feasible != best_feasible {
feasible
} else if feasible {
cost < best_cost
} else {
violation < best_violation
|| (violation == best_violation && cost < best_cost)
}
});
if improved {
state.select_current();
}
}
}
impl<So, V, F> InitialState<V> for AugmentedLagrangianMethod<So, V, F>
where
F: Scalar,
V: Clone,
{
type State = SelectedState<V, F>;
fn seed(&self, x: &V) -> Self::State {
SelectedState::new(x.clone())
}
}
impl<P, V, So, F> Solver<P, SelectedState<V, F>>
for AugmentedLagrangianMethod<So, V, F>
where
F: Scalar,
P: LinearEqualityGradient<V, F> + Gradient<Gradient = V>,
V: crate::VectorLen,
V: ScaledAdd<F>
+ Dot<F>
+ NormSquared<F>
+ NormInfinity<F>
+ ScaleInPlace<F>
+ Clone,
So: WarmStart<V>
+ for<'a> Solver<
AugmentedLagrangian<'a, P, Vec<F>, 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: SelectedState<V, F>,
) -> Result<SelectedState<V, F>, Self::Error> {
state.reset();
self.rho = self.rho0;
self.c_norm = F::infinity();
self.c_norm_prev = F::infinity();
self.stationarity_norm = F::infinity();
self.stationarity_initial_norm = F::infinity();
self.last_inner = None;
problem
.inner()
.validate_linear(state.param().vec_len())
.map_err(invalid)?;
let lambda = vec![F::zero(); problem.inner().linear_count()];
self.lambda = Some(lambda);
let cost = problem.cost(state.param())?;
let residual = problem
.inner()
.linear_residual(state.param())
.map_err(invalid)?;
let violation = residual.norm_squared().sqrt();
state.replace(
state.param().clone(),
cost,
ViolationReport::new(violation, ViolationMeasure::L2),
);
self.select_incumbent(&mut state);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: SelectedState<V, F>,
) -> Result<crate::SolverStep<SelectedState<V, F>>, Self::Error> {
self.stationarity_norm = F::infinity();
self.stationarity_initial_norm = F::infinity();
let lambda = self.lambda.as_ref().expect("init populates lambda");
let mut al_wrapper = Problem::new(AugmentedLagrangian::new(
problem.inner(),
lambda,
self.rho,
));
let mut control = crate::core::run_control::RunControl::new()
.max_iter(self.inner_max_iter);
let diagnostic_start = self
.inner_diagnostics
.then(|| (state.param().clone(), lambda.clone()));
let inner_state = self.inner_solver.seed(state.param());
let result = run_loop_with_control(
&mut al_wrapper,
inner_state,
&mut self.inner_solver,
&mut control,
);
let inner_counts = *al_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,
)
})
.map(|r| {
let initial =
al_wrapper.gradient(state.param())?.norm_infinity();
let final_norm =
al_wrapper.gradient(r.state.param())?.norm_infinity();
Ok::<_, Self::Error>((initial, final_norm))
})
.transpose();
let total_counts = *al_wrapper.counts();
problem.counts_mut().add(&total_counts);
self.last_inner = diagnostic_start.map(|(start, lambda)| {
AugmentedLagrangianInnerDiagnostics {
rho: self.rho,
lambda,
start,
returned: result.as_ref().ok().map(|r| r.state.param().clone()),
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;
}
if !result
.report
.termination
.can_continue_as_inner(crate::PartialResultPolicy::Consume)
{
return Ok(crate::SolverStep::from((
state,
result.report.into_outer_termination(
crate::PartialResultPolicy::Consume,
),
)));
}
let candidate = result.state.param();
let cost = problem.cost(candidate)?;
let c = problem
.inner()
.linear_residual(candidate)
.map_err(invalid)?;
let c_norm = c.norm_squared().sqrt();
if !cost.is_finite() || !c_norm.is_finite() {
return Ok(crate::SolverStep::stopped(
state,
Termination::numerical_failure(
"The augmented-Lagrangian solve returned a non-finite original objective or equality residual.",
),
));
}
self.c_norm_prev = self.c_norm;
self.c_norm = c_norm;
state.replace(
candidate.clone(),
cost,
ViolationReport::new(self.c_norm, ViolationMeasure::L2),
);
self.select_incumbent(&mut state);
if self.c_norm <= self.feasibility_decrease * self.c_norm_prev {
let lambda = self.lambda.as_mut().expect("init populates lambda");
lambda.scaled_add(self.rho, &c);
} else {
self.rho = self.rho * self.rho_increase;
}
Ok(crate::SolverStep::from((state, None)))
}
fn should_check_iterate_change(&self) -> bool {
self.stationarity_satisfied()
&& self.c_norm.is_finite()
&& self.c_norm <= self.tol.unwrap_or(F::zero())
}
fn terminate(
&self,
_state: &SelectedState<V, F>,
) -> Option<Termination<F>> {
if self.stationarity_satisfied()
&& self.tol.is_some_and(|tol| self.c_norm <= tol)
{
let mut measurements = vec![
crate::Measurement {
name: "residual_norm".into(),
value: self.c_norm,
},
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(
"augmented_lagrangian.feasibility",
concat!(
"residual_norm <= tolerance AND (the finite surrogate-gradient infinity norm ",
"meets an enabled absolute stationarity bound OR its relative bound times ",
"the finite initial norm). The two stationarity tests combine with OR; ",
"disabling both bypasses their guard."
),
measurements,
))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
type Builder = AugmentedLagrangianMethod<(), Vec<f64>>;
#[test]
#[should_panic(expected = "rho0 must be > 0")]
fn rejects_nonpositive_rho0() {
let _ = Builder::with_inner_solver(()).rho0(0.0);
}
#[test]
#[should_panic(expected = "rho_increase must be > 1")]
fn rejects_rho_increase_not_greater_than_one() {
let _ = Builder::with_inner_solver(()).with_rho_increase(1.0);
}
#[test]
#[should_panic(expected = "feasibility_decrease must be in (0, 1)")]
fn rejects_feasibility_decrease_out_of_range() {
let _ = Builder::with_inner_solver(()).with_feasibility_decrease(1.0);
}
#[test]
#[should_panic(expected = "inner_max_iter must be ≥ 1")]
fn rejects_zero_inner_max_iter() {
let _ = Builder::with_inner_solver(()).with_inner_max_iter(0);
}
}