use crate::{
Curve, HeadLossFormula, Link, LinkKind, LinkStatus, Network, PumpCurveType, ValveType,
};
use super::shared::{HydraulicError, PumpCoeffs, Py, C_INF, GAMMA_WATER, G_MIN, Q_CLOSED};
use super::{pipe_total_hg, valve::valve_py, HW_EXP};
pub(super) fn initialise_flows(
network: &Network,
flows: &mut [f64],
statuses: &[LinkStatus],
settings: &[f64],
pump_coeffs: &[Option<PumpCoeffs>],
pump_curve_idx: &[Option<usize>],
) {
for (k, link) in network.links.iter().enumerate() {
let is_closed = matches!(
statuses[k],
LinkStatus::Closed
| LinkStatus::XPressure
| LinkStatus::XFcv
| LinkStatus::XHead
| LinkStatus::TempClosed
);
flows[k] = if is_closed {
Q_CLOSED
} else {
match &link.kind {
LinkKind::Pump(_) => {
let omega = settings[k];
let q_design =
pump_design_flow(k, link, pump_coeffs, pump_curve_idx, &network.curves);
omega * q_design
}
LinkKind::Pipe(pipe) => std::f64::consts::PI * pipe.diameter * pipe.diameter / 4.0,
LinkKind::Valve(v) => std::f64::consts::PI * v.diameter * v.diameter / 4.0,
}
};
}
}
fn pump_design_flow(
k: usize,
link: &Link,
pump_coeffs: &[Option<PumpCoeffs>],
pump_curve_idx: &[Option<usize>],
curves: &[Curve],
) -> f64 {
let LinkKind::Pump(pump) = &link.kind else {
return 0.028317; };
match pump.curve_type {
PumpCurveType::ConstHp => 0.028317, PumpCurveType::PowerFunction => {
if let Some(idx) = pump_curve_idx[k] {
let curve = &curves[idx];
if curve.points.len() >= 2 {
return curve.points[curve.points.len() / 2].x;
}
}
if let Some(c) = &pump_coeffs[k] {
(c.h0 / c.r).powf(1.0 / c.n)
} else {
0.028317 }
}
PumpCurveType::Custom => {
if let Some(idx) = pump_curve_idx[k] {
let curve = &curves[idx];
if curve.points.len() >= 2 {
let x0 = curve.points.first().unwrap().x;
let xl = curve.points.last().unwrap().x;
(x0 + xl) / 2.0
} else {
0.028317 }
} else {
0.028317 }
}
}
}
pub(super) fn fit_pump_coeffs(curve: &Curve) -> Option<PumpCoeffs> {
let pts = &curve.points;
match pts.len() {
0 => None,
1 => {
let (q1, h1) = (pts[0].x, pts[0].y);
let h0 = 1.33334 * h1;
let r = if q1 > 0.0 {
(h0 - h1) / (q1 * q1)
} else {
return None;
};
if r > 0.0 {
Some(PumpCoeffs { h0, r, n: 2.0 })
} else {
None
}
}
2 => {
let h0 = if pts[0].x == 0.0 {
pts[0].y
} else {
let slope = (pts[1].y - pts[0].y) / (pts[1].x - pts[0].x);
pts[0].y - slope * pts[0].x
};
let (q2, h2) = (pts[1].x, pts[1].y);
let r = if q2 > 0.0 {
(h0 - h2) / (q2 * q2)
} else {
return None;
};
if r > 0.0 && h0 > 0.0 {
Some(PumpCoeffs { h0, r, n: 2.0 })
} else {
None
}
}
_ => {
let h0 = if pts[0].x == 0.0 {
pts[0].y
} else {
let slope = (pts[1].y - pts[0].y) / (pts[1].x - pts[0].x);
pts[0].y - slope * pts[0].x
};
let mid = pts.len() / 2;
let (q1, h1) = (pts[mid].x, pts[mid].y);
let (q2, h2) = (pts[pts.len() - 1].x, pts[pts.len() - 1].y);
PumpCoeffs::from_three_points(h0, q1, h1, q2, h2)
}
}
}
#[inline]
pub(super) fn link_py(
link: &Link,
q: f64,
status: LinkStatus,
setting: f64,
pipe_r_k: f64,
pump_coeffs_k: &Option<PumpCoeffs>,
curves: &[Curve],
formula: HeadLossFormula,
viscosity: f64,
sp_grav: f64,
pump_curve_idx_k: Option<usize>,
rq_tol: f64,
) -> Result<Py, HydraulicError> {
if let LinkKind::Valve(v) = &link.kind {
if matches!(
v.valve_type,
ValveType::Prv | ValveType::Psv | ValveType::Fcv
) && !setting.is_nan()
{
return Ok(Py { p: 0.0, y: q });
}
}
if matches!(
status,
LinkStatus::Closed | LinkStatus::XHead | LinkStatus::TempClosed
) {
return Ok(Py {
p: 1.0 / C_INF,
y: q,
});
}
if status == LinkStatus::Active {
let self_computes = matches!(
&link.kind,
LinkKind::Valve(v)
if matches!(
v.valve_type,
ValveType::Pbv | ValveType::Tcv | ValveType::Gpv | ValveType::Pcv
)
);
if !self_computes {
return Ok(Py { p: 0.0, y: 0.0 });
}
}
match &link.kind {
LinkKind::Pipe(pipe) => {
let (h, g) = pipe_total_hg(
pipe_r_k,
q,
pipe.roughness,
pipe.diameter,
pipe.minor_loss,
formula,
viscosity,
);
Ok(match formula {
HeadLossFormula::DarcyWeisbach => Py {
p: 1.0 / g,
y: h / g,
},
_ => {
let n_f = match formula {
HeadLossFormula::HazenWilliams => HW_EXP,
_ => 2.0,
};
Py::from_hg(h, g, n_f, q)
}
})
}
LinkKind::Pump(pump) => {
if setting == 0.0 {
Ok(Py::closed(q))
} else {
Ok(match pump.curve_type {
PumpCurveType::ConstHp => {
let power = pump.power.unwrap_or(0.0);
let gamma = sp_grav * GAMMA_WATER;
if q.abs() < 1.0e-10 {
Py::closed(q)
} else {
let r_adj = power * setting * setting * setting / gamma;
let hgrad = r_adj / (q * q);
if hgrad > C_INF {
Py::closed(q)
} else if hgrad < G_MIN {
Py {
p: 1.0 / G_MIN,
y: q,
}
} else {
let hloss = -r_adj / q.abs() * q.signum();
Py {
p: 1.0 / hgrad,
y: hloss / hgrad,
}
}
}
}
PumpCurveType::PowerFunction => {
let coeffs = match pump_coeffs_k.as_ref() {
Some(c) => c,
None => {
return Err(HydraulicError::NoPumpCoeffs {
pump_id: link.base.id.clone(),
});
}
};
let omega = setting;
let n = coeffs.n;
let r_adj = coeffs.r * omega.powf(2.0 - n);
let h0_adj = -(omega * omega * coeffs.h0);
let abs_q = q.abs().max(1.0e-12);
let hgrad = n * r_adj * abs_q.powf(n - 1.0);
if (n - 1.0).abs() < 1.0e-10 {
let hloss = h0_adj + r_adj * q;
Py {
p: 1.0 / r_adj.max(G_MIN),
y: hloss / r_adj.max(G_MIN),
}
} else if hgrad < rq_tol {
let hloss = h0_adj + rq_tol * q;
Py {
p: 1.0 / rq_tol,
y: hloss / rq_tol,
}
} else {
let hloss = h0_adj + hgrad * q / n;
Py {
p: 1.0 / hgrad,
y: hloss / hgrad,
}
}
}
PumpCurveType::Custom => {
if let Some(curve) = pump_curve_idx_k.and_then(|idx| curves.get(idx)) {
let omega = setting;
let q_adj = q.abs().max(1.0e-12) / omega;
let (h0_seg, slope) = curve_segment(curve, q_adj);
let hgrad = (-slope * omega).max(G_MIN);
let hloss = -h0_seg * omega * omega + hgrad * q;
Py {
p: 1.0 / hgrad,
y: hloss / hgrad,
}
} else {
Py::closed(q)
}
}
})
}
}
LinkKind::Valve(v) => valve_py(v, q, setting, link, status, curves),
}
}
pub(super) fn curve_segment(curve: &Curve, q: f64) -> (f64, f64) {
let pts = &curve.points;
if pts.len() < 2 {
return (pts.first().map_or(0.0, |p| p.y), 0.0);
}
let k2 = pts.partition_point(|p| p.x < q);
let (k1, k2) = if k2 == 0 {
(0, 1)
} else if k2 >= pts.len() {
(pts.len() - 2, pts.len() - 1)
} else {
(k2 - 1, k2)
};
let slope = (pts[k2].y - pts[k1].y) / (pts[k2].x - pts[k1].x);
let h0 = pts[k1].y - slope * pts[k1].x;
(h0, slope)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CurveKind, CurvePoint};
fn curve(points: &[(f64, f64)]) -> Curve {
Curve {
id: "curve".into(),
kind: CurveKind::PumpHead,
points: points
.iter()
.map(|(x, y)| CurvePoint { x: *x, y: *y })
.collect(),
}
}
#[test]
fn fit_pump_coeffs_single_point_curve_is_quadratic() {
let coeffs = fit_pump_coeffs(&curve(&[(10.0, 90.0)])).expect("fit coeffs");
assert!(coeffs.h0 > 90.0);
assert_eq!(coeffs.n, 2.0);
assert!(coeffs.r > 0.0);
}
#[test]
fn fit_pump_coeffs_two_point_curve_uses_extrapolated_shutoff_head() {
let coeffs = fit_pump_coeffs(&curve(&[(5.0, 100.0), (10.0, 80.0)])).expect("fit coeffs");
assert!(coeffs.h0 > 100.0);
assert_eq!(coeffs.n, 2.0);
}
#[test]
fn curve_segment_extrapolates_beyond_endpoints() {
let c = curve(&[(0.0, 100.0), (10.0, 80.0), (20.0, 40.0)]);
let (_, low_slope) = curve_segment(&c, -1.0);
let (_, high_slope) = curve_segment(&c, 25.0);
assert!((low_slope + 2.0).abs() < 1e-12);
assert!((high_slope + 4.0).abs() < 1e-12);
}
}