Spaces:
Sleeping
Sleeping
File size: 42,477 Bytes
ed65aea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 | """
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.")
|