Skip to main content

cmtool_data/
states.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use std::collections::HashMap;
4
5use crate::FlowMapDescriptor;
6use nalgebra_sparse::CooMatrix;
7use ndarray::Array2;
8
9macro_rules! almost_equal {
10    ($i:expr, $base:expr, $eps:expr) => {
11        (($i - $base).abs() < $eps)
12    };
13}
14
15// macro_rules! non_zero {
16//     ($i:expr, $eps:expr) => {
17//         ($i.abs() > $eps)
18//     };
19// }
20
21// macro_rules! near_one {
22//     ($i:expr, $eps:expr) => {
23//         almost_equal!($i, 1.0, $eps)
24//     };
25// }
26
27macro_rules! round_if_needed {
28    ($val:expr, $base:expr, $tol:expr) => {{
29        if almost_equal!($val, $base, $tol) {
30            $base
31        } else {
32            $val
33        }
34    }};
35}
36fn get_transition_from_fm(fm: Array2<f64>) -> (CooMatrix<f64>, Vec<f64>) {
37    let n_compartments: usize = fm.nrows();
38
39    let mut transition = CooMatrix::new(n_compartments, n_compartments);
40    let mut row_sum = vec![0.; n_compartments];
41
42    (0..n_compartments).for_each(|i_row| {
43        for i_col in 0..n_compartments {
44            if i_row != i_col {
45                let val = *fm.get((i_row, i_col)).expect("Bad formated flowmap");
46                if val != 0.0 {
47                    transition.push(i_row, i_col, val);
48                    row_sum[i_row] += val;
49                }
50            }
51        }
52    });
53
54    (0..n_compartments).for_each(|i_row| {
55        let val = row_sum[i_row];
56        transition.push(i_row, i_row, -val);
57    });
58
59    (transition, row_sum)
60}
61
62#[cfg(feature = "probability")]
63fn get_probability(liquid_neighors: &Array2<usize>, transition: &CooMatrix<f64>) -> Array2<f64> {
64    use nalgebra_sparse::CscMatrix;
65
66    //TODO: this method is called even though there's no flow (0D)
67    //To allow correct behaviour, assert has the condition outflow==0
68    //Find a way cleaner way to : skip test and do not trigger assert
69
70    //TODO change assert to real real and return Result<>
71    let shape = liquid_neighors.dim();
72    //Start with filled with one array to ensure that probability will be increasing
73    let mut proba = Array2::<f64>::ones(shape);
74    let transition_csc = CscMatrix::from(transition);
75    let ghost_neighor = shape.0 + 1;
76    (0..shape.0).for_each(|i_compartment| {
77        let mut cumsum = 0.;
78        let out_flow = round_if_needed!(
79            transition_csc
80                .index_entry(i_compartment, i_compartment)
81                .into_value()
82                .abs(),
83            0.,
84            1e-12
85        );
86
87        //TODO PFR lead to cumsum==-1 how to handle assertion ?
88
89        let mut count_neighbor = 0;
90        liquid_neighors.row(i_compartment).for_each(|&i_neighbor| {
91            if i_neighbor != ghost_neighor {
92                let proba_out: f64 = if out_flow != 0. {
93                    transition_csc
94                        .index_entry(i_compartment, i_neighbor)
95                        .into_value()
96                        / out_flow
97                } else {
98                    0.
99                };
100                debug_assert!(proba_out >= 0.);
101                //round to one if close enough
102                let p_cp = round_if_needed!(proba_out + cumsum, 1., 1e-8);
103
104                *proba
105                    .get_mut((i_compartment, count_neighbor))
106                    .expect("Probability out of bound") = p_cp;
107
108                cumsum += proba_out;
109            }
110            count_neighbor += 1;
111        });
112        //TODO PFR lead to cumsum==-1 how to handle assertion ?
113        // Idea:
114        // let is_pfr = i_compartment == 0 || i_compartment == liquid_neighors.len() - 1;
115        //For PFR NEED TO REMOVE THISASSERT FIXME
116        // assert!(
117        //     (cumsum - 1.0).abs() < 1e-10 || out_flow == 0.,
118        //     "compartment {} cumulative probability = {} < 1 (not conservative)",
119        //     i_compartment,
120        //     cumsum
121        // );
122    });
123
124    proba
125}
126
127pub struct HydroState {
128    pub transition: CooMatrix<f64>,
129    pub out_flows: Vec<f64>,
130    pub volumes: Vec<f64>,
131    pub inverse_volume: Vec<f64>,
132}
133
134impl HydroState {
135    #[inline(always)]
136    pub fn get_volume(&self) -> &[f64] {
137        &self.volumes
138    }
139    #[inline(always)]
140    pub fn get_transition(&self) -> &CooMatrix<f64> {
141        &self.transition
142    }
143    #[inline(always)]
144    pub fn n_compartments(&self) -> usize {
145        self.volumes.len()
146    }
147
148    #[inline(always)]
149    pub fn total_volume(&self) -> f64 {
150        self.volumes.iter().sum()
151    }
152}
153
154impl From<FlowMapDescriptor> for HydroState {
155    fn from(value: FlowMapDescriptor) -> Self {
156        let inverse = value.volumes.iter().map(|val| 1. / val).collect();
157        let (transition, out_flows) = get_transition_from_fm(value.flowmap);
158        HydroState {
159            volumes: value.volumes,
160            out_flows,
161            inverse_volume: inverse,
162            transition,
163        }
164    }
165}
166
167pub struct IterationState {
168    pub liquid: HydroState,
169    pub gas: Option<HydroState>,
170    pub liquid_neighors: Array2<usize>,
171    #[cfg(feature = "probability")]
172    pub liquid_cumulative_probability: Array2<f64>,
173    pub misc: HashMap<String, Vec<f64>>,
174}
175
176impl IterationState {
177    pub fn new(
178        liq: FlowMapDescriptor,
179        gas: Option<FlowMapDescriptor>,
180        misc: HashMap<String, Vec<f64>>,
181    ) -> Self {
182        let liquid_neighors = liq.neighbors.clone(); //Todo find way to remove clone
183
184        let liq_state: HydroState = liq.into();
185
186        //TODO
187        // call this only if liq has flow (transition and neighbors), 0D maps do not have
188        //proba
189        #[cfg(feature = "probability")]
190        let liquid_cumulative_probability =
191            get_probability(&liquid_neighors, &liq_state.transition);
192
193        let ret = Self {
194            liquid: liq_state,
195            gas: gas.map(|_gas| _gas.into()),
196            liquid_neighors,
197            #[cfg(feature = "probability")]
198            liquid_cumulative_probability,
199            misc,
200        };
201
202        if let Some(gas1) = &ret.gas {
203            assert!(gas1.n_compartments() == ret.liquid.n_compartments());
204        }
205
206        ret
207    }
208
209    #[inline(always)]
210    pub fn get(&self, info: &str) -> Option<&[f64]> {
211        self.misc.get(info).map(|v| &**v)
212    }
213
214    #[inline(always)]
215    pub fn n_compartments(&self) -> usize {
216        self.liquid.n_compartments()
217    }
218}
219
220#[cfg(test)]
221mod test {
222    use std::collections::HashMap;
223
224    use crate::test_utils::chain_of_three;
225    use crate::{FlowMapDescriptor, IterationState};
226
227    fn synthetic_descriptor() -> FlowMapDescriptor {
228        let (flow, volume) = chain_of_three();
229        FlowMapDescriptor::from_raw_data(&flow, &volume).unwrap()
230    }
231
232    #[test]
233    fn transition_diagonal_is_minus_out_flow() {
234        let descriptor = synthetic_descriptor();
235        let state = IterationState::new(descriptor, None, HashMap::new());
236
237        // Compartment 0 only sends 1.0 to compartment 1, compartment 1 sends
238        // 0.5 back and 2.0 forward, compartment 2 sends 0.25 back.
239        assert_eq!(state.liquid.out_flows, vec![1.0, 2.5, 0.25]);
240
241        let dense = nalgebra_sparse::CsrMatrix::from(&state.liquid.transition);
242        for (i_compartment, out_flow) in state.liquid.out_flows.iter().enumerate() {
243            let diagonal = dense
244                .get_entry(i_compartment, i_compartment)
245                .unwrap()
246                .into_value();
247            assert!((diagonal + out_flow).abs() < 1e-12);
248        }
249
250        // A transition matrix conserves mass: every row sums to zero.
251        for row in dense.row_iter() {
252            let sum: f64 = row.values().iter().sum();
253            assert!(sum.abs() < 1e-12, "row sum {} is not zero", sum);
254        }
255
256        assert_eq!(state.n_compartments(), 3);
257        assert_eq!(state.liquid.total_volume(), 7.0);
258        assert_eq!(state.liquid.inverse_volume, vec![1.0, 0.5, 0.25]);
259    }
260
261    #[test]
262    fn gas_phase_must_match_liquid_compartment_count() {
263        let state = IterationState::new(
264            synthetic_descriptor(),
265            Some(synthetic_descriptor()),
266            HashMap::new(),
267        );
268
269        assert_eq!(state.gas.unwrap().n_compartments(), 3);
270    }
271
272    #[test]
273    fn construct_itstate_liquid_only() {
274        let _flow_cma = std::env::var("CUVE_SLDMSH_FLOW_PATH");
275        let _volume_cma = std::env::var("CUVE_SLDMSH_VOLUME_PATH");
276
277        if let (Ok(flow_cma), Ok(volume_cma)) = (_flow_cma, _volume_cma) {
278            let descriptor = FlowMapDescriptor::from_path(flow_cma, volume_cma).unwrap();
279
280            let vol_ref = descriptor.volumes.clone();
281
282            let state = IterationState::new(descriptor, None, HashMap::new());
283
284            assert!(state.liquid.volumes == vol_ref);
285        }
286    }
287}