use ndarray::{Array2, Array3, ArrayView2, ArrayView3};
use crate::ClimateError;
pub struct CafecInputs<'a> {
pub precips: ArrayView3<'a, f64>,
pub pet: ArrayView3<'a, f64>,
pub prdat: ArrayView3<'a, f64>,
pub spdat: ArrayView3<'a, f64>,
pub pldat: ArrayView3<'a, f64>,
pub alpha: ArrayView2<'a, f64>,
pub beta: ArrayView2<'a, f64>,
pub gamma: ArrayView2<'a, f64>,
pub delta: ArrayView2<'a, f64>,
}
impl CafecInputs<'_> {
fn validate(&self) -> Result<(), ClimateError> {
let monthly = self.precips.dim();
for (argument, values) in [
("pet", &self.pet),
("prdat", &self.prdat),
("spdat", &self.spdat),
("pldat", &self.pldat),
] {
if values.dim() != monthly {
return Err(ClimateError::ShapeMismatch {
argument,
expected: self.precips.len(),
actual: values.len(),
});
}
}
for (argument, values) in [
("alpha", &self.alpha),
("beta", &self.beta),
("gamma", &self.gamma),
("delta", &self.delta),
] {
check_coefficients(argument, values, monthly)?;
}
Ok(())
}
fn cafec(&self, year: usize, month: usize, cell: usize) -> f64 {
self.alpha[[month, cell]] * self.pet[[year, month, cell]]
+ self.beta[[month, cell]] * self.prdat[[year, month, cell]]
+ self.gamma[[month, cell]] * self.spdat[[year, month, cell]]
- self.delta[[month, cell]] * self.pldat[[year, month, cell]]
}
}
fn check_coefficients(
argument: &'static str,
values: &ArrayView2<'_, f64>,
(_, months, n_cells): (usize, usize, usize),
) -> Result<(), ClimateError> {
if values.dim() != (months, n_cells) {
return Err(ClimateError::ShapeMismatch {
argument,
expected: months * n_cells,
actual: values.len(),
});
}
Ok(())
}
pub fn k_prime_and_dbar(
inputs: &CafecInputs<'_>,
trat: ArrayView2<'_, f64>,
calibration_year_initial_idx: usize,
calibration_year_final_idx: usize,
) -> Result<(Array2<f64>, Array2<f64>), ClimateError> {
inputs.validate()?;
let (n_years, months, n_cells) = inputs.precips.dim();
check_coefficients("trat", &trat, (n_years, months, n_cells))?;
if calibration_year_initial_idx > calibration_year_final_idx
|| calibration_year_final_idx >= n_years
{
return Err(ClimateError::IndexOutOfRange {
argument: "calibration_year_final_idx",
value: calibration_year_final_idx as i64,
minimum: calibration_year_initial_idx as i64,
maximum: n_years as i64 - 1,
});
}
let mut sabsd = Array2::<f64>::zeros((months, n_cells));
for year in calibration_year_initial_idx..=calibration_year_final_idx {
for month in 0..months {
for cell in 0..n_cells {
let phat = inputs.cafec(year, month, cell);
sabsd[[month, cell]] += (inputs.precips[[year, month, cell]] - phat).abs();
}
}
}
let n_calb_years = (calibration_year_final_idx - calibration_year_initial_idx + 1) as f64;
let dbar = sabsd.mapv(|sum| sum / n_calb_years);
let mut k_prime = Array2::<f64>::zeros((months, n_cells));
ndarray::Zip::from(&mut k_prime)
.and(&dbar)
.and(&trat)
.for_each(|k, &d, &t| *k = 1.5 * ((t + 2.8) / d).log10() + 0.5);
Ok((dbar, k_prime))
}
pub fn raw_zindex(
inputs: &CafecInputs<'_>,
ak: ArrayView2<'_, f64>,
) -> Result<Array3<f64>, ClimateError> {
inputs.validate()?;
let dim = inputs.precips.dim();
check_coefficients("ak", &ak, dim)?;
Ok(Array3::from_shape_fn(dim, |(year, month, cell)| {
let cafec = inputs.cafec(year, month, cell);
ak[[month, cell]] * (inputs.precips[[year, month, cell]] - cafec)
}))
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
fn inputs<'a>(
monthly: &'a Array3<f64>,
coefficients: &'a Array2<f64>,
delta: &'a Array2<f64>,
) -> CafecInputs<'a> {
CafecInputs {
precips: monthly.view(),
pet: monthly.view(),
prdat: monthly.view(),
spdat: monthly.view(),
pldat: monthly.view(),
alpha: coefficients.view(),
beta: coefficients.view(),
gamma: coefficients.view(),
delta: delta.view(),
}
}
#[test]
fn zindex_weights_the_cafec_departure() {
let monthly = Array3::<f64>::from_elem((1, 1, 1), 2.0);
let coefficients = array![[0.5]];
let delta = array![[0.25]];
let ak = array![[2.0]];
let z = raw_zindex(&inputs(&monthly, &coefficients, &delta), ak.view()).unwrap();
assert_eq!(z[[0, 0, 0]], 2.0 * (2.0 - 2.5));
}
#[test]
fn k_prime_averages_only_the_calibration_years() {
let monthly = Array3::<f64>::from_elem((2, 1, 1), 2.0);
let coefficients = array![[0.5]];
let delta = array![[0.25]];
let trat = array![[1.2]];
let mut precips = monthly.clone();
precips[[1, 0, 0]] = 4.0;
let mut cafec = inputs(&monthly, &coefficients, &delta);
cafec.precips = precips.view();
let (dbar, k_prime) = k_prime_and_dbar(&cafec, trat.view(), 1, 1).unwrap();
assert_eq!(dbar[[0, 0]], 1.5);
assert_eq!(k_prime[[0, 0]], 1.5 * (4.0_f64 / 1.5).log10() + 0.5);
}
#[test]
fn rejects_a_calibration_row_past_the_record() {
let monthly = Array3::<f64>::from_elem((1, 1, 1), 2.0);
let coefficients = array![[0.5]];
let trat = array![[1.0]];
let error = k_prime_and_dbar(
&inputs(&monthly, &coefficients, &coefficients),
trat.view(),
0,
1,
)
.unwrap_err();
assert!(matches!(error, ClimateError::IndexOutOfRange { .. }));
}
}