use crate::{
alm::*,
constraints,
core::{panoc::PANOCOptimizer, ExitStatus, Problem, SolverStatus},
matrix_operations,
numeric::cast,
FunctionCallResult, SolverError,
};
use lbfgs::LbfgsPrecision;
use num::Float;
use std::{iter::Sum, ops::AddAssign};
const DEFAULT_MAX_OUTER_ITERATIONS: usize = 50;
const DEFAULT_MAX_INNER_ITERATIONS: usize = 5000;
struct InnerProblemStatus {
outer_continue_iterating: bool,
inner_problem_exit_status: ExitStatus,
}
impl InnerProblemStatus {
fn new(outer_continue_iterating: bool, inner_problem_exit_status: ExitStatus) -> Self {
InnerProblemStatus {
outer_continue_iterating,
inner_problem_exit_status,
}
}
}
pub struct AlmOptimizer<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
T = f64,
> where
T: Float + LbfgsPrecision + Sum<T> + AddAssign,
MappingAlm: Fn(&[T], &mut [T]) -> FunctionCallResult,
MappingPm: Fn(&[T], &mut [T]) -> FunctionCallResult,
ParametricGradientType: Fn(&[T], &[T], &mut [T]) -> FunctionCallResult,
ParametricCostType: Fn(&[T], &[T], &mut T) -> FunctionCallResult,
ConstraintsType: constraints::Constraint<T>,
AlmSetC: constraints::Constraint<T>,
LagrangeSetY: constraints::Constraint<T>,
{
alm_cache: &'life mut AlmCache<T>,
alm_problem: AlmProblem<
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
T,
>,
max_outer_iterations: usize,
max_inner_iterations: usize,
max_duration: Option<std::time::Duration>,
epsilon_tolerance: T,
delta_tolerance: T,
penalty_update_factor: T,
epsilon_update_factor: T,
sufficient_decrease_coeff: T,
epsilon_inner_initial: T,
}
impl<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
T,
>
AlmOptimizer<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
T,
>
where
T: Float + LbfgsPrecision + Sum<T> + AddAssign,
MappingAlm: Fn(&[T], &mut [T]) -> FunctionCallResult,
MappingPm: Fn(&[T], &mut [T]) -> FunctionCallResult,
ParametricGradientType: Fn(&[T], &[T], &mut [T]) -> FunctionCallResult,
ParametricCostType: Fn(&[T], &[T], &mut T) -> FunctionCallResult,
ConstraintsType: constraints::Constraint<T>,
AlmSetC: constraints::Constraint<T>,
LagrangeSetY: constraints::Constraint<T>,
{
#[must_use]
pub fn new(
alm_cache: &'life mut AlmCache<T>,
alm_problem: AlmProblem<
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
T,
>,
) -> Self {
alm_cache.panoc_cache.set_akkt_tolerance(cast::<T>(0.1));
AlmOptimizer {
alm_cache,
alm_problem,
max_outer_iterations: DEFAULT_MAX_OUTER_ITERATIONS,
max_inner_iterations: DEFAULT_MAX_INNER_ITERATIONS,
max_duration: None,
epsilon_tolerance: cast::<T>(1e-6),
delta_tolerance: cast::<T>(1e-4),
penalty_update_factor: cast::<T>(5.0),
epsilon_update_factor: cast::<T>(0.1),
sufficient_decrease_coeff: cast::<T>(0.1),
epsilon_inner_initial: cast::<T>(0.1),
}
}
#[must_use]
pub fn with_max_outer_iterations(mut self, max_outer_iterations: usize) -> Self {
assert!(
max_outer_iterations > 0,
"max_outer_iterations must be positive"
);
self.max_outer_iterations = max_outer_iterations;
self
}
#[must_use]
pub fn with_max_inner_iterations(mut self, max_inner_iterations: usize) -> Self {
assert!(
max_inner_iterations > 0,
"max_inner_iterations must be positive"
);
self.max_inner_iterations = max_inner_iterations;
self
}
#[must_use]
pub fn with_max_duration(mut self, max_duration: std::time::Duration) -> Self {
self.max_duration = Some(max_duration);
self
}
#[must_use]
pub fn with_delta_tolerance(mut self, delta_tolerance: T) -> Self {
assert!(
delta_tolerance > T::zero(),
"delta_tolerance must be positive"
);
self.delta_tolerance = delta_tolerance;
self
}
#[must_use]
pub fn with_epsilon_tolerance(mut self, epsilon_tolerance: T) -> Self {
assert!(
epsilon_tolerance > T::zero(),
"epsilon_tolerance must be positive"
);
self.epsilon_tolerance = epsilon_tolerance;
self
}
#[must_use]
pub fn with_penalty_update_factor(mut self, penalty_update_factor: T) -> Self {
assert!(
penalty_update_factor > T::one() + T::epsilon(),
"`penalty_update_factor` must be larger than 1.0 + T::epsilon()"
);
self.penalty_update_factor = penalty_update_factor;
self
}
#[must_use]
pub fn with_inner_tolerance_update_factor(mut self, inner_tolerance_update_factor: T) -> Self {
assert!(
inner_tolerance_update_factor > T::epsilon()
&& inner_tolerance_update_factor < T::one() - T::epsilon(),
"the tolerance update factor needs to be in (T::epsilon(), 1)"
);
self.epsilon_update_factor = inner_tolerance_update_factor;
self
}
#[must_use]
pub fn with_initial_inner_tolerance(mut self, initial_inner_tolerance: T) -> Self {
assert!(
initial_inner_tolerance >= self.epsilon_tolerance,
"the initial tolerance should be no less than the target tolerance"
);
self.epsilon_inner_initial = initial_inner_tolerance;
self.alm_cache
.panoc_cache
.set_akkt_tolerance(initial_inner_tolerance);
self
}
#[must_use]
pub fn with_sufficient_decrease_coefficient(
mut self,
sufficient_decrease_coefficient: T,
) -> Self {
assert!(
sufficient_decrease_coefficient < T::one() - T::epsilon()
&& sufficient_decrease_coefficient > T::epsilon(),
"sufficient_decrease_coefficient must be in (T::epsilon(), 1.0 - T::epsilon())"
);
self.sufficient_decrease_coeff = sufficient_decrease_coefficient;
self
}
#[must_use]
pub fn with_initial_lagrange_multipliers(mut self, y_init: &[T]) -> Self {
let cache = &mut self.alm_cache;
assert!(
y_init.len() == self.alm_problem.n1,
"y_init has wrong length (not equal to n1)"
);
if let Some(xi_in_cache) = &mut cache.xi {
xi_in_cache[1..].copy_from_slice(y_init);
}
self
}
#[must_use]
pub fn with_initial_penalty(self, c0: T) -> Self {
assert!(
c0 > T::epsilon(),
"the initial penalty must be larger than T::epsilon()"
);
if let Some(xi_in_cache) = &mut self.alm_cache.xi {
xi_in_cache[0] = c0;
}
self
}
fn compute_alm_infeasibility(&mut self) -> FunctionCallResult {
let alm_cache = &mut self.alm_cache; if let (Some(y_plus), Some(xi)) = (&alm_cache.y_plus, &alm_cache.xi) {
let norm_diff_squared = matrix_operations::norm2_squared_diff(y_plus, &xi[1..]);
alm_cache.delta_y_norm_plus = norm_diff_squared.sqrt();
}
Ok(())
}
fn compute_pm_infeasibility(&mut self, u: &[T]) -> FunctionCallResult {
let problem = &self.alm_problem; let cache = &mut self.alm_cache;
if let (Some(f2), Some(w_pm_vec)) = (&problem.mapping_f2, &mut cache.w_pm.as_mut()) {
f2(u, w_pm_vec)?;
cache.f2_norm_plus = matrix_operations::norm2(w_pm_vec);
}
Ok(())
}
fn update_lagrange_multipliers(&mut self, u: &[T]) -> FunctionCallResult {
let problem = &self.alm_problem; let cache = &mut self.alm_cache;
if problem.n1 == 0 {
return Ok(()); }
if let (Some(f1), Some(w_alm_aux), Some(y_plus), Some(xi), Some(alm_set_c)) = (
&problem.mapping_f1,
&mut cache.w_alm_aux,
&mut cache.y_plus,
&mut cache.xi,
&problem.alm_set_c,
) {
(f1)(u, w_alm_aux)?;
let y = &xi[1..];
let c = xi[0];
y_plus
.iter_mut()
.zip(y.iter())
.zip(w_alm_aux.iter())
.for_each(|((y_plus_i, y_i), w_alm_aux_i)| *y_plus_i = *w_alm_aux_i + *y_i / c);
alm_set_c.project(y_plus)?;
y_plus
.iter_mut()
.zip(y.iter())
.zip(w_alm_aux.iter())
.for_each(|((y_plus_i, y_i), w_alm_aux_i)| {
*y_plus_i = *y_i + c * (*w_alm_aux_i - *y_plus_i)
});
}
Ok(())
}
fn project_on_set_y(&mut self) -> FunctionCallResult {
let problem = &self.alm_problem;
if let Some(y_set) = &problem.alm_set_y {
if let Some(xi_vec) = self.alm_cache.xi.as_mut() {
y_set.project(&mut xi_vec[1..])?;
}
}
Ok(())
}
fn solve_inner_problem(&mut self, u: &mut [T]) -> Result<SolverStatus<T>, SolverError> {
let alm_problem = &self.alm_problem; let alm_cache = &mut self.alm_cache;
let xi_empty = Vec::<T>::new();
let xi = if let Some(xi_cached) = &alm_cache.xi {
xi_cached
} else {
&xi_empty
};
let psi = |u: &[T], psi_val: &mut T| -> FunctionCallResult {
(alm_problem.parametric_cost)(u, xi, psi_val)
};
let psi_grad = |u: &[T], psi_grad: &mut [T]| -> FunctionCallResult {
(alm_problem.parametric_gradient)(u, xi, psi_grad)
};
let inner_problem = Problem::new(&self.alm_problem.constraints, psi_grad, psi);
let mut inner_solver = PANOCOptimizer::new(inner_problem, &mut alm_cache.panoc_cache)
.with_max_duration(
alm_cache
.available_time
.unwrap_or_else(|| std::time::Duration::from_secs(u64::MAX)),
)
.with_max_iter(self.max_inner_iterations);
inner_solver.solve(u)
}
fn is_exit_criterion_satisfied(&self) -> Result<bool, SolverError> {
let cache = &self.alm_cache;
let problem = &self.alm_problem;
let criterion_1 = problem.n1 == 0
|| if let Some(xi) = &cache.xi {
let c = xi[0];
cache.iteration > 0
&& cache.delta_y_norm_plus <= c * self.delta_tolerance + T::epsilon()
} else {
true
};
let criterion_2 =
problem.n2 == 0 || cache.f2_norm_plus <= self.delta_tolerance + T::epsilon();
let criterion_3 =
cache
.panoc_cache
.akkt_tolerance
.ok_or(SolverError::InvalidProblemState(
"missing inner AKKT tolerance while checking the exit criterion",
))?
<= self.epsilon_tolerance + T::epsilon();
Ok(criterion_1 && criterion_2 && criterion_3)
}
fn is_penalty_stall_criterion(&self) -> bool {
let cache = &self.alm_cache;
let problem = &self.alm_problem;
if cache.iteration == 0 {
return true;
}
let is_alm = problem.n1 > 0;
let is_pm = problem.n2 > 0;
let criterion_alm = cache.delta_y_norm_plus
<= self.sufficient_decrease_coeff * cache.delta_y_norm + T::epsilon();
let criterion_pm =
cache.f2_norm_plus <= self.sufficient_decrease_coeff * cache.f2_norm + T::epsilon();
if is_alm && !is_pm {
return criterion_alm;
} else if !is_alm && is_pm {
return criterion_pm;
} else if is_alm && is_pm {
return criterion_alm && criterion_pm;
}
false
}
fn update_penalty_parameter(&mut self) {
let cache = &mut self.alm_cache;
if let Some(xi) = &mut cache.xi {
xi[0] = xi[0] * self.penalty_update_factor;
}
}
fn update_inner_akkt_tolerance(&mut self) -> FunctionCallResult {
let cache = &mut self.alm_cache;
let akkt_tolerance =
cache
.panoc_cache
.akkt_tolerance
.ok_or(SolverError::InvalidProblemState(
"missing inner AKKT tolerance while updating it",
))?;
let next_tolerance = akkt_tolerance * self.epsilon_update_factor;
cache
.panoc_cache
.set_akkt_tolerance(if next_tolerance > self.epsilon_tolerance {
next_tolerance
} else {
self.epsilon_tolerance
});
Ok(())
}
fn final_cache_update(&mut self) {
let cache = &mut self.alm_cache;
cache.iteration += 1;
cache.delta_y_norm = cache.delta_y_norm_plus;
cache.f2_norm = cache.f2_norm_plus;
if let (Some(xi), Some(y_plus)) = (&mut cache.xi, &cache.y_plus) {
xi[1..].copy_from_slice(y_plus);
}
cache.panoc_cache.reset();
}
fn step(&mut self, u: &mut [T]) -> Result<InnerProblemStatus, SolverError> {
let mut inner_exit_status: ExitStatus = ExitStatus::Converged;
self.project_on_set_y()?;
self.solve_inner_problem(u).map(|status: SolverStatus<T>| {
let inner_iters = status.iterations();
self.alm_cache.last_inner_problem_norm_fpr = status.norm_fpr();
self.alm_cache.inner_iteration_count += inner_iters;
inner_exit_status = status.exit_status();
})?;
self.update_lagrange_multipliers(u)?;
self.compute_pm_infeasibility(u)?; self.compute_alm_infeasibility()?;
if self.is_exit_criterion_satisfied()? {
return Ok(InnerProblemStatus::new(false, inner_exit_status));
} else if !self.is_penalty_stall_criterion() {
self.update_penalty_parameter();
}
self.update_inner_akkt_tolerance()?;
self.final_cache_update();
Ok(InnerProblemStatus::new(true, inner_exit_status)) }
fn compute_cost_at_solution(&mut self, u: &mut [T]) -> Result<T, SolverError> {
let alm_problem = &self.alm_problem; let alm_cache = &mut self.alm_cache; let mut empty_vec = std::vec::Vec::new(); let xi: &mut std::vec::Vec<T> = alm_cache.xi.as_mut().unwrap_or(&mut empty_vec);
let mut __c = T::zero();
if !xi.is_empty() {
__c = xi[0];
xi[0] = T::zero();
}
let mut cost_value = T::zero();
(alm_problem.parametric_cost)(u, xi, &mut cost_value)?;
if !xi.is_empty() {
xi[0] = __c;
}
Ok(cost_value)
}
pub fn solve(&mut self, u: &mut [T]) -> Result<AlmOptimizerStatus<T>, SolverError> {
let mut num_outer_iterations = 0;
let tic = web_time::Instant::now();
let mut exit_status = ExitStatus::Converged;
self.alm_cache.reset(); self.alm_cache.available_time = self.max_duration;
self.alm_cache
.panoc_cache
.set_akkt_tolerance(self.epsilon_inner_initial);
let mut inner = InnerProblemStatus::new(false, ExitStatus::Converged);
for _outer_iters in 1..=self.max_outer_iterations {
if let Some(max_duration) = self.max_duration {
let available_time_left = max_duration.checked_sub(tic.elapsed());
self.alm_cache.available_time = available_time_left;
if available_time_left.is_none() {
exit_status = ExitStatus::NotConvergedOutOfTime;
break;
}
}
num_outer_iterations += 1;
inner = self.step(u)?;
if inner.inner_problem_exit_status == ExitStatus::NotConvergedOutOfTime {
exit_status = ExitStatus::NotConvergedOutOfTime;
break;
}
if !inner.outer_continue_iterating {
break;
}
}
if exit_status != ExitStatus::NotConvergedOutOfTime {
exit_status = inner.inner_problem_exit_status;
}
if num_outer_iterations == self.max_outer_iterations && inner.outer_continue_iterating {
exit_status = ExitStatus::NotConvergedIterations;
}
let c = if let Some(xi) = &self.alm_cache.xi {
xi[0]
} else {
T::zero()
};
let cost = self.compute_cost_at_solution(u)?;
let status = AlmOptimizerStatus::new(exit_status)
.with_solve_time(tic.elapsed())
.with_inner_iterations(self.alm_cache.inner_iteration_count)
.with_outer_iterations(num_outer_iterations)
.with_last_problem_norm_fpr(self.alm_cache.last_inner_problem_norm_fpr)
.with_delta_y_norm(self.alm_cache.delta_y_norm_plus)
.with_f2_norm(self.alm_cache.f2_norm_plus)
.with_penalty(c)
.with_cost(cost);
if self.alm_problem.n1 > 0 {
let status = status.with_lagrange_multipliers(self.alm_cache.y_plus.as_ref().ok_or(
SolverError::InvalidProblemState(
"missing Lagrange multipliers at the ALM solution",
),
)?);
Ok(status)
} else {
Ok(status)
}
}
}
#[cfg(test)]
mod tests {
use crate::{
alm::*,
core::{constraints::*, panoc::*, ExitStatus},
matrix_operations,
mocks::*,
FunctionCallResult,
};
type DummyParametricGradient = fn(&[f64], &[f64], &mut [f64]) -> FunctionCallResult;
type DummyParametricCost = fn(&[f64], &[f64], &mut f64) -> FunctionCallResult;
type DummyMapping = MappingType;
type DummyConstraint = Ball2<'static>;
type DummyAlmProblem = AlmProblem<
DummyMapping,
DummyMapping,
DummyParametricGradient,
DummyParametricCost,
DummyConstraint,
DummyConstraint,
DummyConstraint,
>;
fn make_dummy_alm_problem(n1: usize, n2: usize) -> DummyAlmProblem {
let psi: DummyParametricCost = void_parameteric_cost;
let d_psi: DummyParametricGradient = void_parameteric_gradient;
let bounds = Ball2::new(None, 10.0);
let f1: Option<MappingType> = if n1 == 0 {
NO_MAPPING
} else {
Some(void_mapping)
};
let set_c = if n1 > 0 {
Some(Ball2::new(None, 1.50))
} else {
None::<Ball2>
};
let set_y: Option<Ball2> = if n1 > 0 {
Some(Ball2::new(None, 2.0))
} else {
None::<Ball2>
};
let f2: Option<MappingType> = if n2 == 0 {
NO_MAPPING
} else {
Some(void_mapping)
};
AlmProblem::new(bounds, set_c, set_y, psi, d_psi, f1, f2, n1, n2)
}
#[test]
fn t_setter_methods() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 5, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem);
if let Some(xi) = &alm_optimizer.alm_cache.xi {
assert!(xi[0] > f64::EPSILON);
}
let alm_optimizer = alm_optimizer.with_initial_penalty(7.0);
assert!(alm_optimizer.alm_cache.xi.is_some());
if let Some(xi) = &alm_optimizer.alm_cache.xi {
unit_test_utils::assert_nearly_equal(
7.0,
xi[0],
1e-10,
1e-12,
"initial penalty parameter not set properly",
);
}
let y_init = vec![2.0, 3.0, 4.0, 5.0, 6.0];
let alm_optimizer = alm_optimizer.with_initial_lagrange_multipliers(&y_init);
if let Some(xi) = &alm_optimizer.alm_cache.xi {
unit_test_utils::assert_nearly_equal_array(
&y_init,
&xi[1..],
1e-10,
1e-12,
"initial Langrange multipliers not set properly",
);
}
let alm_optimizer = alm_optimizer.with_initial_inner_tolerance(5e-3);
unit_test_utils::assert_nearly_equal(
5e-3,
alm_optimizer.epsilon_inner_initial,
1e-10,
1e-12,
"initial tolerance not properly set",
);
if let Some(akkt_tolerance) = alm_optimizer.alm_cache.panoc_cache.akkt_tolerance {
unit_test_utils::assert_nearly_equal(
5e-3,
akkt_tolerance,
1e-10,
1e-12,
"initial tolerance not properly set",
);
} else {
panic!("PANOCCache has no (initial) AKKT-tolerance set");
}
}
#[test]
fn t_project_on_set_y() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 4, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_initial_penalty(25.0)
.with_initial_lagrange_multipliers(&[2., 3., 4., 10.]);
alm_optimizer.project_on_set_y().unwrap();
if let Some(xi_after_proj) = &alm_optimizer.alm_cache.xi {
println!("xi = {:#?}", xi_after_proj);
let y_projected_correct = [
0.352_180_362_530_250,
0.528_270_543_795_374,
0.704_360_725_060_499,
1.760_901_812_651_248,
];
unit_test_utils::assert_nearly_equal_array(
&xi_after_proj[1..],
&y_projected_correct,
1e-10,
1e-15,
"wrong projection on Y",
);
unit_test_utils::assert_nearly_equal(
25.0,
xi_after_proj[0],
1e-10,
1e-16,
"penalty parameter affected by projection step (on Y)",
);
} else {
panic!("no xi found after projection!");
}
}
#[test]
fn t_compute_pm_infeasibility() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 0, 2, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let psi = void_parameteric_cost;
let d_psi = void_parameteric_gradient;
let f2 = Some(|u: &[f64], res: &mut [f64]| -> FunctionCallResult {
res[0] = matrix_operations::sum(u);
res[1] = matrix_operations::norm2_squared(u);
Ok(())
});
let bounds = Ball2::new(None, 10.0);
let alm_problem =
AlmProblem::new(bounds, NO_SET, NO_SET, psi, d_psi, NO_MAPPING, f2, n1, n2);
let mut alm_optimizer =
AlmOptimizer::new(&mut alm_cache, alm_problem).with_initial_penalty(10.0);
let u_plus = vec![1.0, 5.0, -2.0, 9.0, -6.0];
assert!(alm_optimizer.compute_pm_infeasibility(&u_plus).is_ok());
let alm_cache = &alm_optimizer.alm_cache;
let f2_u_plus = &alm_cache.w_pm.as_ref().unwrap();
println!("F2(u_plus) = {:#?}", f2_u_plus);
unit_test_utils::assert_nearly_equal_array(
&[7., 147.],
f2_u_plus,
1e-10,
1e-12,
"F2(u) is wrong",
);
println!("||F2(u_plus)|| = {}", alm_cache.f2_norm_plus);
unit_test_utils::assert_nearly_equal(
alm_cache.f2_norm_plus,
147.166_572_291_400,
1e-12,
1e-12,
"||F2(u_plus)|| is wrong",
);
}
#[test]
fn t_compute_alm_infeasibility() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 4, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let psi = void_parameteric_cost;
let d_psi = void_parameteric_gradient;
let f1 = Some(void_mapping);
let set_c = Some(Ball2::new(None, 1.0));
let bounds = Ball2::new(None, 10.0);
let set_y = Some(Ball2::new(None, 2.0));
let alm_problem = AlmProblem::new(bounds, set_c, set_y, psi, d_psi, f1, NO_MAPPING, n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_initial_penalty(10.0)
.with_initial_lagrange_multipliers(&[2., 3., 4., 10.]);
{
let cache = &mut alm_optimizer.alm_cache;
if let Some(y_plus) = &mut cache.y_plus {
y_plus.copy_from_slice(&[10., 20., 11., 100.]);
}
}
assert!(alm_optimizer.compute_alm_infeasibility().is_ok());
unit_test_utils::assert_nearly_equal(
92.206_290_457_864_1,
alm_optimizer.alm_cache.delta_y_norm_plus,
1e-10,
1e-12,
"delta_y_plus is wrong",
);
}
#[test]
fn t_update_lagrange_multipliers() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 2, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let psi = void_parameteric_cost;
let d_psi = void_parameteric_gradient;
let f1 = Some(|u: &[f64], res: &mut [f64]| -> FunctionCallResult {
res[0] = matrix_operations::sum(u);
res[1] = matrix_operations::norm2_squared(u);
Ok(())
});
let set_c = Some(Ball2::new(None, 1.5));
let bounds = Ball2::new(None, 10.0);
let set_y = Some(Ball2::new(None, 2.0));
let alm_problem = AlmProblem::new(bounds, set_c, set_y, psi, d_psi, f1, NO_MAPPING, n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_initial_penalty(10.0)
.with_initial_lagrange_multipliers(&[2., 3.]);
let u = [3.0, 5.0, 7.0, 9.0, 11.];
assert!(alm_optimizer.update_lagrange_multipliers(&u).is_ok());
println!("xi = {:#?}", alm_optimizer.alm_cache.w_alm_aux);
unit_test_utils::assert_nearly_equal_array(
&[350.163_243_585_489, 2_838.112_880_538_07],
alm_optimizer
.alm_cache
.y_plus
.as_ref()
.expect("no y_plus found (it is None)"),
1e-12,
1e-12,
"y_plus is wrong",
);
}
#[test]
fn t_update_inner_akkt_tolerance() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 0, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_epsilon_tolerance(2e-5)
.with_initial_inner_tolerance(1e-1)
.with_inner_tolerance_update_factor(0.2);
alm_optimizer.update_inner_akkt_tolerance().unwrap();
unit_test_utils::assert_nearly_equal(
0.1,
alm_optimizer.epsilon_inner_initial,
1e-16,
1e-12,
"target tolerance altered by update_inner_akkt_tolerance",
);
unit_test_utils::assert_nearly_equal(
0.02,
alm_optimizer
.alm_cache
.panoc_cache
.akkt_tolerance
.expect("there should be a set AKKT tolerance"),
1e-12,
1e-12,
"panoc_cache tolerance is not properly updated",
);
for _i in 1..=5 {
alm_optimizer.update_inner_akkt_tolerance().unwrap();
}
unit_test_utils::assert_nearly_equal(
2e-5,
alm_optimizer
.alm_cache
.panoc_cache
.akkt_tolerance
.expect("there should be a set AKKT tolerance"),
1e-12,
1e-12,
"panoc_cache tolerance is not properly updated",
);
}
#[test]
fn t_update_penalty_parameter() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 0, 2, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_initial_penalty(5.0)
.with_penalty_update_factor(15.0);
if let Some(xi) = &alm_optimizer.alm_cache.xi {
unit_test_utils::assert_nearly_equal(xi[0], 5.0, 1e-16, 1e-12, "wrong initial penalty");
}
alm_optimizer.update_penalty_parameter();
if let Some(xi) = &alm_optimizer.alm_cache.xi {
unit_test_utils::assert_nearly_equal(
xi[0],
75.0,
1e-16,
1e-12,
"wrong updated penalty",
);
}
alm_optimizer.update_penalty_parameter();
if let Some(xi) = &alm_optimizer.alm_cache.xi {
unit_test_utils::assert_nearly_equal(
xi[0],
1125.0,
1e-16,
1e-12,
"wrong updated penalty",
);
}
}
#[test]
fn t_final_cache_update() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 2, 2, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem);
alm_optimizer.alm_cache.reset();
alm_optimizer.alm_cache.delta_y_norm_plus = 1.2345;
alm_optimizer.alm_cache.f2_norm_plus = 3.45678;
if let Some(xi) = &mut alm_optimizer.alm_cache.xi {
xi[1..].copy_from_slice(&[5.6, 7.8]);
}
assert_eq!(
0, alm_optimizer.alm_cache.iteration,
"initial iteration count should be 0"
);
alm_optimizer.final_cache_update();
assert_eq!(
1, alm_optimizer.alm_cache.iteration,
"iteration count not updated"
);
unit_test_utils::assert_nearly_equal(
3.45678,
alm_optimizer.alm_cache.f2_norm,
1e-16,
1e-12,
"f2_norm was not updated after final_cache_update()",
);
unit_test_utils::assert_nearly_equal(
1.2345,
alm_optimizer.alm_cache.delta_y_norm,
1e-16,
1e-12,
"delta_y_norm was not updated after final_cache_update()",
);
assert_eq!(
0, alm_optimizer.alm_cache.panoc_cache.iteration,
"panoc_cache iteration count not updated"
);
println!("cache now = {:#?}", &alm_optimizer.alm_cache);
}
#[test]
fn t_is_exit_criterion_satisfied() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-6, 5, 2, 2, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
alm_cache.iteration = 2;
let alm_problem = make_dummy_alm_problem(n1, n2);
let alm_optimizer =
AlmOptimizer::new(&mut alm_cache, alm_problem).with_delta_tolerance(1e-3);
assert!(
!alm_optimizer.is_exit_criterion_satisfied().unwrap(),
"exists right away"
);
let alm_optimizer = alm_optimizer
.with_initial_inner_tolerance(1e-3)
.with_epsilon_tolerance(1e-3);
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
alm_optimizer.alm_cache.delta_y_norm_plus = 1e-3;
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
alm_optimizer.alm_cache.f2_norm_plus = 1e-3;
assert!(alm_optimizer.is_exit_criterion_satisfied().unwrap());
}
#[test]
fn t_is_penalty_stall_criterion() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 1, 1, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_sufficient_decrease_coefficient(0.1);
assert!(alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.iteration = 4;
assert!(!alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.delta_y_norm = 100.0;
alm_optimizer.alm_cache.delta_y_norm_plus = 10.0;
alm_optimizer.alm_cache.f2_norm = 200_000.0;
alm_optimizer.alm_cache.f2_norm_plus = 20_000.0;
assert!(alm_optimizer.is_penalty_stall_criterion());
println!("cache = {:#?}", alm_optimizer.alm_cache);
}
#[test]
fn t_is_penalty_stall_criterion_alm() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 1, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_sufficient_decrease_coefficient(0.1);
assert!(alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.iteration = 4;
assert!(!alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.delta_y_norm = 100.0;
alm_optimizer.alm_cache.delta_y_norm_plus = 10.0;
alm_optimizer.alm_cache.f2_norm = 0.0;
alm_optimizer.alm_cache.f2_norm_plus = 0.0;
assert!(alm_optimizer.is_penalty_stall_criterion());
println!("cache = {:#?}", alm_optimizer.alm_cache);
}
#[test]
fn t_is_penalty_stall_criterion_pm() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (1e-8, 10, 0, 1, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let alm_problem = make_dummy_alm_problem(n1, n2);
let alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_sufficient_decrease_coefficient(0.1);
assert!(alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.iteration = 4;
assert!(!alm_optimizer.is_penalty_stall_criterion());
alm_optimizer.alm_cache.delta_y_norm = 0.0;
alm_optimizer.alm_cache.delta_y_norm_plus = 0.0;
alm_optimizer.alm_cache.f2_norm = 200_000.0;
alm_optimizer.alm_cache.f2_norm_plus = 20_000.0;
assert!(alm_optimizer.is_penalty_stall_criterion());
println!("cache = {:#?}", alm_optimizer.alm_cache);
}
#[test]
fn t_solve_inner_problem() {
let (tolerance, nx, n1, n2, lbfgs_mem) = (0.1, 5, 2, 0, 3);
let panoc_cache = PANOCCache::new(nx, tolerance, lbfgs_mem);
let mut alm_cache = AlmCache::new(panoc_cache, n1, n2);
let psi = psi_cost_dummy;
let d_psi = psi_gradient_dummy;
let f1 = Some(void_mapping);
let set_c = Some(Ball2::new(None, 1.5));
let xmin = vec![-5.0; nx];
let xmax = vec![0.0; nx];
let bounds = Rectangle::new(Some(&xmin), Some(&xmax));
let set_y = Some(Ball2::new(None, 2.0));
let alm_problem = AlmProblem::new(bounds, set_c, set_y, psi, d_psi, f1, NO_MAPPING, n1, n2);
let mut alm_optimizer = AlmOptimizer::new(&mut alm_cache, alm_problem)
.with_initial_lagrange_multipliers(&[5.0, 6.0])
.with_initial_penalty(1.0)
.with_epsilon_tolerance(1e-12)
.with_initial_inner_tolerance(1e-12);
let mut u = vec![0.0; nx];
let result = alm_optimizer.solve_inner_problem(&mut u);
println!("result = {:#?}", &result);
println!("u = {:#?}", &u);
assert!(result.is_ok());
let solver_status = result.unwrap();
assert!(solver_status.has_converged());
assert_eq!(ExitStatus::Converged, solver_status.exit_status());
unit_test_utils::assert_nearly_equal_array(
&u,
&[-5.0, -5.0, -1.0, -1.0, -1.0],
1e-10,
1e-10,
"inner problem solution is wrong",
);
}
}