hydra-engine-wds 1.0.2

Hydra water distribution engine — data model, hydraulic solver, quality engine, session API, analytics
Documentation
use crate::{DemandModel, Network, NodeKind, NodeState};

use super::FlowBalance;

pub(super) fn accumulate_flow_balance(
    flow_balance: &mut FlowBalance,
    network: &Network,
    node_states: &[NodeState],
    dt: f64,
    t: f64,
    overflow_volume: f64,
) {
    let opts = &network.options;

    for (i, node) in network.nodes.iter().enumerate() {
        let ns = &node_states[i];
        match &node.kind {
            NodeKind::Reservoir(_) => {
                if ns.net_flow < 0.0 {
                    flow_balance.total_inflow += ns.net_flow.abs() * dt;
                } else {
                    flow_balance.total_outflow += ns.net_flow * dt;
                }
            }
            NodeKind::Junction(j) => {
                let d = ns.demand_flow;
                if d < 0.0 {
                    flow_balance.total_inflow += d.abs() * dt;
                } else {
                    flow_balance.total_outflow += (d + ns.emitter_flow + ns.leakage_flow) * dt;
                }
                if opts.demand_model == DemandModel::PressureDriven {
                    let d_full = j.total_demand(t, opts, &network.patterns, &network.pattern_index);
                    let deficit = (d_full - d).max(0.0);
                    flow_balance.demand_deficit += deficit * dt;
                }
            }
            NodeKind::Tank(_) => {}
        }
    }

    flow_balance.total_outflow += overflow_volume;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        DemandCategory, DemandModel, Junction, Network, Node, NodeBase, NodeKind, NodeState,
        Pattern, Reservoir, SimulationOptions,
    };
    use std::collections::HashMap;

    fn balance_network(demand_model: DemandModel) -> Network {
        let options = SimulationOptions {
            demand_model,
            pattern_step: 3600.0,
            pattern_start: 0.0,
            ..Default::default()
        };

        let mut pattern_index = HashMap::new();
        pattern_index.insert("pat".to_string(), 0);

        Network {
            title: vec![],
            options,
            patterns: vec![Pattern {
                id: "pat".to_string(),
                factors: vec![2.0],
            }],
            curves: vec![],
            nodes: vec![
                Node {
                    base: NodeBase {
                        id: "R1".to_string(),
                        index: 1,
                        elevation: 0.0,
                        initial_quality: 0.0,
                    },
                    kind: NodeKind::Reservoir(Reservoir { head_pattern: None }),
                    source: None,
                },
                Node {
                    base: NodeBase {
                        id: "J1".to_string(),
                        index: 2,
                        elevation: 0.0,
                        initial_quality: 0.0,
                    },
                    kind: NodeKind::Junction(Junction {
                        demands: vec![DemandCategory {
                            base_demand: 3.0,
                            pattern: Some("pat".to_string()),
                            name: None,
                        }],
                        emitter_coeff: 0.0,
                        emitter_exp: 0.5,
                    }),
                    source: None,
                },
            ],
            links: vec![],
            controls: vec![],
            rules: vec![],
            pattern_index,
            report: crate::ReportOptions::default(),
            coordinates: std::collections::HashMap::new(),
            vertices: std::collections::HashMap::new(),
            node_tags: std::collections::HashMap::new(),
            link_tags: std::collections::HashMap::new(),
        }
    }

    #[test]
    fn accumulate_flow_balance_tracks_reservoir_junction_and_overflow_terms() {
        let network = balance_network(DemandModel::DemandDriven);
        let mut flow_balance = FlowBalance {
            total_inflow: 0.0,
            total_outflow: 0.0,
            demand_deficit: 0.0,
            initial_tank_volume: 0.0,
        };
        let node_states = vec![
            NodeState {
                net_flow: -4.0,
                ..NodeState::default()
            },
            NodeState {
                demand_flow: 2.0,
                emitter_flow: 0.5,
                leakage_flow: 0.25,
                ..NodeState::default()
            },
        ];

        accumulate_flow_balance(&mut flow_balance, &network, &node_states, 10.0, 0.0, 7.0);

        assert_eq!(flow_balance.total_inflow, 40.0);
        assert_eq!(flow_balance.total_outflow, 34.5);
        assert_eq!(flow_balance.demand_deficit, 0.0);
    }

    #[test]
    fn accumulate_flow_balance_adds_pressure_driven_demand_deficit() {
        let network = balance_network(DemandModel::PressureDriven);
        let mut flow_balance = FlowBalance {
            total_inflow: 0.0,
            total_outflow: 0.0,
            demand_deficit: 0.0,
            initial_tank_volume: 0.0,
        };
        let node_states = vec![
            NodeState::default(),
            NodeState {
                demand_flow: 4.0,
                ..NodeState::default()
            },
        ];

        accumulate_flow_balance(&mut flow_balance, &network, &node_states, 10.0, 0.0, 0.0);

        assert_eq!(flow_balance.total_outflow, 40.0);
        assert_eq!(flow_balance.demand_deficit, 20.0);
    }
}