1use 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
26pub struct Generator {
28 raw_phase: Vec<RawPhase>,
29 existing_case: HashMap<String, PathBuf>,
31}
32
33struct Field0D {
34 #[allow(unused)]
35 name: String,
36 #[allow(unused)]
37 value: f64,
38}
39fn 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
56fn 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
68fn 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
124impl 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 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 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 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 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 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 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 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 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 case,
293 liquid_phase,
294 gas_phase,
295 None,
296 ))
297 }
298}
299
300impl 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(); 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 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 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 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 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 phase.volume = RawDataScalar::from(vec![compartment_volume; n_compartment]); 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 if let Some(s) = out_dir {
411 *case = GenerateContract::new_single_phase(case.clone(), phase, None).write(&s)?;
412 } else {
414 self.raw_phase.push(phase);
415 }
416
417 Ok(())
418 }
419
420 fn merge_phase<'a, I>(
421 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 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 let l = 1.;
546 let d = 0.2;
547
548 let desc = g_descriptor_pfr();
549 let alpha_g = desc.get_gas_fraction();
550 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 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 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}