STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_time_varying_thermal.py
Go to the documentation of this file.
1"""The time-varying solve's wall, geometry and bookkeeping (TH-6, TH-8, TH-9, CN-7, TH-12, TH-2).
2
3What each test guards, and what the code did before:
4 * the nozzle exit stayed put only on paper: it grew at the chamber liner's rate even with
5 ``nozzle_ablative: false`` (eps rose 10 % over the burn);
6 * t = 0 rebuilt the chamber as a full-bore cylinder of cg.length (L* 1.08 for a 1.00 design);
7 * thrust used the post-step throat while Pc was solved on the pre-step one;
8 * the graphite surface was the caller's 2000 K guess and the insert conducted steadily into a
9 300 K sink, so a 3 mm and a 12 mm insert burned identically;
10 * the state carried a 2 GW/m^2 "nozzle heat flux", an 86,000 K nozzle wall and 3e7 K case.
11"""
12import copy
13import math
14import os
15import sys
16
17import numpy as np
18import pytest
19
20sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
22from engine.core.runner import PintleEngineRunner # noqa: E402
23from engine.pipeline.io import load_config # noqa: E402
24
25ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26CFG = os.path.join(ROOT, "configs", "ethalox_6500N.yaml")
27PSI = 6894.757293168361
28P_TANK = 584.27 * PSI
29
30
31@pytest.fixture(autouse=True)
32def _python_physics(monkeypatch):
33 monkeypatch.setenv("ED_ACCEL", "off")
34
35
36def _series(times, mutate=None):
37 cfg = load_config(CFG)
38 if mutate is not None:
39 mutate(cfg)
40 t = np.asarray(times, dtype=float)
41 P = np.full_like(t, P_TANK)
42 return cfg, PintleEngineRunner(copy.deepcopy(cfg)).evaluate_arrays_with_time(t, P, P, use_coupled_solver=True)
43
44
45@pytest.fixture(scope="module")
46def burn():
47 os.environ["ED_ACCEL"] = "off"
48 return _series(np.linspace(0.0, 2.0, 9))
49
50
52 cfg, tv = burn
53 assert cfg.ablative_cooling.nozzle_ablative is False
54 A_exit = np.asarray(tv["A_exit"])
55 assert np.all(A_exit == A_exit[0])
56 assert np.all(np.asarray(tv["recession_exit"]) == 0.0)
57 # only the graphite throat opens, so the expansion ratio falls
58 assert tv["eps"][-1] < tv["eps"][0]
59
60
62 def m(cfg):
63 cfg.ablative_cooling.nozzle_ablative = True
64 _, tv = _series([0.0, 0.5, 1.0], m)
65 assert tv["A_exit"][-1] > tv["A_exit"][0]
66
67
69 cfg, tv = burn
70 cg = cfg.chamber_geometry
71 assert tv["V_chamber"][0] == pytest.approx(cg.volume, rel=1e-12)
72 assert tv["Lstar"][0] == pytest.approx(cg.volume / cg.A_throat, rel=1e-12)
73
74
76 cfg, tv = burn
77 i = len(tv["F"]) - 1
78 c2 = copy.deepcopy(cfg)
79 cg = c2.chamber_geometry
80 cg.A_throat = float(tv["A_throat"][i])
81 cg.A_exit = float(tv["A_exit"][i])
82 cg.expansion_ratio = cg.A_exit / cg.A_throat
83 cg.volume = float(tv["V_chamber"][i])
84 cg.Lstar = cg.volume / cg.A_throat
85 cg.chamber_diameter = float(tv["D_chamber"][i])
86 steady = PintleEngineRunner(c2).evaluate(P_TANK, P_TANK, silent=True)
87 assert float(tv["Pc"][i]) == pytest.approx(float(steady["Pc"]), rel=1e-6)
88 assert float(tv["F"][i]) == pytest.approx(float(steady["F"]), rel=1e-6)
89
90
92 from engine.core.runner import PintleEngineRunner as R
93 from engine.pipeline.time_varying_solver import TimeVaryingCoupledSolver
94 cfg = load_config(CFG)
95 r = R(copy.deepcopy(cfg))
96 s = TimeVaryingCoupledSolver(r.config, r.cea_cache)
97 t = np.array([0.0, 0.25])
98 s.solve_time_series(t, np.full(2, P_TANK), np.full(2, P_TANK))
99 out = s.get_results_dict()
100 for i, d in enumerate(out["diagnostics"]):
101 assert out["eta_cstar"][i] == pytest.approx(d["eta_cstar"], rel=1e-12)
102 assert out["cstar_ideal"][i] == pytest.approx(d["cstar_ideal"], rel=1e-12)
103 assert 0.85 < out["eta_cstar"][0] < 1.0 and out["eta_cstar"][0] != 0.85
104 # c*_ideal is CEA's: 1700-1760 m/s for LOX/ethanol at O/F 1.4-1.6, 400-450 psia
105 assert 1690.0 < out["cstar_ideal"][0] < 1760.0
106
107
109 def thick(t):
110 def m(cfg):
111 cfg.graphite_insert.initial_thickness = t
112 return m
113 _, thin = _series([0.0, 1.0, 2.0], thick(0.003))
114 _, fat = _series([0.0, 1.0, 2.0], thick(0.012))
115 assert thin["T_graphite_surface"][-1] > fat["T_graphite_surface"][-1] + 50.0
116 assert thin["T_graphite_back"][-1] > fat["T_graphite_back"][-1] + 300.0
117 assert thin["recession_graphite"][-1] > fat["recession_graphite"][-1]
118
119
121 """The cold liner first has to heat to its ablation temperature -- semi-infinite onset
122 t = (pi/4) k rho c (T_abl - T0)^2 / q^2, a few tenths of a second here -- so the first
123 0.25 s recedes far less than steady ablation would; by 2 s it ablates at the Landau rate."""
124 cfg, tv = burn
125 abl = cfg.ablative_cooling
126 q = float(tv["q_conv_chamber"][-1] + tv["q_rad_chamber"][-1])
127 t_on = math.pi / 4 * abl.thermal_conductivity * abl.material_density * abl.specific_heat * (
128 abl.ablation_surface_temperature - abl.ambient_temperature) ** 2 / q ** 2
129 assert 0.05 < t_on < 1.0
130 assert tv["recession_chamber"][1] < 0.3 * tv["ablative_recession_rate"][-1] * 0.25
131 assert tv["T_liner_surface"][-1] == pytest.approx(abl.ablation_surface_temperature, abs=1e-6)
132 # late in the burn the transient rate is the quasi-steady one of the same step
133 qs = tv["diagnostics"][-1]["cooling"]["ablative"]["recession_rate_quasi_steady"]
134 assert tv["ablative_recession_rate"][-1] == pytest.approx(qs, rel=0.1)
135 assert 0 < tv["char_depth_chamber"][-1] < abl.initial_thickness
136
137
139 _, tv = burn
140 for k, v in tv.items():
141 if k.startswith("T_") and k not in ("T_exit",):
142 a = np.asarray(v, dtype=float)
143 a = a[np.isfinite(a)]
144 assert np.all((a >= 200.0) & (a <= 4000.0)), k
145 assert not any(k.startswith("nozzle_") for k in tv)
146 assert not any("stainless" in k for k in tv)
147 assert np.all(np.asarray(tv["heat_flux_throat"]) < 1e8)
148
149
151 from engine.core.runner import PintleEngineRunner as R
152 from engine.pipeline.time_varying_solver import TimeVaryingCoupledSolver
153 cfg = load_config(CFG)
154 r = R(copy.deepcopy(cfg))
155 s = TimeVaryingCoupledSolver(r.config, r.cea_cache)
156 t = np.array([0.0, 1.0])
157 s.solve_time_series(t, np.full(2, P_TANK), np.full(2, P_TANK))
158 back_at_burnout = s.get_results_dict()["T_graphite_back"][-1]
159 soak = s.soak_back(30.0)
160 assert soak["throat"]["T_back_peak"] >= back_at_burnout
161
162
164 """The non-coupled path's helper: a micron of recession must not jump the volume to a
165 full-bore cylinder of cg.length (+8 % on this chamber)."""
166 from engine.pipeline.thermal.ablative_geometry import update_chamber_geometry_from_ablation
167 cg = load_config(CFG).chamber_geometry
168 Dt = math.sqrt(4 * cg.A_throat / math.pi)
169 V, At, Dc, Dt_new, _ = update_chamber_geometry_from_ablation(
170 cg.volume, cg.A_throat, cg.chamber_diameter, Dt, cg.length, 1e-6, recession_thickness_throat=0.0)
171 shell = math.pi * ((cg.chamber_diameter / 2 + 1e-6) ** 2 - (cg.chamber_diameter / 2) ** 2) * cg.length
172 assert V == pytest.approx(cg.volume + shell, rel=1e-12)