Skip to main content

cmtool_assemble/
map_generation.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::generators::{Generator, PFRDescription};
4use crate::parser::generated_domain::{self, GeneralSizeType};
5use crate::parser::{PfrGlobalMassBalance, generated_domain::Reactor0DType};
6use crate::{CMError, GenerateContract};
7use cmtool_data::DataError;
8use cmtool_data::{CMCaseJson, DEFAULT_CASE_FILE_NAME, PhaseCM, RawDataFlux};
9
10impl GeneralSizeType {
11    ///Returns volume of reactor considering cylindrical shape
12    pub fn get_volume(&self) -> f64 {
13        match self {
14            generated_domain::GeneralSizeType::Volume(v) => v.content as f64,
15            generated_domain::GeneralSizeType::Dimension(dim) => {
16                (dim.length.content as f64)
17                    * (dim.diameter.content.powf(2.) as f64)
18                    * std::f64::consts::PI
19                    / 4.
20            }
21        }
22    }
23}
24
25fn _generate_reactor_0d<T: cmtool_data::CMCaseWriter>(
26    generator: &mut Generator,
27    root: &Option<impl AsRef<std::path::Path>>,
28    ids: &mut Vec<String>,
29    reactor0d: &Reactor0DType,
30) -> Result<(), CMError> {
31    ids.push(reactor0d.id.clone());
32    let volume = reactor0d.size.get_volume();
33
34    let path = root.as_ref().map(|r| r.as_ref().join(reactor0d.id.clone()));
35
36    let mut opt_path = None;
37    if let Some(p) = &path {
38        std::fs::create_dir_all(p).map_err(DataError::IO)?;
39        opt_path = Some(p.to_string_lossy().to_string());
40    }
41
42    let descriptor = crate::generators::Reactor0DDescriptor::from_fraction(
43        volume,
44        reactor0d.volume_fraction.content as f64,
45    )
46    .map_err(|e| CMError::Custom(format!("Reactor '{}': {}", reactor0d.id, e)))?;
47
48    let case = generator.generate_0d(descriptor, opt_path)?;
49
50    // let case = generator.generate_0d_from_fraction(
51    //     volume,
52    //     reactor0d.volume_fraction.content as f64,
53    //     opt_path,
54    // )?;
55    if let Some(p) = &path {
56        T::write_case(case, &p.join(DEFAULT_CASE_FILE_NAME))?;
57    }
58
59    Ok(())
60}
61
62fn _generate_reactor_1d<T: cmtool_data::CMCaseWriter>(
63    generator: &mut Generator,
64    root: &Option<impl AsRef<std::path::Path>>,
65    ids: &mut Vec<String>,
66    current_pfr: &generated_domain::Reactor1DType,
67    mb: &PfrGlobalMassBalance,
68) -> Result<(), CMError> {
69    ids.push(current_pfr.id.clone());
70
71    let path = root
72        .as_ref()
73        .map(|r| r.as_ref().join(current_pfr.id.clone()));
74
75    let mut opt_path = None;
76    if let Some(p) = &path {
77        std::fs::create_dir_all(p).map_err(DataError::IO)?;
78        opt_path = Some(p.to_string_lossy().to_string());
79    }
80
81    match &current_pfr.size {
82        generated_domain::GeneralSizeType::Volume(_) => {
83            unimplemented!("pfr needs length")
84        }
85        generated_domain::GeneralSizeType::Dimension(dim) => {
86            let desc = PFRDescription::new(
87                current_pfr.compartments.get(),
88                dim.length.content,
89                dim.diameter.content,
90                mb.get_flow(&current_pfr.id, PhaseCM::Liquid)?,
91                mb.get_flow(&current_pfr.id, PhaseCM::Gas)?,
92                current_pfr.volume_fraction.content,
93                1e-9,
94            )
95            //Flow comes from the Feeds/Connections of the reactor, a PFR without any is invalid
96            .map_err(|e| {
97                CMError::Custom(format!(
98                    "Reactor '{}': {}, check the Feeds and Connections declaring its flow",
99                    current_pfr.id, e
100                ))
101            })?;
102
103            let case: cmtool_data::CMCase = generator.generate_1d(desc, opt_path)?;
104            if let Some(p) = &path {
105                // T::write_case(case, std::path::Path::new(&format!("{}/cma_case", p)))?;
106                T::write_case(case, &p.join(DEFAULT_CASE_FILE_NAME))?;
107            }
108        }
109    };
110    Ok(())
111}
112
113///The flow map of a ReactorFromFile is not generated, it is read back from the case it points to
114fn _generate_reactor_from_file(
115    generator: &mut Generator,
116    root: &Option<impl AsRef<std::path::Path>>,
117    ids: &mut Vec<String>,
118    reactor: &generated_domain::ReactorFromFileType,
119) -> Result<(), CMError> {
120    ids.push(reactor.id.clone());
121    generator.add_existing_case(&reactor.id, &reactor.path, root.is_none())
122}
123
124fn generate_partial_flowmap<T: cmtool_data::CMCaseWriter>(
125    generator: &mut Generator,
126    root: Option<impl AsRef<std::path::Path>>,
127    reactors: &generated_domain::ReactorsType,
128    mb: &PfrGlobalMassBalance,
129) -> Result<Vec<String>, CMError> {
130    let mut ids = Vec::with_capacity(reactors.content.len());
131    for reactor in &reactors.content {
132        match reactor {
133            generated_domain::ReactorsTypeContent::Reactor0D(r) => {
134                _generate_reactor_0d::<T>(generator, &root, &mut ids, r)?;
135            }
136            generated_domain::ReactorsTypeContent::Reactor1D(r) => {
137                _generate_reactor_1d::<T>(generator, &root, &mut ids, r, mb)?;
138            }
139            generated_domain::ReactorsTypeContent::Reactor3D(reactor3_dtype) => {
140                todo!("{:?}", reactor3_dtype)
141            }
142            generated_domain::ReactorsTypeContent::ReactorFromFile(r) => {
143                _generate_reactor_from_file(generator, &root, &mut ids, r)?;
144            }
145        }
146    }
147    Ok(ids)
148}
149
150pub fn generate_flowmap(
151    root: Option<std::path::PathBuf>,
152    reactors: &generated_domain::ReactorsType,
153    mb: &PfrGlobalMassBalance,
154    connections: Option<[RawDataFlux; 2]>,
155) -> Result<Option<GenerateContract>, CMError> {
156    let mut generator = Generator::new();
157
158    //A root means the partial cases are written next to the merged one
159    let ids = generate_partial_flowmap::<CMCaseJson>(&mut generator, root.clone(), reactors, mb)?;
160
161    if ids.is_empty() {
162        return Err(CMError::Custom("No flowmap to generate".to_owned()));
163    }
164
165    //Merging in place needs no contract, the partial cases are already on disk
166    let gc = match root {
167        Some(root) => {
168            generator.merge(root, &ids, connections)?;
169            None
170        }
171        None => Some(generator.merge_from_memory(connections)?),
172    };
173    Ok(gc)
174}
175
176#[cfg(test)]
177mod test {
178    use cmtool_data::{CMAExportType, CMCaseJson, CMCaseReader, CMCaseWriter, RawData};
179
180    ///Existing case of a single compartment, as a ReactorFromFile would point to
181    fn write_existing_case(path: &str, gas_fraction: f64) {
182        std::fs::create_dir_all(path).unwrap();
183        let case = crate::generators::Generator::new()
184            .generate_0d(
185                crate::generators::Reactor0DDescriptor::from_fraction(10., gas_fraction)
186                    .expect("descriptor"),
187                Some(path.to_owned()),
188            )
189            .expect("existing case");
190        CMCaseJson::write_case(
191            case,
192            &std::path::Path::new(path).join(cmtool_data::DEFAULT_CASE_FILE_NAME),
193        )
194        .expect("existing cma_case");
195    }
196
197    fn domain_xml(existing_path: &str) -> String {
198        format!(
199            r#"<?xml version="1.0"?>
200<Root run_id="from_file" version="3">
201  <Reactors>
202    <ReactorFromFile id="existing">
203      <Path>{existing_path}</Path>
204    </ReactorFromFile>
205    <Reactor0D id="str">
206      <Size>
207        <Volume>5</Volume>
208      </Size>
209      <VolumeFraction>0</VolumeFraction>
210    </Reactor0D>
211  </Reactors>
212  <Connections>
213    <Flux phase="liquid">
214      <Source id="existing" compartment_id="0"></Source>
215      <Target id="str" compartment_id="0"></Target>
216      <Value unit="l/min">2</Value>
217    </Flux>
218  </Connections>
219</Root>"#
220        )
221    }
222
223    ///A ReactorFromFile is a reactor like the others: its existing flow map is merged with the
224    ///generated ones instead of replacing the whole domain
225    #[test]
226    fn test_merge_reactor_from_file_with_generated_reactor() {
227        let root = "/tmp/test_reactor_from_file";
228        let existing = format!("{}/existing", root);
229        let _ = std::fs::remove_dir_all(root);
230        write_existing_case(&existing, 0.);
231
232        let domain =
233            crate::generate_and_write_domain(root, &domain_xml(&existing)).expect("domain");
234
235        let merged = CMCaseJson::read_case(std::path::Path::new(root).join("cma_case").as_path())
236            .expect("merged case");
237        let flow_path = merged
238            .resolve(root, CMAExportType::LiquidFlow)
239            .expect("flow");
240        let volume_path = merged
241            .resolve(root, CMAExportType::LiquidVolume)
242            .expect("volume");
243
244        let flow = cmtool_data::RawDataFlux::read_raw(flow_path).expect("liquid flow");
245        let volume = cmtool_data::RawDataScalar::read_raw(volume_path).expect("liquid volume");
246
247        //The existing compartment comes first, the generated one is offset behind it
248        assert_eq!(flow.header.n_zone, 2);
249        assert_eq!(volume.values.len(), 2);
250        assert_eq!(volume.values[0].value, 10.);
251        assert_eq!(volume.values[1].value, 5.);
252
253        //The connection between both reactors survived the merge
254        assert!(
255            flow.fluxes
256                .iter()
257                .any(|f| f.id_source == 0 && f.id_target == 1 && f.flux_source_target == 2.)
258        );
259        assert_eq!(domain.info().total_number_compartment, 2);
260
261        std::fs::remove_dir_all(root).unwrap();
262    }
263
264    ///A typo in a connection id used to abort the process
265    #[test]
266    fn test_unknown_connection_id() {
267        let root = "/tmp/test_unknown_connection_id";
268        let xml = r#"<?xml version="1.0"?>
269<Root run_id="typo" version="3">
270  <Reactors>
271    <Reactor0D id="str">
272      <Size>
273        <Volume>5</Volume>
274      </Size>
275      <VolumeFraction>0</VolumeFraction>
276    </Reactor0D>
277  </Reactors>
278  <Connections>
279    <Flux phase="liquid">
280      <Source id="str" compartment_id="0"></Source>
281      <Target id="typo_here" compartment_id="0"></Target>
282      <Value unit="l/min">2</Value>
283    </Flux>
284  </Connections>
285</Root>"#;
286
287        let error = match crate::generate_and_write_domain(root, xml) {
288            Ok(_) => panic!("an unknown connection id must not generate"),
289            Err(error) => error.to_string(),
290        };
291        let _ = std::fs::remove_dir_all(root);
292
293        assert!(error.contains("typo_here"), "{}", error);
294    }
295
296    ///An unsupported schema version used to panic with "ALED"
297    #[test]
298    fn test_unsupported_version() {
299        let root = "/tmp/test_unsupported_version";
300        let xml = r#"<?xml version="1.0"?>
301<Root run_id="old" version="2">
302  <Reactors>
303    <Reactor0D id="str">
304      <Size>
305        <Volume>5</Volume>
306      </Size>
307      <VolumeFraction>0</VolumeFraction>
308    </Reactor0D>
309  </Reactors>
310</Root>"#;
311
312        let error = match crate::generate_and_write_domain(root, xml) {
313            Ok(_) => panic!("version 2 is not supported"),
314            Err(error) => error.to_string(),
315        };
316        let _ = std::fs::remove_dir_all(root);
317
318        assert!(error.contains("version"), "{}", error);
319    }
320
321    ///A gas fraction outside [0, 1] used to panic in Reactor0DDescriptor::from_fraction
322    #[test]
323    fn test_invalid_gas_fraction() {
324        let root = "/tmp/test_invalid_gas_fraction";
325        let xml = r#"<?xml version="1.0"?>
326<Root run_id="bad_fraction" version="3">
327  <Reactors>
328    <Reactor0D id="str">
329      <Size>
330        <Volume>5</Volume>
331      </Size>
332      <VolumeFraction>1.5</VolumeFraction>
333    </Reactor0D>
334  </Reactors>
335</Root>"#;
336
337        let error = match crate::generate_and_write_domain(root, xml) {
338            Ok(_) => panic!("a gas fraction above 1 must not generate"),
339            Err(error) => error.to_string(),
340        };
341        let _ = std::fs::remove_dir_all(root);
342
343        assert!(error.contains("str"), "{}", error);
344        assert!(error.contains("Gas fraction"), "{}", error);
345    }
346
347    ///A PFR gets its flow from the Feeds/Connections, without any it is invalid and the error
348    ///has to name the reactor
349    #[test]
350    fn test_pfr_without_feed() {
351        let root = "/tmp/test_pfr_without_feed";
352        let xml = r#"<?xml version="1.0"?>
353<Root run_id="no_feed" version="3">
354  <Reactors>
355    <Reactor1D id="pfr">
356      <Size>
357        <Dimension>
358          <Diameter unit="m">0.01</Diameter>
359          <Length unit="m">1</Length>
360        </Dimension>
361      </Size>
362      <VolumeFraction>0</VolumeFraction>
363      <Compartments>4</Compartments>
364      <Dispersion unit="m^2/s">0</Dispersion>
365    </Reactor1D>
366  </Reactors>
367</Root>"#;
368
369        let error = match crate::generate_and_write_domain(root, xml) {
370            Ok(_) => panic!("a PFR without flow must not generate"),
371            Err(error) => error.to_string(),
372        };
373        let _ = std::fs::remove_dir_all(root);
374
375        assert!(error.contains("pfr"), "{}", error);
376        assert!(error.contains("Feeds"), "{}", error);
377    }
378
379    ///A liquid-only reactor of a two-phase domain keeps its gas compartments, otherwise every
380    ///gas id declared after it is shifted
381    #[test]
382    fn test_merge_gas_alignment() {
383        let root = "/tmp/test_merge_gas_alignment";
384        let existing = format!("{}/existing", root);
385        let _ = std::fs::remove_dir_all(root);
386        //Gas existing reactor, liquid only generated one
387        write_existing_case(&existing, 0.2);
388
389        crate::generate_and_write_domain(root, &domain_xml(&existing)).expect("domain");
390
391        let merged = CMCaseJson::read_case(std::path::Path::new(root).join("cma_case").as_path())
392            .expect("merged case");
393        let liquid = cmtool_data::RawDataScalar::read_raw(
394            merged
395                .resolve(root, CMAExportType::LiquidVolume)
396                .expect("volume"),
397        )
398        .expect("liquid volume");
399        let gas = cmtool_data::RawDataScalar::read_raw(
400            merged
401                .resolve(root, CMAExportType::GasVolume)
402                .expect("gas volume path"),
403        )
404        .expect("gas volume");
405
406        assert_eq!(liquid.values.len(), gas.values.len());
407        assert_eq!(gas.values.len(), 2);
408        assert_eq!(gas.values[0].value, 2.);
409        //The liquid only reactor contributes a neutral gas compartment
410        assert!(gas.values[1].value < 1e-6);
411
412        std::fs::remove_dir_all(root).unwrap();
413    }
414}
415
416// fn parse_generate()
417// {}