1use serde::{Deserialize, Serialize};
2
3use crate::validation::{validate_field, ValidationError};
4
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct SoilLayer {
13 pub soil_classification: Option<String>, pub thickness: Option<f64>, pub natural_unit_weight: Option<f64>, pub dry_unit_weight: Option<f64>, pub saturated_unit_weight: Option<f64>, pub depth: Option<f64>, pub center: Option<f64>, pub damping_ratio: Option<f64>, pub fine_content: Option<f64>, pub liquid_limit: Option<f64>, pub plastic_limit: Option<f64>, pub plasticity_index: Option<f64>, pub cu: Option<f64>, pub c_prime: Option<f64>, pub phi_u: Option<f64>, pub phi_prime: Option<f64>, pub water_content: Option<f64>, pub poissons_ratio: Option<f64>, pub elastic_modulus: Option<f64>, pub void_ratio: Option<f64>, pub recompression_index: Option<f64>, pub compression_index: Option<f64>, pub preconsolidation_pressure: Option<f64>, pub mv: Option<f64>, pub shear_wave_velocity: Option<f64>, }
39
40impl SoilLayer {
41 pub fn new(thickness: f64) -> Self {
42 Self {
43 thickness: Some(thickness),
44 ..Default::default()
45 }
46 }
47 pub fn validate_fields(&self, fields: &[&str]) -> Result<(), ValidationError> {
55 for &field in fields {
56 let result = match field {
57 "thickness" => validate_field(
58 "thickness",
59 self.thickness,
60 Some(0.0001),
61 None,
62 "soil_profile",
63 ),
64 "natural_unit_weight" => validate_field(
65 "natural_unit_weight",
66 self.natural_unit_weight,
67 Some(0.1),
68 Some(10.0),
69 "soil_profile",
70 ),
71 "dry_unit_weight" => validate_field(
72 "dry_unit_weight",
73 self.dry_unit_weight,
74 Some(0.1),
75 Some(10.0),
76 "soil_profile",
77 ),
78 "saturated_unit_weight" => validate_field(
79 "saturated_unit_weight",
80 self.saturated_unit_weight,
81 Some(0.1),
82 Some(10.0),
83 "soil_profile",
84 ),
85 "damping_ratio" => validate_field(
86 "damping_ratio",
87 self.damping_ratio,
88 Some(0.1),
89 Some(100.0),
90 "soil_profile",
91 ),
92 "fine_content" => validate_field(
93 "fine_content",
94 self.fine_content,
95 Some(0.0),
96 Some(100.),
97 "soil_profile",
98 ),
99 "liquid_limit" => validate_field(
100 "liquid_limit",
101 self.liquid_limit,
102 Some(0.0),
103 Some(100.),
104 "soil_profile",
105 ),
106 "plastic_limit" => validate_field(
107 "plastic_limit",
108 self.plastic_limit,
109 Some(0.0),
110 Some(100.),
111 "soil_profile",
112 ),
113 "plasticity_index" => validate_field(
114 "plasticity_index",
115 self.plasticity_index,
116 Some(0.0),
117 Some(100.),
118 "soil_profile",
119 ),
120 "cu" => validate_field("cu", self.cu, Some(0.0), None, "soil_profile"),
121 "c_prime" => {
122 validate_field("c_prime", self.c_prime, Some(0.0), None, "soil_profile")
123 }
124 "phi_u" => {
125 validate_field("phi_u", self.phi_u, Some(0.0), Some(90.), "soil_profile")
126 }
127 "phi_prime" => validate_field(
128 "phi_prime",
129 self.phi_prime,
130 Some(0.0),
131 Some(90.),
132 "soil_profile",
133 ),
134 "water_content" => validate_field(
135 "water_content",
136 self.water_content,
137 Some(0.),
138 Some(100.),
139 "soil_profile",
140 ),
141 "poissons_ratio" => validate_field(
142 "poissons_ratio",
143 self.poissons_ratio,
144 Some(0.0001),
145 Some(0.5),
146 "soil_profile",
147 ),
148 "elastic_modulus" => validate_field(
149 "elastic_modulus",
150 self.elastic_modulus,
151 Some(0.0001),
152 None,
153 "soil_profile",
154 ),
155 "void_ratio" => validate_field(
156 "void_ratio",
157 self.void_ratio,
158 Some(0.0),
159 None,
160 "soil_profile",
161 ),
162 "compression_index" => validate_field(
163 "compression_index",
164 self.compression_index,
165 Some(0.0),
166 None,
167 "soil_profile",
168 ),
169 "recompression_index" => validate_field(
170 "recompression_index",
171 self.recompression_index,
172 Some(0.0),
173 None,
174 "soil_profile",
175 ),
176 "preconsolidation_pressure" => validate_field(
177 "preconsolidation_pressure",
178 self.preconsolidation_pressure,
179 Some(0.0),
180 None,
181 "soil_profile",
182 ),
183 "mv" => validate_field("mv", self.mv, Some(0.0), None, "soil_profile"),
184 "shear_wave_velocity" => validate_field(
185 "shear_wave_velocity",
186 self.shear_wave_velocity,
187 Some(0.0),
188 None,
189 "soil_profile",
190 ),
191 other => Err(ValidationError {
192 code: "soil_profile.invalid_field".to_string(),
193 message: format!("Field '{}' is not valid for SoilLayer.", other),
194 }),
195 };
196
197 result?;
198 }
199
200 Ok(())
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct SoilProfile {
208 pub layers: Vec<SoilLayer>,
210 pub ground_water_level: Option<f64>, }
213
214impl SoilProfile {
215 pub fn new(layers: Vec<SoilLayer>, ground_water_level: f64) -> Self {
224 if layers.is_empty() {
225 panic!("Soil profile must contain at least one layer.");
226 }
227
228 let mut profile = Self {
229 layers,
230 ground_water_level: Some(ground_water_level),
231 };
232 profile.calc_layer_depths();
233 profile
234 }
235
236 pub fn calc_layer_depths(&mut self) {
238 if self.layers.is_empty() {
239 return;
240 }
241
242 let mut bottom = 0.0;
243
244 for layer in &mut self.layers {
245 let thickness = layer.thickness.unwrap();
246 layer.center = Some(bottom + thickness / 2.0);
247 bottom += thickness;
248 layer.depth = Some(bottom);
249 }
250 }
251
252 pub fn get_layer_index(&self, depth: f64) -> usize {
260 for (i, layer) in self.layers.iter().enumerate() {
261 if let Some(layer_depth) = layer.depth {
262 if layer_depth >= depth {
263 return i;
264 }
265 }
266 }
267 self.layers.len() - 1
268 }
269
270 pub fn get_layer_at_depth(&self, depth: f64) -> &SoilLayer {
278 let index = self.get_layer_index(depth);
279 &self.layers[index]
280 }
281
282 pub fn calc_normal_stress(&self, depth: f64) -> f64 {
290 let layer_index = self.get_layer_index(depth);
291
292 let mut total_stress = 0.0;
293 let mut previous_depth = 0.0;
294 let gwt = self.ground_water_level.unwrap();
295
296 for (i, layer) in self.layers.iter().take(layer_index + 1).enumerate() {
297 let layer_thickness = if i == layer_index {
298 depth - previous_depth } else {
300 layer.thickness.unwrap() };
302 let dry_unit_weight = layer.dry_unit_weight.unwrap_or(0.0);
303 let saturated_unit_weight = layer.saturated_unit_weight.unwrap_or(0.0);
304 if dry_unit_weight <= 1.0 && saturated_unit_weight <= 1.0 {
305 panic!("Dry or saturated unit weight must be greater then 1 for each layer.");
306 }
307 if gwt >= previous_depth + layer_thickness {
308 total_stress += dry_unit_weight * layer_thickness;
310 } else if gwt <= previous_depth {
311 total_stress += saturated_unit_weight * layer_thickness;
313 } else {
314 let dry_thickness = gwt - previous_depth;
316 let submerged_thickness = layer_thickness - dry_thickness;
317 total_stress +=
318 dry_unit_weight * dry_thickness + saturated_unit_weight * submerged_thickness;
319 }
320
321 previous_depth += layer_thickness;
322 }
323
324 total_stress
325 }
326
327 pub fn calc_effective_stress(&self, depth: f64) -> f64 {
335 let normal_stress = self.calc_normal_stress(depth);
336
337 if self.ground_water_level.unwrap() >= depth {
338 normal_stress } else {
340 let pore_pressure = (depth - self.ground_water_level.unwrap()) * 0.981; normal_stress - pore_pressure
342 }
343 }
344
345 pub fn validate(&self, fields: &[&str]) -> Result<(), ValidationError> {
353 if self.layers.is_empty() {
354 return Err(ValidationError {
355 code: "soil_profile.empty".to_string(),
356 message: "Soil profile must contain at least one layer.".to_string(),
357 });
358 }
359
360 for layer in &self.layers {
361 layer.validate_fields(fields)?;
362 }
363
364 validate_field(
365 "ground_water_level",
366 self.ground_water_level,
367 Some(0.0),
368 None,
369 "soil_profile",
370 )?;
371
372 Ok(())
373 }
374}