Skip to main content

groundmodels_core/
lib.rs

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(&params_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), // TODO: implement Coulomb method for passive
358        }
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, // Set to None so error handling tests work
494            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, // keeping for backward compatibility
521}
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            // Excavation cuts through the layer
577            (
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            // Excavation is above the layer - keep the layer
591            (self.clone(), false)
592        } else {
593            // Excavation removes the entire layer
594            (
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                // In a real implementation, you would apply rock conversion logic here
613                // For now, just return a clone
614                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(&params_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, // groundwater at 5.0 mAOD
1050            "test".to_string(),
1051        );
1052
1053        // Above groundwater
1054        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        // Below groundwater
1058        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(); // Empty reference
1087
1088        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        // Excavation cuts through the layer
1146        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        // Excavation above the layer
1152        let (excavated, should_delete) = layer.excavate_layer(12.0);
1153        assert!(!should_delete);
1154        assert_eq!(excavated.top_level, 10.0);
1155
1156        // Excavation removes the entire layer
1157        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, // No base level specified
1197                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        // Test with layer that has base_level
1226        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        // Test with layer that has no base_level (should default to 0.0)
1231        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),  // phi_prime in radians
1253            Some(10.0), // c_prime in kPa
1254            18.0,       // unit_weight
1255            Some(50.0), // cu
1256            0.1,        // mv
1257            10000.0,    // youngs_modulus
1258            0.3,        // poissons_ratio
1259            1e-8,       // coefficient_of_consolidation
1260            Some(65.0), // gsi
1261            Some(25.0), // ucs
1262            Some(10.0), // mi
1263            0.0,        // disturbance
1264        );
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); // Changed gamma_gamma to 1.1
1281        let params = SoilParams::with_all_fields(
1282            "test".to_string(),
1283            SoilType::Cohesive,
1284            Some(0.5), // ~28.6 degrees
1285            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        // Check that values are reduced
1303        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); // Now this should work
1306        assert!(factored.cu.unwrap() < params.cu.unwrap());
1307
1308        // Test removing factors
1309        let unfactored = factored.remove_partial_factors().unwrap();
1310        assert!(!unfactored.factored);
1311        assert!(unfactored.factors.is_none());
1312
1313        // Values should be approximately back to original (within floating point precision)
1314        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()), // 30 degrees
1324            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        // Test K_active for 30 degree friction angle
1338        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        // Test K_passive
1343        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        // Test K0
1348        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), // gsi
1367            Some(25.0), // ucs in MPa
1368            Some(10.0), // mi
1369            0.0,        // disturbance
1370        );
1371
1372        // Test mb calculation
1373        let mb = params.mb().unwrap();
1374        assert!(mb > 0.0);
1375
1376        // Test s calculation
1377        let s = params.s().unwrap();
1378        assert!(s > 0.0);
1379
1380        // Test a calculation
1381        let a = params.a().unwrap();
1382        assert!(a > 0.0 && a < 1.0);
1383
1384        // Test rock E value
1385        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), // gsi
1403            Some(25.0), // ucs in MPa
1404            Some(10.0), // mi
1405            0.0,        // disturbance
1406        );
1407
1408        let sig3 = 100.0; // confining stress in kPa
1409
1410        // Test equivalent phi angle
1411        let phi_equiv = params.hb_equiv_phi_ang(sig3).unwrap();
1412        assert!(phi_equiv >= 0.0 && phi_equiv <= 1.0); // Should be reasonable phi value in radians
1413
1414        // Test equivalent cohesion
1415        let c_equiv = params.hb_equiv_c_prime(sig3).unwrap();
1416        assert!(c_equiv > 0.0);
1417
1418        // Test full conversion
1419        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        // Test that methods fail appropriately when required values are missing
1432        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        // Test removing factors when none are present
1443        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); // Default behavior
1498    }
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}