1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4use ordered_float::OrderedFloat;
5
6use crate::{
7 enums::SelectionMethod,
8 validation::{validate_field, ValidationError},
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PointLoadSample {
24 pub depth: Option<f64>,
25 pub sample_no: Option<u32>,
26 pub p: Option<f64>,
27 pub is: Option<f64>,
28 pub f: Option<f64>,
29 pub is50: Option<f64>,
30 pub l: Option<f64>,
31 pub d: Option<f64>,
32}
33
34impl PointLoadSample {
35 pub fn new(depth: f64, is50: f64, d: f64) -> Self {
36 Self {
37 depth: Some(depth),
38 sample_no: None,
39 p: None,
40 is: None,
41 f: None,
42 is50: Some(is50),
43 l: None,
44 d: Some(d),
45 }
46 }
47 pub fn validate(&self, fields: &[&str]) -> Result<(), ValidationError> {
55 for &field in fields {
56 let result = match field {
57 "depth" => validate_field("depth", self.depth, Some(0.0), None, "point_load_test"),
58 "sample_no" => validate_field(
59 "sample_no",
60 self.sample_no,
61 Some(0),
62 None,
63 "point_load_test",
64 ),
65 "p" => validate_field("p", self.p, Some(0.0001), None, "point_load_test"),
66 "is" => validate_field("is", self.is, Some(0.00001), None, "point_load_test"),
67 "f" => validate_field("f", self.f, Some(0.00001), None, "point_load_test"),
68 "is50" => validate_field("is50", self.is50, Some(0.00001), None, "point_load_test"),
69 "l" => validate_field("l", self.l, Some(0.00001), None, "point_load_test"),
70 "d" => validate_field("d", self.d, Some(0.00001), None, "point_load_test"),
71 unknown => Err(ValidationError {
72 code: "point_load_test.invalid_field".into(),
73 message: format!("Field '{}' is not valid for Point Load Test.", unknown),
74 }),
75 };
76
77 result?; }
79
80 Ok(())
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct PointLoadExp {
91 pub borehole_id: String,
92 pub samples: Vec<PointLoadSample>,
93}
94
95impl PointLoadExp {
96 pub fn new(borehole_id: String, samples: Vec<PointLoadSample>) -> Self {
97 Self {
98 borehole_id,
99 samples,
100 }
101 }
102
103 pub fn add_sample(&mut self, sample: PointLoadSample) {
104 self.samples.push(sample);
105 }
106
107 pub fn get_sample_at_depth(&self, depth: f64) -> &PointLoadSample {
120 self.samples
121 .iter()
122 .find(|exp| exp.depth.unwrap() >= depth)
123 .unwrap_or_else(|| self.samples.last().unwrap())
124 }
125
126 pub fn validate(&self, fields: &[&str]) -> Result<(), ValidationError> {
134 if self.samples.is_empty() {
135 return Err(ValidationError {
136 code: "point_load_test.empty_samples".into(),
137 message: "No samples provided for Point Load Experiment.".into(),
138 });
139 }
140 for sample in &self.samples {
141 sample.validate(fields)?;
142 }
143
144 Ok(())
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct PointLoadTest {
155 pub exps: Vec<PointLoadExp>,
156 pub idealization_method: SelectionMethod,
157}
158
159impl PointLoadTest {
160 pub fn new(exps: Vec<PointLoadExp>, idealization_method: SelectionMethod) -> Self {
161 Self {
162 exps,
163 idealization_method,
164 }
165 }
166
167 pub fn add_borehole(&mut self, exp: PointLoadExp) {
168 self.exps.push(exp);
169 }
170
171 pub fn get_idealized_exp(&self, name: String) -> PointLoadExp {
180 if self.exps.is_empty() {
181 return PointLoadExp::new(name, vec![]);
182 }
183
184 let mode = self.idealization_method;
185
186 let mut depth_map: BTreeMap<
187 OrderedFloat<f64>,
188 Vec<(OrderedFloat<f64>, OrderedFloat<f64>)>,
189 > = BTreeMap::new();
190
191 for exp in &self.exps {
193 for sample in &exp.samples {
194 depth_map
195 .entry(OrderedFloat(sample.depth.unwrap()))
196 .or_default()
197 .push((
198 OrderedFloat(sample.is50.unwrap()),
199 OrderedFloat(sample.d.unwrap()),
200 ));
201 }
202 }
203
204 let mut idealized_samples = Vec::new();
206
207 for (&depth, is50_d_pairs) in &depth_map {
208 let selected_is50 = match mode {
209 SelectionMethod::Min => is50_d_pairs.iter().min_by_key(|&(is50, _)| is50).unwrap(),
210 SelectionMethod::Max => is50_d_pairs.iter().max_by_key(|&(is50, _)| is50).unwrap(),
211 SelectionMethod::Avg => {
212 let sum_is50: f64 =
213 is50_d_pairs.iter().map(|(is50, _)| is50.into_inner()).sum();
214 let sum_d: f64 = is50_d_pairs.iter().map(|(_, d)| d.into_inner()).sum();
215 let count = is50_d_pairs.len() as f64;
216 &(OrderedFloat(sum_is50 / count), OrderedFloat(sum_d / count))
217 }
218 };
219
220 idealized_samples.push(PointLoadSample::new(
222 depth.into_inner(),
223 selected_is50.0.into_inner(),
224 selected_is50.1.into_inner(),
225 ));
226 }
227
228 PointLoadExp::new(name, idealized_samples)
229 }
230 pub fn validate(&self, fields: &[&str]) -> Result<(), ValidationError> {
238 if self.exps.is_empty() {
239 return Err(ValidationError {
240 code: "point_load_test.empty_exps".into(),
241 message: "No experiments provided for Point Load Test.".into(),
242 });
243 }
244 for exp in &self.exps {
245 exp.validate(fields)?;
246 }
247
248 Ok(())
249 }
250}