use crate::numeric::cast;
use lbfgs::LbfgsPrecision;
use num::Float;
use std::iter::Sum;
fn default_sy_epsilon<T: Float>() -> T {
cast::<T>(1e-10)
}
fn default_cbfgs_epsilon<T: Float>() -> T {
cast::<T>(1e-8)
}
fn default_cbfgs_alpha<T: Float>() -> T {
T::one()
}
#[derive(Debug)]
pub struct PANOCCache<T = f64>
where
T: Float + LbfgsPrecision + Sum<T>,
{
pub(crate) lbfgs: lbfgs::Lbfgs<T>,
pub(crate) gradient_u: Vec<T>,
pub(crate) gradient_u_previous: Option<Vec<T>>,
pub(crate) u_half_step: Vec<T>,
pub(crate) best_u_half_step: Vec<T>,
pub(crate) gradient_step: Vec<T>,
pub(crate) direction_lbfgs: Vec<T>,
pub(crate) u_plus: Vec<T>,
pub(crate) rhs_ls: T,
pub(crate) lhs_ls: T,
pub(crate) gamma_fpr: Vec<T>,
pub(crate) gamma: T,
pub(crate) tolerance: T,
pub(crate) norm_gamma_fpr: T,
pub(crate) best_norm_gamma_fpr: T,
pub(crate) gradient_u_norm_sq: T,
pub(crate) gradient_step_u_half_step_diff_norm_sq: T,
pub(crate) tau: T,
pub(crate) lipschitz_constant: T,
pub(crate) sigma: T,
pub(crate) cost_value: T,
pub(crate) iteration: usize,
pub(crate) akkt_tolerance: Option<T>,
}
impl<T> PANOCCache<T>
where
T: Float + LbfgsPrecision + Sum<T>,
{
pub fn new(problem_size: usize, tolerance: T, lbfgs_memory_size: usize) -> PANOCCache<T> {
assert!(tolerance > T::zero(), "tolerance must be positive");
PANOCCache {
gradient_u: vec![T::zero(); problem_size],
gradient_u_previous: None,
u_half_step: vec![T::zero(); problem_size],
best_u_half_step: vec![T::zero(); problem_size],
gamma_fpr: vec![T::zero(); problem_size],
direction_lbfgs: vec![T::zero(); problem_size],
gradient_step: vec![T::zero(); problem_size],
u_plus: vec![T::zero(); problem_size],
gamma: T::zero(),
tolerance,
norm_gamma_fpr: T::infinity(),
best_norm_gamma_fpr: T::infinity(),
gradient_u_norm_sq: T::zero(),
gradient_step_u_half_step_diff_norm_sq: T::zero(),
lbfgs: lbfgs::Lbfgs::new(problem_size, lbfgs_memory_size)
.with_cbfgs_alpha(default_cbfgs_alpha())
.with_cbfgs_epsilon(default_cbfgs_epsilon())
.with_sy_epsilon(default_sy_epsilon()),
lhs_ls: T::zero(),
rhs_ls: T::zero(),
tau: T::one(),
lipschitz_constant: T::zero(),
sigma: T::zero(),
cost_value: T::zero(),
iteration: 0,
akkt_tolerance: None,
}
}
pub fn set_akkt_tolerance(&mut self, akkt_tolerance: T) {
assert!(
akkt_tolerance > T::zero(),
"akkt_tolerance must be positive"
);
self.akkt_tolerance = Some(akkt_tolerance);
self.gradient_u_previous = Some(vec![T::zero(); self.gradient_step.len()]);
}
pub fn cache_previous_gradient(&mut self) {
if self.iteration >= 1 {
if let Some(df_previous) = &mut self.gradient_u_previous {
df_previous.copy_from_slice(&self.gradient_u);
}
}
}
fn akkt_residual(&self) -> T {
let mut r = T::zero();
if let Some(df_previous) = &self.gradient_u_previous {
r = self
.gamma_fpr
.iter()
.zip(self.gradient_u.iter())
.zip(df_previous.iter())
.fold(T::zero(), |sum, ((&gamma_fpr_i, &df_i), &dfp_i)| {
sum + (gamma_fpr_i + self.gamma * (df_i - dfp_i)).powi(2)
})
.sqrt();
}
r
}
fn fpr_exit_condition(&self) -> bool {
self.norm_gamma_fpr < self.tolerance
}
fn akkt_exit_condition(&self) -> bool {
let mut exit_condition = true;
if let Some(akkt_tol) = self.akkt_tolerance {
let res = self.akkt_residual();
exit_condition = res < akkt_tol;
}
exit_condition
}
pub fn exit_condition(&self) -> bool {
self.fpr_exit_condition() && self.akkt_exit_condition()
}
pub fn reset(&mut self) {
self.lbfgs.reset();
self.best_u_half_step.fill(T::zero());
self.best_norm_gamma_fpr = T::infinity();
self.norm_gamma_fpr = T::infinity();
self.gradient_u_norm_sq = T::zero();
self.gradient_step_u_half_step_diff_norm_sq = T::zero();
self.lhs_ls = T::zero();
self.rhs_ls = T::zero();
self.tau = T::one();
self.lipschitz_constant = T::zero();
self.sigma = T::zero();
self.cost_value = T::zero();
self.iteration = 0;
self.gamma = T::zero();
}
pub(crate) fn cache_best_half_step(&mut self) {
if self.norm_gamma_fpr < self.best_norm_gamma_fpr {
self.best_norm_gamma_fpr = self.norm_gamma_fpr;
self.best_u_half_step.copy_from_slice(&self.u_half_step);
}
}
#[must_use]
pub fn with_cbfgs_parameters(mut self, alpha: T, epsilon: T, sy_epsilon: T) -> Self {
self.lbfgs = self
.lbfgs
.with_cbfgs_alpha(alpha)
.with_cbfgs_epsilon(epsilon)
.with_sy_epsilon(sy_epsilon);
self
}
}
#[cfg(test)]
mod tests {
use super::PANOCCache;
#[test]
fn t_cache_best_half_step() {
let mut cache = PANOCCache::new(2, 1e-6, 3);
cache.u_half_step.copy_from_slice(&[1.0, 2.0]);
cache.norm_gamma_fpr = 3.0;
cache.cache_best_half_step();
assert_eq!(3.0, cache.best_norm_gamma_fpr);
assert_eq!(&[1.0, 2.0], &cache.best_u_half_step[..]);
cache.u_half_step.copy_from_slice(&[10.0, 20.0]);
cache.norm_gamma_fpr = 5.0;
cache.cache_best_half_step();
assert_eq!(3.0, cache.best_norm_gamma_fpr);
assert_eq!(&[1.0, 2.0], &cache.best_u_half_step[..]);
cache.u_half_step.copy_from_slice(&[-1.0, -2.0]);
cache.norm_gamma_fpr = 2.0;
cache.cache_best_half_step();
assert_eq!(2.0, cache.best_norm_gamma_fpr);
assert_eq!(&[-1.0, -2.0], &cache.best_u_half_step[..]);
}
#[test]
fn t_cache_best_half_step_f32() {
let mut cache = PANOCCache::<f32>::new(2, 1e-6_f32, 3);
cache.u_half_step.copy_from_slice(&[1.0_f32, 2.0]);
cache.norm_gamma_fpr = 3.0_f32;
cache.cache_best_half_step();
assert_eq!(3.0_f32, cache.best_norm_gamma_fpr);
assert_eq!(&[1.0_f32, 2.0], &cache.best_u_half_step[..]);
cache.reset();
assert!(cache.best_norm_gamma_fpr.is_infinite());
assert!(cache.norm_gamma_fpr.is_infinite());
assert_eq!(0.0_f32, cache.gamma);
assert_eq!(1.0_f32, cache.tau);
}
}