use anyhow::Result;
use chrono::NaiveDateTime;
use csv::ReaderBuilder;
use serde::Deserialize;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SoilType {
Sand,
LoamySandA,
LoamySandB,
SandyLoamA,
SandyLoamB,
Loam,
SiltLoam,
Peat,
Water,
Universal,
SandTms1,
LoamySandTms1,
SiltLoamTms1,
}
impl SoilType {
fn coeffs(self) -> (f64, f64, f64) {
match self {
SoilType::Sand => (-3.00e-09, 0.000_161_192, -0.109_956_5),
SoilType::LoamySandA => (-1.90e-08, 0.000_265_610, -0.154_089_3),
SoilType::LoamySandB => (-2.30e-08, 0.000_282_473, -0.167_211_2),
SoilType::SandyLoamA => (-3.80e-08, 0.000_339_449, -0.214_921_8),
SoilType::SandyLoamB => (-9.00e-10, 0.000_261_847, -0.158_618_3),
SoilType::Loam => (-5.10e-08, 0.000_397_984, -0.291_046_4),
SoilType::SiltLoam => (1.70e-08, 0.000_118_119, -0.101_168_5),
SoilType::Peat => (1.23e-07, -0.000_144_644, 0.202_927_9),
SoilType::Water => (0.00e+00, 0.000_306_700, -0.134_927_9),
SoilType::Universal => (-1.34e-08, 0.000_249_622, -0.157_888_8),
SoilType::SandTms1 => (0.00e+00, 0.000_260_000, -0.133_040_0),
SoilType::LoamySandTms1 => (0.00e+00, 0.000_330_000, -0.193_890_0),
SoilType::SiltLoamTms1 => (0.00e+00, 0.000_380_000, -0.294_270_0),
}
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
SoilType::Sand => "Sand",
SoilType::LoamySandA => "Loamy Sand A",
SoilType::LoamySandB => "Loamy Sand B",
SoilType::SandyLoamA => "Sandy Loam A",
SoilType::SandyLoamB => "Sandy Loam B",
SoilType::Loam => "Loam",
SoilType::SiltLoam => "Silt Loam",
SoilType::Peat => "Peat",
SoilType::Water => "Water",
SoilType::Universal => "Universal",
SoilType::SandTms1 => "Sand TMS1",
SoilType::LoamySandTms1 => "Loamy Sand TMS1",
SoilType::SiltLoamTms1 => "Silt Loam TMS1",
}
}
pub const ALL: [SoilType; 13] = [
SoilType::Sand,
SoilType::LoamySandA,
SoilType::LoamySandB,
SoilType::SandyLoamA,
SoilType::SandyLoamB,
SoilType::Loam,
SoilType::SiltLoam,
SoilType::Peat,
SoilType::Water,
SoilType::Universal,
SoilType::SandTms1,
SoilType::LoamySandTms1,
SoilType::SiltLoamTms1,
];
}
impl TryFrom<&str> for SoilType {
type Error = String;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"sand" => Ok(Self::Sand),
"loamy sand a" | "loamysanda" => Ok(Self::LoamySandA),
"loamy sand b" | "loamysandb" => Ok(Self::LoamySandB),
"sandy loam a" | "sandyloama" => Ok(Self::SandyLoamA),
"sandy loam b" | "sandyloamb" => Ok(Self::SandyLoamB),
"loam" => Ok(Self::Loam),
"silt loam" | "siltloam" => Ok(Self::SiltLoam),
"peat" => Ok(Self::Peat),
"water" => Ok(Self::Water),
"universal" => Ok(Self::Universal),
"sand tms1" | "sandtms1" => Ok(Self::SandTms1),
"loamy sand tms1" | "loamysandtms1" => Ok(Self::LoamySandTms1),
"silt loam tms1" | "siltloamtms1" => Ok(Self::SiltLoamTms1),
_ => Err(format!("Unknown soil type: {s}")),
}
}
}
const REF_T: f64 = 24.0; const ACOR_T: f64 = 1.911_327; const WCOR_T: f64 = 0.64108;
fn mc_calc_vwc(raw_value: f64, temp_value: f64, soil: SoilType) -> f64 {
let (a, b, c) = soil.coeffs();
let vwc = a * raw_value * raw_value + b * raw_value + c;
let dcor_t = WCOR_T - ACOR_T;
let tcor = if temp_value.is_nan() {
raw_value
} else {
raw_value + (REF_T - temp_value) * (ACOR_T + dcor_t * vwc)
};
let cal_cor_factor = 0.0;
let cal_cor_slope = 0.0;
let corrected_raw = tcor + cal_cor_factor + cal_cor_slope * vwc;
let vwc_cor = a * corrected_raw * corrected_raw + b * corrected_raw + c;
vwc_cor.clamp(0.0, 1.0)
}
#[derive(Debug, Deserialize)]
struct RawRecord {
_field0: String, datetime: String, _field2: String, temp: f64, _field4: String, _field5: String, raw: f64, _field7: String, _field8: String, }
pub fn process_file(path: String, soil: SoilType) -> Result<Vec<(NaiveDateTime, f64, f64, f64)>> {
let mut rdr = ReaderBuilder::new()
.delimiter(b';')
.has_headers(false)
.from_path(path)?;
let mut out = Vec::new();
for result in rdr.deserialize() {
let rec: RawRecord = result?;
let dt = NaiveDateTime::parse_from_str(&rec.datetime, "%Y.%m.%d %H:%M")?;
let vwc = mc_calc_vwc(rec.raw, rec.temp, soil);
out.push((dt, rec.raw, rec.temp, vwc));
}
Ok(out)
}