ode-pump-dashboard / cycle2mdot_cached.py
pandeydigant31's picture
Upload folder using huggingface_hub
ed65aea verified
Raw
History Blame Contribute Delete
56.2 kB
"""
Cryogenic Pump Cycle Analysis - Performance-Cached Version using CoolProp
This module calculates mass flow rate based on pump cycle analysis for cryogenic fluids.
Converted from VBA code that used REFPROP, now using CoolProp as a free alternative.
Performance optimizations over cycle2mdot_updated.py (the base model):
1. Fluid property cache (_get_cached_fluid_props):
cp/cv ratio (k), critical flow factor (cf), and molecular weight (mwt) are
computed once per fluid at standard conditions (1 atm, 15 °C) and stored in
_FLUID_PROP_CACHE. Avoids 3 repeated CoolProp evaluations per call to
flow_RF_kgpm().
2. 1D isentropic P,S flash table (_build_ps_table_1d):
The slow CoolProp P,S flash calls (d2hat, h2hat in the flow formula) for the
DCV constant-entropy path are pre-computed as a smooth 5000-point 1D lookup
table h(P) and d(P) at fixed upstream entropy. Linear interpolation error
scales as O(ΔP²); 5000 points yields ~0.00002% per-step error.
The fast P,T upstream flash calls and the exact flow formula (including the
zero at P1≈P2) are preserved.
3. Pre-computed constant upstream states:
Four values that are constant for the entire simulation are computed once
before the main loop: _s_exit, _h1_exit_J (DCV upstream entropy and
enthalpy), _s_tank, _h1_tank_J (ICV-reverse upstream entropy and enthalpy).
4. Inline cached flow helpers (_flow_core, _cached_flow_1d):
_flow_core extracts the shared real-gas flow formula into a reusable
function. _cached_flow_1d wraps it with 1D table np.interp lookups,
completely bypassing CoolProp for constant-entropy flow paths.
5. DCV leak rate branching logic:
In the main loop, DCV leak rate now branches:
- Pexit > pc (dominant path): uses _cached_flow_1d (table lookup, no
CoolProp calls).
- Pexit <= pc (reverse flow): falls back to direct flow_RF_kgpm() using
Tc_K (chamber temperature) as upstream, instead of Tout_K.
Initial DCV leak rate also uses _cached_flow_1d instead of flow_RF_kgpm.
6. ICV initial upstream temperature correction:
At initialisation when pc > Ptank, the ICV upstream temperature uses Tc_K
(chamber temperature) instead of Tout_K (discharge temperature) from the
base model.
7. Cached convection heat transfer:
free_conv_2cyl_Wpm() (~8 CoolProp calls per invocation) is only recomputed
when the chamber temperature changes by more than 2 K. The result is stored
in _Qconv_base and reused otherwise. Applied both in the pre-loop
initialisation and in every timestep of the main loop.
Author: Converted from VBA original
Date: 2025
"""
import numpy as np
from typing import Optional, Tuple, List, Dict, Any
import time
# CoolProp import
try:
from CoolProp.CoolProp import PropsSI
COOLPROP_AVAILABLE = True
except ImportError:
COOLPROP_AVAILABLE = False
print("Warning: CoolProp not installed. Install with: pip install CoolProp")
# Constants
PI = np.pi
STEFAN_BOLTZMANN = 5.67e-8 # W/m²/K⁴
RU = 8314.0 # J/kmol/K, universal gas constant
def coolprop_fluid_name(fluid: str) -> str:
"""Convert common fluid names to CoolProp format."""
fluid_map = {
'h2': 'Hydrogen',
'hydrogen': 'Hydrogen',
'he': 'Helium',
'helium': 'Helium',
'n2': 'Nitrogen',
'nitrogen': 'Nitrogen',
'o2': 'Oxygen',
'oxygen': 'Oxygen',
'air': 'Air',
'ar': 'Argon',
'argon': 'Argon',
'co2': 'CarbonDioxide',
'ch4': 'Methane',
'methane': 'Methane',
}
return fluid_map.get(fluid.lower(), fluid)
def refprop(prop: str, fluid: str, input_type: str = "pt",
units: str = "si", val1: float = 0.101325, val2: float = 300.0) -> float:
"""
CoolProp wrapper mimicking REFPROP function interface.
Parameters:
-----------
prop : str
Property to calculate (t, p, d, h, s, cp, cv, vis, tcx, etc.)
fluid : str
Fluid name
input_type : str
Input specification type (pt, pq, pd, ph, ps, ds)
units : str
Unit system (si)
val1, val2 : float
Input values (pressure in MPa for 'p', temperature in K for 't', etc.)
Returns:
--------
float : Calculated property value
"""
if not COOLPROP_AVAILABLE:
raise ImportError("CoolProp is required but not installed")
fluid_cp = coolprop_fluid_name(fluid)
# Property mapping from REFPROP names to CoolProp names
prop_map = {
't': 'T', # Temperature, K
'p': 'P', # Pressure, Pa (need to convert from MPa)
'd': 'D', # Density, kg/m³
'h': 'H', # Enthalpy, J/kg (need to convert to kJ/kg)
's': 'S', # Entropy, J/kg/K (need to convert to kJ/kg/K)
'e': 'U', # Internal energy, J/kg (need to convert to kJ/kg)
'cp': 'C', # Specific heat at constant pressure, J/kg/K (convert to kJ/kg/K)
'cv': 'O', # Specific heat at constant volume, J/kg/K (convert to kJ/kg/K)
'vis': 'V', # Dynamic viscosity, Pa·s (convert to µPa·s)
'kv': 'V', # Kinematic viscosity (need density too)
'tcx': 'L', # Thermal conductivity, W/m/K (convert to mW/m/K)
'm': 'M', # Molar mass, kg/mol (convert to g/mol)
'pc': 'PCRIT', # Critical pressure, Pa (convert to MPa)
'tc': 'TCRIT', # Critical temperature, K
'prandtl': 'PRANDTL', # Prandtl number
'beta': 'ISOBARIC_EXPANSION_COEFFICIENT', # 1/K
'bs': 'ISOTHERMAL_COMPRESSIBILITY', # Bulk modulus (inverse)
'qmass': 'Q', # Quality (mass basis)
'heatvapz': 'H', # Heat of vaporization (need special handling)
'phase': 'Phase', # Phase identifier
}
# Input type mapping
input_map = {
'pt': ('P', 'T'), # Pressure, Temperature
'pq': ('P', 'Q'), # Pressure, Quality
'pd': ('P', 'D'), # Pressure, Density
'ph': ('P', 'H'), # Pressure, Enthalpy
'ps': ('P', 'S'), # Pressure, Entropy
'ds': ('D', 'S'), # Density, Entropy
'td': ('T', 'D'), # Temperature, Density
'de': ('D', 'U'), # Density, Internal energy (val2 in kJ/kg)
'dh': ('D', 'H'), # Density, Enthalpy (val2 in kJ/kg)
}
prop_lower = prop.lower()
# Handle special properties that don't need state inputs
if prop_lower == 'm':
return PropsSI('M', fluid_cp) * 1000 # Convert kg/mol to g/mol
elif prop_lower == 'pc':
return PropsSI('PCRIT', fluid_cp) / 1e6 # Convert Pa to MPa
elif prop_lower == 'tc':
return PropsSI('TCRIT', fluid_cp) # K
# Get CoolProp property name
cp_prop = prop_map.get(prop_lower, prop.upper())
# Get input specification
if input_type.lower() not in input_map:
raise ValueError(f"Unknown input type: {input_type}")
inp1_type, inp2_type = input_map[input_type.lower()]
# Convert input values to CoolProp units (SI base)
# CoolProp uses Pa, J, K as base units
if inp1_type == 'P':
inp1_val = val1 * 1e6 # MPa to Pa
elif inp1_type == 'H':
inp1_val = val1 * 1000 # kJ/kg to J/kg
elif inp1_type == 'S':
inp1_val = val1 * 1000 # kJ/kg/K to J/kg/K
else:
inp1_val = val1
if inp2_type == 'P':
inp2_val = val2 * 1e6 # MPa to Pa
elif inp2_type == 'H':
inp2_val = val2 * 1000 # kJ/kg to J/kg
elif inp2_type == 'S':
inp2_val = val2 * 1000 # kJ/kg/K to J/kg/K
elif inp2_type == 'U':
inp2_val = val2 * 1000 # kJ/kg to J/kg
else:
inp2_val = val2
# Special handling for heat of vaporization
if prop_lower == 'heatvapz':
try:
h_liq = PropsSI('H', inp1_type, inp1_val, 'Q', 0, fluid_cp)
h_vap = PropsSI('H', inp1_type, inp1_val, 'Q', 1, fluid_cp)
return (h_vap - h_liq) / 1000 # Convert J/kg to kJ/kg
except:
return 0.0
# Special handling for bulk modulus (isothermal)
if prop_lower == 'bs':
try:
# Bulk modulus = 1 / isothermal compressibility
# K = -V * (dP/dV)_T = rho * (dP/drho)_T
kappa_T = PropsSI('ISOTHERMAL_COMPRESSIBILITY', inp1_type, inp1_val, inp2_type, inp2_val, fluid_cp)
return 1.0 / kappa_T / 1e6 # Convert Pa to MPa
except:
return 1000.0 # Default fallback
# Special handling for kinematic viscosity
if prop_lower == 'kv':
try:
mu = PropsSI('V', inp1_type, inp1_val, inp2_type, inp2_val, fluid_cp) # Pa·s
rho = PropsSI('D', inp1_type, inp1_val, inp2_type, inp2_val, fluid_cp) # kg/m³
return mu / rho * 1e4 # Convert m²/s to cm²/s (stokes)
except:
return 0.0
# Get the property from CoolProp
try:
result = PropsSI(cp_prop, inp1_type, inp1_val, inp2_type, inp2_val, fluid_cp)
except Exception as e:
print(f"CoolProp error: {e}")
return 0.0
# Convert output to REFPROP-compatible units
if prop_lower == 'h':
return result / 1000 # J/kg to kJ/kg
elif prop_lower == 'e':
return result / 1000 # J/kg to kJ/kg
elif prop_lower == 's':
return result / 1000 # J/kg/K to kJ/kg/K
elif prop_lower in ['cp', 'cv']:
return result / 1000 # J/kg/K to kJ/kg/K
elif prop_lower == 'vis':
return result * 1e6 # Pa·s to µPa·s
elif prop_lower == 'tcx':
return result * 1000 # W/m/K to mW/m/K
elif prop_lower == 'p':
return result / 1e6 # Pa to MPa
else:
return result
def subcool_K(pvap_barg: float, pliq_sat_barg: float, fluid: str = "h2") -> float:
"""
Returns the subcool in K for a fluid.
Parameters:
-----------
pvap_barg : float
Vapor pressure in barg
pliq_sat_barg : float
Liquid saturation pressure in barg
fluid : str
Fluid name
Returns:
--------
float : Subcool temperature in K
"""
tvap = refprop("t", fluid, "pq", "si", pvap_barg / 10 + 0.101325, 0)
tliq = refprop("t", fluid, "pq", "si", pliq_sat_barg / 10 + 0.101325, 0)
return tvap - tliq
def kv_from_Cd_and_RO_dia(area_equiv_d_mm: float, Cd: float = 0.61) -> float:
"""
Calculate Kv from discharge coefficient and area-equivalent diameter.
Derived from fundamental equations. See Eq 7 in memo
"Compressed hydrogen fill models," March 2023.
Parameters:
-----------
area_equiv_d_mm : float
Area equivalent diameter in mm
Cd : float
Discharge coefficient (0.61 for sharp orifice, 0.99 for venturi)
Returns:
--------
float : Kv in m³/hr
"""
d_mm = area_equiv_d_mm
area = PI * (d_mm / 1000) ** 2 / 4 # m²
k = 1 / Cd ** 2 # Loss factor
return 3.6e4 * area * np.sqrt(2 / k) # m³/hr
def fluid_phase_qual(P_barg: float, den_kgm3: float, fluid: str = "h2") -> float:
"""
Returns quality for given pressure and density.
Parameters:
-----------
P_barg : float
Pressure in barg
den_kgm3 : float
Density in kg/m³
fluid : str
Fluid name
Returns:
--------
float : Quality (0 = liquid, 1 = vapor, between = two-phase)
"""
pc = refprop("pc", fluid) # MPa, critical pressure
tc = refprop("tc", fluid) # K, critical temperature
p_MPa = P_barg / 10 + 0.101325
try:
T_K = refprop("t", fluid, "pd", "si", p_MPa, den_kgm3)
except:
return 0.0
if T_K < tc: # Subcritical
try:
dliq = refprop("d", fluid, "pq", "si", p_MPa, 0)
dvap = refprop("d", fluid, "pq", "si", p_MPa, 1)
if den_kgm3 > dliq:
return 0.0 # Pure liquid
elif den_kgm3 < dvap:
return 1.0 # Pure vapor
else: # Two-phase zone
return refprop("qmass", fluid, "pd", "si", p_MPa, den_kgm3)
except:
return 0.0
elif P_barg > pc * 10 - 1.01325: # Above critical T and P
return 0.0 # Dense supercritical fluid
else:
return 1.0 # Light supercritical fluid (gas)
def mixture_pump_prop(prop: str, Pin_barg: float = 2.0, Pout_barg: float = 700.0,
quality: float = 0.0, comp_eff: float = 0.7,
fluid: str = "h2", Tif_in_is_SC_K: float = 0.0,
throttling: int = 1) -> float:
"""
Returns thermo property after compressing a mixture of given quality.
Can also do expansion with real fluid if inlet P > outlet P.
Parameters:
-----------
prop : str
Property to calculate (d, s, h, t, qmass)
Pin_barg : float
Inlet pressure in barg
Pout_barg : float
Outlet pressure in barg
quality : float
Inlet quality (if subcritical)
comp_eff : float
Compression/expansion efficiency
fluid : str
Fluid name
Tif_in_is_SC_K : float
Temperature if supercritical (required if P > Pc)
throttling : int
1 = throttling (h=const), else = turbine/piston with efficiency
Returns:
--------
float : Calculated property value
"""
Tin = Tif_in_is_SC_K
pcf = refprop("pc", fluid) # MPa, critical pressure
piMPa = Pin_barg / 10 + 0.101325
poMPa = Pout_barg / 10 + 0.101325
sg = 1.0 if piMPa < poMPa else -1.0
if piMPa > poMPa: # Expansion
if Tin <= 0:
return 0.0
qq = fluid_phase_qual(Pin_barg, Tin)
single_phase = (qq == 0.0 or qq == 1.0)
if piMPa >= pcf or single_phase: # Supercritical or single phase
s0 = refprop("s", fluid, "pt", "si", piMPa, Tin)
h0 = refprop("h", fluid, "pt", "si", piMPa, Tin)
hs = refprop("h", fluid, "ps", "si", poMPa, s0)
if throttling == 1:
ha = h0 # Throttling: constant enthalpy
else:
ha = h0 - comp_eff * (h0 - hs) # Expansion with efficiency
else: # Compression (or expansion in two-phase)
if Tin <= 0: # Use "pq" method
s0 = refprop("s", fluid, "pq", "si", piMPa, quality)
h0 = refprop("h", fluid, "pq", "si", piMPa, quality)
else: # Use "pt" method
s0 = refprop("s", fluid, "pt", "si", piMPa, Tin)
h0 = refprop("h", fluid, "pt", "si", piMPa, Tin)
hs = refprop("h", fluid, "ps", "si", poMPa, s0)
ha = h0 + sg * (hs - h0) / comp_eff
if prop.lower() == "qmass":
den = refprop("d", fluid, "ph", "si", poMPa, ha)
return fluid_phase_qual(Pout_barg, den, fluid)
else:
return refprop(prop, fluid, "ph", "si", poMPa, ha)
def flow_RF_kgpm(p1_barg: float, p2_barg: float, Tupstream_C: float,
GasKv: float, fluid: str = "H2", RealGas: int = 1,
LiqSubcool_K: float = 0.0) -> float:
"""
Calculate mass flow rate using real fluid properties.
Based on compressible flow thermodynamics. See memo
"Compressed Hydrogen Fill Models.docx"
Parameters:
-----------
p1_barg : float
Upstream pressure in barg
p2_barg : float
Downstream pressure in barg
Tupstream_C : float
Upstream temperature in °C
GasKv : float
Orifice coefficient in m³/hr
fluid : str
Fluid name
RealGas : int
1 = real gas, else = ideal gas
LiqSubcool_K : float
Liquid subcool in K
Returns:
--------
float : Mass flow rate in kg/min
"""
if abs(p1_barg - p2_barg) < 0.0001:
return 0.0
# Determine flow direction
sg = 1
Phigh = p1_barg
Plow = p2_barg
if p2_barg > p1_barg:
Phigh = p2_barg
Plow = p1_barg
sg = -1
Phigha = Phigh + 1.01325 # bara
Plowa = Plow + 1.01325 # bara
T_K = Tupstream_C + 273.15 - LiqSubcool_K
rho0 = 1000.0 # kg/m³, water density (Kv definition)
p0 = 1.0 # bar (Kv definition)
# Cached specific heat ratio at 1 atm, 15 deg C (constant per fluid)
_props = _get_cached_fluid_props(fluid)
k = _props["k"]
cf = _props["cf"]
pc = Phigha * cf # Critical pressure
if RealGas == 1: # Real gas properties
p2hat = max(Plowa, pc) # Throat pressure (sonic transition)
h1 = refprop("h", fluid, "pt", "si", Phigha / 10, T_K) * 1000 # J/kg
s1 = refprop("s", fluid, "pt", "si", Phigha / 10, T_K) # kJ/kg/K
# Downstream/throat properties (isentropic)
d2hat = refprop("d", fluid, "ps", "si", p2hat / 10, s1)
h2hat = refprop("h", fluid, "ps", "si", p2hat / 10, s1) * 1000 # J/kg
dh = h1 - h2hat
if dh < 0:
dh = 0.0
mdot = GasKv * d2hat * np.sqrt(rho0 / 1e5 / p0) * np.sqrt(dh) # kg/hr
else: # Ideal gas
mwt = _props["mwt"]
Rgas = 8314.0 / mwt # J/kg/K
if Plowa > pc: # Subcritical
pratio = Plowa / Phigha
mdot = np.sqrt(k / (k - 1) / Rgas / T_K) * pratio ** (1 / k)
mdot *= np.sqrt(1 - pratio ** ((k - 1) / k))
else: # Supercritical (choked)
mdot = np.sqrt(k / 2 / Rgas / T_K * (2 / (k + 1)) ** ((k + 1) / (k - 1)))
mdot = mdot * GasKv * (Phigha * 1e5) / 10 # kg/hr
return sg * mdot / 60 # kg/min
# =============================================================================
# PERFORMANCE: Cached fluid properties and valve flow lookup tables
# =============================================================================
_FLUID_PROP_CACHE = {}
def _get_cached_fluid_props(fluid: str) -> dict:
"""Cache heat capacity ratio and related constants at standard conditions.
cp/cv is computed at 1 atm, 15 deg C -- constant for a given fluid.
Called once per fluid, then reused across all simulations.
"""
key = fluid.lower()
if key not in _FLUID_PROP_CACHE:
cp = refprop("cp", fluid, "pt", "si", 0.101325, 273.15 + 15)
cv = refprop("cv", fluid, "pt", "si", 0.101325, 273.15 + 15)
k = cp / cv
_FLUID_PROP_CACHE[key] = {
"k": k,
"cf": (2 / (k + 1)) ** (k / (k - 1)),
"mwt": refprop("m", fluid),
}
return _FLUID_PROP_CACHE[key]
def _build_ps_table_1d(s_fixed: float, P_range_MPa: Tuple[float, float],
fluid: str, n: int = 200
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""1D isentropic property table: h(P) and d(P) at fixed entropy.
Used for downstream lookups when upstream entropy is constant:
- DCV: s_exit = s(Pexit, Tout_K) is constant
- ICV reverse: s_tank = s(Ptank, Tin_K) is constant
Args:
s_fixed: Fixed entropy [kJ/kg/K]
P_range_MPa: (P_min, P_max) pressure range [MPa]
fluid: Fluid name
n: Grid resolution
Returns:
(P_grid_MPa, h_table_kJkg, d_table_kgm3)
"""
P_lo, P_hi = P_range_MPa
P_grid = np.linspace(P_lo, P_hi, n)
h_tbl = np.full(n, np.nan)
d_tbl = np.full(n, np.nan)
for i, P_val in enumerate(P_grid):
try:
h_tbl[i] = refprop("h", fluid, "ps", "si", P_val, s_fixed)
d_tbl[i] = refprop("d", fluid, "ps", "si", P_val, s_fixed)
except Exception:
pass
# Fill NaN gaps by nearest-neighbor interpolation
valid = ~np.isnan(h_tbl)
if np.sum(valid) >= 2:
h_tbl = np.interp(P_grid, P_grid[valid], h_tbl[valid])
d_tbl = np.interp(P_grid, P_grid[valid], d_tbl[valid])
elif np.sum(valid) == 1:
h_tbl[:] = h_tbl[valid][0]
d_tbl[:] = d_tbl[valid][0]
return P_grid, h_tbl, d_tbl
def composite_thermal_conductivity(composite_type: int, k1: float, vf1: float, k2: float) -> float:
"""
Calculate effective thermal conductivity of a two-material composite.
Parameters:
-----------
composite_type : int
1 = series, 2 = parallel, 3 = isotropic mixture, 4 = spherical inclusion
k1 : float
Thermal conductivity of material 1
vf1 : float
Volume fraction of material 1
k2 : float
Thermal conductivity of material 2
Returns:
--------
float : Effective thermal conductivity in W/m/K
"""
vf2 = 1 - vf1
if composite_type == 1: # Series
return 1 / (vf1 / k1 + vf2 / k2)
elif composite_type == 2: # Parallel
return vf1 * k1 + vf2 * k2
elif composite_type == 3: # Isotropic (geometric mean)
return k1 ** vf1 * k2 ** vf2
elif composite_type == 4: # Maxwell-Eucken (spherical inclusion)
top = k1 + 2 * k2 + 2 * vf1 * (k1 - k2)
bot = k1 + 2 * k2 - vf1 * (k1 - k2)
return k2 * top / bot
else:
return 0.0
def mean_free_path_meter(gas_name: str, P_micronHg: float, T_K: float) -> float:
"""
Calculate mean free path of a gas.
Reference: R. Barron, Cryogenic systems, McGraw Hill, 1966
Parameters:
-----------
gas_name : str
Gas name
P_micronHg : float
Pressure in microns of Hg (mTorr)
T_K : float
Temperature in K
Returns:
--------
float : Mean free path in meters
"""
p_MPa = P_micronHg / 1000 / 760 * 0.101325
mwt = refprop("m", gas_name, "pt", "si", p_MPa, T_K)
mu = refprop("vis", gas_name, "pt", "si", p_MPa, T_K) / 1e6 # Pa·s
return 3 * mu / (p_MPa * 1e6) * np.sqrt(PI * RU * T_K / 8 / mwt)
def molecular_cond_WpmK(T_K: float, fluid: str = "air") -> float:
"""
Molecular conductivity of gases when mean free path is ~10% of relevant length scale.
Uses Chapman-Enskog kinetic theory with Lennard-Jones parameters.
Reference: R.B. Bird, W.E. Stewart and E.N. Lightfoot, Transport Phenomena,
2nd Ed, John Wiley & Sons, New York, 2007.
At low pressures, thermal conductivity does not depend on pressure because
heat transfer is determined by particle collisions between enclosures,
not among gas molecules.
Parameters:
-----------
T_K : float
Temperature in Kelvin
fluid : str
Fluid name ('air', 'n2', 'h2', 'he', or others)
Returns:
--------
float : Thermal conductivity in W/m/K
"""
# Lennard-Jones parameters (epsilon/k and sigma) from BSL Table E.1
fluid_upper = fluid.upper()
if fluid_upper == "AIR":
ek = 97.0 # epsilon/k_B in K
sigma = 3.617 # collision diameter in Angstrom
elif fluid_upper == "N2" or fluid_upper == "NITROGEN":
ek = 99.8
sigma = 3.667
elif fluid_upper == "H2" or fluid_upper == "HYDROGEN":
ek = 38.0
sigma = 2.915
elif fluid_upper == "HE" or fluid_upper == "HELIUM":
ek = 10.2
sigma = 2.576
elif fluid_upper == "O2" or fluid_upper == "OXYGEN":
ek = 113.0
sigma = 3.433
elif fluid_upper == "AR" or fluid_upper == "ARGON":
ek = 124.0
sigma = 3.418
elif fluid_upper == "CO2" or fluid_upper == "CARBONDIOXIDE":
ek = 190.0
sigma = 3.996
elif fluid_upper == "CH4" or fluid_upper == "METHANE":
ek = 137.0
sigma = 3.822
else:
# Estimate from critical properties (BSL p. 26)
tcr = refprop("tc", fluid) # K, critical temp
pcr = refprop("pc", fluid) * 10 / 1.01325 # atm, critical pressure
ek = 0.77 * tcr
sigma = 2.44 * (tcr / pcr) ** (1/3)
# Reduced temperature
Tstar = T_K / ek
# Collision integral for viscosity and thermal conductivity (BSL Table E.2)
# Neufeld-Janzen-Aziz correlation
omega = (1.16145 / Tstar**0.14874 +
0.52487 / np.exp(0.7732 * Tstar) +
2.16178 / np.exp(2.43787 * Tstar))
# Molecular weight
mwt = refprop("m", fluid) # g/mol
# Ideal gas heat capacities (monatomic basis)
Cv = 3/2 * RU / mwt # J/kg/K
cp = 5/2 * RU / mwt # J/kg/K
# Dynamic viscosity (Chapman-Enskog for monatomic gases)
# mu = 2.6693e-5 * sqrt(M*T) / sigma^2 / omega [g/cm/s]
mu_cgs = 2.6693e-5 * np.sqrt(mwt * T_K) / sigma**2 / omega # g/cm/s
mu_SI = mu_cgs / 10 # Convert to kg/m/s (Pa.s)
# Thermal conductivity (BSL p. 276)
if fluid_upper in ["HE", "HELIUM", "AR", "ARGON"]:
# Monatomic gas
tcond = 5/2 * Cv * mu_SI # W/m/K
else:
# Polyatomic gas (modified Eucken correlation)
tcond = (cp + 5/4 * RU / mwt) * mu_SI # W/m/K
return tcond
def free_conv_2cyl_Wpm(Di_m: float, Ti_K: float, Do_m: float, To_K: float,
p_Pa_abs: float = 101325, fluid: str = "air") -> float:
"""
Free convection between two infinitely long horizontal cylinders.
Reference: Incropera & DeWitt, Fundamentals of Heat and Mass Transfer,
2nd Ed, 1985, p.441
Parameters:
-----------
Di_m, Ti_K : float
Inner cylinder diameter (m) and temperature (K)
Do_m, To_K : float
Outer cylinder diameter (m) and temperature (K)
p_Pa_abs : float
Absolute pressure in Pa
fluid : str
Fluid name
Returns:
--------
float : Heat transfer rate in W/m
"""
Tavg = (Ti_K + To_K) / 2
pMPa = p_Pa_abs / 1e6
Gap = (Do_m - Di_m) / 2
g = 9.80665
cp = refprop("cp", fluid, "pt", "si", pMPa, Tavg) # kJ/kg/K
den = refprop("d", fluid, "pt", "si", pMPa, Tavg)
Pr = refprop("prandtl", fluid, "pt", "si", pMPa, Tavg)
nu = refprop("kv", fluid, "pt", "si", pMPa, Tavg) / 1e4 # m²/s
tc = refprop("tcx", fluid, "pt", "si", pMPa, Tavg) / 1000 # W/m/K
beta = refprop("beta", fluid, "pt", "si", pMPa, Tavg) # 1/K
# Rayleigh number
Ra = g * beta * den ** 2 * cp * abs(Ti_K - To_K) * Gap ** 3 / tc / nu
# Correction factor for cylindrical geometry
f = (np.log(Do_m / Di_m)) ** 4
f = f / (Gap ** 3 * (1 / Di_m ** 0.6 + 1 / Do_m ** 0.6) ** 5)
Ra = f * Ra
p_microns = p_Pa_abs / 0.133
lambda_mfp = mean_free_path_meter(fluid, p_microns, Tavg)
if lambda_mfp / Gap > 0.1:
# Molecular diffusion regime
tc = molecular_cond_WpmK(Tavg, fluid)
keff = tc
else:
# Continuum regime
tc = refprop("tcx", fluid, "pt", "si", pMPa, Tavg) / 1000
if Ra < 100:
keff = tc
else:
keff = tc * 0.386 * (Pr * Ra / (0.861 + Pr)) ** 0.25
return 2 * PI * keff * (Ti_K - To_K) / np.log(Do_m / Di_m)
def minmax(x: float, xmin: float, xmax: float) -> float:
"""Clamp x within [xmin, xmax]."""
return max(xmin, min(xmax, x))
def max_dt_s(Pchamber_barg: float, kv: float, dp_cracking_bar: float,
Ptank_barg: float, Psat_barg: float,
ValveType: int = 1, Vdisp: float = 0.000076533,
fluid: str = "h2") -> float:
"""
Return the maximum stable timestep in seconds.
Based on the valve configuration, cracking pressure and chamber condition,
such that the pressure change from one mass-flow step does not exceed
dp_cracking_bar. Prevents numerical chattering of the poppet.
Parameters
----------
Pchamber_barg : barg, chamber pressure (or discharge pressure for DCV)
kv : valve fully-open Kv from kv_from_Cd_and_RO_dia()
dp_cracking_bar : bar, cracking pressure (max spring force / exposed area)
Ptank_barg : barg, cryotank pressure
Psat_barg : barg, liquid saturation pressure
ValveType : 1 = ICV (default), else = DCV
Vdisp : m³, pump displacement volume
fluid : fluid name
"""
ICV = 1
pc = Pchamber_barg
dp = dp_cracking_bar
pt = Ptank_barg
psat = Psat_barg
PsMPa = psat / 10.0 + 0.101325
PtMPa = pt / 10.0 + 0.101325
PcMPa = pc / 10.0 + 0.101325
Tin_K = refprop("t", fluid, "pq", "si", PsMPa, 0.0)
den_in = refprop("d", fluid, "pt", "si", PtMPa, Tin_K)
Tin_C = Tin_K - 273.15
if ValveType == ICV:
mc = den_in * Vdisp
mdot = flow_RF_kgpm(pc + dp, pc, Tin_C, kv, fluid) / 60.0
else:
den_out = mixture_pump_prop("d", pt, pc, 0.0, 0.8, fluid, Tin_K)
Tout_K = mixture_pump_prop("t", pt, pc, 0.0, 0.8, fluid, Tin_K)
Tout_C = Tout_K - 273.15
mc = den_out * Vdisp
mdot = flow_RF_kgpm(pc + dp, pc, Tout_C, kv, fluid) / 60.0
if mdot == 0:
return 1e-4
dt = mc / mdot * dp / (PcMPa * 10.0)
return dt
def ICV_open(Pexit_barg: float, speed_f: float, Ptank_barg: float,
Psat_barg: float, ICVparam: List[float], DCVparam: List[float],
pump_geom: List[float], proc_param: List[float],
fluid: str = "h2", prtMode: int = 0
) -> Tuple[np.ndarray, Dict[str, Any]]:
"""
Simulate the full pump cycle: DCV closing + ICV opening (retract stroke)
followed by ICV closing + DCV opening (extend stroke).
Translated from VBA ``ICV_open`` (VBA_code_pack_2_-_ICV_open.txt).
Uses an energy-balance formulation (internal-energy tracking) and
adaptive time-stepping controlled by valve activity.
Parameters
----------
Pexit_barg : barg, discharge pressure
speed_f : speed fraction 0–1
Ptank_barg : barg, cryotank headspace pressure
Psat_barg : barg, liquid saturation pressure in the cryotank
ICVparam : list[9] – [port_mm, mass_g, travel_mm, dpArea_mm2,
Fs_N, SC_Npmm, leakKv, comp_eff, Npts]
DCVparam : list[9] – same layout for DCV
pump_geom : list[12] – [bore_mm, stroke_mm, HousingOD_mm, ChamberLen_mm,
em_housing, em_shield, kvoid, khousing,
Vfvoid, Vacuum_micron, design_cpm, dvf]
proc_param : list[8] – [Tamb_K, htc_amb, NetDriveCouplerForce_kgf,
F_multiplier, Kv_BB, Pbbexit_barg,
fric2chamber, Exp_eff]
fluid : fluid name (default "h2")
prtMode : 0 = silent, 1 = print loop trace to stdout
Returns
-------
out : np.ndarray (13, 2) – scalar results with description strings
history : dict – full time-history arrays for plotting
keys: angle_deg, pc, den, yp, mc, Tc_K, hc,
DCV_open_frac, DCV_leak_kgpm,
ICV_open_frac, ICV_leak_kgpm, dm_tot_kgps
"""
t_start = time.time()
# ------------------------------------------------------------------ unpack
ICVport_mm = ICVparam[0]
ICVmass_g = ICVparam[1]
ICVtravel_mm = ICVparam[2]
ICVdpArea_mm2 = ICVparam[3]
ICVFs_N = ICVparam[4]
ICVSC_Npmm = ICVparam[5]
ICVleakKv = ICVparam[6]
ICVcomp_eff = ICVparam[7] # not used directly in this function
ICV_Npts = ICVparam[8]
DCVport_mm = DCVparam[0]
DCVmass_g = DCVparam[1]
DCVtravel_mm = DCVparam[2]
DCVdpArea_mm2 = DCVparam[3]
DCVFs_N = DCVparam[4]
DCVSC_Npmm = DCVparam[5]
DCVleakKv = DCVparam[6]
DCVcomp_eff = DCVparam[7]
DCV_Npts = DCVparam[8]
bore_mm = pump_geom[0]
stroke_mm = pump_geom[1]
HousingOD_mm = pump_geom[2]
ChamberLen_mm = pump_geom[3]
em_housing = pump_geom[4]
em_shield = pump_geom[5]
kvoid = pump_geom[6]
khousing = pump_geom[7]
Vfvoid = pump_geom[8]
Vacuum_micron = pump_geom[9]
design_cpm = pump_geom[10]
dvf = pump_geom[11]
Tamb_K = proc_param[0]
htc_amb = proc_param[1]
NetDriveCouplerForce_kgf = proc_param[2]
F_multiplier = proc_param[3]
Kv_BB = proc_param[4]
Pbbexit_barg = proc_param[5]
fric2chamber = proc_param[6]
Exp_eff = proc_param[7]
# ---------------------------------------------------------------- geometry
stroke = stroke_mm / 1000.0
bore = bore_mm / 1000.0
Vdisp = PI / 4.0 * bore**2 * stroke # m³
V_dead = dvf * Vdisp
pump_cpm = design_cpm * speed_f
tcycle = 60.0 / pump_cpm
tstroke = tcycle / 2.0
vm_piston = PI * stroke * pump_cpm / 60.0 # m/s, max piston velocity
# --------------------------------------------------------- thermodynamics
PsMPa = Psat_barg / 10.0 + 0.101325
PtMPa = Ptank_barg / 10.0 + 0.101325
PeMPa = Pexit_barg / 10.0 + 0.101325
Tin_K = refprop("t", fluid, "pq", "si", PsMPa, 0.0)
den_in = refprop("d", fluid, "pt", "si", PtMPa, Tin_K)
h_in = refprop("h", fluid, "pt", "si", PtMPa, Tin_K)
den_out = mixture_pump_prop("d", Ptank_barg, Pexit_barg, 0.0, DCVcomp_eff, fluid, Tin_K)
h_out = mixture_pump_prop("h", Ptank_barg, Pexit_barg, 0.0, DCVcomp_eff, fluid, Tin_K)
Tout_K = mixture_pump_prop("t", Ptank_barg, Pexit_barg, 0.0, DCVcomp_eff, fluid, Tin_K)
Tout_C = Tout_K - 273.15
# ------------------------------------------------ property-level P,S flash cache
# Strategy: cache the SLOW CoolProp P,S flash calls (d2hat, h2hat in the
# flow formula) as a 1D table h(P) and d(P) at constant upstream entropy.
# The fast P,T flash calls (h1, s1 upstream) are kept exact.
# The flow formula is applied inline with exact discontinuity handling.
_props = _get_cached_fluid_props(fluid)
_cf = _props["cf"] # critical pressure ratio (~0.528 for H2)
# Constant upstream states (pre-computed once)
_s_exit = refprop("s", fluid, "pt", "si", PeMPa, Tout_K) # DCV upstream entropy
_h1_exit_J = refprop("h", fluid, "pt", "si", PeMPa, Tout_K) * 1000.0 # DCV upstream h [J/kg]
_s_tank = refprop("s", fluid, "pt", "si", PtMPa, Tin_K) # ICV-reverse upstream entropy
_h1_tank_J = refprop("h", fluid, "pt", "si", PtMPa, Tin_K) * 1000.0 # ICV-reverse upstream h [J/kg]
# ---- 1D tables: ultra-high-accuracy for constant-entropy upstream (DCV, ICV reverse)
# These handle the DOMINANT flow paths. Linear interpolation error scales as
# O(ΔP²), so 5000 points gives ~100× less per-step error than 500 points.
# Over 38k adaptive steps, compound error drops from ~22% to ~0.2%.
_p2hat_lo = 0.101325 # atmospheric minimum [MPa]
_p2hat_hi = PeMPa + 1.0 # above exit pressure [MPa]
_dcv1d_P, _dcv1d_h, _dcv1d_d = _build_ps_table_1d(
s_fixed=_s_exit,
P_range_MPa=(_p2hat_lo, _p2hat_hi),
fluid=fluid,
n=5000,
)
# NOTE: ICV uses direct flow_RF_kgpm calls (pc always > Ptank, variable entropy)
# ---- Flow computation helpers ------------------------------------------------
def _flow_core(p1_barg, p2_barg, h1_J, Kv, d2hat, h2hat_kJ):
"""Shared flow formula. d2hat and h2hat come from 1D table or direct call."""
if abs(p1_barg - p2_barg) < 0.0001:
return 0.0
sg = 1.0
if p2_barg > p1_barg:
sg = -1.0
h2hat_J = h2hat_kJ * 1000.0
dh = h1_J - h2hat_J
if dh < 0.0:
dh = 0.0
mdot_kghr = Kv * d2hat * np.sqrt(1000.0 / 1e5) * np.sqrt(dh)
return sg * mdot_kghr / 60.0
def _cached_flow_1d(p1_barg, p2_barg, h1_J, Kv, P_grid, h_tbl, d_tbl):
"""Flow using 1D table at pre-computed constant entropy. Returns kg/min."""
if abs(p1_barg - p2_barg) < 0.0001:
return 0.0
Phigha = max(p1_barg, p2_barg) + 1.01325
Plowa = min(p1_barg, p2_barg) + 1.01325
p2hat_MPa = max(Plowa / 10.0, Phigha * _cf / 10.0)
d2hat = float(np.interp(p2hat_MPa, P_grid, d_tbl))
h2hat_kJ = float(np.interp(p2hat_MPa, P_grid, h_tbl))
return _flow_core(p1_barg, p2_barg, h1_J, Kv, d2hat, h2hat_kJ)
# ---------------------------------------------------- DCV physical params
DCVmass = DCVmass_g / 1000.0
DCVSC_Npm = DCVSC_Npmm * 1000.0
DCVtravel = DCVtravel_mm / 1000.0
DCVdpArea = DCVdpArea_mm2 / 1e6
tc_Fs_DCV = np.sqrt(2.0 * DCVmass * DCVtravel / max(DCVFs_N, 1e-9))
# ---------------------------------------------------- ICV physical params
ICVmass = ICVmass_g / 1000.0
ICVSC_Npm = ICVSC_Npmm * 1000.0
ICVtravel = ICVtravel_mm / 1000.0
ICVdpArea = ICVdpArea_mm2 / 1e6
K_bulk = refprop("bs", fluid, "pt", "si", PtMPa, Tin_K) # MPa
tc_Fs_ICV = np.sqrt(2.0 * ICVmass * ICVtravel / max(ICVFs_N, 1e-9))
# -------------------------------------------------- chamber initial state
mc = V_dead * den_out
hc = h_out
mc0 = mc
vc = Vdisp * dvf
den = den_out
pc = Pexit_barg
yp = 0.0
kv_DCV = DCVleakKv + kv_from_Cd_and_RO_dia(DCVport_mm)
# At init pc=Pexit_barg → |Pexit-pc|≈0 → flow ≈ 0; use exit upstream 1D table
leak_rate_DCV = _cached_flow_1d(Pexit_barg, pc, _h1_exit_J, kv_DCV, _dcv1d_P, _dcv1d_h, _dcv1d_d) / 60.0
leak_rate_prev_DCV = leak_rate_DCV
# At initialisation the pressure is known exactly (pc = Pexit_barg), so use
# (P, H) — the most reliable CoolProp input pair at high pressure — to get T.
Pexit_MPa = (Pexit_barg + 1.01325) / 10.0 # barg → MPa
Tc_K = refprop("t", fluid, "ph", "si", Pexit_MPa, hc)
# Internal energy from the exact identity u = h − p/ρ
p_init_Pa = (pc + 1.01325) * 1e5 # barg → absolute Pa
uc = hc - p_init_Pa / den / 1000 # kJ/kg
# -------------------------------------------------- ICV initial state
t = 0.0
xip = 0.0; vip = 0.0
Fdp_ICV = (Ptank_barg - pc) * 1e5 * ICVdpArea
vwave = np.sqrt(K_bulk * 1e6 / den)
v_piston = vm_piston * np.sin(2.0 * PI * t / tcycle)
WHdp = den * v_piston * vwave * ICVdpArea
Fs_ICV = ICVFs_N - ICVSC_Npm * (ICVtravel - xip)
Ftot_ICV = (-Fs_ICV + Fdp_ICV + WHdp)
aip = Ftot_ICV / ICVmass
kv_ICV = ICVleakKv
# At init pc=Pexit_barg > Ptank → upstream = chamber; direct call (variable entropy)
TupstreamC = (Tc_K if pc > Ptank_barg else Tin_K) - 273.15
leak_rate_ICV = flow_RF_kgpm(Ptank_barg, pc, TupstreamC, kv_ICV, fluid) / 60.0
leak_rate_prev_ICV = leak_rate_ICV
# -------------------------------------------------- DCV initial state
xp = 0.0; vp = 0.0
Fs_DCV = DCVFs_N - DCVSC_Npm * xp
ap = Fs_DCV / DCVmass
# -------------------------------------------------- output accumulators
Fmax_ICV = 0.0
Fmax_DCV = 0.0
ICVmax_frac = 0.0
Fmax_ICV_close = 0.0
Vmax_ICVopen = 0.0
m_in = 0.0
m_out = 0.0
# Timing flags / results
retract = True
Extend = False
DCVmoving = True
ICVmoving = False
DCV_ct = 0.0
DCV_ot = tcycle # default: never opened during extend
ICV_openst = 0.0
ICV_ct = tcycle # default: never closed during extend
# -------------------------------------------------- adaptive time-step
dt0 = max(tc_Fs_ICV / max(ICV_Npts, 1), tc_Fs_DCV / max(DCV_Npts, 1)) / 10.0
kv1 = kv_from_Cd_and_RO_dia(ICVport_mm)
dp_icv = ICVFs_N / ICVdpArea / 1e5
dtmax_ICV = max_dt_s(Ptank_barg, kv1, dp_icv, Ptank_barg, Psat_barg, 1, Vdisp, fluid) / 2.0
kv1 = kv_from_Cd_and_RO_dia(DCVport_mm)
dp_dcv = DCVFs_N / DCVdpArea / 1e5
dtmax_DCV = max_dt_s(Pexit_barg, kv1, dp_dcv, Ptank_barg, Psat_barg, 0, Vdisp, fluid) / 2.0
dtmax = 5e-6
DCVFs_min = DCVFs_N - DCVSC_Npm * DCVtravel
# two-point linear fit: large spring force → small dt, small force → large dt
if (DCVFs_N - DCVFs_min) != 0:
slp = (dt0 - dtmax) / (DCVFs_N - DCVFs_min)
icp = dtmax - slp * DCVFs_min
else:
slp = 0.0
icp = dt0
# -------------------------------------------------- radiation/convection geometry
bot = 1.0 / em_housing + (1.0 - em_housing) / em_housing * (bore_mm / HousingOD_mm)
ds = (bore_mm + HousingOD_mm) / 2.0
bot = bot + 2.0 * (1.0 - em_shield) / em_shield * (bore_mm / ds)
keff = composite_thermal_conductivity(1, kvoid, Vfvoid, khousing)
htc_all = 1.0 / (1.0 / htc_amb + (HousingOD_mm - bore_mm) / 2.0 / 1000.0 / keff)
pa = Vacuum_micron / 1000.0 * (101325.0 / 760.0) # Pa
Ffric = NetDriveCouplerForce_kgf * F_multiplier * 9.80665
Qfric = Ffric * vm_piston * fric2chamber # W (initial, updates each step)
# Pre-compute first-step heat/blowby (used in step 1's energy balance)
Qrad = PI * bore_mm * ChamberLen_mm * STEFAN_BOLTZMANN * (Tamb_K**4 - Tc_K**4) / (2.0 * bot) / 1e6
_Qconv_base = free_conv_2cyl_Wpm(bore_mm / 1000.0, Tc_K, HousingOD_mm / 1000.0, Tamb_K, pa, "air")
_Tc_last_conv = Tc_K
Qconv = -_Qconv_base * (ChamberLen_mm / 1000.0)
Qconv = Qconv + 2.0 * PI / 4.0 * HousingOD_mm**2 * htc_amb * (Tamb_K - Tc_K) / 1e6
dt = dt0
Qig = (Qrad + Qconv) * dt / 1000.0
Qf = Qfric * dt / 1000.0
dm_bb = flow_RF_kgpm(Pbbexit_barg, pc, Tc_K - 273.15, Kv_BB, fluid, 1) / 60.0 * dt
h_bb = hc
pc_last = pc
vc_prev = vc
del_pc = 0.0
# ------------------------------------------- history storage
hist_t : List[float] = []
hist_pc : List[float] = []
hist_den : List[float] = []
hist_yp : List[float] = []
hist_mc : List[float] = []
hist_Tc : List[float] = []
hist_hc : List[float] = []
hist_dcvof : List[float] = [] # DCV open fraction (1 - x_frac)
hist_dcvlk : List[float] = [] # DCV leak kg/min
hist_icvof : List[float] = [] # ICV open fraction (xi_frac)
hist_icvlk : List[float] = [] # ICV leak kg/min
hist_dmtot : List[float] = [] # total dm/dt kg/s
j = 1
# ============================= main integration loop =======================
while t < tcycle and j < 50000:
if t > tstroke and not Extend:
retract = False
Extend = True
# ---- adaptive dt
ICVopenfr = ICV_openst / tstroke if tstroke > 0 else 0.0
ICVmovfr = 1.0 - ICVopenfr
n_seg = 5
df = ICVmovfr / n_seg if ICVmovfr > 0 else 0.2
xn = (yp - ICVopenfr) / df if df > 0 else 0.0
if DCVmoving:
dt = minmax(slp * Fs_DCV + icp, dt0, dtmax)
elif ICVmoving:
dt = dt0 * (50.0 ** xn)
dt = minmax(dt, dt0, dtmax * 2.0)
elif (pc - Ptank_barg < 2.0) and not ICVmoving:
dt = dt0
elif (Pexit_barg - pc < abs(del_pc)) and not DCVmoving:
dt = dt0
else:
dt = dtmax
t += dt
# ---- chamber heat/mass inputs (from previous step, forward Euler)
dm_DCV = leak_rate_prev_DCV * dt
dm_ICV = leak_rate_prev_ICV * dt
dm_tot = dm_ICV + dm_DCV + dm_bb
m_out += dm_DCV
m_in += dm_ICV
mc_prev = mc
mc = max(mc + dm_tot, mc0 / 1000.0)
dh_DCV = (h_out if leak_rate_prev_DCV > 0 else hc) * dm_DCV
dh_ICV = (h_in if leak_rate_prev_ICV > 0 else hc) * dm_ICV
dh_bb = (h_bb if dm_bb > 0 else hc) * dm_bb
# pV work (kJ): retract dV>0 → no work in; extend dV<0 → work in
work = (-pc * vc + pc_last * vc_prev) * 100.0 # bar·m³ → kJ (*100)
# Save uc and cv BEFORE updating uc so we can compute ΔT afterward.
# cv is evaluated at the current (Tc_K, den) which is the most reliable
# state we have; for small dt this is an excellent approximation.
uc_prev_step = uc
try:
cv_kJkgK = refprop("cv", fluid, "td", "si", Tc_K, den)
if cv_kJkgK <= 0.0:
cv_kJkgK = 10.0 # fallback: ~cv of H₂ at 20 K
except Exception:
cv_kJkgK = 10.0
uc = (uc * mc_prev + (dh_DCV + dh_ICV + dh_bb + Qig + Qf) + work) / mc
yp = 0.5 * (1.0 - np.cos(2.0 * PI * t / tcycle))
vc_prev = vc
vc = Vdisp * (dvf + yp)
den = mc / vc
# ---- Robust state update via (T, D) flash --------------------------------
# CoolProp's (D, U) and (D, H) flash routines both suffer from multi-root
# convergence failures for compressed H₂ at 700+ bar — the EOS has very
# similar densities at very different pressures for the same enthalpy.
# The (T, D) input pair is always unique in single-phase and is CoolProp's
# most numerically robust specification.
#
# Strategy:
# 1. Estimate ΔT from the internal-energy change: ΔT ≈ Δu / cv
# 2. Flash (T_est, ρ_new) → p_new, h_new [always converges]
# 3. Refine once: updated p → updated u → corrected ΔT → one more (T,D)
# This converges to within ~1 ppm in two passes for the small dt used here.
try:
dT_est = (uc - uc_prev_step) / cv_kJkgK
T_est = max(Tc_K + dT_est, 14.0) # clamp at H₂ triple point (~13.8 K)
pc_new = refprop("p", fluid, "td", "si", T_est, den) * 10.0 - 1.01325
hc_new = refprop("h", fluid, "td", "si", T_est, den)
Tc_new = T_est # (T,D) flash: T_est IS the input, no need to re-query T
# One refinement: use the just-obtained pressure to get a better uc
# estimate, then recompute ΔT and re-flash.
p_new_Pa = (pc_new + 1.01325) * 1e5
uc_new_ref = hc_new - p_new_Pa / den / 1000 # u = h - p/ρ
dT_ref = (uc - uc_new_ref + (uc - uc_prev_step)) / cv_kJkgK
T_ref = max(Tc_K + dT_ref, 14.0)
pc_new = refprop("p", fluid, "td", "si", T_ref, den) * 10.0 - 1.01325
hc_new = refprop("h", fluid, "td", "si", T_ref, den)
Tc_new = T_ref
except Exception:
pc_new = pc_last
hc_new = hc
Tc_new = Tc_K
del_pc = pc_new - pc_last
pc_last = pc_new
pc = pc_new
hc = hc_new
Tc_K = Tc_new
# Keep uc consistent with the converged (p, h, ρ) state for the next step
uc = hc - (pc + 1.01325) * 1e5 / den / 1000
# ---- DCV motion (xp=0 fully open, xp=DCVtravel fully closed)
Fs_DCV = DCVFs_N - DCVSC_Npm * xp
Fdp_DCV = (Pexit_barg - pc) * 1e5 * DCVdpArea
Fmax_DCV = max(Fmax_DCV, Fs_DCV + Fdp_DCV)
ap = (Fs_DCV + Fdp_DCV) / DCVmass
vp += ap * dt
if (xp == 0.0 and vp < 0.0) or (xp == DCVtravel and vp > 0.0):
vp = 0.0
xp = xp + vp * dt
xp = minmax(xp, 0.0, DCVtravel)
x_frac = minmax(xp / DCVtravel, 0.0, 1.0)
if retract and x_frac >= 1.0 and DCVmoving:
DCVmoving = False
DCV_ct = t
elif Extend and x_frac < 1.0 and not DCVmoving:
DCVmoving = True
DCV_ot = t
kv_DCV = DCVleakKv + kv_from_Cd_and_RO_dia(DCVport_mm * (1.0 - x_frac))
if Pexit_barg > pc:
# Constant-entropy path → perfect 1D table (0.00002% error)
leak_rate_DCV = _cached_flow_1d(Pexit_barg, pc, _h1_exit_J, kv_DCV, _dcv1d_P, _dcv1d_h, _dcv1d_d) / 60.0
else:
# Variable-entropy path → direct call (Tc_K as upstream)
TupstreamC = Tc_K - 273.15
leak_rate_DCV = flow_RF_kgpm(Pexit_barg, pc, TupstreamC, kv_DCV, fluid) / 60.0
leak_rate_prev_DCV = leak_rate_DCV
# ---- ICV motion (xip=0 fully closed, xip=ICVtravel fully open)
Fs_ICV = ICVFs_N - ICVSC_Npm * (ICVtravel - xip)
Fdp_ICV = (Ptank_barg - pc) * 1e5 * ICVdpArea
vwave = np.sqrt(K_bulk * 1e6 / max(den, 1e-6))
v_piston = vm_piston * np.sin(2.0 * PI * t / tcycle)
WHdp = (1.0 if retract else -1.0) * den * v_piston * vwave * ICVdpArea
Ftot_ICV = Fdp_ICV - Fs_ICV + WHdp
Fmax_ICV = max(Fmax_ICV, Ftot_ICV)
Fmax_ICV_close = min(Fmax_ICV_close, Ftot_ICV)
aip = Ftot_ICV / ICVmass
vip += aip * dt
if (xip == 0.0 and vip < 0.0) or (xip == ICVtravel and vip > 0.0):
vip = 0.0
Vmax_ICVopen = max(Vmax_ICVopen, vip)
xip = xip + vip * dt
xip = minmax(xip, 0.0, ICVtravel)
xi_frac = xip / ICVtravel
if retract and xip > 0.0 and not ICVmoving:
ICVmoving = True
ICV_openst = t
elif Extend and xi_frac == 0.0 and ICVmoving:
ICVmoving = False
ICV_ct = t
ICVmax_frac = max(ICVmax_frac, xi_frac)
kv_ICV = ICVleakKv + kv_from_Cd_and_RO_dia(ICVport_mm * xi_frac)
# ICV: both paths use variable temperature → direct calls
TupstreamC = (Tc_K if pc > Ptank_barg else Tin_K) - 273.15
leak_rate_ICV = flow_RF_kgpm(Ptank_barg, pc, TupstreamC, kv_ICV, fluid) / 60.0
leak_rate_prev_ICV = leak_rate_ICV
# ---- heat ingress, friction, blowby (computed for next step)
Qrad = PI * bore_mm * ChamberLen_mm * STEFAN_BOLTZMANN * (Tamb_K**4 - Tc_K**4) / (2.0 * bot) / 1e6
# Cache convection: only recompute free_conv_2cyl_Wpm when Tc changes >2 K
if abs(Tc_K - _Tc_last_conv) > 2.0:
_Qconv_base = free_conv_2cyl_Wpm(bore_mm / 1000.0, Tc_K, HousingOD_mm / 1000.0, Tamb_K, pa, "air")
_Tc_last_conv = Tc_K
Qconv = -_Qconv_base * (ChamberLen_mm / 1000.0)
Qconv = Qconv + 2.0 * PI / 4.0 * HousingOD_mm**2 * htc_amb * (Tamb_K - Tc_K) / 1e6
Qig = (Qrad + Qconv) * dt / 1000.0
Qfric = Ffric * abs(v_piston) * fric2chamber
Qf = Qfric * dt / 1000.0
dm_bb = flow_RF_kgpm(Pbbexit_barg, pc, Tc_K - 273.15, Kv_BB, fluid, 1) / 60.0 * dt
h_bb = hc
if prtMode:
print(f"j={j:5d} angle={t*360/tcycle:6.1f}° pc={pc:8.3f} bar"
f" den={den:8.3f} kg/m³ yp={yp:.4f} mc={mc*1000:.4f} g"
f" DCV x/L={x_frac:.3f} ICV xi/L={xi_frac:.3f}"
f" Tc={Tc_K:.2f} K hc={hc:.3f} kJ/kg")
# ---- record history
hist_t.append(t * 360.0 / tcycle) # degrees
hist_pc.append(pc)
hist_den.append(den)
hist_yp.append(yp)
hist_mc.append(mc * 1000.0) # g
hist_Tc.append(Tc_K)
hist_hc.append(hc)
hist_dcvof.append(1.0 - x_frac) # DCV open fraction
hist_dcvlk.append(leak_rate_DCV * 60.0) # kg/min
hist_icvof.append(xi_frac) # ICV open fraction
hist_icvlk.append(leak_rate_ICV * 60.0) # kg/min
hist_dmtot.append(dm_tot / dt if dt > 0 else 0.0) # kg/s
j += 1
# ============================ post-processing ============================
mass_eff = m_in / (Vdisp * den_in)
m_out = -m_out # outflow was tracked as negative; flip sign
m_out_eff = m_out / (Vdisp * den_in)
mdot_out = m_out / tcycle * 60.0 # kg/min
t_end = time.time()
# ----------------------------------------------------------------- output
out = np.zeros((13, 2), dtype=object)
out[0, 0] = t_end - t_start; out[0, 1] = "s, cpu time"
out[1, 0] = mass_eff; out[1, 1] = ", mass inflow efficiency"
out[2, 0] = DCV_ct; out[2, 1] = "s, DCV closure time"
out[3, 0] = ICV_openst / tstroke; out[3, 1] = ", stroke fraction ICV starts to open"
out[4, 0] = ICVmax_frac; out[4, 1] = ", max ICV open fraction"
out[5, 0] = Fmax_DCV; out[5, 1] = "N, max closure force on DCV"
out[6, 0] = Fmax_ICV; out[6, 1] = "N, max open force on ICV"
out[7, 0] = Fmax_ICV_close; out[7, 1] = "N, max closure force on ICV"
out[8, 0] = Vmax_ICVopen; out[8, 1] = "m/s, max ICV opening velocity"
out[9, 0] = m_out; out[9, 1] = "kg, total discharged mass per cycle"
out[10, 0] = m_out_eff; out[10, 1] = ", mass efficiency of cycle"
out[11, 0] = DCV_ot / tstroke - 1.0; out[11, 1] = ", stroke fraction DCV starts to open"
out[12, 0] = ICV_ct - tstroke; out[12, 1] = "s, ICV closure time after extend start"
history = {
'angle_deg': np.array(hist_t),
'pc': np.array(hist_pc),
'den': np.array(hist_den),
'yp': np.array(hist_yp),
'mc_g': np.array(hist_mc),
'Tc_K': np.array(hist_Tc),
'hc': np.array(hist_hc),
'DCV_open_frac': np.array(hist_dcvof),
'DCV_leak_kgpm': np.array(hist_dcvlk),
'ICV_open_frac': np.array(hist_icvof),
'ICV_leak_kgpm': np.array(hist_icvlk),
'dm_tot_kgps': np.array(hist_dmtot),
'mdot_kgpm': mdot_out,
'tcycle_s': tcycle,
'tstroke_s': tstroke,
'steps': j,
}
return out, history
def print_results(results: np.ndarray) -> None:
"""Print results in a formatted table."""
print("\n" + "=" * 60)
print("PUMP CYCLE ANALYSIS RESULTS (ICV_open)")
print("=" * 60)
for i in range(len(results)):
if results[i, 1] is not None and results[i, 1] != 0:
val = results[i, 0]
desc = results[i, 1]
if isinstance(val, (int, float)):
print(f"{val:12.6g} {desc}")
else:
print(f"{val} {desc}")
print("=" * 60)
# Example usage and test
if __name__ == "__main__":
print("Cryogenic Pump Cycle Analysis - CoolProp Version")
print("-" * 50)
if not COOLPROP_AVAILABLE:
print("ERROR: CoolProp is not installed.")
print("Install with: pip install CoolProp")
exit(1)
print("\nTesting CoolProp interface...")
try:
T_sat = refprop("t", "h2", "pq", "si", 0.101325, 0)
print(f"H2 saturation temperature at 1 atm: {T_sat:.2f} K")
rho = refprop("d", "h2", "pt", "si", 0.5, 25)
print(f"H2 density at 5 bar, 25 K: {rho:.2f} kg/m3")
print("CoolProp interface working correctly!")
except Exception as e:
print(f"Error testing CoolProp: {e}")
# Example parameters (typical CSH2 pump)
print("\n" + "-" * 50)
print("Running full pump cycle analysis via ICV_open()...")
# [port_mm, mass_g, travel_mm, dpArea_mm2, Fs_N, SC_Npmm, leakKv, comp_eff, Npts]
ICVparam = [6.0, 28.0, 8.0, 897.7, 11.4, 1.134, 0.01, 1.0, 200]
DCVparam = [8.3, 25.0, 4.0, 78.54, 3.1, 0.419, 0.01, 0.8, 400]
# [bore_mm, stroke_mm, HousingOD_mm, ChamberLen_mm, em_housing, em_shield,
# kvoid, khousing, Vfvoid, Vacuum_micron, design_cpm, dvf]
pump_geom = [40.3, 60.0, 120.0, 300.0, 1.0, 1.0, 0.026, 16.0, 0.5, 760000, 500.0, 0.02]
# [Tamb_K, htc_amb, NetDriveCouplerForce_kgf, F_multiplier,
# Kv_BB, Pbbexit_barg, fric2chamber, Exp_eff]
proc_param = [293.0, 10.0, 4.0, 30.0, 0.01, 0.0, 0.5, 0.2]
try:
results, history = ICV_open(
Pexit_barg=700.0,
speed_f=1.0,
Ptank_barg=9.0,
Psat_barg=2.0,
ICVparam=ICVparam,
DCVparam=DCVparam,
pump_geom=pump_geom,
proc_param=proc_param,
fluid="h2",
return_history=True
)
print_results(results)
print(f"Simulation steps: {len(history['t_ms'])}")
except Exception as e:
print(f"Error in analysis: {e}")
import traceback
traceback.print_exc()