rcompat-lbfgsb 0.1.6

Rust implementation of R-style stats::optim L-BFGS-B behavior
Documentation
use crate::bounds::Bounds;
use crate::error::OptimError;

#[derive(Debug, Clone)]
pub(crate) struct Scaling {
    fnscale: f64,
    parscale: Vec<f64>,
}

impl Scaling {
    pub(crate) fn new(fnscale: f64, parscale: Vec<f64>) -> Self {
        Self { fnscale, parscale }
    }

    pub(crate) fn user_to_internal_par(&self, par: &[f64]) -> Vec<f64> {
        par.iter()
            .zip(self.parscale.iter())
            .map(|(&value, &scale)| value / scale)
            .collect()
    }

    pub(crate) fn internal_to_user_par(&self, x: &[f64]) -> Vec<f64> {
        x.iter()
            .zip(self.parscale.iter())
            .map(|(&value, &scale)| value * scale)
            .collect()
    }

    pub(crate) fn user_value_to_internal(&self, value: f64) -> f64 {
        value / self.fnscale
    }

    pub(crate) fn internal_value_to_user(&self, value: f64) -> f64 {
        value * self.fnscale
    }

    pub(crate) fn user_gradient_to_internal(
        &self,
        gradient: &[f64],
    ) -> Result<Vec<f64>, OptimError> {
        if gradient.len() != self.parscale.len() {
            return Err(OptimError::InvalidGradient {
                index: None,
                value: None,
                reason: format!(
                    "expected length {}, got {}",
                    self.parscale.len(),
                    gradient.len()
                ),
            });
        }
        gradient
            .iter()
            .zip(self.parscale.iter())
            .enumerate()
            .map(|(index, (&value, &scale))| {
                if value.is_finite() {
                    Ok(value * scale / self.fnscale)
                } else {
                    Err(OptimError::InvalidGradient {
                        index: Some(index),
                        value: Some(value),
                        reason: "gradient entries must be finite".to_string(),
                    })
                }
            })
            .collect()
    }

    pub(crate) fn scale_bounds(&self, bounds: &Bounds) -> Bounds {
        Bounds {
            lower: bounds
                .lower
                .iter()
                .zip(self.parscale.iter())
                .map(|(&value, &scale)| value / scale)
                .collect(),
            upper: bounds
                .upper
                .iter()
                .zip(self.parscale.iter())
                .map(|(&value, &scale)| value / scale)
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn converts_parameters_both_ways() {
        let scaling = Scaling::new(1.0, vec![2.0, 4.0]);
        let internal = scaling.user_to_internal_par(&[8.0, -8.0]);
        assert_eq!(internal, vec![4.0, -2.0]);
        assert_eq!(scaling.internal_to_user_par(&internal), vec![8.0, -8.0]);
    }

    #[test]
    fn scales_value_by_fnscale() {
        let scaling = Scaling::new(-2.0, vec![1.0]);
        assert_eq!(scaling.user_value_to_internal(6.0), -3.0);
        assert_eq!(scaling.internal_value_to_user(-3.0), 6.0);
    }

    #[test]
    fn scales_gradient_for_internal_coordinates() {
        let scaling = Scaling::new(2.0, vec![4.0]);
        let gradient = scaling.user_gradient_to_internal(&[3.0]).unwrap();
        assert_eq!(gradient, vec![6.0]);
    }

    #[test]
    fn scales_bounds() {
        let scaling = Scaling::new(1.0, vec![2.0]);
        let bounds = Bounds::new(vec![-4.0], vec![8.0]).unwrap();
        let scaled = scaling.scale_bounds(&bounds);
        assert_eq!(scaled.lower, vec![-2.0]);
        assert_eq!(scaled.upper, vec![4.0]);
    }
}