use nalgebra::DVector;
use crate::fcn::{FCN, FCNGradient};
use crate::minimum::gradient::FunctionGradient;
use crate::minimum::parameters::MinimumParameters;
use crate::user_transformation::MnUserTransformation;
pub struct AnalyticalGradientCalculator;
impl AnalyticalGradientCalculator {
pub fn compute(
fcn: &dyn FCNGradient,
trafo: &MnUserTransformation,
params: &MinimumParameters,
) -> FunctionGradient {
let n = trafo.variable_parameters();
let eps2 = trafo.precision().eps2();
let internal_vec = params.vec();
let external_vals = trafo.transform(internal_vec.as_slice());
let ext_gradient = fcn.gradient(&external_vals);
let mut grad = DVector::zeros(n);
let mut g2 = DVector::zeros(n);
let mut gstep = DVector::zeros(n);
let error_def = fcn.error_def();
for i in 0..n {
let ext_idx = trafo.ext_of_int(i);
let int_val = internal_vec[i];
let dext_dint = trafo.dint2ext(ext_idx, int_val);
let g_ext = ext_gradient[ext_idx];
let g_int = g_ext * dext_dint;
let werr = trafo.parameters()[ext_idx].error();
let sav = trafo.int2ext(ext_idx, int_val);
let mut sav_plus = sav + werr;
let p = &trafo.parameters()[ext_idx];
if p.has_upper_limit() && sav_plus > p.upper_limit() {
sav_plus = p.upper_limit();
}
let var_plus = trafo.ext2int(ext_idx, sav_plus);
let vplu = var_plus - int_val;
let mut sav_minus = sav - werr;
if p.has_lower_limit() && sav_minus < p.lower_limit() {
sav_minus = p.lower_limit();
}
let var_minus = trafo.ext2int(ext_idx, sav_minus);
let vmin = var_minus - int_val;
let gsmin = 8.0 * eps2 * (int_val.abs() + eps2);
let dirin = (0.5 * (vplu.abs() + vmin.abs())).max(gsmin);
let g2i = 2.0 * error_def / (dirin * dirin);
let mut gstepi = gsmin.max(0.1 * dirin);
if p.has_limits() && gstepi > 0.5 {
gstepi = 0.5;
}
grad[i] = g_int;
g2[i] = g2i;
gstep[i] = gstepi;
}
let mut result = FunctionGradient::new(grad, g2, gstep);
result.set_analytical(true);
result
}
pub fn can_compute_g2(fcn: &dyn FCN) -> bool {
fcn.has_g2() || fcn.has_hessian()
}
pub fn can_compute_hessian(fcn: &dyn FCN) -> bool {
fcn.has_hessian()
}
pub fn g2(fcn: &dyn FCN, parameters: &[f64]) -> Option<Vec<f64>> {
if fcn.has_g2() {
Some(fcn.g2(parameters))
} else if fcn.has_hessian() {
let h = fcn.hessian(parameters);
if h.is_empty() {
None
} else {
let mut n = 0usize;
while n * (n + 1) / 2 < h.len() {
n += 1;
}
if n * (n + 1) / 2 != h.len() {
return None;
}
let mut out = Vec::new();
for i in 0..n {
let diag = i * (i + 3) / 2;
out.push(h[diag]);
}
Some(out)
}
} else {
None
}
}
pub fn hessian(fcn: &dyn FCN, parameters: &[f64]) -> Option<Vec<f64>> {
if fcn.has_hessian() {
Some(fcn.hessian(parameters))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fcn::{FCN, FCNGradient};
use crate::parameter::MinuitParameter;
struct QuadraticWithGrad;
impl FCN for QuadraticWithGrad {
fn value(&self, p: &[f64]) -> f64 {
p[0] * p[0] + 4.0 * p[1] * p[1]
}
}
impl FCNGradient for QuadraticWithGrad {
fn gradient(&self, p: &[f64]) -> Vec<f64> {
vec![2.0 * p[0], 8.0 * p[1]]
}
}
#[test]
fn analytical_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 x = DVector::from_vec(vec![3.0, 2.0]);
let f_val = 9.0 + 16.0; let min_params = MinimumParameters::new(x, f_val);
let grad = AnalyticalGradientCalculator::compute(&QuadraticWithGrad, &trafo, &min_params);
assert!(
(grad.grad()[0] - 6.0).abs() < 1e-10,
"dfdx should be ~6.0, got {}",
grad.grad()[0]
);
assert!(
(grad.grad()[1] - 16.0).abs() < 1e-10,
"dfdy should be ~16.0, got {}",
grad.grad()[1]
);
assert!(grad.g2()[0] > 0.0, "g2[0] should be positive");
assert!(grad.g2()[1] > 0.0, "g2[1] should be positive");
assert!(grad.gstep()[0] > 0.0, "gstep[0] should be positive");
assert!(grad.gstep()[1] > 0.0, "gstep[1] should be positive");
assert!(
grad.is_analytical(),
"gradient should be marked as analytical"
);
}
#[test]
fn analytical_gradient_bounded_param() {
let params = vec![
MinuitParameter::with_limits(0, "x", 5.0, 0.1, 0.0, 10.0),
MinuitParameter::new(1, "y", 2.0, 0.1),
];
let trafo = MnUserTransformation::new(params);
let x = DVector::from_vec(vec![0.0, 2.0]); let min_params = MinimumParameters::new(x, 100.0);
let grad = AnalyticalGradientCalculator::compute(&QuadraticWithGrad, &trafo, &min_params);
assert!(grad.is_valid());
assert!(grad.is_analytical());
assert!(grad.grad().len() == 2);
assert!(grad.g2().len() == 2);
assert!(grad.gstep().len() == 2);
}
}