use crate::{
alm::*,
constraints,
core::{panoc::PANOCOptimizer, ExitStatus, Optimizer, Problem, SolverStatus},
matrix_operations, SolverError,
};
const DEFAULT_MAX_OUTER_ITERATIONS: usize = 50;
const DEFAULT_MAX_INNER_ITERATIONS: usize = 5000;
const DEFAULT_EPSILON_TOLERANCE: f64 = 1e-6;
const DEFAULT_DELTA_TOLERANCE: f64 = 1e-4;
const DEFAULT_PENALTY_UPDATE_FACTOR: f64 = 5.0;
const DEFAULT_EPSILON_UPDATE_FACTOR: f64 = 0.1;
const DEFAULT_INFEAS_SUFFICIENT_DECREASE_FACTOR: f64 = 0.1;
const DEFAULT_INITIAL_TOLERANCE: f64 = 0.1;
const SMALL_EPSILON: f64 = std::f64::EPSILON;
pub struct AlmOptimizer<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
> where
MappingAlm: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
MappingPm: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
ParametricGradientType: Fn(&[f64], &[f64], &mut [f64]) -> Result<(), SolverError>,
ParametricCostType: Fn(&[f64], &[f64], &mut f64) -> Result<(), SolverError>,
ConstraintsType: constraints::Constraint,
AlmSetC: constraints::Constraint,
LagrangeSetY: constraints::Constraint,
{
alm_cache: &'life mut AlmCache,
alm_problem: AlmProblem<
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
>,
max_outer_iterations: usize,
max_inner_iterations: usize,
max_duration: Option<std::time::Duration>,
epsilon_tolerance: f64,
delta_tolerance: f64,
penalty_update_factor: f64,
epsilon_update_factor: f64,
sufficient_decrease_coeff: f64,
epsilon_inner_initial: f64,
}
impl<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
>
AlmOptimizer<
'life,
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
>
where
MappingAlm: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
MappingPm: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
ParametricGradientType: Fn(&[f64], &[f64], &mut [f64]) -> Result<(), SolverError>,
ParametricCostType: Fn(&[f64], &[f64], &mut f64) -> Result<(), SolverError>,
ConstraintsType: constraints::Constraint,
AlmSetC: constraints::Constraint,
LagrangeSetY: constraints::Constraint,
{
pub fn new(
alm_cache: &'life mut AlmCache,
alm_problem: AlmProblem<
MappingAlm,
MappingPm,
ParametricGradientType,
ParametricCostType,
ConstraintsType,
AlmSetC,
LagrangeSetY,
>,
) -> Self {
alm_cache
.panoc_cache
.set_akkt_tolerance(DEFAULT_INITIAL_TOLERANCE);
AlmOptimizer {
alm_cache,
alm_problem,
max_outer_iterations: DEFAULT_MAX_OUTER_ITERATIONS,
max_inner_iterations: DEFAULT_MAX_INNER_ITERATIONS,
max_duration: None,
epsilon_tolerance: DEFAULT_EPSILON_TOLERANCE,
delta_tolerance: DEFAULT_DELTA_TOLERANCE,
penalty_update_factor: DEFAULT_PENALTY_UPDATE_FACTOR,
epsilon_update_factor: DEFAULT_EPSILON_UPDATE_FACTOR,
sufficient_decrease_coeff: DEFAULT_INFEAS_SUFFICIENT_DECREASE_FACTOR,
epsilon_inner_initial: DEFAULT_INITIAL_TOLERANCE,
}
}
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
}
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
}
pub fn with_max_duration(mut self, max_duration: std::time::Duration) -> Self {
self.max_duration = Some(max_duration);
self
}
pub fn with_delta_tolerance(mut self, delta_tolerance: f64) -> Self {
assert!(delta_tolerance > 0.0, "delta_tolerance must be positive");
self.delta_tolerance = delta_tolerance;
self
}
pub fn with_epsilon_tolerance(mut self, epsilon_tolerance: f64) -> Self {
assert!(
epsilon_tolerance > 0.0,
"epsilon_tolerance must be positive"
);
self.epsilon_tolerance = epsilon_tolerance;
self
}
pub fn with_penalty_update_factor(mut self, penalty_update_factor: f64) -> Self {
assert!(
penalty_update_factor > 1.0 + SMALL_EPSILON,
"`penalty_update_factor` must be larger than 1.0 + f64::EPSILON"
);
self.penalty_update_factor = penalty_update_factor;
self
}
pub fn with_inner_tolerance_update_factor(
mut self,
inner_tolerance_update_factor: f64,
) -> Self {
assert!(
inner_tolerance_update_factor > SMALL_EPSILON
&& inner_tolerance_update_factor < 1.0 - SMALL_EPSILON,
"the tolerance update factor needs to be in (f64::EPSILON, 1)"
);
self.epsilon_update_factor = inner_tolerance_update_factor;
self
}
pub fn with_initial_inner_tolerance(mut self, initial_inner_tolerance: f64) -> 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
}
pub fn with_sufficient_decrease_coefficient(
mut self,
sufficient_decrease_coefficient: f64,
) -> Self {
assert!(
sufficient_decrease_coefficient < 1.0 - SMALL_EPSILON
&& sufficient_decrease_coefficient > SMALL_EPSILON,
"sufficient_decrease_coefficient must be in (f64::EPSILON, 1.0 - f64::EPSILON)"
);
self.sufficient_decrease_coeff = sufficient_decrease_coefficient;
self
}
pub fn with_initial_lagrange_multipliers(mut self, y_init: &[f64]) -> 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
}
pub fn with_initial_penalty(self, c0: f64) -> Self {
assert!(
c0 > SMALL_EPSILON,
"the initial penalty must be larger than f64::EPSILON"
);
if let Some(xi_in_cache) = &mut self.alm_cache.xi {
xi_in_cache[0] = c0;
}
self
}
fn compute_alm_infeasibility(&mut self) -> Result<(), SolverError> {
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: &[f64]) -> Result<(), SolverError> {
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: &[f64]) -> Result<(), SolverError> {
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) {
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..]);
}
}
}
fn solve_inner_problem(&mut self, u: &mut [f64]) -> Result<SolverStatus, SolverError> {
let alm_problem = &self.alm_problem;
let alm_cache = &mut self.alm_cache;
let xi_empty = Vec::new();
let xi = if let Some(xi_cached) = &alm_cache.xi {
&xi_cached
} else {
&xi_empty
};
let psi = |u: &[f64], psi_val: &mut f64| -> Result<(), SolverError> {
(alm_problem.parametric_cost)(u, &xi, psi_val)
};
let psi_grad = |u: &[f64], psi_grad: &mut [f64]| -> Result<(), SolverError> {
(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(std::time::Duration::from_secs(std::u64::MAX)),
);
inner_solver.solve(u)
}
fn is_exit_criterion_satisfied(&self) -> bool {
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 + SMALL_EPSILON
} else {
true
};
let criterion_2 =
problem.n2 == 0 || cache.f2_norm_plus <= self.delta_tolerance + SMALL_EPSILON;
let criterion_3 =
cache.panoc_cache.akkt_tolerance.unwrap() <= self.epsilon_tolerance + SMALL_EPSILON;
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
|| (problem.n1 > 0
&& cache.delta_y_norm_plus
<= self.sufficient_decrease_coeff * cache.delta_y_norm + SMALL_EPSILON)
|| (problem.n2 > 0
&& cache.f2_norm_plus
<= self.sufficient_decrease_coeff * cache.f2_norm + SMALL_EPSILON)
{
return true;
}
false
}
fn update_penalty_parameter(&mut self) {
let cache = &mut self.alm_cache;
if let Some(xi) = &mut cache.xi {
xi[0] *= self.penalty_update_factor;
}
}
fn update_inner_akkt_tolerance(&mut self) {
let cache = &mut self.alm_cache;
cache.panoc_cache.set_akkt_tolerance(f64::max(
cache.panoc_cache.akkt_tolerance.unwrap() * self.epsilon_update_factor,
self.epsilon_tolerance,
));
}
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 [f64]) -> Result<bool, SolverError> {
self.project_on_set_y();
self.solve_inner_problem(u).map(|status: SolverStatus| {
let inner_iters = status.iterations();
self.alm_cache.last_inner_problem_norm_fpr = status.norm_fpr();
self.alm_cache.inner_iteration_count += inner_iters;
})?;
self.update_lagrange_multipliers(u)?;
self.compute_pm_infeasibility(u)?;
self.compute_alm_infeasibility()?;
if self.is_exit_criterion_satisfied() {
return Ok(false);
} else if !self.is_penalty_stall_criterion() {
self.update_penalty_parameter();
}
self.update_inner_akkt_tolerance();
self.final_cache_update();
return Ok(true);
}
pub fn solve(&mut self, u: &mut [f64]) -> Result<AlmOptimizerStatus, SolverError> {
let mut num_outer_iterations = 0;
let tic = std::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);
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;
if !self.step(u)? {
break;
}
}
let c = if let Some(xi) = &self.alm_cache.xi {
xi[0]
} else {
0.0
};
Ok(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_lagrange_multipliers(&self.alm_cache.y_plus.as_ref().unwrap_or(&Vec::new()))
.with_last_problem_norm_fpr(self.alm_cache.last_inner_problem_norm_fpr)
.with_penalty(c))
}
}
#[cfg(test)]
mod tests {
use crate::{
alm::*,
core::{constraints::*, panoc::*, ExitStatus},
matrix_operations,
mocks::*,
SolverError,
};
fn make_dummy_alm_problem(
n1: usize,
n2: usize,
) -> AlmProblem<
impl Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
impl Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
impl Fn(&[f64], &[f64], &mut [f64]) -> Result<(), SolverError>,
impl Fn(&[f64], &[f64], &mut f64) -> Result<(), SolverError>,
impl Constraint,
impl Constraint,
impl Constraint,
> {
let psi = void_parameteric_cost;
let d_psi = void_parameteric_gradient;
let bounds = Ball2::new(None, 10.0);
let f1: Option<fn(&[f64], &mut [f64]) -> Result<(), SolverError>> = 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<fn(&[f64], &mut [f64]) -> Result<(), SolverError>> = 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] > std::f64::EPSILON);
}
let alm_optimizer = alm_optimizer.with_initial_penalty(7.0);
assert!(!alm_optimizer.alm_cache.xi.is_none());
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(&vec![2., 3., 4., 10.]);
alm_optimizer.project_on_set_y();
if let Some(xi_after_proj) = &alm_optimizer.alm_cache.xi {
println!("xi = {:#?}", xi_after_proj);
let y_projected_correct = [
0.352180362530250,
0.528270543795374,
0.704360725060499,
1.760901812651248,
];
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]| -> Result<(), SolverError> {
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.166572291400,
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(&vec![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(&vec![10., 20., 11., 100.]);
}
}
assert!(alm_optimizer.compute_alm_infeasibility().is_ok());
unit_test_utils::assert_nearly_equal(
92.2062904578641,
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]| -> Result<(), SolverError> {
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(&vec![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.163243585489, 2838.112880538070],
&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();
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();
}
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(&vec![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(), "exists right away");
let mut alm_optimizer = alm_optimizer
.with_initial_inner_tolerance(1e-3)
.with_epsilon_tolerance(1e-3);
assert!(!alm_optimizer.is_exit_criterion_satisfied());
alm_optimizer.alm_cache.delta_y_norm_plus = 1e-3;
assert!(!alm_optimizer.is_exit_criterion_satisfied());
alm_optimizer.alm_cache.f2_norm_plus = 1e-3;
assert!(alm_optimizer.is_exit_criterion_satisfied());
}
#[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 mut 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 = 200000.0;
alm_optimizer.alm_cache.f2_norm_plus = 20000.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(&vec![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",
);
}
}