hydra_engine_wds/quality/
shared.rs1use std::collections::VecDeque;
2
3pub(super) const C_MAX: f64 = 1.0e6;
5
6pub(super) const Q_STAG: f64 = 3.154e-7;
9
10pub(super) fn qual_flow_dir(q: f64) -> i8 {
13 if q.abs() < Q_STAG {
14 0
15 } else if q > 0.0 {
16 1
17 } else {
18 -1
19 }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct Segment {
25 pub volume: f64,
27 pub concentration: f64,
29}
30
31#[derive(Debug, Clone)]
33pub struct PipeQuality {
34 pub segments: VecDeque<Segment>,
35 pub flow_dir: i8,
37}
38
39#[derive(Debug, Clone)]
41pub enum TankQuality {
42 Cstr {
43 volume: f64,
44 conc: f64,
45 },
46 TwoComp {
47 mix_vol: f64,
48 mix_conc: f64,
49 stag_vol: f64,
50 stag_conc: f64,
51 },
52 Fifo {
53 segments: VecDeque<Segment>,
54 },
55 Lifo {
56 segments: Vec<Segment>,
57 },
58}
59
60#[derive(Debug, PartialEq, Eq, Clone)]
62pub enum QualityError {
63 ModeNone,
65}
66
67pub use crate::io::MassBalance;
68
69pub struct QualityState {
70 pub pipe_quality: Vec<Option<PipeQuality>>,
71 pub tank_quality: Vec<Option<TankQuality>>,
72 pub node_conc: Vec<f64>,
73 pub(super) node_links: Vec<Vec<usize>>,
74 pub(super) topo_order: Vec<usize>,
75 pub(super) adjacency: Vec<Vec<(usize, bool)>>,
76 pub(super) flow_dir: Vec<i8>,
77 pub(super) needs_topo: bool,
78 pub mass_balance: MassBalance,
79 pub pipe_rate_coeff: Vec<f64>,
80 pub(super) tank_overflows: Vec<bool>,
81}
82
83pub(super) fn tank_outflow_conc(tq: &TankQuality) -> f64 {
85 match tq {
86 TankQuality::Cstr { conc, .. } => *conc,
87 TankQuality::TwoComp { mix_conc, .. } => *mix_conc,
88 TankQuality::Fifo { segments } => segments.front().map_or(0.0, |s| s.concentration),
89 TankQuality::Lifo { segments } => segments.last().map_or(0.0, |s| s.concentration),
90 }
91}
92
93pub(super) fn push_segment_merge(segs: &mut VecDeque<Segment>, new: Segment, tol: f64) {
95 if tol > 0.0 {
96 if let Some(back) = segs.back_mut() {
97 if (back.concentration - new.concentration).abs() <= tol {
98 back.concentration = (back.concentration * back.volume
99 + new.concentration * new.volume)
100 / (back.volume + new.volume);
101 back.volume += new.volume;
102 return;
103 }
104 }
105 }
106 segs.push_back(new);
107}
108
109pub(super) fn total_mass(state: &QualityState) -> f64 {
111 let pipe_mass: f64 = state
112 .pipe_quality
113 .iter()
114 .flatten()
115 .flat_map(|pq| pq.segments.iter())
116 .map(|s| s.concentration * s.volume)
117 .sum();
118 let tank_mass: f64 = state
119 .tank_quality
120 .iter()
121 .flatten()
122 .map(|tq| match tq {
123 TankQuality::Cstr { volume, conc } => conc * volume,
124 TankQuality::TwoComp {
125 mix_vol,
126 mix_conc,
127 stag_vol,
128 stag_conc,
129 } => mix_conc * mix_vol + stag_conc * stag_vol,
130 TankQuality::Fifo { segments } => {
131 segments.iter().map(|s| s.concentration * s.volume).sum()
132 }
133 TankQuality::Lifo { segments } => {
134 segments.iter().map(|s| s.concentration * s.volume).sum()
135 }
136 })
137 .sum();
138 pipe_mass + tank_mass
139}
140
141#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
150 fn qual_flow_dir_stagnant_below_threshold() {
151 assert_eq!(qual_flow_dir(0.0), 0);
152 assert_eq!(qual_flow_dir(Q_STAG * 0.5), 0);
153 assert_eq!(qual_flow_dir(-Q_STAG * 0.5), 0);
154 }
155
156 #[test]
157 fn qual_flow_dir_positive_above_threshold() {
158 assert_eq!(qual_flow_dir(Q_STAG * 2.0), 1);
159 assert_eq!(qual_flow_dir(1.0), 1);
160 }
161
162 #[test]
163 fn qual_flow_dir_negative_above_threshold() {
164 assert_eq!(qual_flow_dir(-Q_STAG * 2.0), -1);
165 assert_eq!(qual_flow_dir(-1.0), -1);
166 }
167
168 #[test]
171 fn tank_outflow_conc_cstr_returns_conc() {
172 let tq = TankQuality::Cstr {
173 volume: 100.0,
174 conc: 0.5,
175 };
176 assert!((tank_outflow_conc(&tq) - 0.5).abs() < 1e-12);
177 }
178
179 #[test]
180 fn tank_outflow_conc_two_comp_returns_mix_zone() {
181 let tq = TankQuality::TwoComp {
182 mix_vol: 50.0,
183 mix_conc: 1.2,
184 stag_vol: 50.0,
185 stag_conc: 0.3,
186 };
187 assert!((tank_outflow_conc(&tq) - 1.2).abs() < 1e-12);
188 }
189
190 #[test]
191 fn tank_outflow_conc_fifo_returns_front() {
192 let mut segs = VecDeque::new();
193 segs.push_back(Segment {
194 volume: 10.0,
195 concentration: 0.7,
196 });
197 segs.push_back(Segment {
198 volume: 10.0,
199 concentration: 0.4,
200 });
201 let tq = TankQuality::Fifo { segments: segs };
202 assert!((tank_outflow_conc(&tq) - 0.7).abs() < 1e-12);
203 }
204
205 #[test]
206 fn tank_outflow_conc_lifo_returns_last() {
207 let tq = TankQuality::Lifo {
208 segments: vec![
209 Segment {
210 volume: 10.0,
211 concentration: 0.1,
212 },
213 Segment {
214 volume: 10.0,
215 concentration: 0.9,
216 },
217 ],
218 };
219 assert!((tank_outflow_conc(&tq) - 0.9).abs() < 1e-12);
220 }
221
222 #[test]
225 fn push_segment_merge_appends_when_outside_tolerance() {
226 let mut segs = VecDeque::new();
227 segs.push_back(Segment {
228 volume: 1.0,
229 concentration: 0.0,
230 });
231 push_segment_merge(
232 &mut segs,
233 Segment {
234 volume: 2.0,
235 concentration: 1.0,
236 },
237 0.01,
238 );
239 assert_eq!(segs.len(), 2);
240 assert!((segs.back().unwrap().concentration - 1.0).abs() < 1e-12);
241 }
242
243 #[test]
244 fn push_segment_merge_merges_when_within_tolerance() {
245 let mut segs = VecDeque::new();
246 segs.push_back(Segment {
247 volume: 2.0,
248 concentration: 1.0,
249 });
250 push_segment_merge(
252 &mut segs,
253 Segment {
254 volume: 2.0,
255 concentration: 1.005,
256 },
257 0.01,
258 );
259 assert_eq!(segs.len(), 1);
260 let merged = segs.back().unwrap();
261 assert!((merged.volume - 4.0).abs() < 1e-12);
262 assert!((merged.concentration - 1.0025).abs() < 1e-12);
264 }
265
266 #[test]
267 fn push_segment_merge_zero_tol_always_appends() {
268 let mut segs = VecDeque::new();
269 segs.push_back(Segment {
270 volume: 1.0,
271 concentration: 1.0,
272 });
273 push_segment_merge(
274 &mut segs,
275 Segment {
276 volume: 1.0,
277 concentration: 1.0,
278 },
279 0.0,
280 );
281 assert_eq!(segs.len(), 2);
283 }
284}