ode-pump-dashboard / cycle2mdot_ode_v2.py
pandeydigant31's picture
Upload folder using huggingface_hub
ed65aea verified
Raw
History Blame Contribute Delete
42.5 kB
"""
Cryogenic Pump Cycle ODE — 10-Variable System with Fast Engine Architecture
Merges VBA Pack 5 physics (10 state variables, downstream volumes, multi-cycle)
into cycle2mdot_fast_v2's performance layer (AbstractState, specify_phase,
1D DCV table, flash caching, two-pass Newton EOS).
State vector (10 variables, full downstream):
y = [mc, uc, vp, xp, vip, xip, msb, usb, mchss, uchss]
When exit_param is None, 6-variable reduced system (no downstream).
Dual-backend EOS (Phase 2):
- H2: Numba-JIT Helmholtz (break_coolprop/h2_props) — ~0.7μs per state eval
- N2 / other fluids: CoolProp AbstractState with specify_phase — ~15-30μs
Key optimizations vs original VBA5 port:
1. Numba Helmholtz EOS for H2 — 15-20x faster than CoolProp per call
2. CoolProp AbstractState + specify_phase for N2 and other fluids
3. 1D DCV table — zero EOS calls for ~70% of DCV flow evaluations
4. Flash caching — ICV+BB shared upstream PT flash (CoolProp path)
5. Two-pass Newton EOS — faster convergence than DmassUmass
6. Corrected energy balance — fixes VBA line 322 bug
Author: ODE v2 — Phase 2 (dual-backend)
"""
import sys
import os
import math
import time
import numpy as np
import CoolProp
from scipy.integrate import solve_ivp
from typing import Optional, Tuple, List, Dict, Any
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from cycle2mdot_cached import (
refprop, coolprop_fluid_name, kv_from_Cd_and_RO_dia, flow_RF_kgpm,
mixture_pump_prop, free_conv_2cyl_Wpm, composite_thermal_conductivity,
_get_cached_fluid_props, _build_ps_table_1d, print_results,
minmax, PI, STEFAN_BOLTZMANN,
)
from cycle2mdot_fast import _build_ps_table_1d_cached, tc_ss304
# Numba Helmholtz EOS for H2 (Phase 2 fast path — graceful fallback)
_HAS_NUMBA_H2 = False
try:
from break_coolprop import h2_props as h2_numba
_HAS_NUMBA_H2 = True
except ImportError:
h2_numba = None
# ============================================================================
# Constants (cached from CoolProp to avoid attribute lookups in hot path)
# ============================================================================
_H2_TC = 33.145 # K — H2 critical temperature
_H2_PC_PA = 1.2964e6 # Pa — H2 critical pressure
_PHASE_SUPERCRITICAL_GAS = CoolProp.iphase_supercritical_gas
_PHASE_SUPERCRITICAL_LIQUID = CoolProp.iphase_supercritical_liquid
_PHASE_LIQUID = CoolProp.iphase_liquid
_PT_INPUTS = CoolProp.PT_INPUTS
_PSmass_INPUTS = CoolProp.PSmass_INPUTS
_DmassT_INPUTS = CoolProp.DmassT_INPUTS
_sqrt = math.sqrt
_sin = math.sin
_cos = math.cos
_fabs = math.fabs
_TWO_PI = 2.0 * PI
# ============================================================================
# Phase specification guards (from cycle2mdot_fast_v2.py)
# ============================================================================
def _specify_phase_safe(AS, P_Pa, T_K, fluid_Tc=_H2_TC, fluid_Pc=_H2_PC_PA):
"""Specify CoolProp phase for supercritical fluid, skip near critical."""
if T_K > fluid_Tc and P_Pa > fluid_Pc:
AS.specify_phase(_PHASE_SUPERCRITICAL_GAS)
else:
AS.unspecify_phase()
def _specify_phase_td(AS, T_K, den, fluid_Tc=_H2_TC):
"""Phase specification for T,D flash. At T > Tc, any density is supercritical."""
if T_K > fluid_Tc:
AS.specify_phase(_PHASE_SUPERCRITICAL_GAS)
else:
AS.unspecify_phase()
def Kv_from_Cv(Cv):
return 0.865 * Cv
# ============================================================================
# RHS factory: builds the closure-based ODE right-hand side
# ============================================================================
def _make_rhs_v2(p, AS, dcv1d_P, dcv1d_h, dcv1d_d, corrected=True):
"""Build a high-performance RHS function from parameter dict p.
Returns (rhs, rhs_calls) where rhs(t, y) -> dy/dt.
Uses v2's AbstractState API, specify_phase, 1D DCV table, flash caching,
and two-pass Newton EOS — NOT PropsSI or DmassUmass.
"""
# ── Unpack all parameters from dict ──
pump_cpm = p['pump_cpm']; Ptank_barg = p['Ptank_barg']; Tin_K = p['Tin_K']
flash_eff = p['flash_eff']; has_ds = p['has_downstream']
ICVport_mm = p['ICVport_mm']; ICVmass = p['ICVmass']; ICVtravel = p['ICVtravel']
ICVdpArea = p['ICVdpArea']; ICVFs_N = p['ICVFs_N']; ICVSC_Npm = p['ICVSC_Npm']
ICVleakKv = p['ICVleakKv']
DCVport_mm = p['DCVport_mm']; DCVmass = p['DCVmass']; DCVtravel = p['DCVtravel']
DCVdpArea = p['DCVdpArea']; DCVFs_N = p['DCVFs_N']; DCVSC_Npm = p['DCVSC_Npm']
DCVleakKv = p['DCVleakKv']
bore = p['bore']; stroke = p['stroke']; HousingOD = p['HousingOD']
ChamberLen = p['ChamberLen']; em_housing = p['em_housing']; em_shield = p['em_shield']
keff = p['keff']; Vacuum_micron = p['Vacuum_micron']; dvf = p['dvf']
Tamb_K = p['Tamb_K']; htc_amb = p['htc_amb']; Ffric = p['Ffric']
Kv_BB = p['Kv_BB']; Pbbexit_barg = p['Pbbexit_barg']
fric2chamber = p['fric2chamber']; K_bulk = p['K_bulk']
h_in = p['h_in']; h_out = p['h_out']; mc0 = p['mc0']
Tout_K = p['Tout_K']; fluid = p['fluid']
FillType = p.get('FillType', 1); Vsnubber = p.get('Vsnubber', 0.001)
VCHSS = p.get('VCHSS', 0.001)
kv_AOV140 = p.get('kv_AOV140', 0.0); kv_vent = p.get('kv_vent', 0.0)
Pexit_fixed = p.get('Pexit_barg', 0.0)
_cf = p['_cf']; _h1_exit_J = p['_h1_exit_J']
_rho0_p0_factor = p['_rho0_p0_factor']
_fluid_Tc = p.get('_fluid_Tc', _H2_TC)
_fluid_Pc = p.get('_fluid_Pc', _H2_PC_PA)
# ── Derived geometry ──
Vdisp = PI / 4.0 * bore**2 * stroke
tcycle = 60.0 / pump_cpm
tstroke = tcycle / 2.0
vm_max = PI * stroke * pump_cpm / 60.0
_two_pi_inv_tcycle = _TWO_PI / tcycle
# ── Radiation geometry ──
bore_mm = bore * 1000.0; HousingOD_mm = HousingOD * 1000.0
ChamberLen_mm = ChamberLen * 1000.0
bot = 1.0 / em_housing + (1.0 - em_housing) / em_housing * (bore_mm / HousingOD_mm)
ds = (bore_mm + HousingOD_mm) / 2.0
bot += 2.0 * (1.0 - em_shield) / em_shield * (bore_mm / ds)
pa = Vacuum_micron / 1000.0 * (101325.0 / 760.0)
_rad_factor = PI * bore_mm * ChamberLen_mm * STEFAN_BOLTZMANN / (2.0 * bot) / 1e6
_Tamb4 = Tamb_K**4
_conv_htc_factor = 2.0 * PI / 4.0 * HousingOD_mm**2 * htc_amb / 1e6
_ChamberLen_m = ChamberLen / 1.0 # already in meters
# ── Thermal mass (VBA Pack 3) ──
tmass = 0.0
if flash_eff > 0.0:
_theta_max = Tout_K - Tin_K
_theta_avg = 0.215 * _theta_max
_tc_w = tc_ss304(Tin_K + _theta_avg)
_kappa = _tc_w / 7800.0 / 500.0
_delta = 2.6 * _sqrt(_kappa * tcycle)
_a_ch = 2.0 * (PI / 4.0 * bore**2) + PI * bore * stroke
tmass = flash_eff * 7800.0 * 500.0 * _a_ch * _delta * _theta_avg / tstroke / 1000.0
# Penalty springs — adaptive stiffness to keep penetration < 1% of travel for any fluid
# Peak water hammer force: WHdp_peak ~ den_in * vm_max * sqrt(K_bulk*1e6/den_in) * ICVdpArea
vm_max = PI * stroke * pump_cpm / 60.0
den_est = p.get('den_in', 70.0) # inlet density estimate
vwave_est = _sqrt(K_bulk * 1e6 / max(den_est, 1.0))
WHdp_peak = den_est * vm_max * vwave_est * ICVdpArea
# k must keep penetration under 1% of valve travel
# 100x safety factor ensures near-zero penetration + strong critical damping
k_min = 5e5 # baseline (fine for H2)
k_from_WHdp = 100.0 * WHdp_peak / (0.01 * min(ICVtravel, DCVtravel))
k_wall = max(k_min, k_from_WHdp)
c_DCV = 2.0 * _sqrt(k_wall * DCVmass)
c_ICV = 2.0 * _sqrt(k_wall * ICVmass)
# ── Convection cache (mutable) ──
Tc_init = p['Tc_init']
_cc = {
'Qcb': free_conv_2cyl_Wpm(bore, Tc_init, HousingOD, Tamb_K, pa, "air"),
'Tl': Tc_init,
}
# ── EOS state trackers (mutable, for two-pass Newton warm-start) ──
_eos_chamber = {'Tc': Tc_init, 'pc': Pexit_fixed, 'hc': h_out, 'cv': 10.0}
_eos_snubber = {'Tc': Tc_init, 'pc': Pexit_fixed, 'hc': h_out, 'cv': 10.0}
_eos_chss = {'Tc': Tc_init, 'pc': Pexit_fixed, 'hc': h_out, 'cv': 10.0}
rhs_calls = [0]
# ── Backend selection: Numba H2 vs CoolProp ──
use_numba = p.get('_use_numba', False)
_Pg = p.get('_Pg', None) # Numba PS LUT arrays
_Sg = p.get('_Sg', None)
_d_tbl = p.get('_d_tbl', None)
_h_tbl = p.get('_h_tbl', None)
# ── EOS flash: dual-backend (uc, ρ) → (Tc, pc, hc, cv) ──
def _eos_flash(uc_val, den_val, state):
Tc_prev = state['Tc']; pc_prev = state['pc']
hc_prev = state['hc']; cv_prev = state['cv']
if use_numba:
# ── NUMBA PATH: direct Helmholtz evaluation ──
den_safe = max(den_val, 0.1)
cv_kJkgK = h2_numba.cv_td(max(Tc_prev, 14.0), den_safe)
if cv_kJkgK <= 0.0:
cv_kJkgK = cv_prev
# Consistent: evaluate h,P at (Tc_prev, den_current)
pc_at_prev, hc_at_prev = h2_numba.state_td(max(Tc_prev, 14.0), den_safe)
uc_prev_est = hc_at_prev - (pc_at_prev + 1.01325) * 1e5 / den_safe / 1000.0
dT_est = (uc_val - uc_prev_est) / max(cv_kJkgK, 0.1)
# Clamp dT to prevent runaway (solver probes can produce extreme states)
dT_est = max(-200.0, min(200.0, dT_est))
T_est = max(Tc_prev + dT_est, 14.0)
T_est = min(T_est, 500.0)
pc_new, hc_new = h2_numba.state_td(T_est, den_safe)
uc_check = hc_new - (pc_new + 1.01325) * 1e5 / den_safe / 1000.0
dT_ref = (uc_val - uc_check) / max(cv_kJkgK, 0.1)
dT_ref = max(-200.0, min(200.0, dT_ref))
T_ref = max(T_est + dT_ref, 14.0)
T_ref = min(T_ref, 500.0)
pc_new, hc_new = h2_numba.state_td(T_ref, den_safe)
Tc_new = T_ref
else:
# ── COOLPROP PATH: AbstractState + specify_phase ──
den_safe = max(den_val, 0.1)
try:
_specify_phase_td(AS, Tc_prev, den_safe, _fluid_Tc)
AS.update(_DmassT_INPUTS, den_safe, Tc_prev)
cv_kJkgK = AS.cvmass() / 1000.0
# Get h and P at (Tc_prev, den_current) for consistent uc_prev_est
hc_at_prev = AS.hmass() / 1000.0
pc_at_prev = AS.p() / 1e5 - 1.01325
if cv_kJkgK <= 0.0:
cv_kJkgK = cv_prev
except Exception:
cv_kJkgK = cv_prev
hc_at_prev = hc_prev
pc_at_prev = pc_prev
finally:
AS.unspecify_phase()
uc_prev_est = hc_at_prev - (pc_at_prev + 1.01325) * 1e5 / den_safe / 1000.0
dT_est = (uc_val - uc_prev_est) / max(cv_kJkgK, 0.1)
T_est = max(Tc_prev + dT_est, 14.0)
try:
_specify_phase_td(AS, T_est, den_val, _fluid_Tc)
AS.update(_DmassT_INPUTS, den_val, T_est)
pc_new = AS.p() / 1e5 - 1.01325
hc_new = AS.hmass() / 1000.0
uc_check = hc_new - (pc_new + 1.01325) * 1e5 / max(den_val, 0.1) / 1000.0
dT_ref = (uc_val - uc_check) / max(cv_kJkgK, 0.1)
T_ref = max(T_est + dT_ref, 14.0)
_specify_phase_td(AS, T_ref, den_val, _fluid_Tc)
AS.update(_DmassT_INPUTS, den_val, T_ref)
pc_new = AS.p() / 1e5 - 1.01325
hc_new = AS.hmass() / 1000.0
Tc_new = T_ref
except Exception:
pc_new = pc_prev; hc_new = hc_prev; Tc_new = Tc_prev
finally:
AS.unspecify_phase()
state['Tc'] = Tc_new; state['pc'] = pc_new
state['hc'] = hc_new; state['cv'] = cv_kJkgK
return Tc_new, pc_new, hc_new, cv_kJkgK
# ── Flow functions: dual-backend ──
def _flow_core(p1_barg, p2_barg, h1_J, Kv, d2hat, h2hat_kJ):
if _fabs(p1_barg - p2_barg) < 0.0001:
return 0.0
sg = -1.0 if p2_barg > p1_barg else 1.0
dh = h1_J - h2hat_kJ * 1000.0
if dh < 0.0:
dh = 0.0
return sg * Kv * d2hat * _rho0_p0_factor * _sqrt(dh) / 60.0
def _cached_flow_1d(p1_barg, p2_barg, h1_J, Kv):
if _fabs(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, dcv1d_P, dcv1d_d))
h2hat_kJ = float(np.interp(p2hat_MPa, dcv1d_P, dcv1d_h))
return _flow_core(p1_barg, p2_barg, h1_J, Kv, d2hat, h2hat_kJ)
def _flow_calc(p1_barg, p2_barg, T_upstream_K, GasKv):
"""Flow calculation — dispatches to Numba or CoolProp."""
if _fabs(p1_barg - p2_barg) < 0.0001:
return 0.0
if use_numba:
# Numba path: single call does PT + PS + flow formula
return h2_numba.flow_calc(p1_barg, p2_barg, T_upstream_K, GasKv,
_cf, _Pg, _Sg, _d_tbl, _h_tbl) / 60.0
else:
# CoolProp path: AbstractState PT + PS flash
return _flow_AS(p1_barg, p2_barg, T_upstream_K - 273.15, GasKv) / 60.0
def _flow_AS(p1_barg, p2_barg, Tupstream_C, GasKv):
"""Flow via CoolProp AbstractState with specify_phase (N2 / fallback)."""
if _fabs(p1_barg - p2_barg) < 0.0001:
return 0.0
sg = 1.0
Phigh, Plow = p1_barg, p2_barg
if p2_barg > p1_barg:
Phigh, Plow = p2_barg, p1_barg
sg = -1.0
Phigha = Phigh + 1.01325; Plowa = Plow + 1.01325
T_K = Tupstream_C + 273.15; P_Pa = Phigha * 1e5
pc_bara = Phigha * _cf; p2hat_bara = max(Plowa, pc_bara)
P2_Pa = p2hat_bara * 1e5
_specify_phase_safe(AS, P_Pa, T_K, _fluid_Tc, _fluid_Pc)
try:
AS.update(_PT_INPUTS, P_Pa, T_K)
except Exception:
try:
AS.update(_PT_INPUTS, P_Pa, T_K - 0.05)
except Exception:
AS.unspecify_phase()
return 0.0
h1_J = AS.hmass(); s1 = AS.smass()
_specify_phase_safe(AS, P2_Pa, T_K, _fluid_Tc, _fluid_Pc)
try:
AS.update(_PSmass_INPUTS, P2_Pa, s1)
d2hat = AS.rhomass(); h2hat_J = AS.hmass()
except Exception:
d2hat = 1.0; h2hat_J = h1_J + (p2hat_bara - Phigha) * 1e5 / max(d2hat, 1.0)
finally:
AS.unspecify_phase()
dh = max(h1_J - h2hat_J, 0.0)
return sg * GasKv * d2hat * _rho0_p0_factor * _sqrt(dh) / 60.0
def _flow_AS_downstream_only(p1_barg, p2_barg, h1_J_cached, s1_cached, GasKv, T_upstream_K=300.0):
"""Flow using pre-computed upstream PT flash (CoolProp flash caching).
Phase specification strategy for PS flash:
- P > Pc: use iphase_supercritical_gas (works for CoolProp PS flash
regardless of temperature — tells solver to skip saturation search)
- P <= Pc and T < Tc: use iphase_liquid to avoid two-phase region
- P <= Pc and T >= Tc: unspecify (gas phase, no saturation issue)
"""
if _fabs(p1_barg - p2_barg) < 0.0001:
return 0.0
sg = 1.0
Phigh, Plow = p1_barg, p2_barg
if p2_barg > p1_barg:
Phigh, Plow = p2_barg, p1_barg
sg = -1.0
Phigha = Phigh + 1.01325; Plowa = Plow + 1.01325
pc_bara = Phigha * _cf; p2hat_bara = max(Plowa, pc_bara)
P2_Pa = p2hat_bara * 1e5
if P2_Pa > _fluid_Pc:
# Supercritical pressure: iphase_supercritical_gas works for PS flash
# even at T < Tc (tells CoolProp to skip saturation boundary search)
AS.specify_phase(_PHASE_SUPERCRITICAL_GAS)
elif T_upstream_K < _fluid_Tc:
# Subcritical P, subcritical T: liquid phase
try:
AS.specify_phase(_PHASE_LIQUID)
except Exception:
AS.unspecify_phase()
else:
AS.unspecify_phase()
try:
AS.update(_PSmass_INPUTS, P2_Pa, s1_cached)
d2hat = AS.rhomass(); h2hat_J = AS.hmass()
except Exception:
AS.unspecify_phase()
return 0.0
finally:
AS.unspecify_phase()
dh = max(h1_J_cached - h2hat_J, 0.0)
return sg * GasKv * d2hat * _rho0_p0_factor * _sqrt(dh) / 60.0
# ── The RHS function ──
def rhs(t, y):
rhs_calls[0] += 1
mc = max(y[0], mc0 * 1e-6)
uc = y[1]
vp = y[2]; xp = y[3]; vip = y[4]; xip = y[5]
if has_ds:
msb = max(y[6], 1e-12); usb = y[7]
mchss = max(y[8], 1e-12); uchss = y[9]
# ── Kinematics (multi-cycle) ──
t_mod = t % tcycle
retract = (t_mod <= tstroke)
phase_a = _two_pi_inv_tcycle * t
yp_pos = 0.5 * (1.0 - _cos(phase_a))
v_pist = vm_max * _sin(phase_a)
vc = Vdisp * (dvf + yp_pos)
den = mc / vc
dvc_dt = Vdisp * PI / tcycle * _sin(phase_a)
# ── Chamber EOS (two-pass Newton, NOT DmassUmass) ──
Tc_K, pc, hc, cv_val = _eos_flash(uc, den, _eos_chamber)
# ── Downstream EOS ──
if has_ds:
dsb = msb / Vsnubber
Tsb, psb, hsb, _ = _eos_flash(usb, dsb, _eos_snubber)
Pexit = psb
pchss = 0.0
if FillType == 1:
dchss = mchss / VCHSS
_, pchss, _, _ = _eos_flash(uchss, dchss, _eos_chss)
else:
Pexit = Pexit_fixed
psb = Pexit; Tsb = Tc_K; hsb = hc
# ── DCV flow (1D table for forward, AbstractState for reverse) ──
xp_c = max(0.0, min(DCVtravel, xp))
x_fr = xp_c / DCVtravel
kv_d = DCVleakKv + kv_from_Cd_and_RO_dia(DCVport_mm * (1.0 - x_fr))
if Pexit > pc:
# Forward flow: DCV upstream is exit/snubber. Use 1D table.
mdot_DCV = _cached_flow_1d(Pexit, pc, _h1_exit_J, kv_d) / 60.0
else:
# Reverse flow: chamber is upstream
mdot_DCV = _flow_calc(Pexit, pc, Tc_K, kv_d)
h_DCV = (hsb if has_ds and mdot_DCV > 0 else (h_out if mdot_DCV > 0 else hc))
# ── ICV + blowby flow with flash caching ──
xip_c = max(0.0, min(ICVtravel, xip))
xi_fr = xip_c / ICVtravel
kv_i = ICVleakKv + kv_from_Cd_and_RO_dia(ICVport_mm * xi_fr)
icv_up_is_chamber = (pc > Ptank_barg)
bb_up_is_chamber = (pc > Pbbexit_barg)
Tu_icv_K = Tc_K if icv_up_is_chamber else Tin_K
if use_numba:
# ── NUMBA PATH: _flow_calc handles /60 (kg/min → kg/s) ──
mdot_ICV = _flow_calc(Ptank_barg, pc, Tu_icv_K, kv_i)
mdot_BB = _flow_calc(Pbbexit_barg, pc, Tc_K, Kv_BB)
elif icv_up_is_chamber and bb_up_is_chamber:
# ── COOLPROP PATH: shared upstream flash caching ──
Phigha = pc + 1.01325
P_Pa_up = Phigha * 1e5
_specify_phase_safe(AS, P_Pa_up, Tc_K, _fluid_Tc, _fluid_Pc)
try:
AS.update(_PT_INPUTS, P_Pa_up, Tc_K)
_cached_h1 = AS.hmass()
_cached_s1 = AS.smass()
mdot_ICV = _flow_AS_downstream_only(Ptank_barg, pc, _cached_h1, _cached_s1, kv_i, Tc_K) / 60.0
mdot_BB = _flow_AS_downstream_only(Pbbexit_barg, pc, _cached_h1, _cached_s1, Kv_BB, Tc_K) / 60.0
except Exception:
AS.unspecify_phase()
mdot_ICV = _flow_AS(Ptank_barg, pc, Tu_icv_K - 273.15, kv_i) / 60.0
mdot_BB = _flow_AS(Pbbexit_barg, pc, Tc_K - 273.15, Kv_BB) / 60.0
else:
# ── COOLPROP PATH: separate flow calcs ──
mdot_ICV = _flow_AS(Ptank_barg, pc, Tu_icv_K - 273.15, kv_i) / 60.0
mdot_BB = _flow_AS(Pbbexit_barg, pc, Tc_K - 273.15, Kv_BB) / 60.0
h_ICV = h_in if mdot_ICV > 0 else hc
h_bb = hc # blowby always carries chamber enthalpy out
# ── Heat terms ──
Qrad = _rad_factor * (_Tamb4 - Tc_K**4)
if _fabs(Tc_K - _cc['Tl']) > 2.0:
_cc['Qcb'] = free_conv_2cyl_Wpm(bore, Tc_K, HousingOD, Tamb_K, pa, "air")
_cc['Tl'] = Tc_K
Qconv = -_cc['Qcb'] * _ChamberLen_m + _conv_htc_factor * (Tamb_K - Tc_K)
Qig = (Qrad + Qconv) / 1000.0 # kW
Qf = Ffric * _fabs(v_pist) * fric2chamber / 1000.0 # kW
work = -pc * dvc_dt * 100.0 # kW
Qtmass = (1.0 if retract else -1.0) * tmass # kW
# ── ODE #1-2: chamber mass and energy ──
dmc = mdot_ICV + mdot_DCV + mdot_BB
e_in = (mdot_ICV * h_ICV + mdot_DCV * h_DCV + mdot_BB * h_bb
+ Qig + Qf + work + Qtmass)
duc = (e_in - uc * dmc) / mc if corrected else e_in
# ── ODE #3-4: DCV dynamics + penalty walls ──
Fs_d = DCVFs_N - DCVSC_Npm * xp_c
Fdp_d = (Pexit - pc) * 1e5 * DCVdpArea
Fw_d = 0.0
if xp < 0.0:
Fw_d = -k_wall * xp - c_DCV * vp
elif xp > DCVtravel:
Fw_d = -k_wall * (xp - DCVtravel) - c_DCV * vp
dvp = (Fs_d + Fdp_d + Fw_d) / DCVmass
# ── ODE #5-6: ICV dynamics + penalty walls ──
Fs_i = ICVFs_N - ICVSC_Npm * (ICVtravel - xip_c)
Fdp_i = (Ptank_barg - pc) * 1e5 * ICVdpArea
vwave = _sqrt(K_bulk * 1e6 / max(den, 1e-6))
WHdp = (1.0 if retract else -1.0) * den * v_pist * vwave * ICVdpArea
Fw_i = 0.0
if xip < 0.0:
Fw_i = -k_wall * xip - c_ICV * vip
elif xip > ICVtravel:
Fw_i = -k_wall * (xip - ICVtravel) - c_ICV * vip
dvip = (Fdp_i - Fs_i + WHdp + Fw_i) / ICVmass
if not has_ds:
return [dmc, duc, dvp, vp, dvip, vip]
# ── ODE #7-8: Snubber mass + energy ──
if FillType == 1:
mdot_sb = _flow_calc(psb, pchss, Tsb, kv_AOV140)
else:
mdot_sb = _flow_calc(psb, 0.0, Tsb, kv_vent)
dmsb = -mdot_DCV - mdot_sb
# Corrected specific energy form: du = (sum(h*mdot) - u*dm) / m
h_into_sb = hc if mdot_DCV < 0 else h_DCV # DCV discharge enters snubber
e_in_sb = -mdot_DCV * h_into_sb - mdot_sb * hsb
dusb = (e_in_sb - usb * dmsb) / msb if corrected else e_in_sb
# ── ODE #9-10: CHSS ──
dmchss = mdot_sb if FillType == 1 else 0.0
duchss_raw = mdot_sb * hsb if FillType == 1 else 0.0
duchss = (duchss_raw - uchss * dmchss) / mchss if (corrected and FillType == 1) else duchss_raw
return [dmc, duc, dvp, vp, dvip, vip, dmsb, dusb, dmchss, duchss]
return rhs, rhs_calls
# ============================================================================
# ODE Driver
# ============================================================================
def ODE_driver_v2(Pexit_barg, speed_f, Ptank_barg, Psat_barg,
ICVparam, DCVparam, pump_geom, proc_param,
fluid="h2", prtMode=0, flash_eff=0.02,
exit_param=None, n_cycles=1,
use_corrected=True, method='RK45', rtol=1e-6, atol=1e-8):
"""10-variable ODE pump cycle with v2's performance architecture.
Returns (out, history) matching ICV_open() format.
"""
t_start = time.time()
has_ds = exit_param is not None
# ── Create AbstractState (reused for ALL CoolProp calls) ──
fluid_cp = coolprop_fluid_name(fluid)
AS = CoolProp.AbstractState('HEOS', fluid_cp)
_fluid_Tc = AS.T_critical() # K (H2: 33.145, N2: 126.21)
_fluid_Pc = AS.p_critical() # Pa (H2: 1.2964e6, N2: 3.396e6)
# ── Unpack input arrays ──
ICVport_mm, ICVmass_g, ICVtravel_mm = ICVparam[0], ICVparam[1], ICVparam[2]
ICVdpArea_mm2, ICVFs_N, ICVSC_Npmm = ICVparam[3], ICVparam[4], ICVparam[5]
ICVleakKv, ICVcomp_eff = ICVparam[6], ICVparam[7]
DCVport_mm, DCVmass_g, DCVtravel_mm = DCVparam[0], DCVparam[1], DCVparam[2]
DCVdpArea_mm2, DCVFs_N, DCVSC_Npmm = DCVparam[3], DCVparam[4], DCVparam[5]
DCVleakKv, DCVcomp_eff = DCVparam[6], DCVparam[7]
bore_mm, stroke_mm = pump_geom[0], pump_geom[1]
HousingOD_mm, ChamberLen_mm = pump_geom[2], pump_geom[3]
em_housing, em_shield = pump_geom[4], pump_geom[5]
kvoid, khousing, Vfvoid = pump_geom[6], pump_geom[7], pump_geom[8]
Vacuum_micron, design_cpm, dvf = pump_geom[9], pump_geom[10], pump_geom[11]
Tamb_K, htc_amb = proc_param[0], proc_param[1]
NetDriveCouplerForce_kgf, F_multiplier = proc_param[2], proc_param[3]
Kv_BB, Pbbexit_barg = proc_param[4], proc_param[5]
fric2chamber, Exp_eff = proc_param[6], proc_param[7]
# ── Geometry ──
stroke = stroke_mm / 1000.0; bore = bore_mm / 1000.0
Vdisp = PI / 4.0 * bore**2 * stroke; V_dead = dvf * Vdisp
pump_cpm = design_cpm * speed_f
tcycle = 60.0 / pump_cpm; tstroke = tcycle / 2.0
# ── Thermodynamics (CoolProp one-time, pre-loop) ──
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)
_props = _get_cached_fluid_props(fluid)
_cf = _props["cf"]
_s_exit = refprop("s", fluid, "pt", "si", PeMPa, Tout_K)
_h1_exit_J = refprop("h", fluid, "pt", "si", PeMPa, Tout_K) * 1000.0
_rho0_p0_factor = _sqrt(1000.0 / 1e5 / 1.0)
# ── Build 1D DCV table (zero CoolProp in hot path) ──
dcv1d_P, dcv1d_h, dcv1d_d = _build_ps_table_1d_cached(
s_fixed=_s_exit, P_range_MPa=(0.101325, PeMPa + 1.0),
fluid=fluid, n=2000)
# ── Valve physical params (SI) ──
DCVmass = DCVmass_g / 1000.0; DCVSC_Npm = DCVSC_Npmm * 1000.0
DCVtravel = DCVtravel_mm / 1000.0; DCVdpArea = DCVdpArea_mm2 / 1e6
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)
keff = composite_thermal_conductivity(1, kvoid, Vfvoid, khousing)
Ffric = NetDriveCouplerForce_kgf * F_multiplier * 9.80665
# ── Chamber initial conditions ──
mc0 = V_dead * den_out
pc = Pexit_barg; Tc_K = Tout_K
uc = h_out - (pc + 1.01325) * 1e5 / den_out / 1000.0
# ── Downstream volumes ──
FillType = 1; Vsnubber = 0.001; VCHSS = 0.001
kv_AOV140 = 0.0; kv_vent = 0.0
msb = 0.0; usb = 0.0; mchss = 0.0; uchss = 0.0
if has_ds:
FillType = int(exit_param[0])
Vsnubber = max(exit_param[1] / 1000.0, 0.001)
VCHSS = max(exit_param[2] / 1000.0, 0.001)
AOV140f = exit_param[3]; ROdia_mm = exit_param[4]
Kv_RO = kv_from_Cd_and_RO_dia(ROdia_mm)
kv_AOV140 = Kv_from_Cv(0.25) * AOV140f
kv_vent = 1.0 / (1.0 / max(kv_AOV140, 1e-12) + 1.0 / max(Kv_RO, 1e-12))
msb = Vsnubber * den_out; usb = uc
mchss = VCHSS * den_out; uchss = uc
# ── Numba backend init (H2 only) ──
_use_numba = False
_Pg = _Sg = _d_tbl = _h_tbl = None
if _HAS_NUMBA_H2 and fluid.lower() in ('h2', 'hydrogen'):
ps_lut_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'h2_ps_lut.npz')
if os.path.exists(ps_lut_path):
_Pg, _Sg, _d_tbl, _h_tbl = h2_numba.init(ps_lut_path)
_use_numba = True
if prtMode:
print(" Phase 2: Numba Helmholtz EOS active (H2)")
if not _use_numba and prtMode:
print(f" Phase 1: CoolProp AbstractState ({fluid})")
# ── Pack params for RHS factory ──
params = dict(
pump_cpm=pump_cpm, Ptank_barg=Ptank_barg, Tin_K=Tin_K,
flash_eff=flash_eff, has_downstream=has_ds,
FillType=FillType, Vsnubber=Vsnubber, VCHSS=VCHSS,
kv_AOV140=kv_AOV140, kv_vent=kv_vent, K_bulk=K_bulk,
ICVport_mm=ICVport_mm, ICVmass=ICVmass, ICVtravel=ICVtravel,
ICVdpArea=ICVdpArea, ICVFs_N=ICVFs_N, ICVSC_Npm=ICVSC_Npm, ICVleakKv=ICVleakKv,
DCVport_mm=DCVport_mm, DCVmass=DCVmass, DCVtravel=DCVtravel,
DCVdpArea=DCVdpArea, DCVFs_N=DCVFs_N, DCVSC_Npm=DCVSC_Npm, DCVleakKv=DCVleakKv,
bore=bore, stroke=stroke, HousingOD=HousingOD_mm / 1000.0,
ChamberLen=ChamberLen_mm / 1000.0, em_housing=em_housing, em_shield=em_shield,
keff=keff, Vacuum_micron=Vacuum_micron, dvf=dvf,
Tamb_K=Tamb_K, htc_amb=htc_amb, Ffric=Ffric,
Kv_BB=Kv_BB, Pbbexit_barg=Pbbexit_barg,
fric2chamber=fric2chamber, Exp_eff=Exp_eff,
h_in=h_in, den_in=den_in, h_out=h_out,
Tout_K=Tout_K, Tc_init=Tc_K, mc0=mc0,
Pexit_barg=Pexit_barg, fluid=fluid,
_cf=_cf, _h1_exit_J=_h1_exit_J, _rho0_p0_factor=_rho0_p0_factor,
_use_numba=_use_numba, _Pg=_Pg, _Sg=_Sg, _d_tbl=_d_tbl, _h_tbl=_h_tbl,
_fluid_Tc=_fluid_Tc, _fluid_Pc=_fluid_Pc,
)
rhs, rhs_calls = _make_rhs_v2(params, AS, dcv1d_P, dcv1d_h, dcv1d_d,
corrected=use_corrected)
# ── Initial state ──
y0 = ([mc0, uc, 0.0, 0.0, 0.0, 0.0, msb, usb, mchss, uchss] if has_ds
else [mc0, uc, 0.0, 0.0, 0.0, 0.0])
y0 = np.array(y0)
t_pre = time.time()
sol = solve_ivp(rhs, [0.0, n_cycles * tcycle], y0, method=method,
rtol=rtol, atol=atol, dense_output=True,
max_step=tcycle / 200, first_step=tcycle / 10000)
t_solve = time.time() - t_pre
if sol.status != 0:
print(f"WARNING: solve_ivp status={sol.status}: {sol.message}")
# ============================== Post-processing ==============================
t_arr = sol.t; n_pts = len(t_arr)
mc_arr, uc_arr = sol.y[0], sol.y[1]
xp_arr, xip_arr = sol.y[3], sol.y[5]
angle_arr = t_arr * 360.0 / tcycle
yp_arr = 0.5 * (1.0 - np.cos(_TWO_PI * t_arr / tcycle))
vc_arr = Vdisp * (dvf + yp_arr); den_arr = mc_arr / vc_arr
# Reconstruct (pc, Tc, hc) via two-pass Newton at each point
pc_arr = np.zeros(n_pts); Tc_arr = np.zeros(n_pts); hc_arr = np.zeros(n_pts)
dcv_of = np.zeros(n_pts); icv_of = np.zeros(n_pts)
if _use_numba:
# NUMBA POST-PROCESSING: direct Helmholtz state evaluation
_eos_Tc_prev = Tout_K
for i in range(n_pts):
den_i = max(den_arr[i], 0.1)
cv_i = h2_numba.cv_td(max(_eos_Tc_prev, 14.0), den_i)
if cv_i <= 0.0: cv_i = 10.0
_eos_pc_prev = pc_arr[max(i-1, 0)] if i > 0 else Pexit_barg
_eos_hc_prev = hc_arr[max(i-1, 0)] if i > 0 else h_out
uc_prev_est = _eos_hc_prev - (_eos_pc_prev + 1.01325) * 1e5 / den_i / 1000.0
dT = min(200.0, max(-200.0, (uc_arr[i] - uc_prev_est) / max(cv_i, 0.1)))
T_est = min(500.0, max(14.0, _eos_Tc_prev + dT))
pc_1, hc_1 = h2_numba.state_td(T_est, den_i)
uc_chk = hc_1 - (pc_1 + 1.01325) * 1e5 / den_i / 1000.0
dT2 = min(200.0, max(-200.0, (uc_arr[i] - uc_chk) / max(cv_i, 0.1)))
T_ref = min(500.0, max(14.0, T_est + dT2))
pc_arr[i], hc_arr[i] = h2_numba.state_td(T_ref, den_i)
Tc_arr[i] = T_ref
_eos_Tc_prev = T_ref
dcv_of[i] = 1.0 - max(0.0, min(1.0, xp_arr[i] / DCVtravel))
icv_of[i] = max(0.0, min(1.0, xip_arr[i] / ICVtravel))
else:
_eos_post = {'Tc': Tout_K, 'pc': Pexit_barg, 'hc': h_out, 'cv': 10.0}
for i in range(n_pts):
Tc_i, pc_i, hc_i, _ = _eos_flash_post(AS, uc_arr[i], den_arr[i], _eos_post, _fluid_Tc)
pc_arr[i] = pc_i; Tc_arr[i] = Tc_i; hc_arr[i] = hc_i
dcv_of[i] = 1.0 - max(0.0, min(1.0, xp_arr[i] / DCVtravel))
icv_of[i] = max(0.0, min(1.0, xip_arr[i] / ICVtravel))
# Flow rates for output
dcv_leak = np.zeros(n_pts); icv_leak = np.zeros(n_pts)
if _use_numba:
# NUMBA POST-PROCESSING: use flow_calc instead of flow_RF_kgpm
for i in range(n_pts):
kv_d = DCVleakKv + kv_from_Cd_and_RO_dia(DCVport_mm * dcv_of[i])
p_ex = Pexit_barg
if p_ex > pc_arr[i]:
dcv_leak[i] = _cached_flow_1d_post(p_ex, pc_arr[i], _h1_exit_J,
kv_d, _cf, _rho0_p0_factor,
dcv1d_P, dcv1d_h, dcv1d_d) * 60.0
else:
dcv_leak[i] = h2_numba.flow_calc(p_ex, pc_arr[i], max(Tc_arr[i], 14.0),
kv_d, _cf, _Pg, _Sg, _d_tbl, _h_tbl)
kv_i = ICVleakKv + kv_from_Cd_and_RO_dia(ICVport_mm * icv_of[i])
Tu_i = Tc_arr[i] if pc_arr[i] > Ptank_barg else Tin_K
icv_leak[i] = h2_numba.flow_calc(Ptank_barg, pc_arr[i], max(Tu_i, 14.0),
kv_i, _cf, _Pg, _Sg, _d_tbl, _h_tbl)
else:
for i in range(n_pts):
kv_d = DCVleakKv + kv_from_Cd_and_RO_dia(DCVport_mm * dcv_of[i])
p_ex = Pexit_barg
if has_ds and sol.y.shape[0] > 6:
try:
_eos_sb = {'Tc': Tout_K, 'pc': Pexit_barg, 'hc': h_out, 'cv': 10.0}
_, p_ex, _, _ = _eos_flash_post(AS, sol.y[7, i], sol.y[6, i] / Vsnubber, _eos_sb, _fluid_Tc)
except Exception:
pass
if p_ex > pc_arr[i]:
dcv_leak[i] = _cached_flow_1d_post(p_ex, pc_arr[i], _h1_exit_J,
kv_d, _cf, _rho0_p0_factor,
dcv1d_P, dcv1d_h, dcv1d_d) * 60.0
else:
dcv_leak[i] = flow_RF_kgpm(p_ex, pc_arr[i], Tc_arr[i] - 273.15, kv_d, fluid)
kv_i = ICVleakKv + kv_from_Cd_and_RO_dia(ICVport_mm * icv_of[i])
Tu_i = (Tc_arr[i] if pc_arr[i] > Ptank_barg else Tin_K) - 273.15
icv_leak[i] = flow_RF_kgpm(Ptank_barg, pc_arr[i], Tu_i, kv_i, fluid)
# Mass discharged
dt_arr = np.diff(t_arr, prepend=0.0)
dt_arr[0] = dt_arr[1] if len(dt_arr) > 1 else 1e-6
m_out = -np.sum(np.minimum(dcv_leak / 60.0, 0.0) * dt_arr)
m_in = np.sum(np.maximum(icv_leak / 60.0, 0.0) * dt_arr)
mdot_out = m_out / (n_cycles * tcycle) * 60.0
mass_eff = m_in / (Vdisp * den_in); m_out_eff = m_out / (Vdisp * den_in)
# pV work
dvc = np.diff(vc_arr); pc_mid = 0.5 * (pc_arr[:-1] + pc_arr[1:])
work_inc = -pc_mid * dvc * 100.0
ret_mask = (t_arr[:-1] % tcycle) <= tstroke
kWh_ret = np.sum(work_inc[ret_mask]); kWh_ext = np.sum(work_inc[~ret_mask])
kWh_ret_out = kWh_ret / 3600.0 / m_out if m_out > 0 else 0.0
kWh_ext_out = kWh_ext / 3600.0 / m_out if m_out > 0 else 0.0
ICVmax_frac = float(np.max(icv_of)); pc_peak = float(np.max(pc_arr))
t_wall = time.time()
# Output array
out = np.zeros((15, 2), dtype=object)
labels = ["s, cpu time", ", mass inflow efficiency", "s, DCV closure time",
", stroke fraction ICV starts to open", ", max ICV open fraction",
"N, max closure force on DCV", "N, max open force on ICV",
"N, max closure force on ICV", "m/s, max ICV opening velocity",
"kg, total discharged mass per cycle", ", mass efficiency of cycle",
", stroke fraction DCV starts to open", "s, ICV closure time after extend start",
"kWh/kg, retract pV work", "kWh/kg, extend pV work"]
vals = [t_wall - t_start, mass_eff, 0.0, 0.0, ICVmax_frac, 0.0, 0.0, 0.0, 0.0,
m_out, m_out_eff, 0.0, 0.0, kWh_ret_out, kWh_ext_out]
for i in range(15):
out[i, 0] = vals[i]; out[i, 1] = labels[i]
history = {
'angle_deg': angle_arr, 'pc': pc_arr, 'den': den_arr, 'yp': yp_arr,
'mc_g': mc_arr * 1000.0, 'Tc_K': Tc_arr, 'hc': hc_arr,
'DCV_open_frac': dcv_of, 'DCV_leak_kgpm': dcv_leak,
'ICV_open_frac': icv_of, 'ICV_leak_kgpm': icv_leak,
'mdot_kgpm': mdot_out, 'tcycle_s': tcycle, 'tstroke_s': tstroke,
'steps': n_pts, 'kWh_retract': kWh_ret_out, 'kWh_extend': kWh_ext_out,
'rhs_calls': rhs_calls[0], 'nfev': sol.nfev,
'njev': getattr(sol, 'njev', 0), 'nlu': getattr(sol, 'nlu', 0),
'pc_peak': pc_peak, 'n_cycles': n_cycles,
'method': method, 'use_corrected': use_corrected,
'solve_time': t_solve, 'pre_time': t_pre - t_start,
}
# Downstream histories
if has_ds:
_eos_sb_post = {'Tc': Tout_K, 'pc': Pexit_barg, 'hc': h_out, 'cv': 10.0}
msb_a = sol.y[6]; psb_a = np.zeros(n_pts); Tsb_a = np.zeros(n_pts)
for i in range(n_pts):
try:
Tsb_a[i], psb_a[i], _, _ = _eos_flash_post(AS, sol.y[7, i], msb_a[i] / Vsnubber, _eos_sb_post, _fluid_Tc)
except Exception:
psb_a[i] = psb_a[max(i-1, 0)]; Tsb_a[i] = Tsb_a[max(i-1, 0)]
history['psnub'] = psb_a; history['Tsnub'] = Tsb_a
if FillType == 1:
_eos_ch_post = {'Tc': Tout_K, 'pc': Pexit_barg, 'hc': h_out, 'cv': 10.0}
mc_a = sol.y[8]; pc_a = np.zeros(n_pts)
for i in range(n_pts):
try:
_, pc_a[i], _, _ = _eos_flash_post(AS, sol.y[9, i], mc_a[i] / VCHSS, _eos_ch_post, _fluid_Tc)
except Exception:
pc_a[i] = pc_a[max(i-1, 0)]
history['pchss'] = pc_a; history['mchss'] = mc_a
return out, history
# ============================================================================
# Post-processing EOS flash (standalone, not inside RHS closure)
# ============================================================================
def _eos_flash_post(AS, uc_val, den_val, state, fluid_Tc=_H2_TC):
"""Two-pass Newton EOS flash for post-processing."""
Tc_prev = state['Tc']; pc_prev = state['pc']
hc_prev = state['hc']; cv_prev = state['cv']
den_safe = max(den_val, 0.1)
try:
_specify_phase_td(AS, Tc_prev, den_safe, fluid_Tc)
AS.update(_DmassT_INPUTS, den_safe, Tc_prev)
cv_kJkgK = AS.cvmass() / 1000.0
if cv_kJkgK <= 0.0:
cv_kJkgK = cv_prev
except Exception:
cv_kJkgK = cv_prev
finally:
AS.unspecify_phase()
uc_prev_est = hc_prev - (pc_prev + 1.01325) * 1e5 / den_safe / 1000.0
dT_est = (uc_val - uc_prev_est) / max(cv_kJkgK, 0.1)
T_est = max(Tc_prev + dT_est, 14.0)
try:
_specify_phase_td(AS, T_est, den_safe, fluid_Tc)
AS.update(_DmassT_INPUTS, den_safe, T_est)
pc_new = AS.p() / 1e5 - 1.01325
hc_new = AS.hmass() / 1000.0
uc_check = hc_new - (pc_new + 1.01325) * 1e5 / den_safe / 1000.0
dT_ref = (uc_val - uc_check) / max(cv_kJkgK, 0.1)
T_ref = max(T_est + dT_ref, 14.0)
_specify_phase_td(AS, T_ref, den_safe, fluid_Tc)
AS.update(_DmassT_INPUTS, den_safe, T_ref)
pc_new = AS.p() / 1e5 - 1.01325
hc_new = AS.hmass() / 1000.0
Tc_new = T_ref
except Exception:
pc_new = pc_prev; hc_new = hc_prev; Tc_new = Tc_prev
finally:
AS.unspecify_phase()
state['Tc'] = Tc_new; state['pc'] = pc_new
state['hc'] = hc_new; state['cv'] = cv_kJkgK
return Tc_new, pc_new, hc_new, cv_kJkgK
def _cached_flow_1d_post(p1_barg, p2_barg, h1_J, Kv, cf, rho0_p0_factor,
P_grid, h_tbl, d_tbl):
"""1D table flow for post-processing (standalone)."""
if _fabs(p1_barg - p2_barg) < 0.0001:
return 0.0
sg = -1.0 if p2_barg > p1_barg else 1.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))
dh = h1_J - h2hat_kJ * 1000.0
if dh < 0.0:
dh = 0.0
return sg * Kv * d2hat * rho0_p0_factor * _sqrt(dh) / 60.0
# ============================================================================
# Validation
# ============================================================================
if __name__ == "__main__":
from cycle2mdot_fast import ICV_open as ICV_open_fast
print("=" * 80)
print("cycle2mdot_ode_v2.py — Phase 2 Dual-Backend — Validation")
print("=" * 80)
ICVp = [20.5, 48, 5, 779.3, 33.4, 3.75, 0.0000736, 1.0, 100]
DCVp = [8.3, 25, 3.79, 78.54, 7.8, 1.053, 0.0000026, 0.8, 200]
pump = [40.3, 60, 120, 300, 0.8, 0.8, 0.026, 15, 37, 760000, 500, 0.01]
proc = [300, 10, 4, 30, 0.006, 0, 0.5, 0.2]
for fl in ['h2', 'n2']:
backend = "Numba Helmholtz" if (fl == 'h2' and _HAS_NUMBA_H2) else "CoolProp AS"
print(f"\n--- Fluid: {fl.upper()} ({backend}) ---")
print(f"{'Pexit':>6} {'rtol':>7} | {'Fast':>10} {'ODEv2':>10} "
f"{'Err%':>7} | {'tFast':>7} {'tODE':>7} {'tSolve':>7} | "
f"{'Node':>7} {'RHS':>8}")
print("-" * 100)
tols = [(1e-6, 1e-8), (1e-5, 1e-7)] if fl == 'h2' else [(1e-6, 1e-8)]
for P in [350, 900]:
t0 = time.time()
out_f, hist_f = ICV_open_fast(P, 0.8, 7, 2, ICVp, DCVp, pump, proc, fl)
t_fast = time.time() - t0
for rtol, atol in tols:
t0 = time.time()
out_c, hist_c = ODE_driver_v2(P, 0.8, 7, 2, ICVp, DCVp, pump, proc, fl,
flash_eff=0.0, use_corrected=True,
rtol=rtol, atol=atol)
t_ode = time.time() - t0
mf = hist_f['mdot_kgpm']; mc = hist_c['mdot_kgpm']
err = abs(mc - mf) / mf * 100 if mf != 0 else 0.0
print(f"{P:>6} {rtol:>7.0e} | {mf:>10.4f} {mc:>10.4f} "
f"{err:>6.2f}% | {t_fast:>6.1f}s {t_ode:>6.1f}s "
f"{hist_c['solve_time']:>6.1f}s | "
f"{hist_c['steps']:>7} {hist_c['rhs_calls']:>8}")
print("\nDone.")