use nalgebra::DVector;
use crate::minimum::gradient::FunctionGradient;
use crate::minimum::parameters::MinimumParameters;
use crate::mn_fcn::MnFcn;
use crate::strategy::MnStrategy;
use crate::user_transformation::MnUserTransformation;
pub struct Numerical2PGradientCalculator {
strategy: MnStrategy,
}
impl Numerical2PGradientCalculator {
pub fn new(strategy: MnStrategy) -> Self {
Self { strategy }
}
pub fn compute(
&self,
fcn: &MnFcn,
params: &MinimumParameters,
trafo: &MnUserTransformation,
initial_gradient: &FunctionGradient,
) -> FunctionGradient {
let n = trafo.variable_parameters();
let eps2 = trafo.precision().eps2();
let fcnmin = params.fval();
let dfmin = 8.0 * eps2 * (fcnmin.abs() + fcn.up());
let vrysml = 8.0 * eps2 * eps2;
let x = params.vec();
let ncycles = self.strategy.grad_ncycles();
let step_tol = self.strategy.grad_step_tol();
let grad_tol = self.strategy.grad_tol();
let mut grad = DVector::zeros(n);
let mut g2 = DVector::zeros(n);
let mut gstep = DVector::zeros(n);
for i in 0..n {
let ext_idx = trafo.ext_of_int(i);
let xi = x[i];
let p = &trafo.parameters()[ext_idx];
let has_limits = p.has_limits() || p.has_lower_limit() || p.has_upper_limit();
let mut gstepi = initial_gradient.gstep()[i].max(vrysml);
let mut g2i = initial_gradient.g2()[i];
for _cycle in 0..ncycles {
let optstp = (dfmin / (g2i.abs() + eps2)).sqrt();
let mut step = optstp.max(0.1 * gstepi.abs());
if has_limits {
step = step.min(0.5);
}
let stpmax = 10.0 * gstepi.abs();
let stpmin = vrysml.max(8.0 * eps2 * xi.abs());
step = step.clamp(stpmin, stpmax);
let stepb4 = gstepi;
let grdb4 = grad[i];
gstepi = step;
let mut xp = x.clone();
let mut xm = x.clone();
xp[i] = xi + step;
xm[i] = xi - step;
let fp = fcn.call(xp.as_slice());
let fm = fcn.call(xm.as_slice());
let grdi = 0.5 * (fp - fm) / step;
let g2i_new = (fp + fm - 2.0 * fcnmin) / (step * step);
grad[i] = grdi;
g2[i] = g2i_new;
gstep[i] = gstepi;
g2i = g2i_new;
if _cycle > 0 {
let step_change = (gstepi - stepb4).abs() / gstepi.abs();
if step_change < step_tol {
break;
}
let grad_change = (grdi - grdb4).abs() / (grdi.abs() + dfmin / step);
if grad_change < grad_tol {
break;
}
}
}
}
FunctionGradient::new(grad, g2, gstep)
}
pub fn compute_with_previous(
&self,
fcn: &MnFcn,
params: &MinimumParameters,
trafo: &MnUserTransformation,
previous: &FunctionGradient,
) -> FunctionGradient {
let n = trafo.variable_parameters();
let eps2 = trafo.precision().eps2();
let fcnmin = params.fval();
let dfmin = 8.0 * eps2 * (fcnmin.abs() + fcn.up());
let vrysml = 8.0 * eps2 * eps2;
let x = params.vec();
let ncycles = self.strategy.grad_ncycles();
let step_tol = self.strategy.grad_step_tol();
let grad_tol = self.strategy.grad_tol();
let mut grad = DVector::zeros(n);
let mut g2 = DVector::zeros(n);
let mut gstep = DVector::zeros(n);
for i in 0..n {
let ext_idx = trafo.ext_of_int(i);
let xi = x[i];
let p = &trafo.parameters()[ext_idx];
let has_limits = p.has_limits() || p.has_lower_limit() || p.has_upper_limit();
let mut gstepi = previous.gstep()[i].max(vrysml);
let mut g2i = previous.g2()[i];
for _cycle in 0..ncycles {
let optstp = (dfmin / (g2i.abs() + eps2)).sqrt();
let mut step = optstp.max(0.1 * gstepi.abs());
if has_limits {
step = step.min(0.5);
}
let stpmax = 10.0 * gstepi.abs();
let stpmin = vrysml.max(8.0 * eps2 * xi.abs());
step = step.clamp(stpmin, stpmax);
let stepb4 = gstepi;
let grdb4 = grad[i];
gstepi = step;
let mut xp = x.clone();
let mut xm = x.clone();
xp[i] = xi + step;
xm[i] = xi - step;
let fp = fcn.call(xp.as_slice());
let fm = fcn.call(xm.as_slice());
let grdi = 0.5 * (fp - fm) / step;
let g2i_new = (fp + fm - 2.0 * fcnmin) / (step * step);
grad[i] = grdi;
g2[i] = g2i_new;
gstep[i] = gstepi;
g2i = g2i_new;
if _cycle > 0 {
let step_change = (gstepi - stepb4).abs() / gstepi.abs();
if step_change < step_tol {
break;
}
let grad_change = (grdi - grdb4).abs() / (grdi.abs() + dfmin / step);
if grad_change < grad_tol {
break;
}
}
}
}
FunctionGradient::new(grad, g2, gstep)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fcn::FCN;
use crate::parameter::MinuitParameter;
use crate::user_transformation::MnUserTransformation;
struct Quadratic;
impl FCN for Quadratic {
fn value(&self, p: &[f64]) -> f64 {
p[0] * p[0] + 4.0 * p[1] * p[1]
}
}
#[test]
fn numerical_gradient_quadratic() {
let params = vec![
MinuitParameter::new(0, "x", 3.0, 0.1),
MinuitParameter::new(1, "y", 2.0, 0.1),
];
let trafo = MnUserTransformation::new(params);
let fcn = MnFcn::new(&Quadratic, &trafo);
let strategy = MnStrategy::default();
let x = DVector::from_vec(vec![3.0, 2.0]);
let min_params = MinimumParameters::new(x, 25.0);
let ig = crate::gradient::InitialGradientCalculator::new(strategy);
let init_grad = ig.compute(&fcn, &min_params, &trafo);
let calc = Numerical2PGradientCalculator::new(strategy);
let grad = calc.compute(&fcn, &min_params, &trafo, &init_grad);
assert!(
(grad.grad()[0] - 6.0).abs() < 0.01,
"dfdx should be ~6.0, got {}",
grad.grad()[0]
);
assert!(
(grad.grad()[1] - 16.0).abs() < 0.1,
"dfdy should be ~16.0, got {}",
grad.grad()[1]
);
}
}