Source code for simulator.pumps.cavitation
from __future__ import annotations
"""NPSH and cavitation-margin helpers for pump analysis."""
from dataclasses import dataclass, asdict
from typing import Any, Mapping
FT_PER_M = 3.280839895
M_PER_FT = 1.0 / FT_PER_M
G = 9.80665
[docs]
@dataclass(frozen=True)
class SuctionState:
"""Suction-side state used to estimate NPSH available.
The simplest path is to provide ``fixed_npsha_ft`` in the input JSON. If it
is not provided, the calculation uses absolute suction pressure, vapor
pressure, suction velocity, elevation, and suction-side losses.
For RPM sweeps, the optional flow-dependent loss coefficients make the
model more realistic:
``suction_loss_k_ft_per_flow2``
Additional suction loss in feet: ``h_loss = K * Q**2``. The flow unit is
the native pump/system flow unit for the run, usually gpm.
``suction_loss_k_m_per_flow2``
Same idea, but coefficient is expressed in meters of loss.
These coefficients are intentionally unit-light because the pump package is
curve-first: if the pump/system curves use gpm, then K is ft/gpm^2 or
m/gpm^2; if they use 1000_gal_per_min, then K follows that curve unit.
"""
fixed_npsha_ft: float | None = None
absolute_pressure_kPa: float | None = None
vapor_pressure_kPa: float | None = None
rho_kg_per_m3: float = 1000.0
suction_velocity_m_per_s: float = 0.0
suction_elevation_m: float = 0.0
suction_losses_m: float = 0.0
suction_loss_k_ft_per_flow2: float = 0.0
suction_loss_k_m_per_flow2: float = 0.0
[docs]
@classmethod
def from_dict(cls, data: Mapping[str, Any] | None) -> "SuctionState":
if not data:
return cls()
return cls(
fixed_npsha_ft=_optional_float(data.get("fixed_npsha_ft")),
absolute_pressure_kPa=_optional_float(data.get("absolute_pressure_kPa")),
vapor_pressure_kPa=_optional_float(data.get("vapor_pressure_kPa")),
rho_kg_per_m3=float(data.get("rho_kg_per_m3", data.get("density_kg_per_m3", 1000.0))),
suction_velocity_m_per_s=float(data.get("suction_velocity_m_per_s", data.get("suction_velocity_m_s", 0.0))),
suction_elevation_m=float(data.get("suction_elevation_m", 0.0)),
suction_losses_m=float(data.get("suction_losses_m", data.get("z_loss_terms_m", 0.0))),
suction_loss_k_ft_per_flow2=float(data.get("suction_loss_k_ft_per_flow2", 0.0)),
suction_loss_k_m_per_flow2=float(data.get("suction_loss_k_m_per_flow2", 0.0)),
)
[docs]
def npsha_ft(self, flow: float | None = None) -> float | None:
"""Return NPSH available [ft].
Parameters
----------
flow:
Optional operating flow in the native pump/system flow unit. When
provided, any configured ``K*Q^2`` suction-loss terms are subtracted
from NPSHA. ``fixed_npsha_ft`` remains fixed by design and bypasses
the calculated model.
"""
if self.fixed_npsha_ft is not None:
return float(self.fixed_npsha_ft)
if self.absolute_pressure_kPa is None or self.vapor_pressure_kPa is None:
return None
rho = max(float(self.rho_kg_per_m3), 1e-9)
pressure_head_m = ((float(self.absolute_pressure_kPa) - float(self.vapor_pressure_kPa)) * 1000.0) / (rho * G)
velocity_head_m = float(self.suction_velocity_m_per_s) ** 2 / (2.0 * G)
npsha_m = pressure_head_m + velocity_head_m - float(self.suction_elevation_m) - float(self.suction_losses_m)
if flow is not None:
q = max(float(flow), 0.0)
npsha_m -= float(self.suction_loss_k_m_per_flow2) * q * q
npsha_m -= float(self.suction_loss_k_ft_per_flow2) * q * q * M_PER_FT
return npsha_m * FT_PER_M
[docs]
def to_dict(self) -> dict[str, Any]:
return asdict(self)
[docs]
def npsh_margin_ft(npsha_ft: float | None, npshr_ft: float | None) -> float | None:
if npsha_ft is None or npshr_ft is None:
return None
return float(npsha_ft) - float(npshr_ft)
def _optional_float(value: Any) -> float | None:
if value is None:
return None
return float(value)