1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::io::Write;
4pub mod agsi;
5pub mod soil_description;
6pub mod strip_log;
7
8#[cfg(test)]
9mod soil_description_tests;
10use crate::agsi::AgsiDataParameterValue;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum ConvertType {
14 SoilParams,
15 GroundModel,
16}
17
18impl ConvertType {
19 pub fn from_str(s: &str) -> Result<Self, &'static str> {
20 match s.to_lowercase().as_str() {
21 "soilparams" => Ok(ConvertType::SoilParams),
22 "groundmodel" => Ok(ConvertType::GroundModel),
23 _ => Err("Invalid convert type. Use 'soilparams' or 'groundmodel'"),
24 }
25 }
26}
27
28pub fn convert_agsi_file(
29 file_path: &str,
30 convert_type: ConvertType,
31 output_path: Option<&str>,
32) -> Result<String, Box<dyn std::error::Error>> {
33 let text = fs::read_to_string(file_path)?;
34 let agsi: serde_json::Value = serde_json::from_str(&text)?;
35
36 let output_json = match convert_type {
37 ConvertType::GroundModel => {
38 let ground_model = GroundModel::from_agsi_file(&agsi);
39 serde_json::to_string_pretty(&ground_model)?
40 }
41 ConvertType::SoilParams => {
42 let mut soil_params = Vec::new();
43
44 if let Some(agsi_models) = agsi["agsiModel"].as_array() {
45 if let Some(first_model) = agsi_models.get(0) {
46 if let Some(elements) = first_model["agsiModelElement"].as_array() {
47 for element in elements {
48 if let Some(param_values) = element["agsiDataParameterValue"].as_array()
49 {
50 let params_data: Vec<AgsiDataParameterValue> = param_values
51 .iter()
52 .filter_map(|p| {
53 Some(AgsiDataParameterValue {
54 code_id: p["codeID"].as_str()?.parse().ok()?,
55 case_id: None,
56 data_id: None,
57 remarks: None,
58 value_numeric: p["valueNumeric"].as_f64(),
59 value_profile: None,
60 value_profile_ind_var_code_id: None,
61 value_text: None,
62 })
63 })
64 .collect();
65
66 if !params_data.is_empty() {
67 soil_params
68 .push(SoilParams::from_agsi_data_parameters(¶ms_data));
69 }
70 }
71 }
72 }
73 }
74 }
75
76 serde_json::to_string_pretty(&soil_params)?
77 }
78 };
79
80 if let Some(output_file) = output_path {
81 let mut file = fs::File::create(output_file)?;
82 file.write_all(output_json.as_bytes())?;
83 println!("Output written to {}", output_file);
84 } else {
85 println!("{}", output_json);
86 }
87
88 Ok(output_json)
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct AdvancedParameter {
93 pub name: String,
94 pub value: f64,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
98pub enum SoilType {
99 Cohesive,
100 Granular,
101 Rock,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct PartialFactors {
106 pub gamma_phi: f64,
107 pub gamma_c: f64,
108 pub gamma_gamma: f64,
109 pub gamma_cu: f64,
110}
111
112impl PartialFactors {
113 pub fn new(gamma_phi: f64, gamma_c: f64, gamma_gamma: f64, gamma_cu: f64) -> Self {
114 PartialFactors {
115 gamma_phi,
116 gamma_c,
117 gamma_gamma,
118 gamma_cu,
119 }
120 }
121}
122
123impl Default for PartialFactors {
124 fn default() -> Self {
125 PartialFactors {
126 gamma_phi: 1.0,
127 gamma_c: 1.0,
128 gamma_gamma: 1.0,
129 gamma_cu: 1.0,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct SoilParams {
136 pub reference: String,
137 pub behaviour: SoilType,
138 pub phi_prime: Option<f64>,
139 pub c_prime: Option<f64>,
140 pub unit_weight: f64,
141 pub cu: Option<f64>,
142 pub mv: f64,
143 pub youngs_modulus: f64,
144 pub poissons_ratio: f64,
145 pub coefficient_of_consolidation: f64,
146 pub gsi: Option<f64>,
147 pub ucs: Option<f64>,
148 pub mi: Option<f64>,
149 pub disturbance: f64,
150 pub advanced_parameters: Option<Vec<AdvancedParameter>>,
151 pub factored: bool,
152 pub factors: Option<PartialFactors>,
153}
154
155impl SoilParams {
156 pub fn new(
157 reference: String,
158 mv: f64,
159 youngs_modulus: f64,
160 poissons_ratio: f64,
161 coefficient_of_consolidation: f64,
162 behaviour: SoilType,
163 unit_weight: f64,
164 ) -> Self {
165 SoilParams {
166 reference,
167 behaviour,
168 phi_prime: None,
169 c_prime: None,
170 unit_weight,
171 cu: None,
172 mv,
173 youngs_modulus,
174 poissons_ratio,
175 coefficient_of_consolidation,
176 gsi: None,
177 ucs: None,
178 mi: None,
179 disturbance: 0.0,
180 advanced_parameters: None,
181 factored: false,
182 factors: None,
183 }
184 }
185
186 pub fn from_agsi_data_parameters(data: &[AgsiDataParameterValue]) -> Self {
187 let mut sp = SoilParams::default();
188
189 for item in data {
190 match item.code_id.as_str() {
191 "UnitWeight" => {
192 sp.unit_weight = item.value_numeric.unwrap_or(0.0);
193 }
194 "AngleFriction" | "EffectiveFrictionAngle" => {
195 sp.phi_prime = item.value_numeric;
196 }
197 "UndrainedShearStrength" => {
198 if let Some(value) = item.value_numeric {
199 if value > 0.0 {
200 sp.cu = Some(value);
201 sp.behaviour = SoilType::Cohesive;
202 } else {
203 sp.cu = Some(0.0);
204 }
205 } else {
206 sp.cu = Some(0.0);
207 }
208 }
209 "YoungsModulus" => {
210 sp.youngs_modulus = item.value_numeric.unwrap_or(0.0);
211 }
212 "Cohesion" | "EffectiveCohesion" => {
213 sp.c_prime = item.value_numeric;
214 }
215 "ModulusOfVolumeCompressibility" => {
216 sp.mv = item.value_numeric.unwrap_or(0.0);
217 }
218 "GeologicalStrengthIndex" => {
219 sp.gsi = item.value_numeric;
220 }
221 "UnconfinedCompressiveStrength" => {
222 sp.ucs = item.value_numeric;
223 if item.value_numeric.is_some() {
224 sp.behaviour = SoilType::Rock;
225 }
226 }
227 "HoekBrownParamMi" => {
228 sp.mi = item.value_numeric;
229 }
230 "Disturbance" => {
231 sp.disturbance = item.value_numeric.unwrap_or(0.0);
232 }
233 _ => {
234 if sp.advanced_parameters.is_none() {
235 sp.advanced_parameters = Some(Vec::new());
236 }
237 sp.advanced_parameters
238 .as_mut()
239 .unwrap()
240 .push(AdvancedParameter {
241 name: item.code_id.to_string(),
242 value: item.value_numeric.unwrap_or(0.0),
243 });
244 }
245 }
246 }
247
248 sp
249 }
250
251 pub fn with_all_fields(
252 reference: String,
253 behaviour: SoilType,
254 phi_prime: Option<f64>,
255 c_prime: Option<f64>,
256 unit_weight: f64,
257 cu: Option<f64>,
258 mv: f64,
259 youngs_modulus: f64,
260 poissons_ratio: f64,
261 coefficient_of_consolidation: f64,
262 gsi: Option<f64>,
263 ucs: Option<f64>,
264 mi: Option<f64>,
265 disturbance: f64,
266 ) -> Self {
267 SoilParams {
268 reference,
269 behaviour,
270 phi_prime,
271 c_prime,
272 unit_weight,
273 cu,
274 mv,
275 youngs_modulus,
276 poissons_ratio,
277 coefficient_of_consolidation,
278 gsi,
279 ucs,
280 mi,
281 disturbance,
282 advanced_parameters: None,
283 factored: false,
284 factors: None,
285 }
286 }
287
288 pub fn apply_partial_factors(&self, pf: &PartialFactors) -> SoilParams {
289 let mut result = self.clone();
290
291 if let Some(phi) = self.phi_prime {
292 result.phi_prime = Some((phi.tan() / pf.gamma_phi).atan());
293 }
294
295 if let Some(c) = self.c_prime {
296 result.c_prime = Some(c / pf.gamma_c);
297 }
298
299 result.unit_weight = self.unit_weight / pf.gamma_gamma;
300
301 if let Some(cu_val) = self.cu {
302 result.cu = Some(cu_val / pf.gamma_cu);
303 }
304
305 result.factored = true;
306 result.factors = Some(pf.clone());
307 result
308 }
309
310 pub fn remove_partial_factors(&self) -> Result<SoilParams, &'static str> {
311 let pf = self
312 .factors
313 .as_ref()
314 .ok_or("No factors stored on this instance")?;
315 let mut result = self.clone();
316
317 if let Some(phi) = self.phi_prime {
318 result.phi_prime = Some((phi.tan() * pf.gamma_phi).atan());
319 }
320
321 if let Some(c) = self.c_prime {
322 result.c_prime = Some(c * pf.gamma_c);
323 }
324
325 result.unit_weight = self.unit_weight * pf.gamma_gamma;
326
327 if let Some(cu_val) = self.cu {
328 result.cu = Some(cu_val * pf.gamma_cu);
329 }
330
331 result.factored = false;
332 result.factors = None;
333 Ok(result)
334 }
335
336 fn coulomb_ka(&self, phi: f64, beta: f64) -> f64 {
337 let cos_beta = beta.cos();
338 let cos_phi = phi.cos();
339 let discriminant = cos_beta.powi(2) - cos_phi.powi(2);
340 let sqrt_disc = discriminant.sqrt();
341
342 (cos_beta - sqrt_disc) / (cos_beta + sqrt_disc)
343 }
344
345 pub fn get_k_active(&self, slope: Option<f64>) -> Result<f64, &'static str> {
346 let phi = self.phi_prime.ok_or("Phi must be a value")?;
347
348 match slope {
349 None => Ok((1.0 - phi.sin()) / (1.0 + phi.sin())),
350 Some(beta) => Ok(self.coulomb_ka(phi, beta)),
351 }
352 }
353
354 pub fn get_k_passive(&self, slope: Option<f64>) -> Result<f64, &'static str> {
355 match slope {
356 None => Ok(1.0 / self.get_k_active(None)?),
357 Some(_) => self.get_k_passive(None), }
359 }
360
361 pub fn k0(&self) -> Result<f64, &'static str> {
362 let phi = self.phi_prime.ok_or("Phi must be a value")?;
363 Ok(1.0 - phi.sin())
364 }
365
366 pub fn mb(&self) -> Result<f64, &'static str> {
367 let mi = self
368 .mi
369 .ok_or("mi (rock mass factor) must be set for mb calculation")?;
370 let gsi = self
371 .gsi
372 .ok_or("gsi (geological strength index) must be set for mb calculation")?;
373
374 let exponent = (gsi - 100.0) / (28.0 - (14.0 * self.disturbance));
375 Ok(mi * exponent.exp())
376 }
377
378 pub fn s(&self) -> Result<f64, &'static str> {
379 let gsi = self
380 .gsi
381 .ok_or("gsi (geological strength index) must be set for s calculation")?;
382
383 let exponent = (gsi - 100.0) / (9.0 - (3.0 * self.disturbance));
384 Ok(exponent.exp())
385 }
386
387 pub fn a(&self) -> Result<f64, &'static str> {
388 let gsi = self
389 .gsi
390 .ok_or("gsi (geological strength index) must be set for a calculation")?;
391
392 Ok(0.5 + ((-gsi / 15.0).exp() - (-20.0_f64 / 3.0).exp()))
393 }
394
395 fn hb_to_mc_conv(&self, sig3: f64) -> Result<f64, &'static str> {
396 let ucs = self.ucs.ok_or("ucs (unconfined compressive strength) must be set and nonzero for Hoek-Brown conversion")?;
397 if ucs == 0.0 {
398 return Err(
399 "ucs (unconfined compressive strength) must be nonzero for Hoek-Brown conversion",
400 );
401 }
402
403 let sig3n = sig3 / ucs;
404 let first_bit = 6.0 * self.a()? * self.mb()?;
405 let second_bit = (self.s()? + (self.mb()? * sig3n)).powf(self.a()? - 1.0);
406
407 Ok(first_bit * second_bit)
408 }
409
410 pub fn hb_equiv_phi_ang(&self, sig3: f64) -> Result<f64, &'static str> {
411 let top = self.hb_to_mc_conv(sig3)?;
412 let bottom = (2.0 * (1.0 + self.a()?) * (2.0 + self.a()?)) + top;
413
414 if bottom == 0.0 {
415 return Err("Denominator for equivalent phi angle calculation is zero");
416 }
417
418 Ok(top / bottom)
419 }
420
421 pub fn hb_equiv_c_prime(&self, sig3: f64) -> Result<f64, &'static str> {
422 let ucs = self.ucs.ok_or("ucs (unconfined compressive strength) must be set and nonzero for equivalent cohesion calculation")?;
423 if ucs == 0.0 {
424 return Err("ucs must be nonzero for equivalent cohesion calculation");
425 }
426
427 let sig3n = sig3 / ucs;
428 let a_val = self.a()?;
429 let s_val = self.s()?;
430 let mb_val = self.mb()?;
431
432 let first_brack = ((1.0 + (2.0 * a_val)) * s_val) + ((1.0 - a_val) * mb_val * sig3n);
433 let top = ucs * first_brack * ((s_val * mb_val * sig3n).powf(a_val - 1.0));
434 let denom = (1.0 + a_val) * (2.0 + a_val);
435
436 if denom == 0.0 {
437 return Err("Denominator for equivalent cohesion calculation is zero");
438 }
439
440 let sqrt_bit = 1.0 + (self.hb_to_mc_conv(sig3)? / denom);
441 if sqrt_bit < 0.0 {
442 return Err("sqrtBit for equivalent cohesion calculation is negative");
443 }
444
445 let bottom = denom * sqrt_bit.sqrt();
446 if bottom == 0.0 {
447 return Err("Denominator for equivalent cohesion calculation is zero");
448 }
449
450 Ok(top / bottom)
451 }
452
453 pub fn rock_e_val(&self) -> Result<f64, &'static str> {
454 let ucs = self.ucs.ok_or(
455 "ucs (unconfined compressive strength) must be set for Young's Modulus calculation",
456 )?;
457 let gsi = self
458 .gsi
459 .ok_or("gsi (geological strength index) must be set for Young's Modulus calculation")?;
460
461 let rock_val = if ucs < 100.0 { 1.0 } else { ucs / 100.0 };
462
463 Ok((1.0 - (self.disturbance / 2.0))
464 * rock_val.sqrt()
465 * (10.0_f64.powf((gsi - 10.0) / 40.0)))
466 }
467
468 pub fn convert_equivalent_rock(&self, sig3: f64) -> Result<SoilParams, &'static str> {
469 Ok(SoilParams::with_all_fields(
470 self.reference.clone(),
471 self.behaviour,
472 Some(self.hb_equiv_phi_ang(sig3)?),
473 Some(self.hb_equiv_c_prime(sig3)?),
474 self.unit_weight,
475 self.cu,
476 self.mv,
477 self.rock_e_val()?,
478 self.poissons_ratio,
479 self.coefficient_of_consolidation,
480 self.gsi,
481 self.ucs,
482 self.mi,
483 self.disturbance,
484 ))
485 }
486}
487
488impl Default for SoilParams {
489 fn default() -> Self {
490 SoilParams {
491 reference: String::new(),
492 behaviour: SoilType::Granular,
493 phi_prime: None, c_prime: None,
495 unit_weight: 0.0,
496 cu: None,
497 mv: 0.0,
498 youngs_modulus: 0.0,
499 poissons_ratio: 0.0,
500 coefficient_of_consolidation: 0.0,
501 gsi: None,
502 ucs: None,
503 mi: None,
504 disturbance: 0.0,
505 advanced_parameters: None,
506 factored: false,
507 factors: None,
508 }
509 }
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct SoilLayer {
514 pub unit_reference: String,
515 pub top_level: f64,
516 pub base_level: Option<f64>,
517 pub base_unit_reference: Option<String>,
518 pub typical_description: String,
519 pub geol_code: String,
520 pub reference: String, }
522
523impl SoilLayer {
524 pub fn new(top_level: f64, base_level: f64, reference: String) -> Self {
525 SoilLayer {
526 unit_reference: String::new(),
527 top_level,
528 base_level: Some(base_level),
529 base_unit_reference: None,
530 typical_description: String::new(),
531 geol_code: String::new(),
532 reference,
533 }
534 }
535
536 pub fn with_unit_reference(
537 top_level: f64,
538 base_level: f64,
539 reference: String,
540 unit_reference: String,
541 ) -> Self {
542 SoilLayer {
543 unit_reference,
544 top_level,
545 base_level: Some(base_level),
546 base_unit_reference: None,
547 typical_description: String::new(),
548 geol_code: String::new(),
549 reference,
550 }
551 }
552
553 pub fn with_all_fields(
554 unit_reference: String,
555 top_level: f64,
556 base_level: Option<f64>,
557 base_unit_reference: Option<String>,
558 typical_description: String,
559 geol_code: String,
560 ) -> Self {
561 SoilLayer {
562 unit_reference,
563 top_level,
564 base_level,
565 base_unit_reference,
566 typical_description,
567 geol_code,
568 reference: String::new(),
569 }
570 }
571
572 pub fn excavate_layer(&self, level: f64) -> (SoilLayer, bool) {
573 let base_level = self.base_level.unwrap_or(0.0);
574
575 if level < self.top_level && level > base_level {
576 (
578 SoilLayer {
579 unit_reference: self.unit_reference.clone(),
580 top_level: level,
581 base_level: self.base_level,
582 base_unit_reference: self.base_unit_reference.clone(),
583 typical_description: self.typical_description.clone(),
584 geol_code: self.geol_code.clone(),
585 reference: self.reference.clone(),
586 },
587 false,
588 )
589 } else if level > self.top_level {
590 (self.clone(), false)
592 } else {
593 (
595 SoilLayer {
596 unit_reference: self.unit_reference.clone(),
597 top_level: self.top_level,
598 base_level: self.base_level,
599 base_unit_reference: self.base_unit_reference.clone(),
600 typical_description: self.typical_description.clone(),
601 geol_code: "DELETE".to_string(),
602 reference: self.reference.clone(),
603 },
604 true,
605 )
606 }
607 }
608
609 pub fn process_layer(&self, sig3: f64, soil_type: SoilType) -> SoilLayer {
610 match soil_type {
611 SoilType::Rock => {
612 let mut processed = self.clone();
615 processed.typical_description = format!(
616 "{} (processed with sig3: {})",
617 processed.typical_description, sig3
618 );
619 processed
620 }
621 _ => self.clone(),
622 }
623 }
624}
625
626impl Default for SoilLayer {
627 fn default() -> Self {
628 SoilLayer {
629 unit_reference: String::new(),
630 top_level: 100.0,
631 base_level: Some(0.0),
632 base_unit_reference: None,
633 typical_description: String::new(),
634 geol_code: String::new(),
635 reference: String::new(),
636 }
637 }
638}
639
640#[derive(Debug, Serialize, Deserialize, Clone)]
641pub struct GroundModel {
642 pub soil_layers: Vec<SoilLayer>,
643 pub soil_params: Vec<SoilParams>,
644 pub rigid_boundary: Option<f64>,
645 pub groundwater: f64,
646 pub reference: String,
647}
648
649impl GroundModel {
650 pub fn new(soil_layers: Vec<SoilLayer>, soil_params: Vec<SoilParams>) -> Self {
651 GroundModel {
652 soil_layers,
653 soil_params,
654 rigid_boundary: None,
655 groundwater: 0.0,
656 reference: String::new(),
657 }
658 }
659
660 pub fn from_agsi_file(agsi_json: &serde_json::Value) -> Self {
661 let mut soil_params = Vec::new();
662
663 if let Some(agsi_models) = agsi_json["agsiModel"].as_array() {
664 if let Some(first_model) = agsi_models.get(0) {
665 if let Some(elements) = first_model["agsiModelElement"].as_array() {
666 for element in elements {
667 if let Some(param_values) = element["agsiDataParameterValue"].as_array() {
668 let params_data: Vec<AgsiDataParameterValue> = param_values
669 .iter()
670 .filter_map(|p| {
671 Some(AgsiDataParameterValue {
672 code_id: p["codeID"].as_str()?.parse().ok()?,
673 case_id: None,
674 data_id: None,
675 remarks: None,
676 value_numeric: p["valueNumeric"].as_f64(),
677 value_profile: None,
678 value_profile_ind_var_code_id: None,
679 value_text: None,
680 })
681 })
682 .collect();
683
684 if !params_data.is_empty() {
685 let mut soil_param =
686 SoilParams::from_agsi_data_parameters(¶ms_data);
687 soil_param.reference = element["elementName"]
688 .as_str()
689 .unwrap_or("unknown")
690 .to_string();
691 soil_params.push(soil_param);
692 }
693 }
694 }
695 }
696 }
697 }
698
699 GroundModel::new(Vec::new(), soil_params)
700 }
701
702 pub fn with_all_fields(
703 soil_layers: Vec<SoilLayer>,
704 soil_params: Vec<SoilParams>,
705 rigid_boundary: Option<f64>,
706 groundwater: f64,
707 reference: String,
708 ) -> Self {
709 GroundModel {
710 soil_layers,
711 soil_params,
712 rigid_boundary,
713 groundwater,
714 reference,
715 }
716 }
717
718 pub fn get_base_level(&self) -> f64 {
719 let mut base = 10000.0;
720 for layer in &self.soil_layers {
721 let layer_base = layer.base_level.unwrap_or(0.0);
722 if base > layer_base {
723 base = layer_base;
724 }
725 }
726 base
727 }
728
729 pub fn get_top_level(&self) -> f64 {
730 let mut top = -10000.0;
731 for layer in &self.soil_layers {
732 if top < layer.top_level {
733 top = layer.top_level;
734 }
735 }
736 top
737 }
738
739 pub fn get_soil_params(&self, reference: &str) -> Option<&SoilParams> {
740 self.soil_params
741 .iter()
742 .find(|param| param.reference == reference)
743 }
744
745 pub fn get_params_at_level(&self, level: f64) -> Result<&SoilParams, &'static str> {
746 for layer in &self.soil_layers {
747 let base_level = layer.base_level.unwrap_or(0.0);
748 if layer.top_level >= level && level >= base_level {
749 let unit_ref = &layer.unit_reference;
750 for param in &self.soil_params {
751 if param.reference == *unit_ref {
752 return Ok(param);
753 }
754 }
755 }
756 }
757 Err("Layer not present")
758 }
759
760 pub fn get_layer_at_level(&self, level: f64) -> Result<&SoilLayer, &'static str> {
761 for layer in &self.soil_layers {
762 let base_level = layer.base_level.unwrap_or(0.0);
763 if layer.top_level >= level && level >= base_level {
764 return Ok(layer);
765 }
766 }
767 Err("Layer not present")
768 }
769
770 pub fn get_soil_params_at_level(&self, level: f64) -> Option<&SoilParams> {
771 for (i, layer) in self.soil_layers.iter().enumerate() {
772 let base_level = layer.base_level.unwrap_or(0.0);
773 if level <= layer.top_level && level >= base_level {
774 return self.soil_params.get(i);
775 }
776 }
777 None
778 }
779
780 fn get_unit_weight_at_level(&self, level: f64) -> Result<f64, &'static str> {
781 Ok(self.get_params_at_level(level)?.unit_weight)
782 }
783
784 pub fn get_pwp_at_level(&self, level: f64) -> f64 {
785 if level > self.groundwater {
786 0.0
787 } else {
788 10.0 * (self.groundwater - level)
789 }
790 }
791
792 pub fn get_total_stress_at_level(&self, level: f64) -> f64 {
793 let top = self.get_top_level();
794 let spacing = 0.1;
795 let mut result = 0.0;
796 let mut current_level = level;
797
798 while current_level <= top {
799 if let Ok(unit_weight) = self.get_unit_weight_at_level(current_level) {
800 result += unit_weight;
801 }
802 current_level += spacing;
803 }
804
805 result * spacing
806 }
807
808 pub fn get_effective_stress_at_level(&self, level: f64) -> f64 {
809 self.get_total_stress_at_level(level) - self.get_pwp_at_level(level)
810 }
811
812 pub fn quick_init(soil_params: SoilParams, top_level: f64, groundwater_level: f64) -> Self {
813 let reference = if soil_params.reference.is_empty() {
814 "gm_soil".to_string()
815 } else {
816 soil_params.reference.clone()
817 };
818
819 let mut params = soil_params;
820 params.reference = reference.clone();
821
822 let layer = SoilLayer::with_all_fields(
823 reference.clone(),
824 top_level,
825 Some(top_level - 1000.0),
826 None,
827 String::new(),
828 String::new(),
829 );
830
831 GroundModel {
832 soil_layers: vec![layer],
833 soil_params: vec![params],
834 rigid_boundary: None,
835 groundwater: groundwater_level,
836 reference,
837 }
838 }
839
840 pub fn get_depth_at_level(&self, level: f64) -> Option<f64> {
841 if self.soil_layers.is_empty() {
842 return None;
843 }
844 let ground_surface = self.soil_layers[0].top_level;
845 if level > ground_surface {
846 return None;
847 }
848 Some(ground_surface - level)
849 }
850}
851
852impl Default for GroundModel {
853 fn default() -> Self {
854 GroundModel {
855 soil_layers: vec![SoilLayer::default()],
856 soil_params: vec![SoilParams::default()],
857 rigid_boundary: None,
858 groundwater: 0.0,
859 reference: String::new(),
860 }
861 }
862}
863
864#[cfg(test)]
865mod tests {
866 use super::*;
867
868 #[test]
869 fn test_ground_model_creation() {
870 let layers = vec![
871 SoilLayer::new(10.0, 5.0, "Layer 1".to_string()),
872 SoilLayer::new(5.0, 0.0, "Layer 2".to_string()),
873 ];
874 let params = vec![
875 SoilParams::new(
876 "Cohesive soil".to_string(),
877 0.1,
878 10000.0,
879 0.3,
880 1e-8,
881 SoilType::Cohesive,
882 18.0,
883 ),
884 SoilParams::new(
885 "Granular soil".to_string(),
886 0.0,
887 50000.0,
888 0.25,
889 0.0,
890 SoilType::Granular,
891 20.0,
892 ),
893 ];
894
895 let ground_model = GroundModel::new(layers, params);
896 assert_eq!(ground_model.soil_layers.len(), 2);
897 assert_eq!(ground_model.soil_params.len(), 2);
898 }
899
900 #[test]
901 fn test_get_soil_params_at_level() {
902 let layers = vec![
903 SoilLayer::new(10.0, 5.0, "Layer 1".to_string()),
904 SoilLayer::new(5.0, 0.0, "Layer 2".to_string()),
905 ];
906 let params = vec![
907 SoilParams::new(
908 "Cohesive soil".to_string(),
909 0.1,
910 10000.0,
911 0.3,
912 1e-8,
913 SoilType::Cohesive,
914 18.0,
915 ),
916 SoilParams::new(
917 "Granular soil".to_string(),
918 0.0,
919 50000.0,
920 0.25,
921 0.0,
922 SoilType::Granular,
923 20.0,
924 ),
925 ];
926
927 let ground_model = GroundModel::new(layers, params);
928
929 let soil_at_7 = ground_model.get_soil_params_at_level(7.0);
930 assert!(soil_at_7.is_some());
931 assert_eq!(soil_at_7.unwrap().behaviour, SoilType::Cohesive);
932
933 let soil_at_3 = ground_model.get_soil_params_at_level(3.0);
934 assert!(soil_at_3.is_some());
935 assert_eq!(soil_at_3.unwrap().behaviour, SoilType::Granular);
936
937 let soil_at_15 = ground_model.get_soil_params_at_level(15.0);
938 assert!(soil_at_15.is_none());
939 }
940
941 #[test]
942 fn test_get_depth_at_level() {
943 let layers = vec![SoilLayer::new(10.0, 5.0, "Layer 1".to_string())];
944 let params = vec![SoilParams::default()];
945
946 let ground_model = GroundModel::new(layers, params);
947
948 assert_eq!(ground_model.get_depth_at_level(8.0), Some(2.0));
949 assert_eq!(ground_model.get_depth_at_level(10.0), Some(0.0));
950 assert_eq!(ground_model.get_depth_at_level(12.0), None);
951 }
952
953 #[test]
954 fn test_get_base_and_top_level() {
955 let layers = vec![
956 SoilLayer::new(15.0, 8.0, "Layer 1".to_string()),
957 SoilLayer::new(8.0, 2.0, "Layer 2".to_string()),
958 SoilLayer::new(2.0, -5.0, "Layer 3".to_string()),
959 ];
960 let params = vec![SoilParams::default(); 3];
961
962 let ground_model = GroundModel::new(layers, params);
963
964 assert_eq!(ground_model.get_top_level(), 15.0);
965 assert_eq!(ground_model.get_base_level(), -5.0);
966 }
967
968 #[test]
969 fn test_get_soil_params_by_reference() {
970 let params = vec![
971 SoilParams::new(
972 "clay".to_string(),
973 0.1,
974 10000.0,
975 0.3,
976 1e-8,
977 SoilType::Cohesive,
978 18.0,
979 ),
980 SoilParams::new(
981 "sand".to_string(),
982 0.0,
983 50000.0,
984 0.25,
985 0.0,
986 SoilType::Granular,
987 20.0,
988 ),
989 ];
990
991 let ground_model = GroundModel::new(vec![], params);
992
993 let clay_params = ground_model.get_soil_params("clay");
994 assert!(clay_params.is_some());
995 assert_eq!(clay_params.unwrap().behaviour, SoilType::Cohesive);
996
997 let sand_params = ground_model.get_soil_params("sand");
998 assert!(sand_params.is_some());
999 assert_eq!(sand_params.unwrap().behaviour, SoilType::Granular);
1000
1001 let unknown_params = ground_model.get_soil_params("rock");
1002 assert!(unknown_params.is_none());
1003 }
1004
1005 #[test]
1006 fn test_get_params_at_level_with_unit_reference() {
1007 let layers = vec![
1008 SoilLayer::with_unit_reference(10.0, 5.0, "Layer 1".to_string(), "clay".to_string()),
1009 SoilLayer::with_unit_reference(5.0, 0.0, "Layer 2".to_string(), "sand".to_string()),
1010 ];
1011 let params = vec![
1012 SoilParams::new(
1013 "clay".to_string(),
1014 0.1,
1015 10000.0,
1016 0.3,
1017 1e-8,
1018 SoilType::Cohesive,
1019 18.0,
1020 ),
1021 SoilParams::new(
1022 "sand".to_string(),
1023 0.0,
1024 50000.0,
1025 0.25,
1026 0.0,
1027 SoilType::Granular,
1028 20.0,
1029 ),
1030 ];
1031
1032 let ground_model = GroundModel::new(layers, params);
1033
1034 let params_at_7 = ground_model.get_params_at_level(7.0);
1035 assert!(params_at_7.is_ok());
1036 assert_eq!(params_at_7.unwrap().behaviour, SoilType::Cohesive);
1037
1038 let params_at_3 = ground_model.get_params_at_level(3.0);
1039 assert!(params_at_3.is_ok());
1040 assert_eq!(params_at_3.unwrap().behaviour, SoilType::Granular);
1041 }
1042
1043 #[test]
1044 fn test_pwp_calculation() {
1045 let ground_model = GroundModel::with_all_fields(
1046 vec![],
1047 vec![],
1048 None,
1049 5.0, "test".to_string(),
1051 );
1052
1053 assert_eq!(ground_model.get_pwp_at_level(6.0), 0.0);
1055 assert_eq!(ground_model.get_pwp_at_level(5.0), 0.0);
1056
1057 assert_eq!(ground_model.get_pwp_at_level(4.0), 10.0);
1059 assert_eq!(ground_model.get_pwp_at_level(3.0), 20.0);
1060 }
1061
1062 #[test]
1063 fn test_quick_init() {
1064 let soil_params = SoilParams::new(
1065 "test_soil".to_string(),
1066 0.1,
1067 10000.0,
1068 0.3,
1069 1e-8,
1070 SoilType::Cohesive,
1071 18.0,
1072 );
1073
1074 let ground_model = GroundModel::quick_init(soil_params, 10.0, 5.0);
1075
1076 assert_eq!(ground_model.soil_layers.len(), 1);
1077 assert_eq!(ground_model.soil_params.len(), 1);
1078 assert_eq!(ground_model.groundwater, 5.0);
1079 assert_eq!(ground_model.soil_layers[0].top_level, 10.0);
1080 assert_eq!(ground_model.soil_layers[0].base_level, Some(-990.0));
1081 assert_eq!(ground_model.soil_params[0].reference, "test_soil");
1082 }
1083
1084 #[test]
1085 fn test_quick_init_with_empty_reference() {
1086 let soil_params = SoilParams::default(); let ground_model = GroundModel::quick_init(soil_params, 10.0, 5.0);
1089
1090 assert_eq!(ground_model.soil_params[0].reference, "gm_soil");
1091 assert_eq!(ground_model.reference, "gm_soil");
1092 }
1093
1094 #[test]
1095 fn test_layer_at_level() {
1096 let layers = vec![
1097 SoilLayer::new(10.0, 5.0, "Upper layer".to_string()),
1098 SoilLayer::new(5.0, 0.0, "Lower layer".to_string()),
1099 ];
1100
1101 let ground_model = GroundModel::new(layers, vec![]);
1102
1103 let layer_at_7 = ground_model.get_layer_at_level(7.0);
1104 assert!(layer_at_7.is_ok());
1105 assert_eq!(layer_at_7.unwrap().reference, "Upper layer");
1106
1107 let layer_at_3 = ground_model.get_layer_at_level(3.0);
1108 assert!(layer_at_3.is_ok());
1109 assert_eq!(layer_at_3.unwrap().reference, "Lower layer");
1110
1111 let layer_at_15 = ground_model.get_layer_at_level(15.0);
1112 assert!(layer_at_15.is_err());
1113 }
1114
1115 #[test]
1116 fn test_soil_layer_new_structure() {
1117 let layer = SoilLayer::with_all_fields(
1118 "CLAY".to_string(),
1119 10.0,
1120 Some(5.0),
1121 Some("SAND".to_string()),
1122 "Firm brown clay".to_string(),
1123 "CL".to_string(),
1124 );
1125
1126 assert_eq!(layer.unit_reference, "CLAY");
1127 assert_eq!(layer.top_level, 10.0);
1128 assert_eq!(layer.base_level, Some(5.0));
1129 assert_eq!(layer.base_unit_reference, Some("SAND".to_string()));
1130 assert_eq!(layer.typical_description, "Firm brown clay");
1131 assert_eq!(layer.geol_code, "CL");
1132 }
1133
1134 #[test]
1135 fn test_excavate_layer() {
1136 let layer = SoilLayer::with_all_fields(
1137 "CLAY".to_string(),
1138 10.0,
1139 Some(5.0),
1140 None,
1141 "Firm brown clay".to_string(),
1142 "CL".to_string(),
1143 );
1144
1145 let (excavated, should_delete) = layer.excavate_layer(7.0);
1147 assert!(!should_delete);
1148 assert_eq!(excavated.top_level, 7.0);
1149 assert_eq!(excavated.base_level, Some(5.0));
1150
1151 let (excavated, should_delete) = layer.excavate_layer(12.0);
1153 assert!(!should_delete);
1154 assert_eq!(excavated.top_level, 10.0);
1155
1156 let (excavated, should_delete) = layer.excavate_layer(3.0);
1158 assert!(should_delete);
1159 assert_eq!(excavated.geol_code, "DELETE");
1160 }
1161
1162 #[test]
1163 fn test_process_layer() {
1164 let layer = SoilLayer::with_all_fields(
1165 "ROCK".to_string(),
1166 10.0,
1167 Some(5.0),
1168 None,
1169 "Weathered limestone".to_string(),
1170 "LS".to_string(),
1171 );
1172
1173 let processed = layer.process_layer(100.0, SoilType::Rock);
1174 assert!(processed
1175 .typical_description
1176 .contains("processed with sig3: 100"));
1177
1178 let processed_soil = layer.process_layer(0.0, SoilType::Cohesive);
1179 assert_eq!(processed_soil.typical_description, "Weathered limestone");
1180 }
1181
1182 #[test]
1183 fn test_updated_ground_model_with_optional_base_level() {
1184 let layers = vec![
1185 SoilLayer::with_all_fields(
1186 "clay".to_string(),
1187 10.0,
1188 Some(5.0),
1189 None,
1190 "Firm brown clay".to_string(),
1191 "CL".to_string(),
1192 ),
1193 SoilLayer::with_all_fields(
1194 "sand".to_string(),
1195 5.0,
1196 None, None,
1198 "Dense sand".to_string(),
1199 "SP".to_string(),
1200 ),
1201 ];
1202 let params = vec![
1203 SoilParams::new(
1204 "clay".to_string(),
1205 0.1,
1206 10000.0,
1207 0.3,
1208 1e-8,
1209 SoilType::Cohesive,
1210 18.0,
1211 ),
1212 SoilParams::new(
1213 "sand".to_string(),
1214 0.0,
1215 50000.0,
1216 0.25,
1217 0.0,
1218 SoilType::Granular,
1219 20.0,
1220 ),
1221 ];
1222
1223 let ground_model = GroundModel::new(layers, params);
1224
1225 let params_at_7 = ground_model.get_params_at_level(7.0);
1227 assert!(params_at_7.is_ok());
1228 assert_eq!(params_at_7.unwrap().behaviour, SoilType::Cohesive);
1229
1230 let params_at_3 = ground_model.get_params_at_level(3.0);
1232 assert!(params_at_3.is_ok());
1233 assert_eq!(params_at_3.unwrap().behaviour, SoilType::Granular);
1234 }
1235
1236 #[test]
1237 fn test_soil_layer_default() {
1238 let layer = SoilLayer::default();
1239 assert_eq!(layer.unit_reference, "");
1240 assert_eq!(layer.top_level, 100.0);
1241 assert_eq!(layer.base_level, Some(0.0));
1242 assert_eq!(layer.base_unit_reference, None);
1243 assert_eq!(layer.typical_description, "");
1244 assert_eq!(layer.geol_code, "");
1245 }
1246
1247 #[test]
1248 fn test_soil_params_new_structure() {
1249 let params = SoilParams::with_all_fields(
1250 "test_soil".to_string(),
1251 SoilType::Cohesive,
1252 Some(0.5), Some(10.0), 18.0, Some(50.0), 0.1, 10000.0, 0.3, 1e-8, Some(65.0), Some(25.0), Some(10.0), 0.0, );
1265
1266 assert_eq!(params.reference, "test_soil");
1267 assert_eq!(params.behaviour, SoilType::Cohesive);
1268 assert_eq!(params.phi_prime, Some(0.5));
1269 assert_eq!(params.c_prime, Some(10.0));
1270 assert_eq!(params.unit_weight, 18.0);
1271 assert_eq!(params.cu, Some(50.0));
1272 assert_eq!(params.gsi, Some(65.0));
1273 assert_eq!(params.ucs, Some(25.0));
1274 assert_eq!(params.mi, Some(10.0));
1275 assert!(!params.factored);
1276 }
1277
1278 #[test]
1279 fn test_partial_factors() {
1280 let pf = PartialFactors::new(1.25, 1.25, 1.1, 1.4); let params = SoilParams::with_all_fields(
1282 "test".to_string(),
1283 SoilType::Cohesive,
1284 Some(0.5), Some(20.0),
1286 18.0,
1287 Some(100.0),
1288 0.1,
1289 10000.0,
1290 0.3,
1291 1e-8,
1292 None,
1293 None,
1294 None,
1295 0.0,
1296 );
1297
1298 let factored = params.apply_partial_factors(&pf);
1299 assert!(factored.factored);
1300 assert!(factored.factors.is_some());
1301
1302 assert!(factored.phi_prime.unwrap() < params.phi_prime.unwrap());
1304 assert!(factored.c_prime.unwrap() < params.c_prime.unwrap());
1305 assert!(factored.unit_weight < params.unit_weight); assert!(factored.cu.unwrap() < params.cu.unwrap());
1307
1308 let unfactored = factored.remove_partial_factors().unwrap();
1310 assert!(!unfactored.factored);
1311 assert!(unfactored.factors.is_none());
1312
1313 let phi_diff = (unfactored.phi_prime.unwrap() - params.phi_prime.unwrap()).abs();
1315 assert!(phi_diff < 1e-10);
1316 }
1317
1318 #[test]
1319 fn test_earth_pressure_coefficients() {
1320 let params = SoilParams::with_all_fields(
1321 "test".to_string(),
1322 SoilType::Granular,
1323 Some(30.0_f64.to_radians()), Some(0.0),
1325 20.0,
1326 None,
1327 0.0,
1328 50000.0,
1329 0.25,
1330 0.0,
1331 None,
1332 None,
1333 None,
1334 0.0,
1335 );
1336
1337 let k_active = params.get_k_active(None).unwrap();
1339 let expected_ka = (1.0 - 30.0_f64.to_radians().sin()) / (1.0 + 30.0_f64.to_radians().sin());
1340 assert!((k_active - expected_ka).abs() < 1e-10);
1341
1342 let k_passive = params.get_k_passive(None).unwrap();
1344 let expected_kp = 1.0 / expected_ka;
1345 assert!((k_passive - expected_kp).abs() < 1e-10);
1346
1347 let k0 = params.k0().unwrap();
1349 let expected_k0 = 1.0 - 30.0_f64.to_radians().sin();
1350 assert!((k0 - expected_k0).abs() < 1e-10);
1351 }
1352
1353 #[test]
1354 fn test_hoek_brown_parameters() {
1355 let params = SoilParams::with_all_fields(
1356 "rock".to_string(),
1357 SoilType::Rock,
1358 None,
1359 None,
1360 25.0,
1361 None,
1362 0.0,
1363 0.0,
1364 0.25,
1365 0.0,
1366 Some(65.0), Some(25.0), Some(10.0), 0.0, );
1371
1372 let mb = params.mb().unwrap();
1374 assert!(mb > 0.0);
1375
1376 let s = params.s().unwrap();
1378 assert!(s > 0.0);
1379
1380 let a = params.a().unwrap();
1382 assert!(a > 0.0 && a < 1.0);
1383
1384 let e_val = params.rock_e_val().unwrap();
1386 assert!(e_val > 0.0);
1387 }
1388
1389 #[test]
1390 fn test_hoek_brown_conversion() {
1391 let params = SoilParams::with_all_fields(
1392 "rock".to_string(),
1393 SoilType::Rock,
1394 None,
1395 None,
1396 25.0,
1397 None,
1398 0.0,
1399 0.0,
1400 0.25,
1401 0.0,
1402 Some(65.0), Some(25.0), Some(10.0), 0.0, );
1407
1408 let sig3 = 100.0; let phi_equiv = params.hb_equiv_phi_ang(sig3).unwrap();
1412 assert!(phi_equiv >= 0.0 && phi_equiv <= 1.0); let c_equiv = params.hb_equiv_c_prime(sig3).unwrap();
1416 assert!(c_equiv > 0.0);
1417
1418 let converted = params.convert_equivalent_rock(sig3).unwrap();
1420 assert!(converted.phi_prime.is_some());
1421 assert!(converted.c_prime.is_some());
1422 assert!(converted.phi_prime.unwrap() > 0.0);
1423 assert!(converted.c_prime.unwrap() > 0.0);
1424 assert_eq!(converted.unit_weight, params.unit_weight);
1425 }
1426
1427 #[test]
1428 fn test_error_handling() {
1429 let params = SoilParams::default();
1430
1431 assert!(params.get_k_active(None).is_err());
1433 assert!(params.k0().is_err());
1434 assert!(params.mb().is_err());
1435 assert!(params.s().is_err());
1436 assert!(params.a().is_err());
1437 assert!(params.rock_e_val().is_err());
1438 assert!(params.hb_equiv_phi_ang(100.0).is_err());
1439 assert!(params.hb_equiv_c_prime(100.0).is_err());
1440 assert!(params.convert_equivalent_rock(100.0).is_err());
1441
1442 assert!(params.remove_partial_factors().is_err());
1444 }
1445
1446 #[test]
1447 fn test_from_agsi_data_parameters() {
1448 let data = vec![
1449 AgsiDataParameterValue {
1450 code_id: "UnitWeight".parse().unwrap(),
1451 case_id: None,
1452 data_id: None,
1453 remarks: None,
1454 value_numeric: Some(18.0),
1455 value_profile: None,
1456 value_profile_ind_var_code_id: None,
1457 value_text: None,
1458 },
1459 AgsiDataParameterValue {
1460 code_id: "EffectiveCohesion".parse().unwrap(),
1461 case_id: None,
1462 data_id: None,
1463 remarks: None,
1464 value_numeric: Some(5.0),
1465 value_profile: None,
1466 value_profile_ind_var_code_id: None,
1467 value_text: None,
1468 },
1469 AgsiDataParameterValue {
1470 code_id: "EffectiveFrictionAngle".parse().unwrap(),
1471 case_id: None,
1472 data_id: None,
1473 remarks: None,
1474 value_numeric: Some(30.0),
1475 value_profile: None,
1476 value_profile_ind_var_code_id: None,
1477 value_text: None,
1478 },
1479 AgsiDataParameterValue {
1480 code_id: "YoungsModulus".parse().unwrap(),
1481 case_id: None,
1482 data_id: None,
1483 remarks: None,
1484 value_numeric: Some(50000.0),
1485 value_profile: None,
1486 value_profile_ind_var_code_id: None,
1487 value_text: None,
1488 },
1489 ];
1490
1491 let soil_params = SoilParams::from_agsi_data_parameters(&data);
1492
1493 assert_eq!(soil_params.unit_weight, 18.0);
1494 assert_eq!(soil_params.phi_prime, Some(30.0));
1495 assert_eq!(soil_params.c_prime, Some(5.0));
1496 assert_eq!(soil_params.youngs_modulus, 50000.0);
1497 assert_eq!(soil_params.behaviour, SoilType::Granular); }
1499
1500 #[test]
1501 fn test_from_agsi_data_parameters_cohesive() {
1502 let data = vec![AgsiDataParameterValue {
1503 code_id: "UndrainedShearStrength".parse().unwrap(),
1504 case_id: None,
1505 data_id: None,
1506 remarks: None,
1507 value_numeric: Some(100.0),
1508 value_profile: None,
1509 value_profile_ind_var_code_id: None,
1510 value_text: None,
1511 }];
1512
1513 let soil_params = SoilParams::from_agsi_data_parameters(&data);
1514
1515 assert_eq!(soil_params.cu, Some(100.0));
1516 assert_eq!(soil_params.behaviour, SoilType::Cohesive);
1517 }
1518
1519 #[test]
1520 fn test_from_agsi_data_parameters_rock() {
1521 let data = vec![
1522 AgsiDataParameterValue {
1523 code_id: "UnconfinedCompressiveStrength".parse().unwrap(),
1524 case_id: None,
1525 data_id: None,
1526 remarks: None,
1527 value_numeric: Some(25.0),
1528 value_profile: None,
1529 value_profile_ind_var_code_id: None,
1530 value_text: None,
1531 },
1532 AgsiDataParameterValue {
1533 code_id: "GeologicalStrengthIndex".parse().unwrap(),
1534 case_id: None,
1535 data_id: None,
1536 remarks: None,
1537 value_numeric: Some(65.0),
1538 value_profile: None,
1539 value_profile_ind_var_code_id: None,
1540 value_text: None,
1541 },
1542 AgsiDataParameterValue {
1543 code_id: "HoekBrownParamMi".parse().unwrap(),
1544 case_id: None,
1545 data_id: None,
1546 remarks: None,
1547 value_numeric: Some(10.0),
1548 value_profile: None,
1549 value_profile_ind_var_code_id: None,
1550 value_text: None,
1551 },
1552 ];
1553
1554 let soil_params = SoilParams::from_agsi_data_parameters(&data);
1555
1556 assert_eq!(soil_params.ucs, Some(25.0));
1557 assert_eq!(soil_params.gsi, Some(65.0));
1558 assert_eq!(soil_params.mi, Some(10.0));
1559 assert_eq!(soil_params.behaviour, SoilType::Rock);
1560 }
1561
1562 #[test]
1563 fn test_from_agsi_data_parameters_advanced_params() {
1564 let data = vec![
1565 AgsiDataParameterValue {
1566 code_id: "CustomParameter".parse().unwrap(),
1567 case_id: None,
1568 data_id: None,
1569 remarks: None,
1570 value_numeric: Some(42.0),
1571 value_profile: None,
1572 value_profile_ind_var_code_id: None,
1573 value_text: None,
1574 },
1575 AgsiDataParameterValue {
1576 code_id: "AnotherCustom".parse().unwrap(),
1577 case_id: None,
1578 data_id: None,
1579 remarks: None,
1580 value_numeric: Some(123.0),
1581 value_profile: None,
1582 value_profile_ind_var_code_id: None,
1583 value_text: None,
1584 },
1585 ];
1586
1587 let soil_params = SoilParams::from_agsi_data_parameters(&data);
1588
1589 assert!(soil_params.advanced_parameters.is_some());
1590 let advanced = soil_params.advanced_parameters.unwrap();
1591 assert_eq!(advanced.len(), 2);
1592 assert_eq!(advanced[0].name, "CustomParameter");
1593 assert_eq!(advanced[0].value, 42.0);
1594 assert_eq!(advanced[1].name, "AnotherCustom");
1595 assert_eq!(advanced[1].value, 123.0);
1596 }
1597}