1use 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 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 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 ¤t_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(¤t_pfr.id, PhaseCM::Liquid)?,
91 mb.get_flow(¤t_pfr.id, PhaseCM::Gas)?,
92 current_pfr.volume_fraction.content,
93 1e-9,
94 )
95 .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, &p.join(DEFAULT_CASE_FILE_NAME))?;
107 }
108 }
109 };
110 Ok(())
111}
112
113fn _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 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 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 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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 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 assert!(gas.values[1].value < 1e-6);
411
412 std::fs::remove_dir_all(root).unwrap();
413 }
414}
415
416