""" 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.")