pub(super) const C_INF: f64 = 1.0e8;
pub(super) const G_MIN: f64 = 1.0e-6;
pub(super) const Q_CLOSED: f64 = 1.0e-6;
pub(super) const GAMMA_WATER: f64 = 9810.0;
#[derive(Debug, Clone)]
pub struct PumpCoeffs {
pub h0: f64,
pub r: f64,
pub n: f64,
}
impl PumpCoeffs {
pub fn from_three_points(h_shutoff: f64, q1: f64, h1: f64, q2: f64, h2: f64) -> Option<Self> {
if h_shutoff <= h1 || h1 <= h2 || h2 < 0.0 {
return None;
}
if q1 <= 0.0 || q2 <= q1 {
return None;
}
let n = ((h_shutoff - h2) / (h_shutoff - h1)).ln() / (q2 / q1).ln();
if n <= 0.0 || n > 20.0 {
return None;
}
let r = (h_shutoff - h1) / q1.powf(n);
if r <= 0.0 {
return None;
}
Some(PumpCoeffs {
h0: h_shutoff,
r,
n,
})
}
}
pub(super) fn lower_barrier(dq: f64) -> (f64, f64) {
let a = 1.0e9 * dq;
let b = (a * a + 1.0e-6_f64).sqrt();
((a - b) / 2.0, 5.0e8 * (1.0 - a / b))
}
pub(super) fn upper_barrier(dq: f64) -> (f64, f64) {
let a = 1.0e9 * dq;
let b = (a * a + 1.0e-6_f64).sqrt();
((a + b) / 2.0, 5.0e8 * (1.0 + a / b))
}
pub(super) struct Py {
pub(super) p: f64,
pub(super) y: f64,
}
impl Py {
pub(super) fn closed(q: f64) -> Self {
Py {
p: 1.0 / C_INF,
y: q,
}
}
pub(super) fn from_hg(h: f64, g: f64, n_f: f64, q: f64) -> Self {
if g < G_MIN {
let g2 = G_MIN / n_f;
let h2 = g2 * q;
Py {
p: 1.0 / g2,
y: h2 / g2,
}
} else {
Py {
p: 1.0 / g,
y: h / g,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolveResult {
Converged,
Unbalanced,
}
#[derive(Debug, Clone)]
pub enum HydraulicError {
NotConverged,
SingularMatrix {
junction_step: usize,
},
NoPumpCoeffs {
pump_id: String,
},
}
impl std::fmt::Display for HydraulicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotConverged => write!(f, "hydraulic solve did not converge"),
Self::SingularMatrix { junction_step } => {
write!(
f,
"sparse matrix singular at permuted junction step {junction_step}"
)
}
Self::NoPumpCoeffs { pump_id } => {
write!(
f,
"pump '{pump_id}' requires power-function coefficients but none were fitted"
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::{lower_barrier, upper_barrier, PumpCoeffs};
#[test]
fn pump_coeffs_three_point_fit() {
let c = PumpCoeffs::from_three_points(100.0, 1.0, 90.0, 2.0, 60.0).unwrap();
assert!((c.n - 2.0).abs() < 1.0e-10, "N={}", c.n);
assert!((c.r - 10.0).abs() < 1.0e-10, "r={}", c.r);
assert!((c.h0 - 100.0).abs() < 1.0e-10);
}
#[test]
fn pump_coeffs_rejects_bad_points() {
assert!(PumpCoeffs::from_three_points(90.0, 1.0, 90.0, 2.0, 60.0).is_none());
assert!(PumpCoeffs::from_three_points(100.0, 0.0, 90.0, 2.0, 60.0).is_none());
assert!(PumpCoeffs::from_three_points(100.0, 1.0, 99.99999, 2.0, 0.001).is_none());
}
#[test]
fn lower_barrier_feasible_near_zero() {
let (dh, _) = lower_barrier(1.0);
assert!(dh.abs() < 1.0e-3, "Δh={dh}");
}
#[test]
fn lower_barrier_violation_strongly_negative() {
let (dh, _) = lower_barrier(-1.0);
assert!(dh < -1.0e3, "Δh={dh}");
}
#[test]
fn upper_barrier_feasible_near_zero() {
let (dh, _) = upper_barrier(-1.0);
assert!(dh.abs() < 1.0e-3, "Δh={dh}");
}
}