代码仓库移植
This commit is contained in:
commit
0f57ba94f3
44 files changed
+7119
No files matched your search
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_ROOT = os.path.dirname(os.path.dirname(_HERE))
|
||||
sys.path.insert(0, os.path.join(_ROOT, "src"))
|
||||
@@ -0,0 +1,47 @@
|
||||
# tests/cryo_tank/test_heat_leak.py
|
||||
"""Tests for heat leak models."""
|
||||
from cryo_tank.heat_leak import HeatLeakModel, MLIHeatLeak, FoamHeatLeak
|
||||
|
||||
|
||||
class TestMLIHeatLeak:
|
||||
|
||||
def test_mli_returns_constant_heat_flux(self):
|
||||
model = MLIHeatLeak(A_total=3.306, q_mli=1.5)
|
||||
Q = model.compute(T_inner=78.0, T_env=300.0)
|
||||
assert abs(Q - 3.306 * 1.5) < 1e-10
|
||||
|
||||
def test_mli_default_q_is_1(self):
|
||||
model = MLIHeatLeak(A_total=3.306)
|
||||
Q = model.compute(T_inner=78.0, T_env=300.0)
|
||||
assert abs(Q - 3.306) < 1e-10
|
||||
|
||||
def test_mli_independent_of_temperature(self):
|
||||
model = MLIHeatLeak(A_total=3.306, q_mli=2.0)
|
||||
Q1 = model.compute(T_inner=78.0, T_env=300.0)
|
||||
Q2 = model.compute(T_inner=80.0, T_env=250.0)
|
||||
assert abs(Q1 - Q2) < 1e-10
|
||||
|
||||
|
||||
class TestFoamHeatLeak:
|
||||
|
||||
def test_foam_constant_k(self):
|
||||
model = FoamHeatLeak(A_total=3.306, k_eff=0.03, delta=0.05)
|
||||
Q = model.compute(T_inner=78.0, T_env=300.0)
|
||||
expected = 3.306 * 0.03 * (300.0 - 78.0) / 0.05
|
||||
assert abs(Q - expected) < 1e-6
|
||||
|
||||
def test_foam_callable_k(self):
|
||||
def k_func(T):
|
||||
return 0.01 + 0.0001 * T # linear k(T)
|
||||
|
||||
model = FoamHeatLeak(A_total=3.306, k_eff=k_func, delta=0.05)
|
||||
Q = model.compute(T_inner=78.0, T_env=300.0)
|
||||
T_mean = (300.0 + 78.0) / 2.0
|
||||
k_at_mean = k_func(T_mean)
|
||||
expected = 3.306 * k_at_mean * (300.0 - 78.0) / 0.05
|
||||
assert abs(Q - expected) < 1e-6
|
||||
|
||||
def test_foam_zero_dT_gives_zero_Q(self):
|
||||
model = FoamHeatLeak(A_total=3.306, k_eff=0.03, delta=0.05)
|
||||
Q = model.compute(T_inner=300.0, T_env=300.0)
|
||||
assert abs(Q) < 1e-10
|
||||
@@ -0,0 +1,79 @@
|
||||
# tests/cryo_tank/test_integration.py
|
||||
"""Integration tests for the cryogenic tank simulation."""
|
||||
import numpy as np
|
||||
from cryo_tank.tank_model import CryoTank
|
||||
from cryo_tank.heat_leak import MLIHeatLeak
|
||||
from cryo_tank.solver import run
|
||||
from cryo_tank.config import (
|
||||
V_TOTAL, H_TANK, P_WORKING, T_INIT, ULLAGE_FRACTION,
|
||||
MDOT_IN_LN2, T_IN_LN2, MDOT_OUT_LN2, T_IN_HE,
|
||||
H_CONV_SURFACE, T_ENV, A_TOTAL,
|
||||
)
|
||||
|
||||
|
||||
def _make_tank(**overrides):
|
||||
"""Create a tank with default config, allowing overrides."""
|
||||
kw = dict(
|
||||
V_total=V_TOTAL, H_tank=H_TANK,
|
||||
P_work=P_WORKING,
|
||||
T_init=T_INIT, ullage_fraction=ULLAGE_FRACTION,
|
||||
mdot_in_ln2=MDOT_IN_LN2, T_in_ln2=T_IN_LN2,
|
||||
mdot_out_ln2=MDOT_OUT_LN2,
|
||||
T_in_he=T_IN_HE,
|
||||
h_conv=H_CONV_SURFACE, T_env=T_ENV,
|
||||
heat_leak_model=MLIHeatLeak(A_total=A_TOTAL, q_mli=1.0),
|
||||
)
|
||||
kw.update(overrides)
|
||||
return CryoTank(**kw)
|
||||
|
||||
|
||||
class TestMassConservation:
|
||||
|
||||
def test_liquid_mass_change_matches_net_flow(self):
|
||||
"""Over a short run, dm_liq should equal (mdot_in - mdot_out) * dt."""
|
||||
tank = _make_tank()
|
||||
history = run(tank, t_end=10.0, max_step=1.0)
|
||||
|
||||
m_liq_0 = history['m_liq'][0]
|
||||
m_liq_f = history['m_liq'][-1]
|
||||
t_f = history['t'][-1]
|
||||
|
||||
expected_dm = (MDOT_IN_LN2 - MDOT_OUT_LN2) * t_f
|
||||
actual_dm = m_liq_f - m_liq_0
|
||||
|
||||
rel_err = abs(actual_dm - expected_dm) / abs(expected_dm)
|
||||
assert rel_err < 1e-6, f"Mass conservation error: rel_err={rel_err:.2e}"
|
||||
|
||||
|
||||
class TestSteadyState:
|
||||
|
||||
def test_zero_flow_zero_leak_is_static(self):
|
||||
"""With no flow and no heat leak, state should not change."""
|
||||
tank = _make_tank(
|
||||
mdot_in_ln2=0.0,
|
||||
mdot_out_ln2=0.0,
|
||||
h_conv=0.0,
|
||||
heat_leak_model=MLIHeatLeak(A_total=A_TOTAL, q_mli=0.0),
|
||||
)
|
||||
history = run(tank, t_end=100.0, max_step=10.0)
|
||||
|
||||
T_liq = history['T_liq']
|
||||
T_ull = history['T_ull']
|
||||
|
||||
assert abs(T_liq[-1] - T_liq[0]) < 0.01, f"T_liq drifted: {T_liq[0]:.3f} -> {T_liq[-1]:.3f}"
|
||||
assert abs(T_ull[-1] - T_ull[0]) < 0.1, f"T_ull drifted: {T_ull[0]:.3f} -> {T_ull[-1]:.3f}"
|
||||
|
||||
|
||||
class TestPhysicalBehavior:
|
||||
|
||||
def test_liquid_level_decreases(self):
|
||||
"""With net outflow, liquid level should decrease."""
|
||||
tank = _make_tank()
|
||||
history = run(tank, t_end=60.0, max_step=5.0)
|
||||
assert history['fill_fraction'][-1] < history['fill_fraction'][0]
|
||||
|
||||
def test_he_flow_rate_positive(self):
|
||||
"""He should always flow in (pressurization), not out."""
|
||||
tank = _make_tank()
|
||||
history = run(tank, t_end=60.0, max_step=5.0)
|
||||
assert np.all(history['mdot_He'] >= -1e-10) # allow tiny numerical noise
|
||||
@@ -0,0 +1,67 @@
|
||||
# tests/cryo_tank/test_properties.py
|
||||
"""Tests for CoolProp property wrappers."""
|
||||
import pytest
|
||||
import sys
|
||||
sys.path.insert(0, "src")
|
||||
|
||||
from cryo_tank.properties import (
|
||||
ln2_rho, ln2_h, ln2_u, ln2_T_from_u,
|
||||
n2_vapor_u, n2_sat_pressure,
|
||||
he_u, he_h, he_cp, he_cv,
|
||||
)
|
||||
from cryo_tank.config import P_WORKING
|
||||
|
||||
|
||||
class TestLN2Properties:
|
||||
"""Liquid nitrogen properties at P = 0.17 MPa."""
|
||||
|
||||
def test_ln2_density_at_78K(self):
|
||||
rho = ln2_rho(78.0, P_WORKING)
|
||||
assert 800 < rho < 810 # ~803 kg/m3
|
||||
|
||||
def test_ln2_enthalpy_at_77K(self):
|
||||
h = ln2_h(77.0, P_WORKING)
|
||||
assert -130000 < h < -110000 # ~-122695 J/kg
|
||||
|
||||
def test_ln2_internal_energy_at_78K(self):
|
||||
u = ln2_u(78.0, P_WORKING)
|
||||
assert -130000 < u < -110000 # ~-120865 J/kg
|
||||
|
||||
def test_ln2_T_from_u_roundtrip(self):
|
||||
T_orig = 78.0
|
||||
u = ln2_u(T_orig, P_WORKING)
|
||||
T_recovered = ln2_T_from_u(u, P_WORKING)
|
||||
assert abs(T_recovered - T_orig) < 0.01
|
||||
|
||||
|
||||
class TestN2VaporProperties:
|
||||
"""N2 vapor properties."""
|
||||
|
||||
def test_n2_sat_pressure_at_78K(self):
|
||||
P_sat = n2_sat_pressure(78.0)
|
||||
assert 0.10e6 < P_sat < 0.12e6 # ~0.1093 MPa
|
||||
|
||||
def test_n2_vapor_internal_energy_at_78K(self):
|
||||
u = n2_vapor_u(78.0)
|
||||
assert 50000 < u < 60000 # ~55547 J/kg
|
||||
|
||||
|
||||
class TestHeliumProperties:
|
||||
"""Helium (ideal gas) properties."""
|
||||
|
||||
def test_he_cp_near_5196(self):
|
||||
cp = he_cp()
|
||||
assert abs(cp - 5196.2) < 10 # monatomic ideal gas
|
||||
|
||||
def test_he_cv_near_3117(self):
|
||||
cv = he_cv()
|
||||
assert abs(cv - 3117.1) < 10
|
||||
|
||||
def test_he_enthalpy_at_100K(self):
|
||||
h = he_h(100.0)
|
||||
# CoolProp gives ~524762 J/kg at 100K
|
||||
assert 500000 < h < 550000
|
||||
|
||||
def test_he_internal_energy_at_78K(self):
|
||||
u = he_u(78.0)
|
||||
assert 200000 < u < 280000 # ~247932 J/kg
|
||||
@@ -0,0 +1,78 @@
|
||||
# tests/cryo_tank/test_tank_model.py
|
||||
"""Tests for CryoTank model."""
|
||||
import pytest
|
||||
import numpy as np
|
||||
from cryo_tank.tank_model import CryoTank
|
||||
from cryo_tank.heat_leak import MLIHeatLeak
|
||||
from cryo_tank.config import (
|
||||
V_TOTAL, H_TANK, A_CROSS, A_TOTAL, P_WORKING,
|
||||
T_INIT, ULLAGE_FRACTION,
|
||||
MDOT_IN_LN2, T_IN_LN2, MDOT_OUT_LN2, T_IN_HE,
|
||||
H_CONV_SURFACE, T_ENV,
|
||||
)
|
||||
|
||||
|
||||
def _make_tank():
|
||||
"""Create a CryoTank with default config and MLI heat leak."""
|
||||
heat_leak = MLIHeatLeak(A_total=A_TOTAL, q_mli=1.0)
|
||||
return CryoTank(
|
||||
V_total=V_TOTAL, H_tank=H_TANK,
|
||||
P_work=P_WORKING,
|
||||
T_init=T_INIT, ullage_fraction=ULLAGE_FRACTION,
|
||||
mdot_in_ln2=MDOT_IN_LN2, T_in_ln2=T_IN_LN2,
|
||||
mdot_out_ln2=MDOT_OUT_LN2,
|
||||
T_in_he=T_IN_HE,
|
||||
h_conv=H_CONV_SURFACE, T_env=T_ENV,
|
||||
heat_leak_model=heat_leak,
|
||||
)
|
||||
|
||||
|
||||
class TestGeometry:
|
||||
|
||||
def test_cross_section_area(self):
|
||||
tank = _make_tank()
|
||||
assert abs(tank.A_cross - 0.8402) < 0.001
|
||||
|
||||
def test_total_surface_area(self):
|
||||
tank = _make_tank()
|
||||
assert abs(tank.A_total - 3.305) < 0.01
|
||||
|
||||
def test_wetted_area_at_70_percent_fill(self):
|
||||
tank = _make_tank()
|
||||
level = 0.7 * H_TANK # 0.35 m
|
||||
A_wet, A_dry = tank.wetted_areas(level)
|
||||
# A_wet = bottom cap + side * level
|
||||
expected_wet = A_CROSS + np.pi * tank.D * level
|
||||
assert abs(A_wet - expected_wet) < 0.01
|
||||
assert abs(A_wet + A_dry - A_TOTAL) < 0.01
|
||||
|
||||
|
||||
class TestInitialState:
|
||||
|
||||
def test_initial_liquid_mass(self):
|
||||
tank = _make_tank()
|
||||
y0 = tank.initial_state()
|
||||
m_liq = y0[0]
|
||||
# rho_LN2(78K, 0.17MPa) ~ 803.3 kg/m3, V_liq = 0.2941 m3
|
||||
assert 235 < m_liq < 237 # ~236.25 kg
|
||||
|
||||
def test_initial_fill_fraction(self):
|
||||
tank = _make_tank()
|
||||
y0 = tank.initial_state()
|
||||
info = tank.derive(y0)
|
||||
assert abs(info['fill_fraction'] - 0.70) < 0.01
|
||||
|
||||
def test_initial_temperatures(self):
|
||||
tank = _make_tank()
|
||||
y0 = tank.initial_state()
|
||||
info = tank.derive(y0)
|
||||
assert abs(info['T_liq'] - T_INIT) < 0.1
|
||||
assert abs(info['T_ull'] - T_INIT) < 1.0
|
||||
|
||||
def test_initial_pressure_components_sum_to_P_working(self):
|
||||
tank = _make_tank()
|
||||
y0 = tank.initial_state()
|
||||
info = tank.derive(y0)
|
||||
P_N2 = info['P_N2']
|
||||
P_He = info['P_He']
|
||||
assert abs(P_N2 + P_He - P_WORKING) / P_WORKING < 1e-6
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
End-to-end conservation tests: after a short run, total system mass
|
||||
and total system energy must be conserved to machine precision. These
|
||||
are the load-bearing tests for the "flux doubling" coupling mechanism.
|
||||
"""
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tank import Tank
|
||||
from pipe import Pipe
|
||||
from solver import run
|
||||
|
||||
|
||||
GAMMA = 1.4
|
||||
R_GAS = 287.0
|
||||
|
||||
|
||||
def _build_scenario():
|
||||
"""Default blowdown scenario at smaller scale — same physics, short run."""
|
||||
tank1 = Tank(V=5.0, P_init=10e6, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
tank2 = Tank(V=10.0, P_init=101325.0, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
pipe = Pipe(L=1.0, D=5e-3, N=20,
|
||||
P_init=101325.0, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
return tank1, tank2, pipe
|
||||
|
||||
|
||||
def _total_mass(tank1, tank2, pipe):
|
||||
return tank1.mass + tank2.mass + float(np.sum(pipe.W[0, :] * pipe.area * pipe.dx))
|
||||
|
||||
|
||||
def _total_energy(tank1, tank2, pipe):
|
||||
return tank1.U + tank2.U + float(np.sum(pipe.W[2, :] * pipe.area * pipe.dx))
|
||||
|
||||
|
||||
def test_total_mass_conserved_over_short_run():
|
||||
tank1, tank2, pipe = _build_scenario()
|
||||
m_init = _total_mass(tank1, tank2, pipe)
|
||||
run(tank1, tank2, pipe, t_end=1e-3, cfl=0.5)
|
||||
m_final = _total_mass(tank1, tank2, pipe)
|
||||
rel_err = abs(m_final - m_init) / m_init
|
||||
assert rel_err < 1e-10, (
|
||||
f"Total mass not conserved: m_init={m_init:.6e}, "
|
||||
f"m_final={m_final:.6e}, rel_err={rel_err:.2e}"
|
||||
)
|
||||
|
||||
|
||||
def test_total_energy_conserved_over_short_run():
|
||||
tank1, tank2, pipe = _build_scenario()
|
||||
U_init = _total_energy(tank1, tank2, pipe)
|
||||
run(tank1, tank2, pipe, t_end=1e-3, cfl=0.5)
|
||||
U_final = _total_energy(tank1, tank2, pipe)
|
||||
rel_err = abs(U_final - U_init) / U_init
|
||||
assert rel_err < 1e-10, (
|
||||
f"Total energy not conserved: U_init={U_init:.6e}, "
|
||||
f"U_final={U_final:.6e}, rel_err={rel_err:.2e}"
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
# tests/test_pipe.py
|
||||
import numpy as np
|
||||
import pytest
|
||||
from pipe import Pipe
|
||||
|
||||
|
||||
GAMMA = 1.4
|
||||
R_GAS = 287.0
|
||||
|
||||
|
||||
def test_pipe_uniform_initialization_all_cells_identical():
|
||||
"""
|
||||
Initial state: uniform P, T, u=0 across all N cells.
|
||||
All cells should have identical W. primitives() should round-trip
|
||||
back to u=0 and P=P_init.
|
||||
"""
|
||||
pipe = Pipe(L=1.0, D=5e-3, N=20,
|
||||
P_init=1e5, T_init=300.0,
|
||||
gamma=GAMMA, R_gas=R_GAS)
|
||||
|
||||
# All cells identical
|
||||
for i in range(1, pipe.N):
|
||||
assert np.allclose(pipe.W[:, i], pipe.W[:, 0], rtol=1e-14)
|
||||
|
||||
rho, u, P, a = pipe.primitives()
|
||||
rho_expected = 1e5 / (R_GAS * 300.0)
|
||||
assert np.allclose(u, 0.0)
|
||||
assert np.allclose(P, 1e5, rtol=1e-10)
|
||||
assert np.allclose(rho, rho_expected, rtol=1e-10)
|
||||
|
||||
# Sound speed a = sqrt(gamma * P / rho)
|
||||
a_expected = np.sqrt(GAMMA * 1e5 / rho_expected)
|
||||
assert np.allclose(a, a_expected, rtol=1e-10)
|
||||
|
||||
# Geometric sanity
|
||||
assert pipe.dx == 1.0 / 20
|
||||
assert pipe.x_centers.shape == (20,)
|
||||
assert abs(pipe.x_centers[0] - 0.025) < 1e-14
|
||||
assert abs(pipe.x_centers[-1] - 0.975) < 1e-14
|
||||
|
||||
|
||||
def test_pipe_uniform_state_plus_matching_boundary_flux_is_static():
|
||||
"""
|
||||
If all cells have identical W (so all internal HLL fluxes are
|
||||
identical to the physical flux F(W) = [0, P, 0] for u=0 state),
|
||||
and we pass in boundary fluxes equal to [0, P, 0] as well, then
|
||||
every difference (flux[i+1] - flux[i]) is zero, so W must not
|
||||
change after one step. Verify to machine precision.
|
||||
"""
|
||||
pipe = Pipe(L=1.0, D=5e-3, N=20,
|
||||
P_init=1e5, T_init=300.0,
|
||||
gamma=GAMMA, R_gas=R_GAS)
|
||||
|
||||
W_before = pipe.W.copy()
|
||||
# Boundary flux matching the static interior: [rho*u, rho*u^2+P, u*(E+P)]
|
||||
# with u=0 -> [0, P, 0]
|
||||
flux_boundary = np.array([0.0, 1e5, 0.0])
|
||||
pipe.step(flux_boundary, flux_boundary, dt=1e-5)
|
||||
assert np.allclose(pipe.W, W_before, atol=1e-6, rtol=1e-12)
|
||||
@@ -0,0 +1,74 @@
|
||||
# tests/test_riemann.py
|
||||
import numpy as np
|
||||
from riemann import hll_flux
|
||||
|
||||
|
||||
GAMMA = 1.4
|
||||
|
||||
|
||||
def _to_conservative(rho, u, P, gamma):
|
||||
"""(rho, u, P) -> [rho, rho*u, rho*E] where E = e + u^2/2."""
|
||||
return np.array([
|
||||
rho,
|
||||
rho * u,
|
||||
P / (gamma - 1) + 0.5 * rho * u ** 2,
|
||||
])
|
||||
|
||||
|
||||
def _physical_flux(W, gamma):
|
||||
"""F(W) = [rho*u, rho*u^2 + P, u*(rho*E + P)]."""
|
||||
rho = W[0]
|
||||
u = W[1] / rho
|
||||
E = W[2]
|
||||
P = (gamma - 1) * (E - 0.5 * rho * u ** 2)
|
||||
return np.array([rho * u, rho * u ** 2 + P, u * (E + P)])
|
||||
|
||||
|
||||
def test_hll_identical_states_returns_physical_flux():
|
||||
"""
|
||||
When W_L == W_R, HLL must return the exact physical flux F(W)
|
||||
with zero numerical dissipation (the (W_R - W_L) term vanishes).
|
||||
"""
|
||||
W = _to_conservative(rho=1.2, u=50.0, P=2.5e5, gamma=GAMMA)
|
||||
F = hll_flux(W, W, GAMMA)
|
||||
expected = _physical_flux(W, GAMMA)
|
||||
assert np.allclose(F, expected, rtol=1e-12), f"F={F}, expected={expected}"
|
||||
|
||||
|
||||
def test_hll_equal_pressure_equal_energy_gives_exact_pressure_flux():
|
||||
"""
|
||||
Two stationary states (u=0) with same P but different rho:
|
||||
- Both have the same energy density E = P/(gamma-1), so the HLL
|
||||
(W_R - W_L)[2] term vanishes -> exact zero energy flux.
|
||||
- F_L[1] = F_R[1] = P, and W_L[1] = W_R[1] = 0, so the momentum
|
||||
flux is exactly P.
|
||||
- The mass flux is NOT exactly zero for HLL (the density jump
|
||||
triggers the (W_R - W_L)[0] dissipation term) — this is a known
|
||||
HLL limitation for stationary contact discontinuities. We do
|
||||
not assert on F[0] here.
|
||||
"""
|
||||
W_L = _to_conservative(rho=10.0, u=0.0, P=1e5, gamma=GAMMA)
|
||||
W_R = _to_conservative(rho=1.0, u=0.0, P=1e5, gamma=GAMMA)
|
||||
F = hll_flux(W_L, W_R, GAMMA)
|
||||
assert abs(F[1] - 1e5) < 1e-6, f"momentum flux should equal P=1e5, got {F[1]}"
|
||||
assert abs(F[2]) < 1e-8, f"energy flux should be exactly 0, got {F[2]}"
|
||||
|
||||
|
||||
def test_hll_sod_shock_tube_directional_fluxes_all_positive():
|
||||
"""
|
||||
Classical Sod initial values:
|
||||
left: (rho, u, P) = (1.0, 0, 1.0)
|
||||
right: (rho, u, P) = (0.125, 0, 0.1)
|
||||
The pressure gradient drives flow from left to right, so the
|
||||
HLL flux at the interface should have all three components
|
||||
strictly positive:
|
||||
F[0] > 0 : mass flux rightward
|
||||
F[1] > 0 : momentum flux rightward
|
||||
F[2] > 0 : energy flux rightward
|
||||
"""
|
||||
W_L = _to_conservative(rho=1.0, u=0.0, P=1.0, gamma=GAMMA)
|
||||
W_R = _to_conservative(rho=0.125, u=0.0, P=0.1, gamma=GAMMA)
|
||||
F = hll_flux(W_L, W_R, GAMMA)
|
||||
assert F[0] > 0, f"expected positive mass flux, got {F[0]}"
|
||||
assert F[1] > 0, f"expected positive momentum flux, got {F[1]}"
|
||||
assert F[2] > 0, f"expected positive energy flux, got {F[2]}"
|
||||
@@ -0,0 +1,57 @@
|
||||
# tests/test_tank.py
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tank import Tank
|
||||
|
||||
|
||||
GAMMA = 1.4
|
||||
R_GAS = 287.0
|
||||
|
||||
|
||||
def test_tank_initial_state_matches_ideal_gas():
|
||||
"""
|
||||
Given P, T, V construct a Tank; mass and U should match ideal gas:
|
||||
rho = P / (R * T)
|
||||
mass = rho * V
|
||||
U = P * V / (gamma - 1) (since u=0 inside tank)
|
||||
T = (U / mass) * (gamma - 1) / R (round-trip)
|
||||
"""
|
||||
tank = Tank(V=5.0, P_init=10e6, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
rho_expected = 10e6 / (R_GAS * 300.0)
|
||||
assert abs(tank.rho - rho_expected) < 1e-9
|
||||
assert abs(tank.mass - rho_expected * 5.0) < 1e-6
|
||||
assert abs(tank.U - 10e6 * 5.0 / (GAMMA - 1)) < 1e-3
|
||||
assert abs(tank.P - 10e6) < 1e-3
|
||||
assert abs(tank.T - 300.0) < 1e-9
|
||||
|
||||
|
||||
def test_tank_ghost_state_is_stagnation_conservative_vector():
|
||||
"""
|
||||
ghost_state() should return [rho, 0, P/(gamma-1)]
|
||||
(u_ghost = 0 per spec §2.3, so total energy density equals
|
||||
internal energy density = P/(gamma-1)).
|
||||
"""
|
||||
tank = Tank(V=5.0, P_init=10e6, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
g = tank.ghost_state()
|
||||
assert g.shape == (3,)
|
||||
assert abs(g[0] - tank.rho) < 1e-12
|
||||
assert g[1] == 0.0
|
||||
assert abs(g[2] - 10e6 / (GAMMA - 1)) < 1e-3
|
||||
|
||||
|
||||
def test_tank_apply_flux_outflow_reduces_mass_energy_and_pressure():
|
||||
"""
|
||||
apply_flux(mdot, edot, dt, sign=-1) should subtract mdot*dt from mass
|
||||
and edot*dt from U. Derived P should decrease correspondingly.
|
||||
"""
|
||||
tank = Tank(V=5.0, P_init=10e6, T_init=300.0, gamma=GAMMA, R_gas=R_GAS)
|
||||
mass_before = tank.mass
|
||||
U_before = tank.U
|
||||
P_before = tank.P
|
||||
mdot = 1.0 # kg/s
|
||||
edot = 5e5 # J/s (enthalpy rate)
|
||||
dt = 1e-3
|
||||
tank.apply_flux(mdot=mdot, edot=edot, dt=dt, sign=-1)
|
||||
assert abs(tank.mass - (mass_before - mdot * dt)) < 1e-12
|
||||
assert abs(tank.U - (U_before - edot * dt)) < 1e-9
|
||||
assert tank.P < P_before
|
||||
Reference in new issue
Block a user