groundmodels_core/
lib.rs

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