1use crate::{
7 CoreError,
8 coordinates::{CartesianCoordinates, CartesianVec3},
9 errors::ModelError,
10 model::{
11 compartments::{CompartmentInfo, CountVolumeElement, ElementVolumeInfo},
12 interfaces::{AInterfacesInfo, InterfaceFlow, InterfaceInfo},
13 },
14};
15use std::{f64, sync::Arc};
16mod data;
17use cmtool_data::{FluxFileHeader, RawDataFlux, RawDataScalar, RawFlux};
18mod balance;
19mod compartments;
20mod geometry;
21mod interfaces;
22mod scalar;
23mod vectors;
24pub use scalar::Scalar;
25pub use vectors::Vector as ModelVector;
26pub use vectors::Vector;
27
28pub use balance::{BalanceReport, BalanceSettings};
29
30pub struct CMModel {
31 geometry: Arc<CMGeometry>,
32 c_info: CompartmentInfo,
33 interfaces: AInterfacesInfo,
34}
35
36pub use geometry::CMGeometry;
37
38fn get_data_flow(
39 n_zones: usize,
40 i_info: &[InterfaceInfo],
41 i_flow: &[InterfaceFlow],
42) -> RawDataFlux {
43 let fluxes: Vec<RawFlux> = i_info
44 .iter()
45 .zip(i_flow)
46 .map(|(k_i_nfo, k_i_flow)| RawFlux {
47 id_source: k_i_nfo.source_id as u32,
48 id_target: k_i_nfo.target_id as u32,
49 flux_source_target: k_i_flow.source_flow,
50 flux_target_source: k_i_flow.target_flow,
51 })
52 .collect();
53 RawDataFlux {
54 header: FluxFileHeader {
55 n_fluxes: i_flow.len() as u32,
56 n_zone: n_zones as u32,
57 },
58 fluxes,
59 }
60}
61
62impl CMModel {
63 fn check_flux(n_zone: u32, rf: &RawFlux) -> bool {
66 let is_flow_valid = |flow: f64| flow.is_finite() && flow.is_sign_positive();
67
68 is_flow_valid(rf.flux_source_target)
69 && is_flow_valid(rf.flux_target_source)
70 && rf.id_source < n_zone
71 && rf.id_target < n_zone
72 }
73
74 pub fn check_flow(&self, raw: &RawDataFlux, max_divergence: f64) -> Result<(), ModelError> {
75 const ABS_TOLERANCE_DIVERGENCE_CELL: f64 = 1e-7;
76
77 if raw
80 .fluxes
81 .iter()
82 .all(|flux| flux.flux_source_target == 0. && flux.flux_target_source == 0.)
83 {
84 return Err(ModelError::EmptyFlow);
85 }
86
87 let mut mass_balance: Vec<InterfaceFlow> =
88 vec![InterfaceFlow::default(); raw.header.n_zone as usize];
89 let mut id_max = u32::MIN;
90
91 for flow in raw.fluxes.iter() {
92 if !Self::check_flux(raw.header.n_zone, flow) {
93 return Err(ModelError::InvalidFlow);
94 }
95
96 id_max = u32::max(id_max, flow.id_source);
97 mass_balance[flow.id_source as usize].source_flow += flow.flux_target_source;
99 mass_balance[flow.id_source as usize].target_flow += flow.flux_source_target;
101
102 mass_balance[flow.id_target as usize].source_flow += flow.flux_source_target;
104 mass_balance[flow.id_target as usize].target_flow += flow.flux_target_source;
106 }
107
108 for flow in &mass_balance {
109 let abs_diff = (flow.source_flow - flow.target_flow).abs();
110 let denom = flow.source_flow.abs() + flow.target_flow.abs();
111
112 let err = if denom > f64::EPSILON {
113 2.0 * abs_diff / denom
114 } else {
115 abs_diff
116 };
117 if abs_diff > ABS_TOLERANCE_DIVERGENCE_CELL && err > max_divergence {
118 return Err(ModelError::CellToCellDivergence(err, max_divergence));
119 }
120 }
121
122 Ok(())
123 }
124}
125
126impl CMModel {
127 pub fn grid(&self) -> &dyn crate::grid::CompartmentMesh {
128 self.geometry.get_grid().unwrap()
129 }
130
131 pub fn init(geometry: Arc<CMGeometry>) -> Self {
132 println!("Init model with {} compartment", geometry.n_zone());
133 let volume_element_count = geometry.get_count_volume_element_first_pass();
134
135 let n_zone_with_volume_element = volume_element_count.n_zone_with_volume_element();
136
137 if n_zone_with_volume_element != geometry.n_zone() {
138 unimplemented!(
139 "Detected compartment should be the same as given by user {} vs {}",
140 n_zone_with_volume_element,
141 geometry.n_zone()
142 )
143 }
144
145 let interface_count_raw = volume_element_count.at_interface.clone();
146
147 let (c_info, interfaces) = volume_element_count.into_reduce();
148
149 if interfaces.n_interfaces() > geometry.get_grid().as_ref().unwrap().n_maximum_interface() {
151 unimplemented!(
152 "RCMTOOL: CMModel::init: should have intefaces < n_maximum_interface {} {}",
153 interfaces.n_interfaces(),
154 geometry.get_grid().as_ref().unwrap().n_maximum_interface()
155 )
156 }
157
158 let mut model = Self {
159 geometry,
160 c_info,
161 interfaces,
162 };
163
164 model.c_info.fill(&model.geometry);
165
166 model.interfaces.fill(&model.geometry, &interface_count_raw);
167
168 model
169 }
170
171 #[allow(unused)]
172 fn compute_volume_integral_per_zone() -> Vec<f64> {
173 todo!()
174 }
175
176 fn normal_velocity(&self, vector: &Vector, element_global_id: usize, axis: usize) -> f64 {
182 let velocity = CartesianVec3(*vector.get_slice_xyz(element_global_id));
183 let CartesianCoordinates(centroid) = self.geometry.volume_elements.xyz[element_global_id];
184 let theta = centroid[1].atan2(centroid[0]);
185
186 velocity.to_cylindrical_vec(theta).0[axis]
187 }
188
189 pub fn compute_flux_between_compartments(
190 &self,
191 vector: Vector,
192 settings: &BalanceSettings,
193 ) -> Result<cmtool_data::RawDataFlux, CoreError> {
194 let n_fluxes = self.interfaces.n_interfaces();
195 let mut flows: Vec<InterfaceFlow> = vec![Default::default(); n_fluxes];
196
197 for (i_interface, flow) in flows.iter_mut().enumerate() {
205 let axis: usize = self.interfaces.normal_axis[i_interface];
206 let areas = &self.interfaces.area[i_interface];
207 let elements = &self.interfaces.global_id_from_interface[i_interface];
208
209 for (&element_global_id, area) in elements.iter().zip(areas) {
210 let f = area * self.normal_velocity(&vector, element_global_id, axis);
211
212 if f > 0. {
214 flow.source_flow += f;
215 } else {
216 flow.target_flow -= f;
217 }
218 }
219 }
220
221 let balance = self.balance(&mut flows, settings);
222 if balance.residual > settings.max_divergence {
223 return Err(ModelError::CellToCellDivergence(
224 balance.residual,
225 settings.max_divergence,
226 )
227 .into());
228 }
229
230 let data_flow = get_data_flow(self.geometry.n_zone(), &self.interfaces.ids, &flows);
231
232 self.check_flow(&data_flow, settings.max_divergence)?;
233 Ok(data_flow)
234 }
235
236 fn balance(&self, flows: &mut [InterfaceFlow], settings: &BalanceSettings) -> BalanceReport {
246 let n_zone = self.geometry.n_zone();
247 let mut inflow = vec![0.0f64; n_zone];
248 let mut outflow = vec![0.0f64; n_zone];
249 let mut report = BalanceReport {
250 iterations: 0,
251 residual: f64::INFINITY,
252 };
253
254 for iteration in 0..settings.max_iterations {
255 inflow.fill(0.);
256 outflow.fill(0.);
257 for (i_interface, flow) in flows.iter().enumerate() {
258 let source_id = self.interfaces.ids[i_interface].source_id;
259 let target_id = self.interfaces.ids[i_interface].target_id;
260 outflow[source_id] += flow.source_flow;
261 inflow[target_id] += flow.source_flow;
262 outflow[target_id] += flow.target_flow;
263 inflow[source_id] += flow.target_flow;
264 }
265
266 report.iterations = iteration;
267 report.residual = (0..n_zone)
268 .map(|zone| {
269 let total = inflow[zone].abs() + outflow[zone].abs();
270 if total > f64::EPSILON {
271 2. * (inflow[zone] - outflow[zone]).abs() / total
272 } else {
273 0.
274 }
275 })
276 .fold(0.0f64, f64::max);
277
278 if report.residual < settings.tolerance {
279 break;
280 }
281
282 let factor: Vec<f64> = (0..n_zone)
284 .map(|zone| {
285 if inflow[zone] > f64::EPSILON && outflow[zone] > f64::EPSILON {
286 (inflow[zone] / outflow[zone]).sqrt()
287 } else {
288 1.
289 }
290 })
291 .collect();
292
293 for (i_interface, flow) in flows.iter_mut().enumerate() {
294 let source_id = self.interfaces.ids[i_interface].source_id;
295 let target_id = self.interfaces.ids[i_interface].target_id;
296 flow.source_flow *= factor[source_id];
297 flow.target_flow *= factor[target_id];
298 }
299 }
300
301 report
302 }
303
304 pub fn export_volume_integral_per_zone(
305 &self,
306 model_scalar: Scalar,
307 ) -> Result<cmtool_data::RawDataScalar, CoreError> {
308 println!("Creating scalar {}", model_scalar.name.trim(),);
309 let mut scalar_field = RawDataScalar::new(self.geometry.n_zone());
310
311 scalar_field.values = vec![(0.).into(); self.geometry.n_zone()];
312
313 let iterator = self.c_info.volumes.iter().zip(&mut scalar_field.values);
314
315 for (volumes_i, field) in iterator {
316 field.value =
317 volumes_i
318 .iter()
319 .fold(0.0, |acc, ElementVolumeInfo { global_id, volume }| {
320 acc + model_scalar[*global_id] * volume
321 });
322 }
323
324 Ok(scalar_field)
325 }
326
327 pub fn compartments_volumes(&self) -> Vec<f64> {
328 todo!("grid compartment calculation")
329 }
330
331 pub fn get_real_volume(&self) -> Vec<f64> {
332 self.c_info
333 .volumes
334 .iter()
335 .map(|zone| zone.iter().map(|v| v.volume).sum())
336 .collect()
337 }
338}
339
340#[cfg(test)]
341mod test {
342 use super::*;
343
344 const N_ZONE: u32 = 2;
345
346 fn flux(id_source: u32, id_target: u32, source_target: f64, target_source: f64) -> RawFlux {
347 RawFlux {
348 id_source,
349 id_target,
350 flux_source_target: source_target,
351 flux_target_source: target_source,
352 }
353 }
354
355 #[test]
356 fn test_check_flux_accepts_valid_flux() {
357 assert!(CMModel::check_flux(N_ZONE, &flux(0, 1, 2., 0.)));
358 }
359
360 #[test]
362 fn test_check_flux_rejects_unknown_compartment() {
363 assert!(!CMModel::check_flux(N_ZONE, &flux(N_ZONE, 1, 2., 0.)));
364 assert!(!CMModel::check_flux(N_ZONE, &flux(0, N_ZONE, 2., 0.)));
365 }
366
367 #[test]
368 fn test_check_flux_rejects_unusable_flow() {
369 assert!(!CMModel::check_flux(N_ZONE, &flux(0, 1, f64::NAN, 0.)));
370 assert!(!CMModel::check_flux(N_ZONE, &flux(0, 1, -2., 0.)));
371 assert!(!CMModel::check_flux(N_ZONE, &flux(0, 1, 2., f64::INFINITY)));
373 assert!(!CMModel::check_flux(N_ZONE, &flux(0, 1, 2., -1.)));
374 }
375}