use crate::{
constraints,
core::{panoc::PANOCCache, AlgorithmEngine, Problem},
matrix_operations, SolverError,
};
const GAMMA_L_COEFF: f64 = 0.95;
const DELTA_LIPSCHITZ: f64 = 1e-12;
const EPSILON_LIPSCHITZ: f64 = 1e-6;
const LIPSCHITZ_UPDATE_EPSILON: f64 = 1e-6;
const MAX_LIPSCHITZ_UPDATE_ITERATIONS: usize = 10;
const MAX_LIPSCHITZ_CONSTANT: f64 = 1e9;
const MAX_LINESEARCH_ITERATIONS: u32 = 10;
pub struct PANOCEngine<'a, GradientType, ConstraintType, CostType>
where
GradientType: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
CostType: Fn(&[f64], &mut f64) -> Result<(), SolverError>,
ConstraintType: constraints::Constraint,
{
problem: Problem<'a, GradientType, ConstraintType, CostType>,
pub(crate) cache: &'a mut PANOCCache,
}
impl<'a, GradientType, ConstraintType, CostType>
PANOCEngine<'a, GradientType, ConstraintType, CostType>
where
GradientType: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
CostType: Fn(&[f64], &mut f64) -> Result<(), SolverError>,
ConstraintType: constraints::Constraint,
{
pub fn new(
problem: Problem<'a, GradientType, ConstraintType, CostType>,
cache: &'a mut PANOCCache,
) -> PANOCEngine<'a, GradientType, ConstraintType, CostType> {
PANOCEngine { problem, cache }
}
fn estimate_loc_lip(&mut self, u: &mut [f64]) -> Result<(), SolverError> {
let mut lipest = crate::lipschitz_estimator::LipschitzEstimator::new(
u,
&self.problem.gradf,
&mut self.cache.gradient_u,
)
.with_delta(DELTA_LIPSCHITZ)
.with_epsilon(EPSILON_LIPSCHITZ);
self.cache.lipschitz_constant = lipest.estimate_local_lipschitz()?;
Ok(())
}
fn compute_fpr(&mut self, u_current: &[f64]) {
let cache = &mut self.cache;
cache
.gamma_fpr
.iter_mut()
.zip(u_current.iter())
.zip(cache.u_half_step.iter())
.for_each(|((fpr, u), uhalf)| *fpr = u - uhalf);
cache.norm_gamma_fpr = matrix_operations::norm2(&cache.gamma_fpr);
}
fn gradient_step(&mut self, u_current: &[f64]) {
let cache = &mut self.cache;
let gamma = cache.gamma;
cache
.gradient_step
.iter_mut()
.zip(u_current.iter())
.zip(cache.gradient_u.iter())
.for_each(|((grad_step, u), grad)| *grad_step = *u - gamma * *grad);
}
fn gradient_step_uplus(&mut self) {
let cache = &mut self.cache;
let gamma = cache.gamma;
cache
.gradient_step
.iter_mut()
.zip(cache.u_plus.iter())
.zip(cache.gradient_u.iter())
.for_each(|((grad_step, u), grad)| *grad_step = *u - gamma * *grad);
}
fn half_step(&mut self) {
let cache = &mut self.cache;
cache.u_half_step.copy_from_slice(&cache.gradient_step);
self.problem.constraints.project(&mut cache.u_half_step);
}
fn lbfgs_direction(&mut self, u_current: &[f64]) {
let cache = &mut self.cache;
cache.lbfgs.update_hessian(&cache.gamma_fpr, u_current);
if cache.iteration > 0 {
cache.direction_lbfgs.copy_from_slice(&cache.gamma_fpr);
cache.lbfgs.apply_hessian(&mut cache.direction_lbfgs);
}
}
fn lipschitz_check_rhs(&mut self) -> f64 {
let cache = &mut self.cache;
let gamma = cache.gamma;
let cost_value = cache.cost_value;
let inner_prod_grad_fpr =
matrix_operations::inner_product(&cache.gradient_u, &cache.gamma_fpr);
cost_value + LIPSCHITZ_UPDATE_EPSILON * cost_value.abs() - inner_prod_grad_fpr
+ (GAMMA_L_COEFF / (2.0 * gamma)) * (cache.norm_gamma_fpr.powi(2))
}
fn update_lipschitz_constant(&mut self, u_current: &[f64]) -> Result<(), SolverError> {
let mut cost_u_half_step = 0.0;
(self.problem.cost)(&self.cache.u_half_step, &mut cost_u_half_step)?;
(self.problem.cost)(u_current, &mut self.cache.cost_value)?;
let mut it_lipschitz_search = 0;
while cost_u_half_step > self.lipschitz_check_rhs()
&& it_lipschitz_search < MAX_LIPSCHITZ_UPDATE_ITERATIONS
&& self.cache.lipschitz_constant < MAX_LIPSCHITZ_CONSTANT
{
self.cache.lbfgs.reset();
self.cache.lipschitz_constant *= 2.;
self.cache.gamma /= 2.;
self.gradient_step(u_current); self.half_step();
(self.problem.cost)(&self.cache.u_half_step, &mut cost_u_half_step)?;
self.compute_fpr(u_current);
it_lipschitz_search += 1;
}
self.cache.sigma = (1.0 - GAMMA_L_COEFF) / (4.0 * self.cache.gamma);
Ok(())
}
fn compute_u_plus(&mut self, u: &[f64]) {
let cache = &mut self.cache;
let _gamma = cache.gamma;
let tau = cache.tau;
let temp_ = 1.0 - tau;
cache
.u_plus
.iter_mut()
.zip(u.iter())
.zip(cache.gamma_fpr.iter())
.zip(cache.direction_lbfgs.iter())
.for_each(|(((u_plus_i, &u_i), &fpr_i), &dir_i)| {
*u_plus_i = u_i - temp_ * fpr_i - tau * dir_i;
});
}
fn compute_rhs_ls(&mut self) {
let cache = &mut self.cache;
let dist_squared =
matrix_operations::norm2_squared_diff(&cache.gradient_step, &cache.u_half_step);
let fbe = cache.cost_value
- 0.5 * cache.gamma * matrix_operations::norm2_squared(&cache.gradient_u)
+ 0.5 * dist_squared / cache.gamma;
let sigma_fpr_sq = cache.sigma * cache.norm_gamma_fpr.powi(2);
cache.rhs_ls = fbe - sigma_fpr_sq;
}
fn line_search_condition(&mut self, u: &[f64]) -> Result<bool, SolverError> {
let gamma = self.cache.gamma;
self.compute_u_plus(&u);
(self.problem.cost)(&self.cache.u_plus, &mut self.cache.cost_value)?;
(self.problem.gradf)(&self.cache.u_plus, &mut self.cache.gradient_u)?;
self.gradient_step_uplus(); self.half_step();
let dist_squared = matrix_operations::norm2_squared_diff(
&self.cache.gradient_step,
&self.cache.u_half_step,
);
self.cache.lhs_ls = self.cache.cost_value
- 0.5 * gamma * matrix_operations::norm2_squared(&self.cache.gradient_u)
+ 0.5 * dist_squared / self.cache.gamma;
Ok(self.cache.lhs_ls > self.cache.rhs_ls)
}
fn update_no_linesearch(&mut self, u_current: &mut [f64]) -> Result<(), SolverError> {
u_current.copy_from_slice(&self.cache.u_half_step); (self.problem.cost)(u_current, &mut self.cache.cost_value)?; (self.problem.gradf)(u_current, &mut self.cache.gradient_u)?; self.gradient_step(u_current); self.half_step();
Ok(())
}
fn linesearch(&mut self, u_current: &mut [f64]) -> Result<(), SolverError> {
self.compute_rhs_ls(); self.cache.tau = 1.0; let mut num_ls_iters = 0;
while self.line_search_condition(u_current)? && num_ls_iters < MAX_LINESEARCH_ITERATIONS {
self.cache.tau /= 2.0;
num_ls_iters += 1;
}
if num_ls_iters == MAX_LINESEARCH_ITERATIONS {
self.cache.tau = 0.;
u_current.copy_from_slice(&self.cache.u_half_step);
}
u_current.copy_from_slice(&self.cache.u_plus);
Ok(())
}
}
impl<'a, GradientType, ConstraintType, CostType> AlgorithmEngine
for PANOCEngine<'a, GradientType, ConstraintType, CostType>
where
GradientType: Fn(&[f64], &mut [f64]) -> Result<(), SolverError>,
CostType: Fn(&[f64], &mut f64) -> Result<(), SolverError>,
ConstraintType: constraints::Constraint,
{
fn step(&mut self, u_current: &mut [f64]) -> Result<bool, SolverError> {
self.compute_fpr(u_current);
if self.cache.norm_gamma_fpr < self.cache.tolerance {
return Ok(false);
}
self.update_lipschitz_constant(u_current)?; self.lbfgs_direction(u_current);
if self.cache.iteration == 0 {
self.update_no_linesearch(u_current)?;
} else {
self.linesearch(u_current)?;
}
self.cache.iteration += 1;
Ok(true)
}
fn init(&mut self, u_current: &mut [f64]) -> Result<(), SolverError> {
self.cache.reset();
(self.problem.cost)(u_current, &mut self.cache.cost_value)?; self.estimate_loc_lip(u_current)?; self.cache.gamma = GAMMA_L_COEFF / self.cache.lipschitz_constant;
self.cache.sigma = (1.0 - GAMMA_L_COEFF) / (4.0 * self.cache.gamma);
self.gradient_step(u_current); self.half_step();
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::constraints;
use crate::core::panoc::panoc_engine::PANOCEngine;
use crate::core::panoc::*;
use crate::core::Problem;
use crate::mocks;
use std::num::NonZeroUsize;
#[test]
fn t_compute_fpr() {
let n = NonZeroUsize::new(2).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let box_constraints = constraints::NoConstraints::new();
let problem = Problem::new(&box_constraints, mocks::my_gradient, mocks::my_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
let mut u = [0.75, -1.4];
panoc_engine.cache.gamma = 0.0234;
panoc_engine.cache.u_half_step.copy_from_slice(&[0.5, 2.9]);
panoc_engine.compute_fpr(&mut u); unit_test_utils::assert_nearly_equal_array(
&[0.25, -4.3],
&panoc_engine.cache.gamma_fpr,
1e-8,
1e-10,
"fpr",
);
unit_test_utils::assert_nearly_equal(
4.307261310856354,
panoc_engine.cache.norm_gamma_fpr,
1e-8,
1e-10,
"norm_fpr",
);
}
#[test]
fn t_gradient_step() {
let n = NonZeroUsize::new(2).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let bounds = constraints::NoConstraints::new();
let problem = Problem::new(&bounds, mocks::void_gradient, mocks::void_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
let mut u = [-0.11, -1.35];
panoc_engine.cache.gamma = 0.545;
panoc_engine.cache.gradient_u.copy_from_slice(&[1.94, 2.6]);
panoc_engine.gradient_step(&mut u);
unit_test_utils::assert_nearly_equal_array(
&[-1.1673, -2.767],
&panoc_engine.cache.gradient_step,
1e-8,
1e-10,
"panoc_engine.cache.gradient_step",
);
}
#[test]
fn t_gradient_step_uplus() {
let n = NonZeroUsize::new(2).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let bounds = constraints::NoConstraints::new();
let problem = Problem::new(&bounds, mocks::void_gradient, mocks::void_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
panoc_engine.cache.gamma = 0.789;
panoc_engine
.cache
.gradient_u
.copy_from_slice(&[45.0, -22.40]);
panoc_engine.cache.u_plus.copy_from_slice(&[-4.90, 10.89]);
panoc_engine.gradient_step_uplus();
unit_test_utils::assert_nearly_equal_array(
&[-40.405, 28.5636],
&panoc_engine.cache.gradient_step,
1e-8,
1e-10,
"panoc_engine.cache.gradient_step",
);
}
#[test]
fn t_half_step() {
let n = NonZeroUsize::new(2).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let bounds = constraints::Ball2::new(None, 0.5);
let problem = Problem::new(&bounds, mocks::void_gradient, mocks::void_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
panoc_engine
.cache
.gradient_step
.copy_from_slice(&[40., 50.]);
panoc_engine.half_step();
unit_test_utils::assert_nearly_equal_array(
&[0.312347523777212, 0.390434404721515],
&panoc_engine.cache.u_half_step,
1e-8,
1e-10,
"panoc_engine.cache.u_half_step",
);
}
#[test]
fn t_lipschitz_update_rhs() {
let n = NonZeroUsize::new(3).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let bounds = constraints::NoConstraints::new();
let problem = Problem::new(&bounds, mocks::void_gradient, mocks::void_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
panoc_engine
.cache
.gradient_u
.copy_from_slice(&[-3.49, -2.35, -103.85]);
panoc_engine.cache.lipschitz_constant = 2001.305974987387;
panoc_engine.cache.gamma = 4.746900333448449e-4;
panoc_engine.cache.gamma_fpr.copy_from_slice(&[
-0.001656668216374,
-0.001115521578360,
-0.049296559962862,
]);
panoc_engine.cache.norm_gamma_fpr = 0.049337001957385;
panoc_engine.cache.cost_value = 5.21035;
let rhs = panoc_engine.lipschitz_check_rhs();
println!("rhs = {}", rhs);
println!("cache = {:#?}", panoc_engine.cache);
unit_test_utils::assert_nearly_equal(2.518233435388051, rhs, 1e-8, 1e-10, "lip rhs");
}
#[test]
fn t_compute_rhs_ls() {
let n = NonZeroUsize::new(2).unwrap();
let mem = NonZeroUsize::new(5).unwrap();
let bounds = constraints::Ball2::new(None, 0.5);
let problem = Problem::new(&bounds, mocks::void_gradient, mocks::void_cost);
let mut panoc_cache = PANOCCache::new(n, 1e-6, mem);
let mut panoc_engine = PANOCEngine::new(problem, &mut panoc_cache);
panoc_engine
.cache
.gradient_step
.copy_from_slice(&[-0.5, -0.4]);
panoc_engine
.cache
.u_half_step
.copy_from_slice(&[15.0, -14.8]);
panoc_engine.cache.cost_value = 24.0;
panoc_engine.cache.gamma = 2.34;
panoc_engine.cache.gradient_u.copy_from_slice(&[2.4, -9.7]);
panoc_engine.cache.sigma = 0.066;
panoc_engine.cache.norm_gamma_fpr = 2.5974;
panoc_engine.compute_rhs_ls();
println!("rhs = {}", panoc_engine.cache.rhs_ls);
unit_test_utils::assert_nearly_equal(
2.373394267002398,
panoc_engine.cache.rhs_ls,
1e-10,
1e-8,
"rhs_ls is wrong",
);
}
}