use crate::errors::QlResult;
use crate::fail;
use crate::math::interpolations::linear::LinearInterpolation;
use crate::math::interpolations::{Interpolation, Interpolator};
use crate::types::Real;
#[derive(Clone, Copy, Default)]
pub struct LogLinear;
impl Interpolator for LogLinear {
type Output = LogLinearInterpolation;
fn interpolate(&self, x: &[Real], y: &[Real]) -> QlResult<LogLinearInterpolation> {
LogLinearInterpolation::new(x.to_vec(), y.to_vec())
}
}
pub struct LogLinearInterpolation {
log_linear: LinearInterpolation,
allow_extrapolation: bool,
}
impl LogLinearInterpolation {
pub fn new(x: Vec<Real>, y: Vec<Real>) -> QlResult<Self> {
for (i, &yi) in y.iter().enumerate() {
if yi.is_nan() || yi <= 0.0 {
fail!("log-linear interpolation requires positive y values, got y[{i}] = {yi}");
}
}
let log_y = y.iter().map(|&yi| yi.ln()).collect();
let log_linear = LinearInterpolation::new(x, log_y)?.with_extrapolation(true);
Ok(LogLinearInterpolation {
log_linear,
allow_extrapolation: false,
})
}
pub fn with_extrapolation(mut self, allow: bool) -> Self {
self.allow_extrapolation = allow;
self
}
pub fn allows_extrapolation(&self) -> bool {
self.allow_extrapolation
}
fn check_range(&self, x: Real) -> QlResult<()> {
if x.is_nan() {
fail!("interpolation cannot be evaluated at NaN");
}
if !self.allow_extrapolation && !self.is_in_range(x) {
fail!(
"interpolation range is [{}, {}]: extrapolation at {x} not allowed",
self.x_min(),
self.x_max()
);
}
Ok(())
}
}
impl Interpolation for LogLinearInterpolation {
fn value(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
Ok(self.log_linear.value(x)?.exp())
}
fn derivative(&self, x: Real) -> QlResult<Real> {
self.check_range(x)?;
Ok(self.log_linear.value(x)?.exp() * self.log_linear.derivative(x)?)
}
fn primitive(&self, _x: Real) -> QlResult<Real> {
fail!("log-linear interpolation primitive is not implemented")
}
fn x_min(&self) -> Real {
self.log_linear.x_min()
}
fn x_max(&self) -> Real {
self.log_linear.x_max()
}
fn is_in_range(&self, x: Real) -> bool {
self.log_linear.is_in_range(x)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::E;
fn exp_sample() -> LogLinearInterpolation {
LogLinearInterpolation::new(vec![0.0, 1.0, 2.0], vec![1.0, E, E * E]).unwrap()
}
fn assert_close(got: Real, expected: Real) {
let tol = 1e-12 * (1.0 + expected.abs());
assert!(
(got - expected).abs() <= tol,
"got {got}, expected {expected}"
);
}
#[test]
fn value_at_nodes_returns_y() {
let f = exp_sample();
assert_close(f.value(0.0).unwrap(), 1.0);
assert_close(f.value(1.0).unwrap(), E);
assert_close(f.value(2.0).unwrap(), E * E);
}
#[test]
fn value_recovers_exp_between_nodes() {
let f = exp_sample();
for &x in &[0.25, 0.5, 1.3, 1.75_f64] {
assert_close(f.value(x).unwrap(), x.exp());
}
}
#[test]
fn midpoint_is_geometric_mean() {
let f = LogLinearInterpolation::new(vec![0.0, 2.0], vec![1.0, 9.0]).unwrap();
assert_close(f.value(1.0).unwrap(), 3.0);
}
#[test]
fn derivative_is_value_times_log_slope() {
let f = exp_sample();
for &x in &[0.25, 0.5, 1.75_f64] {
assert_close(f.derivative(x).unwrap(), x.exp());
}
let g = LogLinearInterpolation::new(vec![0.0, 2.0], vec![1.0, 9.0]).unwrap();
assert_close(g.derivative(1.0).unwrap(), 3.0 * 3.0_f64.ln());
}
#[test]
fn primitive_is_not_implemented() {
let f = exp_sample();
assert!(f.primitive(0.5).is_err());
assert!(f.primitive(1.0).is_err());
}
#[test]
fn domain_and_in_range() {
let f = exp_sample();
assert_eq!(f.x_min(), 0.0);
assert_eq!(f.x_max(), 2.0);
assert!(f.is_in_range(1.0));
assert!(!f.is_in_range(-0.1));
assert!(!f.is_in_range(2.1));
}
#[test]
fn extrapolation_disabled_errors_out_of_range() {
let f = exp_sample();
assert!(f.value(-1.0).is_err());
assert!(f.value(3.0).is_err());
}
#[test]
fn extrapolation_enabled_extends_geometrically() {
let f = exp_sample().with_extrapolation(true);
assert!(f.allows_extrapolation());
assert_close(f.value(-1.0).unwrap(), (-1.0_f64).exp());
assert_close(f.value(3.0).unwrap(), 3.0_f64.exp());
}
#[test]
fn nan_input_is_rejected() {
let f = exp_sample().with_extrapolation(true);
assert!(f.value(Real::NAN).is_err());
assert!(f.derivative(Real::NAN).is_err());
}
#[test]
fn non_positive_y_rejected() {
assert!(LogLinearInterpolation::new(vec![0.0, 1.0], vec![1.0, 0.0]).is_err());
assert!(LogLinearInterpolation::new(vec![0.0, 1.0], vec![1.0, -2.0]).is_err());
assert!(LogLinearInterpolation::new(vec![0.0, 1.0], vec![Real::NAN, 1.0]).is_err());
}
#[test]
fn factory_builds_the_interpolation() {
let f = LogLinear.interpolate(&[0.0, 2.0], &[1.0, 9.0]).unwrap();
assert_close(f.value(1.0).unwrap(), 3.0);
assert_eq!(LogLinear.required_points(), 2);
assert!(LogLinear.interpolate(&[0.0, 1.0], &[1.0, -2.0]).is_err());
}
#[test]
fn invalid_x_rejected() {
assert!(LogLinearInterpolation::new(vec![0.0], vec![1.0]).is_err());
assert!(LogLinearInterpolation::new(vec![0.0, 1.0], vec![1.0]).is_err());
assert!(LogLinearInterpolation::new(vec![1.0, 1.0], vec![1.0, 2.0]).is_err());
}
}