Skip to main content

cmtool_assemble/parser/
reactors.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use super::PfrGlobalMassBalance;
7use super::generated_domain;
8use crate::data::FeedFlow;
9use crate::data::ParsedFeeds;
10use crate::parser::generated_domain::FeedFluxType;
11use crate::parser::generated_domain::FluxType;
12use crate::{
13    CMError,
14    data::{DomainInfo, FlowDirection},
15};
16use cmtool_data::DEFAULT_CASE_FILE_NAME;
17use cmtool_data::{PhaseCM, RawDataFlux};
18
19fn connection_per_phase(
20    info: &DomainInfo,
21    mass_balance: &mut PfrGlobalMassBalance,
22    phase_node: &[generated_domain::FluxType],
23    phase: PhaseCM,
24) -> Result<cmtool_data::RawDataFlux, CMError> {
25    let n_node = phase_node.len();
26
27    let mut rd = RawDataFlux::new(info.total_number_compartment, n_node);
28
29    for (node, flux) in phase_node.iter().zip(rd.fluxes.iter_mut()) {
30        if let (Some(absolute_source_id), Some(absolute_target_id)) = (
31            info.get_relative_compartment_number(&node.source.id, node.source.compartment_id),
32            info.get_relative_compartment_number(&node.target.id, node.target.compartment_id),
33        ) {
34            let flow = f64::from(node.value.content);
35            flux.id_source = absolute_source_id as u32;
36            flux.id_target = absolute_target_id as u32;
37            flux.flux_source_target = flow;
38            flux.flux_target_source = 0.; //Connection is pure PlugFLow
39
40            let f_pfr_source = info.pfr_names.contains(&node.source.id);
41            let is_same_node =
42                absolute_source_id == absolute_target_id && node.source.id == node.target.id;
43
44            if is_same_node && f_pfr_source {
45                mass_balance.update_flow(&node.target.id, phase, FlowDirection::Out, flow);
46            } else {
47                let f_pfr_target = info.pfr_names.contains(&node.target.id);
48                if f_pfr_target {
49                    mass_balance.update_flow(&node.target.id, phase, FlowDirection::In, flow);
50                }
51                if f_pfr_source {
52                    mass_balance.update_flow(&node.source.id, phase, FlowDirection::Out, flow);
53                }
54            }
55        } else {
56            //Either the reactor id is unknown or the compartment does not exist in it
57            return Err(CMError::Custom(format!(
58                "Connection {}[{}] -> {}[{}] does not resolve to a compartment of the domain",
59                node.source.id,
60                node.source.compartment_id,
61                node.target.id,
62                node.target.compartment_id
63            )));
64        }
65    }
66    Ok(rd)
67}
68
69pub fn parse_connection(
70    info: &DomainInfo,
71    connections: &generated_domain::ConnectionsType,
72    mass_balance: &mut PfrGlobalMassBalance,
73) -> Result<[RawDataFlux; 2], CMError> {
74    let liquid_connection: Vec<generated_domain::FluxType> = connections
75        .flux
76        .iter()
77        .filter(|f| f.phase == *"liquid")
78        .cloned()
79        .collect();
80
81    let gas_connection: Vec<generated_domain::FluxType> = connections
82        .flux
83        .iter()
84        .filter(|f| f.phase == *"gas")
85        .cloned()
86        .collect();
87
88    Ok([
89        connection_per_phase(info, mass_balance, &liquid_connection, PhaseCM::Liquid)?,
90        connection_per_phase(info, mass_balance, &gas_connection, PhaseCM::Gas)?,
91    ])
92}
93
94///Flux type is a "derivated" type of flux with all flux information + the flow value
95///Current implementation performs naive copy of common attributes.
96impl From<&FeedFluxType> for FluxType {
97    fn from(feed: &FeedFluxType) -> Self {
98        Self {
99            source: feed.source.clone(),
100            target: feed.target.clone(),
101            phase: feed.phase.clone(),
102            value: feed.value.clone(),
103        }
104    }
105}
106
107fn parse_feed_phase(
108    info: &DomainInfo,
109    feeds: &[&FeedFluxType],
110    phase: PhaseCM,
111    mass_balance: &mut PfrGlobalMassBalance,
112) -> Result<HashMap<String, FeedFlow>, CMError> {
113    let fluxes: Vec<generated_domain::FluxType> =
114        feeds.iter().map(|&feed| FluxType::from(feed)).collect();
115
116    let id: Vec<String> = feeds.iter().map(|feed| feed.id.clone()).collect();
117    let rd = connection_per_phase(info, mass_balance, &fluxes, phase)?;
118
119    Ok(rd
120        .fluxes
121        .iter()
122        .zip(id)
123        .map(|(flux, id)| {
124            (
125                id,
126                FeedFlow {
127                    flow: flux.flux_source_target,
128                    position: flux.id_source as usize,
129                    output_position: None, //TODO
130                },
131            )
132        })
133        .collect())
134}
135
136pub fn parse_feed(
137    info: &DomainInfo,
138    feeds: &generated_domain::FeedsType,
139    mass_balance: &mut PfrGlobalMassBalance,
140) -> Result<Option<ParsedFeeds>, CMError> {
141    let (liquid_feeds, gas_feeds): (Vec<_>, Vec<_>) =
142        feeds.flux.iter().partition(|f| f.phase == *"liquid");
143    if liquid_feeds.is_empty() && gas_feeds.is_empty() {
144        return Ok(None);
145    }
146    Ok(Some(ParsedFeeds {
147        liq: parse_feed_phase(info, &liquid_feeds, PhaseCM::Liquid, mass_balance)?,
148        gas: parse_feed_phase(info, &gas_feeds, PhaseCM::Gas, mass_balance)?,
149    }))
150}
151
152pub fn parse_reactor(reactors: &generated_domain::ReactorsType) -> Result<DomainInfo, CMError> {
153    let mut domain_info = DomainInfo::default();
154    let mut in_place_cumsum = 0;
155    //don't forget to |=
156    for reactor in &reactors.content {
157        match reactor {
158            generated_domain::ReactorsTypeContent::Reactor0D(reactor0_dtype) => {
159                domain_info
160                    .compartment_cumsum
161                    .insert(reactor0_dtype.id.clone(), in_place_cumsum);
162                domain_info.total_number_compartment += 1;
163                in_place_cumsum += 1;
164                domain_info.is_two_phase_flow |= reactor0_dtype.volume_fraction.content != 0.;
165            }
166            generated_domain::ReactorsTypeContent::Reactor1D(current_pfr) => {
167                domain_info
168                    .compartment_cumsum
169                    .insert(current_pfr.id.clone(), in_place_cumsum);
170                let n_c = current_pfr.compartments;
171                domain_info.total_number_compartment += n_c.get();
172                domain_info.is_two_phase_flow |= current_pfr.volume_fraction.content != 0.;
173                domain_info.pfr_names.push(current_pfr.id.clone());
174                in_place_cumsum += n_c.get();
175            }
176            generated_domain::ReactorsTypeContent::ReactorFromFile(reactor_from_file) => {
177                let path = PathBuf::from(&reactor_from_file.path).join(DEFAULT_CASE_FILE_NAME);
178                let case = cmtool_data::read_case(path.as_path())?;
179                // case.n_compartment()
180                domain_info
181                    .compartment_cumsum
182                    .insert(reactor_from_file.id.clone(), in_place_cumsum);
183                let n_c = case.n_compartment() as usize;
184                domain_info.is_two_phase_flow |= case.is_two_phase_flow();
185                domain_info.total_number_compartment += n_c;
186                in_place_cumsum += n_c;
187            }
188            generated_domain::ReactorsTypeContent::Reactor3D(reactor3_dtype) => {
189                todo!("{:?}", reactor3_dtype)
190            }
191        }
192    }
193    Ok(domain_info)
194}
195
196#[cfg(test)]
197mod test {
198    use super::*;
199
200    fn reactor_0d(id: &str, gas_fraction: f32) -> generated_domain::ReactorsTypeContent {
201        generated_domain::ReactorsTypeContent::Reactor0D(generated_domain::Reactor0DType {
202            id: id.to_owned(),
203            volume_fraction: generated_domain::VolumeFractionType {
204                phase: None,
205                content: gas_fraction,
206            },
207            size: generated_domain::GeneralSizeType::Volume(generated_domain::DimensionType {
208                unit: None,
209                content: 1.,
210            }),
211        })
212    }
213
214    ///A single gas reactor makes the whole domain two-phase, whatever follows it
215    #[test]
216    fn test_keep_two_phase_flow() {
217        let reactors = generated_domain::ReactorsType {
218            content: vec![reactor_0d("gas", 0.1), reactor_0d("liquid", 0.)],
219        };
220
221        let info = parse_reactor(&reactors).unwrap();
222
223        assert!(info.is_two_phase_flow);
224        assert_eq!(info.total_number_compartment, 2);
225    }
226
227    #[test]
228    fn liquid_only_domain_is_not_two_phase() {
229        let reactors = generated_domain::ReactorsType {
230            content: vec![reactor_0d("liquid_1", 0.), reactor_0d("liquid_2", 0.)],
231        };
232
233        assert!(!parse_reactor(&reactors).unwrap().is_two_phase_flow);
234    }
235}