use crate::{LinkState, Network, NodeKind, NodeState};
mod energy;
mod flow_balance;
pub(crate) use energy::PrecomputedPumpPower;
pub(crate) use crate::io::{FlowBalance, PumpEnergy};
#[derive(Debug, Clone)]
pub(crate) struct AccountingState {
pub pump_energy: Vec<PumpEnergy>,
pub peak_demand_kw: f64,
pub flow_balance: FlowBalance,
}
pub(crate) fn init_accounting(network: &Network, node_states: &[NodeState]) -> AccountingState {
let initial_tank_volume: f64 = network
.nodes
.iter()
.enumerate()
.filter_map(|(i, n)| {
if matches!(n.kind, NodeKind::Tank(_)) {
Some(node_states[i].volume)
} else {
None
}
})
.sum();
AccountingState {
pump_energy: vec![PumpEnergy::default(); network.links.len()],
peak_demand_kw: 0.0,
flow_balance: FlowBalance {
total_inflow: 0.0,
total_outflow: 0.0,
demand_deficit: 0.0,
initial_tank_volume,
},
}
}
pub(crate) fn precompute_pump_powers(
network: &Network,
node_states: &[NodeState],
link_states: &[LinkState],
) -> Vec<PrecomputedPumpPower> {
energy::precompute_pump_powers(network, node_states, link_states)
}
pub(crate) fn accumulate_step(
state: &mut AccountingState,
network: &Network,
node_states: &[NodeState],
pump_powers: &[PrecomputedPumpPower],
dt: f64,
t: f64,
overflow_volume: f64,
) {
energy::accumulate_pump_energy(state, network, pump_powers, dt, t);
flow_balance::accumulate_flow_balance(
&mut state.flow_balance,
network,
node_states,
dt,
t,
overflow_volume,
);
}
pub(crate) fn peak_demand_cost(state: &AccountingState, network: &Network) -> f64 {
energy::peak_demand_cost(state, network)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
DemandCategory, Junction, Link, LinkBase, LinkKind, LinkStatus, Node, NodeBase, NodeKind,
NodeState, Pipe, Pump, PumpCurveType, SimulationOptions,
};
fn minimal_network() -> Network {
Network {
title: vec![],
options: SimulationOptions::default(),
patterns: vec![],
curves: vec![],
nodes: vec![Node {
base: NodeBase {
id: "J1".into(),
index: 1,
elevation: 0.0,
initial_quality: 0.0,
},
kind: NodeKind::Junction(Junction {
demands: vec![DemandCategory {
base_demand: 0.0,
pattern: None,
name: None,
}],
emitter_coeff: 0.0,
emitter_exp: 0.5,
}),
source: None,
}],
links: vec![Link {
base: LinkBase {
id: "P1".into(),
index: 1,
from_node: 1,
to_node: 1,
initial_status: LinkStatus::Open,
initial_setting: Some(1.0),
},
kind: LinkKind::Pipe(Pipe {
length: 1000.0,
diameter: 0.3,
roughness: 100.0,
minor_loss: 0.0,
check_valve: false,
bulk_coeff: None,
wall_coeff: None,
leak_coeff_1: 0.0,
leak_coeff_2: 0.0,
}),
}],
controls: vec![],
rules: vec![],
pattern_index: std::collections::HashMap::new(),
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 k_power_is_1e_minus_3() {
assert!((energy::K_POWER - 1.0e-3).abs() < 1e-15);
}
#[test]
fn rho_g_approx() {
let rg = energy::rho_times_g(1.0);
assert!((rg - 9810.0_f64).abs() < 1.0);
}
#[test]
fn efficiency_clamps_to_eta_min() {
let pump = Pump {
curve_type: PumpCurveType::PowerFunction,
head_curve: None,
power: None,
efficiency_curve: None,
default_efficiency: 0.0,
speed_pattern: None,
energy_price: None,
price_pattern: None,
};
let net = minimal_network();
let eta = energy::compute_efficiency(&pump, &net, 0.05, 1.0, 0.0);
assert!((eta - energy::ETA_MIN).abs() < 1e-15);
}
#[test]
fn efficiency_uses_global_when_no_curve_and_no_default() {
let pump = Pump {
curve_type: PumpCurveType::PowerFunction,
head_curve: None,
power: None,
efficiency_curve: None,
default_efficiency: 0.0,
speed_pattern: None,
energy_price: None,
price_pattern: None,
};
let net = minimal_network();
let eta = energy::compute_efficiency(&pump, &net, 0.05, 1.0, 0.5);
assert!((eta - 0.5).abs() < 1e-15);
}
#[test]
fn energy_kwh_internal_units() {
let rg = energy::rho_times_g(1.0); let q = 0.05_f64; let dh = 20.0_f64; let eta = 0.75_f64;
let dt = 3600.0_f64;
let w_hyd = rg * q * dh;
let w_elec = w_hyd / eta;
let kwh = w_elec * energy::K_POWER * dt / 3600.0;
assert!((kwh - 13.08).abs() < 0.1, "kwh = {kwh}");
}
#[test]
fn balance_ratio_unity_no_storage_change() {
let fb = FlowBalance {
total_inflow: 100.0,
total_outflow: 100.0,
demand_deficit: 0.0,
initial_tank_volume: 50.0,
};
let ratio = fb.balance_ratio(50.0);
assert!((ratio - 1.0).abs() < 1e-12);
}
#[test]
fn balance_ratio_filling_tank() {
let fb = FlowBalance {
total_inflow: 110.0,
total_outflow: 100.0,
demand_deficit: 0.0,
initial_tank_volume: 50.0,
};
let ratio = fb.balance_ratio(60.0);
assert!((ratio - 1.0).abs() < 1e-12, "ratio = {ratio}");
}
#[test]
fn balance_ratio_draining_tank() {
let fb = FlowBalance {
total_inflow: 100.0,
total_outflow: 110.0,
demand_deficit: 0.0,
initial_tank_volume: 50.0,
};
let ratio = fb.balance_ratio(40.0);
assert!((ratio - 1.0).abs() < 1e-12, "ratio = {ratio}");
}
#[test]
fn init_accounting_sums_tank_volumes() {
let network = minimal_network();
let node_states: Vec<NodeState> = vec![NodeState::default(); network.nodes.len()];
let acc = init_accounting(&network, &node_states);
assert_eq!(acc.pump_energy.len(), network.links.len());
}
#[test]
fn effective_price_uses_global_when_pump_has_none() {
let pump = Pump {
curve_type: PumpCurveType::PowerFunction,
head_curve: None,
power: None,
efficiency_curve: None,
default_efficiency: 0.0,
speed_pattern: None,
energy_price: Some(0.20),
price_pattern: None,
};
let mut net = minimal_network();
net.options.energy_price = 0.12;
let price = energy::effective_price(&pump, &net, 0.0);
assert!((price - 0.20).abs() < 1e-15);
}
}