hydra_engine_wds/simulation/
results.rs1use super::*;
2use crate::io::{FlowBalance, FlowBalanceSummary, MassBalance, PumpEnergy};
3
4#[derive(Debug, Clone, Default)]
9pub struct ResultRanges {
10 pub pressure_min: f64,
12 pub pressure_max: f64,
14 pub head_min: f64,
16 pub head_max: f64,
18 pub demand_min: f64,
20 pub demand_max: f64,
22 pub flow_min: f64,
24 pub flow_max: f64,
26 pub velocity_min: f64,
28 pub velocity_max: f64,
30}
31
32#[derive(Debug, Clone)]
36pub struct NodeResult {
37 pub head: f64,
39 pub pressure: f64,
41 pub demand: f64,
43}
44
45#[derive(Debug, Clone)]
49pub struct LinkResult {
50 pub flow: f64,
52 pub velocity: f64,
54 pub head_loss: f64,
56 pub status: f64,
58}
59
60impl Simulation {
61 pub fn get_node_result(
68 &self,
69 node_id: &str,
70 quantity: NodeQuantity,
71 t: f64,
72 ) -> Result<f64, SessionError> {
73 let network = self.require_loaded_network()?;
74 let node_index = node_index_by_id(network, node_id)
75 .ok_or_else(|| SessionError::UnknownId(node_id.to_string()))?;
76 let snapshot = self
77 .snapshot_near(t)
78 .ok_or(SessionError::NoSnapshotAtTime { requested_t: t })?;
79 let node_state = &snapshot.node_states[node_index];
80 let node = &network.nodes[node_index];
81 let elevation = node.base.elevation;
82 Ok(match quantity {
83 NodeQuantity::Head => node_state.head,
84 NodeQuantity::GaugePressure => {
85 let physical_elevation = match &node.kind {
89 NodeKind::Tank(tank) => elevation - tank.min_level,
90 _ => elevation,
91 };
92 node_state.head - physical_elevation
93 }
94 NodeQuantity::Demand => {
95 match &node.kind {
100 NodeKind::Junction(_) => {
101 node_state.demand_flow + node_state.emitter_flow + node_state.leakage_flow
102 }
103 NodeKind::Reservoir(_) | NodeKind::Tank(_) => node_state.net_flow,
104 }
105 }
106 NodeQuantity::Quality => node_state.quality,
107 })
108 }
109
110 pub fn get_link_result(
112 &self,
113 link_id: &str,
114 quantity: LinkQuantity,
115 t: f64,
116 ) -> Result<f64, SessionError> {
117 let network = self.require_loaded_network()?;
118 let link_index = link_index_by_id(network, link_id)
119 .ok_or_else(|| SessionError::UnknownId(link_id.to_string()))?;
120 let snapshot = self
121 .snapshot_near(t)
122 .ok_or(SessionError::NoSnapshotAtTime { requested_t: t })?;
123 let link_state = &snapshot.link_states[link_index];
124 let link = &network.links[link_index];
125
126 let is_closed = matches!(
129 link_state.status,
130 LinkStatus::Closed | LinkStatus::XHead | LinkStatus::TempClosed
131 );
132
133 Ok(match quantity {
134 LinkQuantity::Flow => {
135 if is_closed {
136 0.0
137 } else {
138 link_state.flow
139 }
140 }
141 LinkQuantity::MeanVelocity => {
142 if let LinkKind::Pipe(pipe) = &link.kind {
143 let area = std::f64::consts::PI * (pipe.diameter / 2.0).powi(2);
144 if area > 0.0 {
145 link_state.flow.abs() / area
146 } else {
147 0.0
148 }
149 } else {
150 0.0
151 }
152 }
153 LinkQuantity::UnitHeadLoss => {
154 if let LinkKind::Pipe(pipe) = &link.kind {
155 let from_node_index = link.base.from_idx();
156 let to_node_index = link.base.to_idx();
157 let head_drop = (snapshot.node_states[from_node_index].head
158 - snapshot.node_states[to_node_index].head)
159 .abs();
160 if pipe.length > 0.0 {
161 head_drop / pipe.length
162 } else {
163 0.0
164 }
165 } else {
166 0.0
167 }
168 }
169 LinkQuantity::FrictionFactor => {
170 use crate::HeadLossFormula;
172 if network.options.head_loss_formula != HeadLossFormula::DarcyWeisbach {
173 return Ok(0.0);
174 }
175 if let LinkKind::Pipe(pipe) = &link.kind {
176 let from_node_index = link.base.from_idx();
177 let to_node_index = link.base.to_idx();
178 let head_drop = (snapshot.node_states[from_node_index].head
179 - snapshot.node_states[to_node_index].head)
180 .abs();
181 const Q_CLOSED: f64 = 1.0e-6;
185 let area = std::f64::consts::PI * (pipe.diameter / 2.0).powi(2);
186 let velocity = if area > 0.0 && link_state.flow.abs() >= Q_CLOSED {
187 link_state.flow.abs() / area
188 } else {
189 0.0
190 };
191 if velocity > 0.0 && pipe.length > 0.0 {
192 let two_g = 2.0 * crate::hydraulics::G_DW;
196 (head_drop * pipe.diameter * two_g) / (pipe.length * velocity * velocity)
197 } else {
198 0.0
199 }
200 } else {
201 0.0
202 }
203 }
204 LinkQuantity::Quality => link_state.quality,
205 LinkQuantity::Status => link_status_to_f64(link_state.status),
206 LinkQuantity::Setting => link_state.setting,
207 })
208 }
209
210 pub fn snapshot_times(&self) -> Vec<f64> {
216 self.hyd_snapshots.iter().map(|s| s.t).collect()
217 }
218
219 pub fn result_ranges(&self) -> Result<ResultRanges, SessionError> {
224 let network = self.require_loaded_network()?;
225 if self.hyd_snapshots.is_empty() {
226 return Err(SessionError::InvalidPhase {
227 expected: "HydraulicsDone".into(),
228 actual: self.phase.name().to_string(),
229 });
230 }
231
232 let mut r = ResultRanges {
233 pressure_min: f64::INFINITY,
234 pressure_max: f64::NEG_INFINITY,
235 head_min: f64::INFINITY,
236 head_max: f64::NEG_INFINITY,
237 demand_min: f64::INFINITY,
238 demand_max: f64::NEG_INFINITY,
239 flow_min: f64::INFINITY,
240 flow_max: f64::NEG_INFINITY,
241 velocity_min: f64::INFINITY,
242 velocity_max: f64::NEG_INFINITY,
243 };
244
245 let pipe_areas: Vec<f64> = network
247 .links
248 .iter()
249 .map(|link| {
250 if let LinkKind::Pipe(pipe) = &link.kind {
251 std::f64::consts::PI * (pipe.diameter / 2.0).powi(2)
252 } else {
253 0.0
254 }
255 })
256 .collect();
257
258 for snap in &self.hyd_snapshots {
259 for (i, ns) in snap.node_states.iter().enumerate() {
261 let node = &network.nodes[i];
262 let elevation = node.base.elevation;
263
264 let h = ns.head;
266 if h < r.head_min {
267 r.head_min = h;
268 }
269 if h > r.head_max {
270 r.head_max = h;
271 }
272
273 let physical_elevation = match &node.kind {
275 NodeKind::Tank(tank) => elevation - tank.min_level,
276 _ => elevation,
277 };
278 let p = h - physical_elevation;
279 if p < r.pressure_min {
280 r.pressure_min = p;
281 }
282 if p > r.pressure_max {
283 r.pressure_max = p;
284 }
285
286 let d = match &node.kind {
288 NodeKind::Junction(_) => ns.demand_flow + ns.emitter_flow + ns.leakage_flow,
289 NodeKind::Reservoir(_) | NodeKind::Tank(_) => ns.net_flow,
290 };
291 if d < r.demand_min {
292 r.demand_min = d;
293 }
294 if d > r.demand_max {
295 r.demand_max = d;
296 }
297 }
298
299 for (i, ls) in snap.link_states.iter().enumerate() {
301 let is_closed = matches!(
302 ls.status,
303 LinkStatus::Closed | LinkStatus::XHead | LinkStatus::TempClosed
304 );
305
306 let f = if is_closed { 0.0 } else { ls.flow };
308 if f < r.flow_min {
309 r.flow_min = f;
310 }
311 if f > r.flow_max {
312 r.flow_max = f;
313 }
314
315 let area = pipe_areas[i];
317 if area > 0.0 {
318 let v = ls.flow.abs() / area;
319 if v < r.velocity_min {
320 r.velocity_min = v;
321 }
322 if v > r.velocity_max {
323 r.velocity_max = v;
324 }
325 }
326 }
327 }
328
329 if r.velocity_min == f64::INFINITY {
331 r.velocity_min = 0.0;
332 r.velocity_max = 0.0;
333 }
334
335 Ok(r)
336 }
337
338 pub fn all_node_results_at(&self, t: f64) -> Result<Vec<NodeResult>, SessionError> {
343 let network = self.require_loaded_network()?;
344 let snapshot = self
345 .snapshot_near(t)
346 .ok_or(SessionError::NoSnapshotAtTime { requested_t: t })?;
347
348 let mut results = Vec::with_capacity(network.nodes.len());
349 for (i, ns) in snapshot.node_states.iter().enumerate() {
350 let node = &network.nodes[i];
351 let elevation = node.base.elevation;
352
353 let pressure = {
354 let physical_elevation = match &node.kind {
355 NodeKind::Tank(tank) => elevation - tank.min_level,
356 _ => elevation,
357 };
358 ns.head - physical_elevation
359 };
360
361 let demand = match &node.kind {
362 NodeKind::Junction(_) => ns.demand_flow + ns.emitter_flow + ns.leakage_flow,
363 NodeKind::Reservoir(_) | NodeKind::Tank(_) => ns.net_flow,
364 };
365
366 results.push(NodeResult {
367 head: ns.head,
368 pressure,
369 demand,
370 });
371 }
372 Ok(results)
373 }
374
375 pub fn all_link_results_at(&self, t: f64) -> Result<Vec<LinkResult>, SessionError> {
380 let network = self.require_loaded_network()?;
381 let snapshot = self
382 .snapshot_near(t)
383 .ok_or(SessionError::NoSnapshotAtTime { requested_t: t })?;
384
385 let mut results = Vec::with_capacity(network.links.len());
386 for (i, ls) in snapshot.link_states.iter().enumerate() {
387 let link = &network.links[i];
388
389 let is_closed = matches!(
390 ls.status,
391 LinkStatus::Closed | LinkStatus::XHead | LinkStatus::TempClosed
392 );
393
394 let flow = if is_closed { 0.0 } else { ls.flow };
395
396 let velocity = if let LinkKind::Pipe(pipe) = &link.kind {
397 let area = std::f64::consts::PI * (pipe.diameter / 2.0).powi(2);
398 if area > 0.0 {
399 ls.flow.abs() / area
400 } else {
401 0.0
402 }
403 } else {
404 0.0
405 };
406
407 let head_loss = if let LinkKind::Pipe(pipe) = &link.kind {
408 let from_idx = link.base.from_idx();
409 let to_idx = link.base.to_idx();
410 let head_drop =
411 (snapshot.node_states[from_idx].head - snapshot.node_states[to_idx].head).abs();
412 if pipe.length > 0.0 {
413 head_drop / pipe.length
414 } else {
415 0.0
416 }
417 } else {
418 0.0
419 };
420
421 let status = link_status_to_f64(ls.status);
422
423 results.push(LinkResult {
424 flow,
425 velocity,
426 head_loss,
427 status,
428 });
429 }
430 Ok(results)
431 }
432
433 pub fn node_ids(&self) -> Vec<&str> {
435 match &self.network {
436 Some(n) => n.nodes.iter().map(|nd| nd.base.id.as_str()).collect(),
437 None => vec![],
438 }
439 }
440
441 pub fn link_ids(&self) -> Vec<&str> {
443 match &self.network {
444 Some(n) => n.links.iter().map(|lk| lk.base.id.as_str()).collect(),
445 None => vec![],
446 }
447 }
448
449 pub fn pump_ids(&self) -> Vec<&str> {
451 match &self.network {
452 Some(n) => n
453 .links
454 .iter()
455 .filter(|l| matches!(l.kind, LinkKind::Pump(_)))
456 .map(|l| l.base.id.as_str())
457 .collect(),
458 None => vec![],
459 }
460 }
461
462 pub fn flow_units(&self) -> Option<FlowUnits> {
464 self.network.as_ref().map(|n| n.options.flow_units)
465 }
466
467 pub fn get_pump_energy(&self, pump_id: &str) -> Result<&PumpEnergy, SessionError> {
469 let network = self.require_loaded_network()?;
470 let link_index = link_index_by_id(network, pump_id)
471 .ok_or_else(|| SessionError::UnknownId(pump_id.to_string()))?;
472 if !matches!(network.links[link_index].kind, LinkKind::Pump(_)) {
474 return Err(SessionError::UnknownId(pump_id.to_string()));
475 }
476 let accounting_state =
477 self.accounting
478 .as_ref()
479 .ok_or_else(|| SessionError::InvalidPhase {
480 expected: "HydraulicsDone".into(),
481 actual: self.phase.name().to_string(),
482 })?;
483 Ok(&accounting_state.pump_energy[link_index])
484 }
485
486 pub fn get_mass_balance(&self) -> Result<&MassBalance, SessionError> {
488 let qs = self
489 .quality_state
490 .as_ref()
491 .ok_or_else(|| SessionError::InvalidPhase {
492 expected: "QualityDone".into(),
493 actual: self.phase.name().to_string(),
494 })?;
495 Ok(&qs.mass_balance)
496 }
497
498 pub fn get_flow_balance(&self) -> Result<&FlowBalance, SessionError> {
500 let acc = self
501 .accounting
502 .as_ref()
503 .ok_or_else(|| SessionError::InvalidPhase {
504 expected: "Loaded".into(),
505 actual: self.phase.name().to_string(),
506 })?;
507 Ok(&acc.flow_balance)
508 }
509
510 pub fn final_tank_volume(&self) -> Result<f64, SessionError> {
514 let network = self.require_loaded_network()?;
515 let vol: f64 = network
516 .nodes
517 .iter()
518 .enumerate()
519 .filter_map(|(i, n)| {
520 if matches!(n.kind, NodeKind::Tank(_)) {
521 Some(self.node_states[i].volume)
522 } else {
523 None
524 }
525 })
526 .sum();
527 Ok(vol)
528 }
529
530 pub fn flow_balance_summary(&self) -> Result<FlowBalanceSummary, SessionError> {
532 let fb = self.get_flow_balance()?;
533 let final_vol = self.final_tank_volume()?;
534 Ok(fb.summarize(final_vol))
535 }
536
537 pub fn warnings(&self) -> &[SimWarning] {
539 &self.warnings
540 }
541}