Skip to main content

cmtool_assemble/generators/
mod.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use crate::CMError;
7mod artefact;
8pub use artefact::GenerateContract;
9mod descriptors;
10
11use cmtool_data::{
12    CMAExportType, CMCase, CMCaseJson, CMCaseReader, DEFAULT_CASE_FILE_NAME, PhaseCM, RawData,
13    RawDataFlux, RawDataScalar, RawFlux, RawPhase,
14};
15pub use descriptors::{PFRDescription, Reactor0DDescriptor};
16
17const LIQUID_PAIR: (CMAExportType, CMAExportType) =
18    (CMAExportType::LiquidFlow, CMAExportType::LiquidVolume);
19
20const GAS_PAIR: (CMAExportType, CMAExportType) = (CMAExportType::GasFlow, CMAExportType::GasVolume);
21
22type PairType = (CMAExportType, CMAExportType);
23
24const PAIRS: (PairType, PairType) = (LIQUID_PAIR, GAS_PAIR);
25
26//TODO improve and change name
27pub struct Generator {
28    raw_phase: Vec<RawPhase>,
29    ///Case directory of the reactors read from an existing flow map, by reactor id
30    existing_case: HashMap<String, PathBuf>,
31}
32
33struct Field0D {
34    #[allow(unused)]
35    name: String,
36    #[allow(unused)]
37    value: f64,
38}
39///Create vector of raw phase from raw
40fn raw_phase_from_flow_vol(
41    flows: Vec<RawDataFlux>,
42    vol: Vec<RawDataScalar>,
43    phase: PhaseCM,
44) -> Vec<RawPhase> {
45    flows
46        .into_iter()
47        .zip(vol)
48        .map(move |(f, v)| RawPhase {
49            flow: f,
50            volume: v,
51            identifier: phase,
52        })
53        .collect()
54}
55
56///Neutral gas phase of a liquid-only reactor, so that gas compartment ids stay aligned with
57///liquid ones when merging
58fn default_gas_phase(n_zone: usize) -> RawPhase {
59    let mut volume = RawDataScalar::new(n_zone);
60    volume.values.push((1e-9).into());
61    RawPhase {
62        flow: RawDataFlux::new(n_zone, 1),
63        volume,
64        identifier: PhaseCM::Gas,
65    }
66}
67
68///Read the phases of a case directory, gas being None when the case does not declare one
69fn read_case_phases(
70    case_dir: &std::path::Path,
71) -> Result<(RawPhase, Option<RawPhase>, [u32; 3]), CMError> {
72    let case = CMCaseJson::read_case(&case_dir.join(DEFAULT_CASE_FILE_NAME))?;
73
74    let read_phase = |flow_type: CMAExportType,
75                      volume_type: CMAExportType,
76                      identifier: PhaseCM|
77     -> Result<Option<RawPhase>, CMError> {
78        let (Some(flow_path), Some(volume_path)) = (
79            case.resolve(path_str(case_dir)?, flow_type),
80            case.resolve(path_str(case_dir)?, volume_type),
81        ) else {
82            return Ok(None);
83        };
84
85        let (Some(flow), Some(volume)) = (
86            RawDataFlux::read_raw(flow_path),
87            RawDataScalar::read_raw(volume_path),
88        ) else {
89            return Err(CMError::Custom(format!(
90                "Error reading {:?} of case {}",
91                identifier,
92                case_dir.display()
93            )));
94        };
95
96        Ok(Some(RawPhase {
97            flow,
98            volume,
99            identifier,
100        }))
101    };
102
103    let (liquid_flow, liquid_volume) = PAIRS.0;
104    let (gas_flow, gas_volume) = PAIRS.1;
105
106    let liquid = read_phase(liquid_flow, liquid_volume, PhaseCM::Liquid)?.ok_or_else(|| {
107        CMError::Custom(format!(
108            "Case {} does not provide a liquid flow map",
109            case_dir.display()
110        ))
111    })?;
112    let gas = read_phase(gas_flow, gas_volume, PhaseCM::Gas)?;
113
114    Ok((liquid, gas, case.n_div))
115}
116
117fn path_str(path: &std::path::Path) -> Result<&str, CMError> {
118    path.to_str()
119        .ok_or_else(|| CMError::Custom(format!("Non UTF-8 path {}", path.display())))
120}
121
122const MERGE_FOLDER_NAME: &str = "merged";
123
124//public
125impl Generator {
126    pub fn new() -> Self {
127        Self {
128            raw_phase: Default::default(),
129            existing_case: Default::default(),
130        }
131    }
132
133    pub fn generate_0d(
134        &mut self,
135        descriptor: Reactor0DDescriptor,
136        dest: Option<String>,
137    ) -> Result<CMCase, CMError> {
138        self.impl_generate_0d(descriptor, None, dest)
139    }
140
141    pub fn generate_1d(
142        &mut self,
143        descriptor: PFRDescription,
144        out_dir: Option<String>,
145    ) -> Result<CMCase, CMError> {
146        //Method expects descriptor has been validated with is_valid method
147
148        let mut case = CMCase::default();
149        case.n_div = [0, 0, descriptor.n_compartment as u32];
150
151        self.impl_generate_1d(&mut case, &descriptor, PhaseCM::Liquid, out_dir.clone())?;
152        if descriptor.gas_fraction != 0. {
153            self.impl_generate_1d(&mut case, &descriptor, PhaseCM::Gas, out_dir)?;
154        }
155
156        Ok(case)
157    }
158
159    pub fn merge_from_memory(
160        &self,
161        connections: Option<[RawDataFlux; 2]>,
162    ) -> Result<GenerateContract, CMError> {
163        let reactors = self.phases_by_reactor();
164        let has_gas = reactors.iter().any(|(_, gas)| gas.is_some());
165
166        //A liquid-only reactor of a two-phase domain still occupies its gas compartments,
167        //skipping it would shift every gas id coming after it
168        let filler: Vec<Option<RawPhase>> = reactors
169            .iter()
170            .map(|(liquid, gas)| {
171                (has_gas && gas.is_none())
172                    .then(|| default_gas_phase(liquid.flow.header.n_zone as usize))
173            })
174            .collect();
175
176        let liquid_phase = reactors.iter().map(|(liquid, _)| *liquid);
177        let gas_phase = reactors
178            .iter()
179            .zip(&filler)
180            .filter_map(|((_, gas), filler)| gas.or(filler.as_ref()));
181
182        let case = CMCase::default();
183        let relative = Some(String::from(MERGE_FOLDER_NAME));
184
185        let (liquid_phase, gas_phase) = Self::impl_merge(liquid_phase, gas_phase, connections)?;
186
187        Ok(GenerateContract::new(
188            case,
189            liquid_phase,
190            gas_phase,
191            relative,
192        ))
193    }
194
195    ///Phases of each reactor in declaration order. Every reactor pushes its liquid phase first
196    ///and its gas one right after when it has one, which is what pairs them back together.
197    fn phases_by_reactor(&self) -> Vec<(&RawPhase, Option<&RawPhase>)> {
198        let mut reactors: Vec<(&RawPhase, Option<&RawPhase>)> = Vec::new();
199        for phase in &self.raw_phase {
200            match phase.identifier {
201                PhaseCM::Liquid => reactors.push((phase, None)),
202                PhaseCM::Gas => {
203                    if let Some(reactor) = reactors.last_mut() {
204                        reactor.1 = Some(phase);
205                    }
206                }
207            }
208        }
209        reactors
210    }
211
212    ///Declare a reactor whose flow map already exists instead of being generated. Its case is
213    ///read where it lies, merge resolves the reactor id through existing_case.
214    pub fn add_existing_case(
215        &mut self,
216        id: &str,
217        case_dir: &str,
218        keep_in_memory: bool,
219    ) -> Result<(), CMError> {
220        let case_dir = PathBuf::from(case_dir);
221
222        if keep_in_memory {
223            let (liquid, gas, _) = read_case_phases(&case_dir)?;
224            self.raw_phase.push(liquid);
225            if let Some(gas) = gas {
226                self.raw_phase.push(gas);
227            }
228        }
229
230        self.existing_case.insert(id.to_owned(), case_dir);
231        Ok(())
232    }
233
234    pub fn merge(
235        &self,
236        root_dir: impl AsRef<std::path::Path>,
237        ids: &[String],
238        connections: Option<[RawDataFlux; 2]>,
239    ) -> Result<GenerateContract, CMError> {
240        let mut n_div = [0, 0, 0];
241        //helper
242        let mut add_ndiv = |n: &[u32; 3]| {
243            n_div[0] += n[0];
244            n_div[1] += n[1];
245            n_div[2] += n[2];
246        };
247
248        //prealloc
249        let mut liquid_flows = Vec::with_capacity(ids.len());
250        let mut liquid_volumes = Vec::with_capacity(ids.len());
251        let mut gas_flows = Vec::with_capacity(ids.len());
252        let mut gas_volumes = Vec::with_capacity(ids.len());
253
254        let mut impl_merge_reactor = |id: &String| -> Result<(), CMError> {
255            //An existing flow map stays where it is, a generated one is written next to its peers
256            let case_dir = self
257                .existing_case
258                .get(id)
259                .cloned()
260                .unwrap_or_else(|| root_dir.as_ref().join(id));
261            let (liquid, gas, partial_n_div) = read_case_phases(&case_dir)?;
262
263            let n_zone = liquid.flow.header.n_zone as usize;
264            add_ndiv(&partial_n_div);
265            liquid_flows.push(liquid.flow);
266            liquid_volumes.push(liquid.volume);
267
268            // Default implementation of RawFlux implies correct workaround
269            let gas = gas.unwrap_or_else(|| default_gas_phase(n_zone));
270            gas_flows.push(gas.flow);
271            gas_volumes.push(gas.volume);
272
273            Ok(())
274        };
275
276        ids.iter().try_for_each(&mut impl_merge_reactor)?;
277
278        let merge_path = root_dir.as_ref().join(MERGE_FOLDER_NAME);
279
280        std::fs::create_dir_all(&merge_path)?;
281        let mut case = CMCase::default();
282        case.n_div = n_div;
283
284        let liquid_phases = raw_phase_from_flow_vol(liquid_flows, liquid_volumes, PhaseCM::Liquid);
285        let gas_phases = raw_phase_from_flow_vol(gas_flows, gas_volumes, PhaseCM::Gas);
286
287        let (liquid_phase, gas_phase) =
288            Self::impl_merge(liquid_phases.iter(), gas_phases.iter(), connections)?;
289
290        Ok(GenerateContract::new(
291            // &path,
292            case,
293            liquid_phase,
294            gas_phase,
295            None,
296        ))
297    }
298}
299
300//private impl
301impl Generator {
302    fn generate_0d_phase(
303        &mut self,
304        case: &mut CMCase,
305        volume: f64,
306        phase: PhaseCM,
307        dest: Option<String>,
308    ) -> Result<(), CMError> {
309        let mut phase = cmtool_data::RawPhase::new(1, 1, phase);
310        phase.flow.fluxes[0] = Default::default(); //Not usefull because new already makes default
311        phase.volume.values.push(volume.into());
312
313        match dest {
314            Some(s) => {
315                *case = GenerateContract::new_single_phase(case.clone(), phase, None).write(&s)?;
316                Ok(())
317            }
318            None => {
319                self.raw_phase.push(phase);
320                Ok(())
321            }
322        }
323    }
324
325    fn impl_generate_0d(
326        &mut self,
327        descriptor: Reactor0DDescriptor,
328        fields: Option<&[Field0D]>,
329        dest: Option<String>,
330    ) -> Result<CMCase, CMError> {
331        let mut case = CMCase::default();
332        case.n_div = [1, 0, 0];
333
334        self.generate_0d_phase(
335            &mut case,
336            descriptor.liquid_volume(),
337            PhaseCM::Liquid,
338            dest.clone(),
339        )?;
340        let gas_volume = descriptor.gas_volume();
341        if gas_volume != 0. {
342            self.generate_0d_phase(&mut case, gas_volume, PhaseCM::Gas, dest)?;
343        }
344
345        if let Some(_scalars) = fields {
346            todo!("Scalar field")
347        }
348
349        Ok(case)
350    }
351
352    fn impl_generate_1d(
353        &mut self,
354        case: &mut CMCase,
355        descriptor: &PFRDescription,
356        phase_d: PhaseCM,
357        out_dir: Option<String>,
358    ) -> Result<(), CMError> {
359        //Get needed info
360        let &PFRDescription {
361            n_compartment,
362            length,
363            diameter,
364            axial_dispersion,
365            ..
366        } = descriptor;
367
368        let (volume_fraction, flow) = descriptor.extract_volume_flow(phase_d);
369
370        debug_assert!(length > 0.);
371        debug_assert!(diameter > 0.);
372        debug_assert!(flow >= 0.);
373        debug_assert!(axial_dispersion >= 0.);
374        debug_assert!(volume_fraction <= 1. && volume_fraction > 0.);
375
376        //Geometrical data
377
378        let dx = length / (n_compartment as f64);
379        let reactor_section_area = std::f64::consts::PI * diameter.powf(2.) / 4.;
380        let compartment_volume = volume_fraction * dx * reactor_section_area;
381        let n_flow = n_compartment - 1;
382
383        //PreCalculate velocity
384        let flow_velocity = flow / reactor_section_area;
385        let flow_source_target = reactor_section_area / dx * (flow_velocity + axial_dispersion);
386        let flow_target_source = reactor_section_area / dx * axial_dispersion;
387
388        //Discretization should preserve geometrical volume
389        assert!(
390            ((compartment_volume * n_compartment as f64)
391                - (volume_fraction * reactor_section_area * length))
392                .abs()
393                < 1e-5
394        );
395
396        let mut phase = RawPhase::new(n_compartment, n_flow, phase_d);
397
398        //n_flow != n_compartment, need to set volume separately
399        phase.volume = RawDataScalar::from(vec![compartment_volume; n_compartment]); //Scalar values are not preallo
400
401        //Set flow for each compartment
402        for (current_index, flow) in phase.flow.fluxes.iter_mut().enumerate() {
403            flow.id_source = current_index as u32;
404            flow.id_target = (current_index + 1) as u32;
405            flow.flux_source_target = flow_source_target;
406            flow.flux_target_source = flow_target_source;
407        }
408
409        //Select whether generate in place or defered
410        if let Some(s) = out_dir {
411            *case = GenerateContract::new_single_phase(case.clone(), phase, None).write(&s)?;
412            // GenerateContract::write_phase(&s, case, phase, None)?;
413        } else {
414            self.raw_phase.push(phase);
415        }
416
417        Ok(())
418    }
419
420    fn merge_phase<'a, I>(
421        // flows: Vec<RawDataFlux>,
422        // volumes: Vec<RawDataScalar>
423        phases: I,
424        connections: Option<RawDataFlux>,
425    ) -> Result<RawPhase, CMError>
426    where
427        I: Iterator<Item = &'a RawPhase>,
428    {
429        let phases = &mut phases.into_iter().peekable();
430
431        if phases.peek().is_none() {
432            return Err(CMError::Custom("Empty phases".to_owned()));
433        }
434
435        let next = phases.peek().unwrap();
436
437        let mut phase = RawPhase::new(0, 0, next.identifier);
438
439        let offset_compartment = std::cell::Cell::new(0u32);
440        let incr_id = |mut flow: RawFlux| -> RawFlux {
441            flow.id_source += offset_compartment.get();
442            flow.id_target += offset_compartment.get();
443            flow
444        };
445        phases.for_each(|p| {
446            let rd = &p.flow;
447            let v = &p.volume;
448            phase.flow.header.n_zone += rd.header.n_zone;
449            phase.volume.header.n_zone += rd.header.n_zone;
450            phase.flow.header.n_fluxes += rd.header.n_fluxes;
451            phase
452                .flow
453                .fluxes
454                .extend(rd.fluxes.iter().map(|&flux| incr_id(flux)));
455            phase.volume.values.extend(v.values.clone());
456            offset_compartment.set(offset_compartment.get() + rd.header.n_zone);
457        });
458
459        if let Some(connections) = connections {
460            phase.flow.header.n_fluxes += connections.header.n_fluxes;
461            phase.flow.fluxes.extend(connections.fluxes);
462        }
463
464        //The header is accumulated apart from the fluxes, they cannot disagree
465        if phase.flow.fluxes.len() != phase.flow.header.n_fluxes as usize {
466            return Err(CMError::Custom(format!(
467                "Merged {:?} phase holds {} fluxes but its header declares {}",
468                phase.identifier,
469                phase.flow.fluxes.len(),
470                phase.flow.header.n_fluxes
471            )));
472        }
473
474        Ok(phase)
475    }
476
477    fn impl_merge<'a>(
478        liquid_phase: impl Iterator<Item = &'a RawPhase>,
479        gas_phase: impl Iterator<Item = &'a RawPhase>,
480        connections: Option<[RawDataFlux; 2]>,
481    ) -> Result<(RawPhase, Option<RawPhase>), CMError> {
482        let liquid_connection = connections.as_ref().map(|c| c[0].clone());
483        let gas_connection = connections.as_ref().map(|c| c[1].clone());
484
485        let merged_liquid_phase = Self::merge_phase(liquid_phase, liquid_connection)?;
486        let gas_phase = &mut gas_phase.peekable();
487        let merged_gas_phase = if gas_phase.peek().is_some() {
488            Some(Self::merge_phase(gas_phase, gas_connection)?)
489        } else {
490            None
491        };
492        Ok((merged_liquid_phase, merged_gas_phase))
493    }
494}
495
496#[cfg(test)]
497mod tests {
498
499    use super::*;
500
501    fn g_descriptor_pfr() -> PFRDescription {
502        let l = 1.;
503        let d = 0.2;
504        let alpha_g = 0.1;
505
506        PFRDescription::new(10, l, d, 0.01, 0.01, alpha_g, 1e-9).unwrap()
507    }
508
509    #[test]
510    fn test_0d() {
511        let path = "/tmp/test_0d";
512        std::fs::create_dir_all(path).unwrap();
513
514        let case = Generator::new()
515            .generate_0d(
516                Reactor0DDescriptor::from_fraction(10., 0.2).expect("descriptor"),
517                Some(path.to_owned()),
518            )
519            .expect("case");
520
521        let liquid_volume_path = case
522            .resolve(path, cmtool_data::CMAExportType::LiquidVolume)
523            .expect("path");
524
525        let liquid_volume =
526            cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()).expect("Liquid error");
527        let gas_volume_path = case
528            .resolve(path, cmtool_data::CMAExportType::GasVolume)
529            .expect("path");
530
531        let gas_volume =
532            cmtool_data::RawDataScalar::read_raw(gas_volume_path.clone()).expect("gas_volume");
533        std::fs::remove_dir_all(path).unwrap();
534
535        assert_eq!(gas_volume.values.len(), 1);
536        assert_eq!(gas_volume.values[0].value, 2.0);
537        assert_eq!(liquid_volume.values[0].value, 8.0);
538    }
539
540    #[test]
541    fn test_merge_lazy() {
542        let path = "/tmp/test_merge_lazy";
543        std::fs::create_dir_all(path).unwrap();
544        //same value in g_descriptor_pfr
545        let l = 1.;
546        let d = 0.2;
547
548        let desc = g_descriptor_pfr();
549        let alpha_g = desc.get_gas_fraction();
550        //volume is h*pi*d^2/4
551        let geo_volume = l * (d * d) * std::f64::consts::PI / 4.;
552        assert!(geo_volume == desc.geometrical_volume());
553
554        let mut generator = Generator::new();
555        let _case = generator.generate_1d(desc, None).expect("case");
556
557        let c = generator.merge_from_memory(None).expect("merge");
558
559        let case = c.write(path).expect("write");
560        let liquid_volume_path = case
561            .resolve(path, cmtool_data::CMAExportType::LiquidVolume)
562            .unwrap();
563
564        let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone())
565            .expect("Liquid error")
566            .values
567            .iter()
568            .map(|v| v.value)
569            .sum();
570        std::fs::remove_dir_all(path).unwrap();
571
572        assert!(liquid_volume - (1. - alpha_g) * geo_volume < 1e-9);
573    }
574
575    #[test]
576    fn test_merge_phase_2() {
577        let path = "/tmp/test_merge_phase_2";
578        std::fs::create_dir_all(path).unwrap();
579
580        let v_0d = 10.;
581        let n_c = 10;
582
583        let desc_pfr = g_descriptor_pfr();
584        let alpha_g = desc_pfr.get_gas_fraction();
585        let geo_volume = desc_pfr.geometrical_volume();
586        let desc_0d = Reactor0DDescriptor::from_fraction(v_0d, alpha_g).expect("descriptor");
587
588        let mut gene = Generator::new();
589
590        gene.generate_1d(desc_pfr, None).unwrap();
591        gene.generate_0d(desc_0d, None).unwrap();
592
593        let gc = gene.merge_from_memory(None).unwrap();
594
595        let case = gc.write(path).unwrap();
596
597        let liquid_volume_path = case
598            .resolve(path, cmtool_data::CMAExportType::LiquidVolume)
599            .expect("path");
600
601        let liquid_volume =
602            cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone()).expect("Liquid error");
603        assert!(liquid_volume.header.n_zone as usize == n_c + 1);
604
605        let total_volume: f64 = liquid_volume.values.iter().map(|v| v.value).sum();
606        let expected_volume = (1. - alpha_g) * (geo_volume + v_0d);
607        assert!((total_volume - expected_volume).abs() < 1e-9);
608
609        std::fs::remove_dir_all(path).unwrap();
610    }
611
612    #[test]
613    fn test_merge_phase_1() {
614        let path = "/tmp/test_merge";
615        std::fs::create_dir_all(path).unwrap();
616        let desc_pfr = g_descriptor_pfr();
617        let alpha_g = desc_pfr.get_gas_fraction();
618        let geo_volume = desc_pfr.geometrical_volume();
619
620        let case = Generator::new()
621            .generate_1d(desc_pfr, Some(path.to_owned()))
622            .expect("case");
623        let liquid_volume_path = case
624            .resolve(path, cmtool_data::CMAExportType::LiquidVolume)
625            .expect("path");
626
627        let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone())
628            .expect("Liquid error")
629            .values
630            .iter()
631            .map(|v| v.value)
632            .sum();
633
634        //volume is h*pi*d^2/4
635        std::fs::remove_dir_all(path).unwrap();
636        assert!(
637            liquid_volume - (1. - alpha_g) * geo_volume < 1e-9,
638            "liquid_volume {}, alpha {}, geo_volume {}",
639            liquid_volume,
640            alpha_g,
641            geo_volume
642        );
643    }
644
645    #[test]
646    fn test_1d() {
647        let path = "/tmp/test_1d";
648        std::fs::create_dir_all(path).unwrap();
649        let l = 1.;
650        let d = 0.2;
651        let alpha_g = 0.1;
652
653        let desc = PFRDescription {
654            n_compartment: 10,
655            length: l,
656            diameter: d,
657            liquid_flow: 0.01,
658            gas_flow: 0.001,
659            gas_fraction: alpha_g,
660            axial_dispersion: 1e-9,
661        };
662
663        let case = Generator::new()
664            .generate_1d(desc, Some(path.to_owned()))
665            .expect("case");
666        let liquid_volume_path = case
667            .resolve(path, cmtool_data::CMAExportType::LiquidVolume)
668            .expect("path");
669
670        let liquid_volume: f64 = cmtool_data::RawDataScalar::read_raw(liquid_volume_path.clone())
671            .expect("Liquid error")
672            .values
673            .iter()
674            .map(|v| v.value)
675            .sum();
676
677        let gas_volume_path = case
678            .resolve(path, cmtool_data::CMAExportType::GasVolume)
679            .expect("path");
680        let gas_volume: f64 = cmtool_data::RawDataScalar::read_raw(gas_volume_path.clone())
681            .expect("gas error")
682            .values
683            .iter()
684            .map(|v| v.value)
685            .sum();
686
687        //volume is h*pi*d^2/4
688        let geo_volume = l * (d * d) * std::f64::consts::PI / 4.;
689        std::fs::remove_dir_all(path).unwrap();
690        assert!(
691            (liquid_volume - (1. - alpha_g) * geo_volume).abs() < 1e-9,
692            "liquid_volume {}, alpha {}, geo_volume {}",
693            liquid_volume,
694            alpha_g,
695            geo_volume
696        );
697
698        assert!(
699            ((liquid_volume + gas_volume) - geo_volume).abs() < 1e-9,
700            "liquid_volume {}, gas_volume {}, geo_volume {}",
701            liquid_volume,
702            gas_volume,
703            geo_volume
704        );
705    }
706}