feat: add high pressure gas cylinder model
This commit is contained in:
1 parent
0f57ba94f3
commit
bf8542bcc4
2 files changed
+246
No files matched your search
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
High-pressure gas cylinder model with configurable working fluid.
|
||||
|
||||
The cylinder is a 0D lumped model. Its primary state is total mass and
|
||||
total internal energy; pressure, temperature, density, enthalpy, and other
|
||||
thermodynamic quantities are recovered from CoolProp on demand.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import CoolProp.CoolProp as CP
|
||||
from CoolProp import AbstractState
|
||||
|
||||
|
||||
DEFAULT_FLUID = "Helium"
|
||||
DEFAULT_P_INIT = 18.031e6 # Pa, absolute
|
||||
DEFAULT_VOLUME = 20e-3 # m^3, 20 L
|
||||
DEFAULT_T_INIT = 78.0 # K
|
||||
|
||||
|
||||
class HighPressureGasCylinder:
|
||||
"""0D high-pressure gas cylinder.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fluid : str
|
||||
CoolProp fluid name, for example ``"Helium"`` or ``"Nitrogen"``.
|
||||
P_init : float
|
||||
Initial absolute pressure [Pa].
|
||||
V : float
|
||||
Cylinder volume [m^3].
|
||||
T_init : float
|
||||
Initial temperature [K].
|
||||
backend : str
|
||||
CoolProp backend name. ``"HEOS"`` is used by default.
|
||||
"""
|
||||
|
||||
def __init__(self, fluid=DEFAULT_FLUID, P_init=DEFAULT_P_INIT,
|
||||
V=DEFAULT_VOLUME, T_init=DEFAULT_T_INIT, backend="HEOS"):
|
||||
if not fluid:
|
||||
raise ValueError("fluid must be a non-empty CoolProp fluid name")
|
||||
if P_init <= 0:
|
||||
raise ValueError("P_init must be > 0")
|
||||
if V <= 0:
|
||||
raise ValueError("V must be > 0")
|
||||
if T_init <= 0:
|
||||
raise ValueError("T_init must be > 0")
|
||||
|
||||
self.fluid = fluid
|
||||
self.backend = backend
|
||||
self.V = V
|
||||
self._state = AbstractState(backend, fluid)
|
||||
|
||||
self._state.update(CP.PT_INPUTS, P_init, T_init)
|
||||
rho = self._state.rhomass()
|
||||
self.mass = rho * V
|
||||
self.U = self.mass * self._state.umass()
|
||||
|
||||
def _update_state(self):
|
||||
rho = self.rho
|
||||
u = self.specific_internal_energy
|
||||
self._state.update(CP.DmassUmass_INPUTS, rho, u)
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def rho(self):
|
||||
"""Gas density [kg/m^3]."""
|
||||
return self.mass / self.V
|
||||
|
||||
@property
|
||||
def specific_internal_energy(self):
|
||||
"""Specific internal energy [J/kg]."""
|
||||
return self.U / self.mass
|
||||
|
||||
@property
|
||||
def P(self):
|
||||
"""Absolute pressure [Pa]."""
|
||||
return self._update_state().p()
|
||||
|
||||
@property
|
||||
def T(self):
|
||||
"""Temperature [K]."""
|
||||
return self._update_state().T()
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
"""Specific enthalpy [J/kg]."""
|
||||
return self._update_state().hmass()
|
||||
|
||||
@property
|
||||
def cp(self):
|
||||
"""Specific heat at constant pressure [J/(kg K)]."""
|
||||
return self._update_state().cpmass()
|
||||
|
||||
@property
|
||||
def cv(self):
|
||||
"""Specific heat at constant volume [J/(kg K)]."""
|
||||
return self._update_state().cvmass()
|
||||
|
||||
@property
|
||||
def gamma(self):
|
||||
"""Local heat capacity ratio cp/cv [-]."""
|
||||
return self.cp / self.cv
|
||||
|
||||
@property
|
||||
def R_specific(self):
|
||||
"""Specific gas constant [J/(kg K)]."""
|
||||
state = self._update_state()
|
||||
return state.gas_constant() / state.molar_mass()
|
||||
|
||||
@property
|
||||
def compressibility_factor(self):
|
||||
"""Compressibility factor Z = P/(rho R T) [-]."""
|
||||
return self.P / (self.rho * self.R_specific * self.T)
|
||||
|
||||
def ghost_state(self):
|
||||
"""Return a stagnant conservative state ``[rho, rho*u, rho*E]``.
|
||||
|
||||
This is useful as a boundary ghost cell for conservative flow models.
|
||||
The state uses the real-fluid internal energy density. A pipe solver
|
||||
that assumes an ideal-gas EOS must still be checked for EOS consistency
|
||||
before coupling it directly to this real-fluid cylinder.
|
||||
"""
|
||||
return np.array([self.rho, 0.0, self.rho * self.specific_internal_energy])
|
||||
|
||||
def apply_flux(self, mdot, edot, dt, sign):
|
||||
"""Update cylinder mass and energy from a boundary flux.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mdot : float
|
||||
Mass flow rate [kg/s].
|
||||
edot : float
|
||||
Energy flow rate [W].
|
||||
dt : float
|
||||
Time step [s].
|
||||
sign : int
|
||||
``-1`` for outflow from the cylinder, ``+1`` for inflow.
|
||||
"""
|
||||
if dt < 0:
|
||||
raise ValueError("dt must be >= 0")
|
||||
if sign not in (-1, 1):
|
||||
raise ValueError("sign must be +1 or -1")
|
||||
|
||||
self.mass += sign * mdot * dt
|
||||
self.U += sign * edot * dt
|
||||
|
||||
if self.mass <= 0:
|
||||
raise RuntimeError(
|
||||
f"Cylinder mass non-positive after apply_flux: mass={self.mass}, "
|
||||
f"mdot={mdot}, edot={edot}, dt={dt}, sign={sign}"
|
||||
)
|
||||
|
||||
# Force a thermodynamic validity check immediately after the update.
|
||||
self._update_state()
|
||||
|
||||
def state_summary(self):
|
||||
"""Return common cylinder state quantities as a dictionary."""
|
||||
return {
|
||||
"fluid": self.fluid,
|
||||
"V": self.V,
|
||||
"mass": self.mass,
|
||||
"rho": self.rho,
|
||||
"P": self.P,
|
||||
"T": self.T,
|
||||
"U": self.U,
|
||||
"u": self.specific_internal_energy,
|
||||
"h": self.h,
|
||||
"gamma": self.gamma,
|
||||
"R_specific": self.R_specific,
|
||||
"Z": self.compressibility_factor,
|
||||
}
|
||||
Reference in new issue
Block a user