Spaces:
Sleeping
Sleeping
| """ | |
| 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.") | |