1use anyhow::Result;
27use chrono::NaiveDateTime;
28use csv::ReaderBuilder;
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
32pub enum SoilType {
33 Sand,
34 LoamySandA,
35 LoamySandB,
36 SandyLoamA,
37 SandyLoamB,
38 Loam,
39 SiltLoam,
40 Peat,
41 Water,
42 Universal,
43 SandTMS1,
44 LoamySandTMS1,
45 SiltLoamTMS1,
46}
47
48#[derive(Debug, Serialize, Deserialize)]
49pub struct SoilTypeModel {
50 pub id: SoilType,
51 pub name: String,
52 pub machine_name: String,
53}
54
55impl SoilType {
56 fn coeffs(self) -> (f64, f64, f64) {
63 match self {
64 SoilType::Sand => (-3.00e-09, 0.000_161_192, -0.109_956_5),
65 SoilType::LoamySandA => (-1.90e-08, 0.000_265_610, -0.154_089_3),
66 SoilType::LoamySandB => (-2.30e-08, 0.000_282_473, -0.167_211_2),
67 SoilType::SandyLoamA => (-3.80e-08, 0.000_339_449, -0.214_921_8),
68 SoilType::SandyLoamB => (-9.00e-10, 0.000_261_847, -0.158_618_3),
69 SoilType::Loam => (-5.10e-08, 0.000_397_984, -0.291_046_4),
70 SoilType::SiltLoam => (1.70e-08, 0.000_118_119, -0.101_168_5),
71 SoilType::Peat => (1.23e-07, -0.000_144_644, 0.202_927_9),
72 SoilType::Water => (0.00e+00, 0.000_306_700, -0.134_927_9),
73 SoilType::Universal => (-1.34e-08, 0.000_249_622, -0.157_888_8),
74 SoilType::SandTMS1 => (0.00e+00, 0.000_260_000, -0.133_040_0),
75 SoilType::LoamySandTMS1 => (0.00e+00, 0.000_330_000, -0.193_890_0),
76 SoilType::SiltLoamTMS1 => (0.00e+00, 0.000_380_000, -0.294_270_0),
77 }
78 }
79
80 #[must_use]
81 pub fn as_str(&self) -> &'static str {
82 match self {
83 SoilType::Sand => "sand",
84 SoilType::LoamySandA => "loamysanda",
85 SoilType::LoamySandB => "loamysandb",
86 SoilType::SandyLoamA => "sandyloama",
87 SoilType::SandyLoamB => "sandyloamb",
88 SoilType::Loam => "loam",
89 SoilType::SiltLoam => "siltloam",
90 SoilType::Peat => "peat",
91 SoilType::Water => "water",
92 SoilType::Universal => "universal",
93 SoilType::SandTMS1 => "sandtms1",
94 SoilType::LoamySandTMS1 => "loamysandtms1",
95 SoilType::SiltLoamTMS1 => "siltloamtms1",
96 }
97 }
98
99 pub const ALL: [SoilType; 13] = [
100 SoilType::Sand,
101 SoilType::LoamySandA,
102 SoilType::LoamySandB,
103 SoilType::SandyLoamA,
104 SoilType::SandyLoamB,
105 SoilType::Loam,
106 SoilType::SiltLoam,
107 SoilType::Peat,
108 SoilType::Water,
109 SoilType::Universal,
110 SoilType::SandTMS1,
111 SoilType::LoamySandTMS1,
112 SoilType::SiltLoamTMS1,
113 ];
114}
115
116impl From<SoilType> for SoilTypeModel {
117 fn from(soil: SoilType) -> Self {
118 match soil {
119 SoilType::Sand => SoilTypeModel {
120 id: SoilType::Sand,
121 name: "Sand".to_string(),
122 machine_name: "sand".to_string(),
123 },
124 SoilType::LoamySandA => SoilTypeModel {
125 id: SoilType::LoamySandA,
126 name: "Loamy Sand A".to_string(),
127 machine_name: "loamysanda".to_string(),
128 },
129 SoilType::LoamySandB => SoilTypeModel {
130 id: SoilType::LoamySandB,
131 name: "Loamy Sand B".to_string(),
132 machine_name: "loamysandb".to_string(),
133 },
134 SoilType::SandyLoamA => SoilTypeModel {
135 id: SoilType::SandyLoamA,
136 name: "Sandy Loam A".to_string(),
137 machine_name: "sandyloama".to_string(),
138 },
139 SoilType::SandyLoamB => SoilTypeModel {
140 id: SoilType::SandyLoamB,
141 name: "Sandy Loam B".to_string(),
142 machine_name: "sandyloamb".to_string(),
143 },
144 SoilType::Loam => SoilTypeModel {
145 id: SoilType::Loam,
146 name: "Loam".to_string(),
147 machine_name: "loam".to_string(),
148 },
149 SoilType::SiltLoam => SoilTypeModel {
150 id: SoilType::SiltLoam,
151 name: "Silt Loam".to_string(),
152 machine_name: "siltloam".to_string(),
153 },
154 SoilType::Peat => SoilTypeModel {
155 id: SoilType::Peat,
156 name: "Peat".to_string(),
157 machine_name: "peat".to_string(),
158 },
159 SoilType::Water => SoilTypeModel {
160 id: SoilType::Water,
161 name: "Water".to_string(),
162 machine_name: "water".to_string(),
163 },
164 SoilType::Universal => SoilTypeModel {
165 id: SoilType::Universal,
166 name: "Universal".to_string(),
167 machine_name: "universal".to_string(),
168 },
169 SoilType::SandTMS1 => SoilTypeModel {
170 id: SoilType::SandTMS1,
171 name: "Sand TMS1".to_string(),
172 machine_name: "sandtms1".to_string(),
173 },
174 SoilType::LoamySandTMS1 => SoilTypeModel {
175 id: SoilType::LoamySandTMS1,
176 name: "Loamy Sand TMS1".to_string(),
177 machine_name: "loamysandtms1".to_string(),
178 },
179 SoilType::SiltLoamTMS1 => SoilTypeModel {
180 id: SoilType::SiltLoamTMS1,
181 name: "Silt Loam TMS1".to_string(),
182 machine_name: "siltloamtms1".to_string(),
183 },
184 }
185 }
186}
187
188impl TryFrom<&str> for SoilTypeModel {
189 type Error = String;
190
191 fn try_from(s: &str) -> Result<Self, Self::Error> {
192 match s.to_lowercase().as_str() {
193 "sand" => Ok(Self::from(SoilType::Sand)),
194 "loamysanda" => Ok(Self::from(SoilType::LoamySandA)),
195 "loamysandb" => Ok(Self::from(SoilType::LoamySandB)),
196 "sandyloama" => Ok(Self::from(SoilType::SandyLoamA)),
197 "sandyloamb" => Ok(Self::from(SoilType::SandyLoamB)),
198 "loam" => Ok(Self::from(SoilType::Loam)),
199 "siltloam" => Ok(Self::from(SoilType::SiltLoam)),
200 "peat" => Ok(Self::from(SoilType::Peat)),
201 "water" => Ok(Self::from(SoilType::Water)),
202 "universal" => Ok(Self::from(SoilType::Universal)),
203 "sandtms1" => Ok(Self::from(SoilType::SandTMS1)),
204 "loamysandtms1" => Ok(Self::from(SoilType::LoamySandTMS1)),
205 "siltloamtms1" => Ok(Self::from(SoilType::SiltLoamTMS1)),
206 _ => Err(format!("Unknown soil type: {s}")),
207 }
208 }
209}
210
211const 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 {
234 let (a, b, c) = soil.coeffs();
235
236 let vwc = a * raw_value * raw_value + b * raw_value + c;
238
239 let dcor_t = WCOR_T - ACOR_T;
241 let tcor = if temp_value.is_nan() {
242 raw_value
243 } else {
244 raw_value + (REF_T - temp_value) * (ACOR_T + dcor_t * vwc)
245 };
246
247 let cal_cor_factor = 0.0;
250 let cal_cor_slope = 0.0;
251 let corrected_raw = tcor + cal_cor_factor + cal_cor_slope * vwc;
252 let vwc_cor = a * corrected_raw * corrected_raw + b * corrected_raw + c;
253
254 vwc_cor.clamp(0.0, 1.0)
256}
257
258#[derive(Debug, Deserialize)]
259struct RawRecord {
260 _field0: String, datetime: String, _field2: String, temp: f64, _field4: String, _field5: String, raw: f64, _field7: String, _field8: String, }
270
271pub fn process_file(path: String, soil: SoilType) -> Result<Vec<(NaiveDateTime, f64, f64, f64)>> {
281 let mut rdr = ReaderBuilder::new()
282 .delimiter(b';')
283 .has_headers(false)
284 .from_path(path)?;
285 let mut out = Vec::new();
286 for result in rdr.deserialize() {
287 let rec: RawRecord = result?;
288 let dt = NaiveDateTime::parse_from_str(&rec.datetime, "%Y.%m.%d %H:%M")?;
289 let vwc = mc_calc_vwc(rec.raw, rec.temp, soil);
290 out.push((dt, rec.raw, rec.temp, vwc));
291 }
292 Ok(out)
293}