use crate::error::Error;
use crate::gas::Gas;
use libm::{log10f, powf};
pub(crate) fn interpolate_sorted(points: &[(f32, f32)], x: f32) -> Result<f32, Error> {
if points.len() < 2 {
return Err(Error::OutOfRange);
}
let first = points[0].0;
let last = points[points.len() - 1].0;
if x < first - 0.001 || x > last + 0.001 {
return Err(Error::OutOfRange);
}
if x <= first {
return Ok(points[0].1);
}
if x >= last {
return Ok(points[points.len() - 1].1);
}
let mut i = 0;
while i < points.len() - 1 {
if x >= points[i].0 && x <= points[i + 1].0 {
let (x0, y0) = points[i];
let (x1, y1) = points[i + 1];
let t = (x - x0) / (x1 - x0);
return Ok(y0 + t * (y1 - y0));
}
i += 1;
}
Err(Error::OutOfRange)
}
const CO_RED: &[(f32, f32)] = &[
(0.0, 0.0), (1.0, -0.222), (2.0, -0.602), (3.0, -1.046), ];
const ETHANOL_RED: &[(f32, f32)] = &[
(1.0, -0.155), (2.0, -0.553), (2.699, -0.854), ];
const H2_RED: &[(f32, f32)] = &[
(0.0, -0.097), (1.0, -0.398), (2.0, -0.824), (3.0, -1.398), ];
const CH4_RED: &[(f32, f32)] = &[
(3.0, 0.0), (4.0, -0.155), (5.0, -0.398), ];
const C3H8_RED: &[(f32, f32)] = &[
(3.0, -0.222), (4.0, -0.602), (5.0, -1.046), ];
const C4H10_RED: &[(f32, f32)] = &[
(3.0, -0.301), (4.0, -0.824), (5.0, -1.398), ];
const NO2_OX: &[(f32, f32)] = &[
(-1.301, -0.046), (-1.0, 0.176), (0.0, 0.903), (1.0, 1.845), ];
const NH3_NH3: &[(f32, f32)] = &[
(0.0, -0.155), (1.0, -0.456), (2.0, -1.046), (2.699, -1.523), ];
pub(crate) fn curve_for(gas: Gas) -> &'static [(f32, f32)] {
match gas {
Gas::CarbonMonoxide => CO_RED,
Gas::NitrogenDioxide => NO2_OX,
Gas::Ammonia => NH3_NH3,
Gas::Ethanol => ETHANOL_RED,
Gas::Hydrogen => H2_RED,
Gas::Methane => CH4_RED,
Gas::Propane => C3H8_RED,
Gas::Isobutane => C4H10_RED,
}
}
pub fn rs_r0_to_ppm(gas: Gas, rs_r0: f32) -> Result<f32, Error> {
if rs_r0 <= 0.0 {
return Err(Error::OutOfRange);
}
let log_ratio = log10f(rs_r0);
let curve = curve_for(gas);
let is_increasing = gas == Gas::NitrogenDioxide;
let mut inverted: [Option<(f32, f32)>; 8] = [None; 8];
let len = curve.len();
for i in 0..len {
if is_increasing {
inverted[i] = Some((curve[i].1, curve[i].0));
} else {
inverted[i] = Some((curve[len - 1 - i].1, curve[len - 1 - i].0));
}
}
let mut buf: [(f32, f32); 8] = [(0.0, 0.0); 8];
for i in 0..len {
buf[i] = inverted[i].unwrap();
}
interpolate_sorted(&buf[..len], log_ratio).map(|log_ppm| powf(10.0, log_ppm))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interpolate_midpoint() {
let points: &[(f32, f32)] = &[(1.0, 0.0), (3.0, 2.0)];
let result = interpolate_sorted(points, 2.0);
assert!((result.unwrap() - 1.0).abs() < 0.01);
}
#[test]
fn test_interpolate_exact_point() {
let points: &[(f32, f32)] = &[(1.0, 0.0), (3.0, 2.0)];
let result = interpolate_sorted(points, 1.0);
assert!((result.unwrap() - 0.0).abs() < 0.01);
}
#[test]
fn test_interpolate_out_of_range() {
let points: &[(f32, f32)] = &[(1.0, 0.0), (3.0, 2.0)];
let result = interpolate_sorted(points, 0.5);
assert_eq!(result, Err(crate::Error::OutOfRange));
}
#[test]
fn test_co_at_known_point() {
let ppm = rs_r0_to_ppm(Gas::CarbonMonoxide, 0.25).unwrap();
assert!((ppm - 100.0).abs() < 15.0, "expected ~100, got {ppm}");
}
#[test]
fn test_no2_at_known_point() {
let ppm = rs_r0_to_ppm(Gas::NitrogenDioxide, 8.0).unwrap();
assert!((ppm - 1.0).abs() < 0.3, "expected ~1.0, got {ppm}");
}
#[test]
fn test_nh3_at_known_point() {
let ppm = rs_r0_to_ppm(Gas::Ammonia, 0.35).unwrap();
assert!((ppm - 10.0).abs() < 3.0, "expected ~10, got {ppm}");
}
#[test]
fn test_out_of_range_ratio() {
assert_eq!(
rs_r0_to_ppm(Gas::CarbonMonoxide, 0.0),
Err(crate::Error::OutOfRange)
);
}
}