#![deny(missing_docs)]
use crate::{numeric::cast, SolverError};
use num::Float;
fn default_delta<T: Float>() -> T {
cast::<T>(1e-6)
}
fn default_epsilon<T: Float>() -> T {
cast::<T>(1e-6)
}
fn norm2<T: Float>(a: &[T]) -> T {
a.iter().fold(T::zero(), |sum, &x| sum + x * x).sqrt()
}
pub struct LipschitzEstimator<'a, T, F>
where
T: Float,
F: Fn(&[T], &mut [T]) -> Result<(), SolverError>,
{
u_decision_var: &'a mut [T],
workspace: Vec<T>,
function_value_at_u: &'a mut [T],
function: &'a F,
epsilon_lip: T,
delta_lip: T,
}
impl<'a, T, F> LipschitzEstimator<'a, T, F>
where
T: Float,
F: Fn(&[T], &mut [T]) -> Result<(), SolverError>,
{
pub fn new(
u_: &'a mut [T],
f_: &'a F,
function_value_: &'a mut [T],
) -> LipschitzEstimator<'a, T, F> {
let n: usize = u_.len();
LipschitzEstimator {
u_decision_var: u_,
workspace: vec![T::zero(); n],
function_value_at_u: function_value_,
function: f_,
epsilon_lip: default_epsilon(),
delta_lip: default_delta(),
}
}
#[must_use]
pub fn with_delta(mut self, delta: T) -> Self {
assert!(delta > T::zero());
self.delta_lip = delta;
self
}
#[must_use]
pub fn with_epsilon(mut self, epsilon: T) -> Self {
assert!(epsilon > T::zero());
self.epsilon_lip = epsilon;
self
}
pub fn get_function_value(&self) -> &[T] {
self.function_value_at_u
}
pub fn estimate_local_lipschitz(&mut self) -> Result<T, SolverError> {
(self.function)(self.u_decision_var, self.function_value_at_u)?;
let epsilon_lip = self.epsilon_lip;
let delta_lip = self.delta_lip;
self.workspace
.iter_mut()
.zip(self.u_decision_var.iter())
.for_each(|(out, &s)| {
*out = if epsilon_lip * s > delta_lip {
epsilon_lip * s
} else {
delta_lip
}
});
let norm_h = norm2(&self.workspace);
self.u_decision_var
.iter_mut()
.zip(self.workspace.iter())
.for_each(|(out, a)| *out = *out + *a);
(self.function)(self.u_decision_var, &mut self.workspace)?;
self.workspace
.iter_mut()
.zip(self.function_value_at_u.iter())
.for_each(|(out, a)| *out = *out - *a);
let norm_workspace = norm2(&self.workspace);
Ok(norm_workspace / norm_h)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mocks;
#[test]
fn t_test_lip_delta_epsilon_0() {
let mut u: [f64; 3] = [1.0, 2.0, 3.0];
let mut function_value = [0.0; 3];
let f =
|u: &[f64], g: &mut [f64]| -> Result<(), SolverError> { mocks::lipschitz_mock(u, g) };
let mut lip_estimator = LipschitzEstimator::new(&mut u, &f, &mut function_value)
.with_delta(1e-4)
.with_epsilon(1e-4);
let lip = lip_estimator.estimate_local_lipschitz().unwrap();
unit_test_utils::assert_nearly_equal(
1.336_306_209_562_331,
lip,
1e-10,
1e-14,
"lipschitz constant",
);
println!("Lx = {}", lip);
}
#[test]
#[should_panic]
fn t_test_lip_delta_epsilon_panic1() {
let mut u: [f64; 3] = [1.0, 2.0, 3.0];
let mut function_value = [0.0; 3];
let _lip_estimator =
LipschitzEstimator::new(&mut u, &mocks::lipschitz_mock, &mut function_value)
.with_epsilon(0.0);
}
#[test]
#[should_panic]
fn t_test_lip_delta_epsilon_panic2() {
let mut u: [f64; 3] = [1.0, 2.0, 3.0];
let mut function_value = [0.0; 3];
let _lip_estimator =
LipschitzEstimator::new(&mut u, &mocks::lipschitz_mock, &mut function_value)
.with_epsilon(0.0);
}
#[test]
fn t_test_lip_estimator_mock() {
let mut u: [f64; 3] = [1.0, 2.0, 3.0];
let f =
|u: &[f64], g: &mut [f64]| -> Result<(), SolverError> { mocks::lipschitz_mock(u, g) };
let mut function_value = [0.0; 3];
let mut lip_estimator = LipschitzEstimator::new(&mut u, &f, &mut function_value);
let lip = lip_estimator.estimate_local_lipschitz().unwrap();
unit_test_utils::assert_nearly_equal(
1.336_306_209_416_582_3,
lip,
1e-8,
1e-14,
"lipschitz constant",
);
println!("L_mock = {}", lip);
}
#[test]
fn t_test_get_function_value() {
let u: [f64; 10] = [1.0, 2.0, 3.0, -5.0, 1.0, 10.0, 14.0, 17.0, 3.0, 5.0];
let mut function_value = [0.0; 10];
let mut u_copy = u;
let f =
|u: &[f64], g: &mut [f64]| -> Result<(), SolverError> { mocks::lipschitz_mock(u, g) };
let mut lip_estimator = LipschitzEstimator::new(&mut u_copy, &f, &mut function_value);
{
let computed_gradient = lip_estimator.get_function_value();
unit_test_utils::assert_nearly_equal_array(
&[0.0; 10],
computed_gradient,
1e-10,
1e-14,
"computed gradient",
);
computed_gradient.iter().for_each(|&s| {
unit_test_utils::assert_nearly_equal(0.0_f64, s, 1e-10, 1e-16, "gradient")
});
}
lip_estimator.estimate_local_lipschitz().unwrap();
let computed_gradient = lip_estimator.get_function_value();
let mut actual_gradient = [0.0; 10];
f(&u, &mut actual_gradient).unwrap();
unit_test_utils::assert_nearly_equal_array(
computed_gradient,
&actual_gradient,
1e-10,
1e-14,
"computed/actual gradient",
);
}
#[test]
fn t_test_lip_estimator_f32() {
let mut u = [1.0_f32, 2.0, 3.0];
let mut function_value = [0.0_f32; 3];
let f = |u: &[f32], g: &mut [f32]| -> Result<(), SolverError> {
g[0] = 3.0 * u[0];
g[1] = 2.0 * u[1];
g[2] = 4.5;
Ok(())
};
let mut lip_estimator = LipschitzEstimator::new(&mut u, &f, &mut function_value)
.with_delta(1e-4_f32)
.with_epsilon(1e-4_f32);
let lip = lip_estimator.estimate_local_lipschitz().unwrap();
let expected = 5.0_f32 / 14.0_f32.sqrt();
assert!((lip - expected).abs() < 1e-4);
}
}