use crate::ConstraintSolveError;
use crate::core::constraint::evaluation::{
LinearEqualityGradient, LinearEqualityProblem, invalid,
};
use crate::core::math::{NormSquared, Scalar, ScaledAdd, VectorLen};
use crate::core::problem::{CostFunction, Gradient};
pub struct AugmentedLagrangian<'a, P, V, F = f64> {
problem: &'a P,
lambda: &'a V,
rho: F,
}
impl<'a, P, V, F: Scalar> AugmentedLagrangian<'a, P, V, F> {
pub fn new(problem: &'a P, lambda: &'a V, rho: F) -> Self {
Self {
problem,
lambda,
rho,
}
}
pub fn rho(&self) -> F {
self.rho
}
}
impl<P, X, R, F> CostFunction for AugmentedLagrangian<'_, P, R, F>
where
F: Scalar,
P: CostFunction<Param = X, Output = F> + LinearEqualityProblem<X, F>,
X: VectorLen,
R: VectorLen + std::ops::Index<usize, Output = F>,
{
type Param = X;
type Output = F;
type Error = ConstraintSolveError<P::Error>;
fn cost(&self, x: &X) -> Result<F, Self::Error> {
let c = self.problem.linear_residual(x).map_err(invalid)?;
crate::core::constraint::model::dimension(
crate::ConstraintBlock::LinearEqualities,
crate::ConstraintAxis::Length,
c.len(),
self.lambda.vec_len(),
)
.map_err(invalid)?;
let multiplier = (0..c.len()).map(|i| self.lambda[i] * c[i]).sum::<F>();
Ok(self.problem.cost(x)?
+ multiplier
+ F::from_f64(0.5).unwrap() * self.rho * c.norm_squared())
}
}
impl<P, X, R, F> Gradient for AugmentedLagrangian<'_, P, R, F>
where
F: Scalar,
P: CostFunction<Param = X, Output = F>
+ LinearEqualityGradient<X, F>
+ Gradient<Gradient = X>,
X: Clone + VectorLen + ScaledAdd<F>,
R: VectorLen + std::ops::Index<usize, Output = F>,
{
type Gradient = X;
fn gradient(&self, x: &X) -> Result<X, <Self as CostFunction>::Error> {
let c = self.problem.linear_residual(x).map_err(invalid)?;
crate::core::constraint::model::dimension(
crate::ConstraintBlock::LinearEqualities,
crate::ConstraintAxis::Length,
c.len(),
self.lambda.vec_len(),
)
.map_err(invalid)?;
let weights: Vec<F> = (0..c.len())
.map(|i| self.lambda[i] + self.rho * c[i])
.collect();
let mut contribution = x.clone();
self.problem
.linear_transpose(&weights, &mut contribution)
.map_err(invalid)?;
let mut g = self.problem.gradient(x)?;
g.scaled_add(F::one(), &contribution);
Ok(g)
}
}
#[cfg(all(test, feature = "nalgebra_all"))]
mod tests {
use super::*;
use nalgebra::{DMatrix, DVector};
struct Probe {
a: DMatrix<f64>,
b: DVector<f64>,
}
impl Probe {
fn new() -> Self {
Self {
a: DMatrix::from_row_slice(1, 2, &[1.0, 1.0]),
b: DVector::from_vec(vec![2.0]),
}
}
}
impl crate::ProblemConstraints for Probe {
type Constraints = crate::Constraints<
crate::NoBounds,
crate::LinearBlocks<
crate::LinearBlock<DMatrix<f64>, DVector<f64>>,
crate::NoLinearBlock,
>,
>;
fn constraint_set(
&self,
) -> <Self::Constraints as crate::ConstraintDescription>::Borrowed<'_>
{
crate::Constraints::new().with_linear_equalities(
crate::LinearBlock::new(&self.a, &self.b),
)
}
}
impl CostFunction for Probe {
type Param = DVector<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(
&self,
x: &DVector<f64>,
) -> Result<f64, std::convert::Infallible> {
Ok(0.5 * x.dot(x))
}
}
impl Gradient for Probe {
type Gradient = DVector<f64>;
fn gradient(
&self,
x: &DVector<f64>,
) -> Result<DVector<f64>, std::convert::Infallible> {
Ok(x.clone())
}
}
#[test]
fn cost_matches_closed_form() {
let p = Probe::new();
let lambda = DVector::from_vec(vec![0.5]);
let rho = 2.0;
let al = AugmentedLagrangian::new(&p, &lambda, rho);
let x = DVector::from_vec(vec![0.3, -0.4]);
let c = (0.3 - 0.4) - 2.0;
let expected = 0.125 + 0.5 * c + 0.5 * rho * c * c;
assert!((al.cost(&x).unwrap() - expected).abs() < 1e-12);
}
#[test]
fn cost_is_finite_at_infeasible_points() {
let p = Probe::new();
let lambda = DVector::from_vec(vec![0.0]);
let al = AugmentedLagrangian::new(&p, &lambda, 1.0);
let x = DVector::from_vec(vec![100.0, 100.0]);
assert!(al.cost(&x).unwrap().is_finite());
}
#[test]
fn gradient_agrees_with_finite_differences() {
let p = Probe::new();
let lambda = DVector::from_vec(vec![0.7]);
let al = AugmentedLagrangian::new(&p, &lambda, 1.3);
let x = DVector::from_vec(vec![0.3, -0.4]);
let analytic = al.gradient(&x).unwrap();
let h = 1e-6;
for j in 0..2 {
let mut xp = x.clone();
let mut xm = x.clone();
xp[j] += h;
xm[j] -= h;
let fd =
(al.cost(&xp).unwrap() - al.cost(&xm).unwrap()) / (2.0 * h);
assert!(
(analytic[j] - fd).abs() < 1e-5,
"component {j}: analytic {} vs fd {}",
analytic[j],
fd
);
}
}
}
impl<P, V, F> crate::ProblemConstraints for AugmentedLagrangian<'_, P, V, F> {
type Constraints = crate::NoConstraints;
fn constraint_set(&self) -> crate::NoConstraints {
crate::NoConstraints::EMPTY
}
}