1"""What a user gets from a NEW design: session start, Doublet, Ethalox (DEF-01..12 defaults).
3The UI builds a new design through the backend's own sequence: a fresh session loads
4configs/default.yaml, each dropdown is POST /api/config/switch (config_to_dict -> switch_config ->
5PintleEngineConfig), and the Layer 1 "Run" on a pintle design switches to impinging the same way.
6These tests build the design exactly so, and check it against numbers that do not come from this
7codebase: CEA (rocketcea) and CoolProp, quoted with the conditions they were read at.
9from __future__
import annotations
12from pathlib
import Path
20 infer_preset_from_fluids,
21 load_canonical_config,
26REPO = Path(__file__).resolve().parents[1]
29CEA_ETHALOX_OF15_430_TC = 3225.45
30CEA_ETHALOX_OF15_430_R = 373.71
33COOLPROP_LOX_K = (0.938e9, 0.983e9)
34COOLPROP_LOX_CP = 1698.6
35COOLPROP_LOX_MU = (1.957e-4, 2.034e-4)
39 return load_config(str(path)).model_dump(mode=
"json")
43 """POST /api/config/switch as the router runs it."""
48 d =
_dump(REPO /
"configs" /
"default.yaml")
50 return _ui_switch(d, propellant_preset=
"ethalox")
57 assert d[
"propellant_preset"] ==
"ethalox"
58 assert d[
"fluids"][
"fuel"][
"name"] ==
"Ethanol"
59 assert d[
"combustion"][
"cea"][
"fuel_name"] ==
"Ethanol"
60 assert d[
"combustion"][
"cea"][
"MR_range"] == [1.0, 2.5]
62 assert d[
"design_requirements"][
"optimal_of_ratio"] == pytest.approx(1.5)
63 assert d[
"chamber_geometry"][
"design_MR"] == pytest.approx(1.5)
68 smd = d[
"spray"][
"smd"]
69 assert smd[
"chamber_gas_T"] == pytest.approx(CEA_ETHALOX_OF15_430_TC, rel=0.01)
70 assert smd[
"chamber_gas_R"] == pytest.approx(CEA_ETHALOX_OF15_430_R, rel=0.01)
73@pytest.mark.parametrize("preset", ["ethalox", "methalox", "kerolox"])
75 p = yaml.safe_load((REPO /
"configs" /
"propellants" / f
"{preset}.yaml").read_text())
76 lox = p[
"fluids"][
"oxidizer"]
77 lo, hi = COOLPROP_LOX_K
78 assert 0.95 * lo <= lox[
"bulk_modulus_pa"] <= 1.05 * hi
79 assert lox[
"specific_heat"] == pytest.approx(COOLPROP_LOX_CP, rel=0.01)
80 assert COOLPROP_LOX_MU[0] * 0.98 <= lox[
"viscosity"] <= COOLPROP_LOX_MU[1] * 1.02
85 assert lox[
"bulk_modulus_pa"] == pytest.approx(0.94e9)
86 assert lox[
"specific_heat"] == pytest.approx(1700.0)
87 assert lox[
"viscosity"] == pytest.approx(2.0e-4)
91 d =
_dump(REPO /
"configs" /
"default.yaml")
92 assert d[
"propellant_preset"] ==
"methalox"
93 s = switch_config(d, propellant_preset=
"ethalox")
94 assert "methalox -> ethalox" in (design_staleness(s)
or "")
98 d =
_dump(REPO /
"configs" /
"default.yaml")
99 d[
"propellant_preset"] =
None
100 assert infer_preset_from_fluids(d) ==
"methalox"
101 s = switch_config(d, propellant_preset=
"ethalox")
102 assert design_staleness(s)
is not None
103 assert s[
"design_requirements"][
"optimal_of_ratio"] == pytest.approx(1.5)
108 d[
"design_requirements"][
"optimal_of_ratio"] = 1.35
109 s = switch_config(d, propellant_preset=
"ethalox")
110 assert s[
"design_requirements"][
"optimal_of_ratio"] == pytest.approx(1.35)
116 start = load_canonical_config(
"pintle")
117 out =
_ui_switch(start, injector_type=
"impinging")
118 assert out[
"injector"][
"type"] ==
"impinging"
119 assert out[
"propellant_preset"] ==
"ethalox"
120 assert out[
"fluids"][
"fuel"][
"name"] ==
"Ethanol"
121 assert out[
"combustion"][
"cea"][
"fuel_name"] ==
"Ethanol"
122 req = out[
"design_requirements"]
123 assert req[
"optimal_of_ratio"] == pytest.approx(start[
"design_requirements"][
"optimal_of_ratio"])
124 assert req[
"target_thrust"] == pytest.approx(start[
"design_requirements"][
"target_thrust"])
125 assert req[
"target_burn_time"] == pytest.approx(start[
"design_requirements"][
"target_burn_time"])
127 assert "methalox -> ethalox" in (design_staleness(out)
or "")
129 assert out[
"discharge"][
"oxidizer"][
"use_geometry_cd"]
is True
130 assert out[
"spray"][
"smd"][
"model"] ==
"ingebo"
134 d =
_dump(REPO /
"configs" /
"ethalox_6500N.yaml")
137 assert d[
"propellant_preset"] ==
"ethalox"
138 assert d[
"fluids"][
"fuel"][
"name"] ==
"Ethanol"
139 assert d[
"design_requirements"][
"target_thrust"] == pytest.approx(6500.0)
143 d =
_dump(REPO /
"configs" /
"ethalox_6500N.yaml")
144 d[
"propellant_preset"] =
"custom"
145 d[
"fluids"][
"fuel"][
"density"] = 801.0
147 assert out[
"propellant_preset"] ==
"custom"
148 assert out[
"fluids"][
"fuel"][
"density"] == pytest.approx(801.0)
154 d =
_dump(REPO /
"configs" /
"ethalox_6500N.yaml")
155 for side, cd
in ((
"oxidizer", 0.72), (
"fuel", 0.68)):
156 d[
"discharge"][side].update(Cd_inf=cd, a_Re=0.05, Cd_min=0.5, use_geometry_cd=
False,
158 d =
_ui_switch(d, propellant_preset=
"kerolox")
159 d =
_ui_switch(d, propellant_preset=
"ethalox")
160 for side, cd
in ((
"oxidizer", 0.72), (
"fuel", 0.68)):
161 assert d[
"discharge"][side][
"Cd_inf"] == pytest.approx(cd)
162 assert d[
"discharge"][side][
"a_Re"] == pytest.approx(0.05)
163 assert d[
"discharge"][side][
"Cd_min"] == pytest.approx(0.5)
169 "layer1_injector_spray_radius_frac",
"layer1_injector_spray_radius_tol",
170 "layer1_injector_plate_thickness_m",
"layer1_injector_min_web_m",
171 "layer1_injector_wall_clearance_m",
"layer1_injector_min_face_incidence_deg",
172 "layer1_injector_counterbore_dia_m",
"layer1_impinging_jet_angle_min_deg",
173 "layer1_max_element_pitch_m",
178 return _ui_switch(load_canonical_config(
"impinging"), propellant_preset=
"ethalox")
181@pytest.mark.parametrize("build", [_new_ethalox_doublet, _blank_via_canonical])
185 req = d[
"design_requirements"]
187 assert req[k]
is not None and req[k] > 0, k
188 assert req[
"layer1_resultant_tilt_from_reach"]
is True
190 assert req[
"layer1_injector_plate_thickness_m"] == pytest.approx(PLATE_THICKNESS_DEFAULT)
191 assert d[
"injector"][
"plate"][
"back"] ==
"channels"
192 assert d[
"injector"][
"igniter"][
"thread"] ==
"1/2 NPT"
194 assert req[
"layer1_injector_spray_radius_frac"] == pytest.approx(2 ** -0.5, abs=1e-4)
196 assert req[
"layer1_impinging_n_doublets_max"] >= 24
200 for f
in (
"configs/default.yaml",
"configs/canonical/impinging.yaml"):
201 req = yaml.safe_load((REPO / f).read_text())[
"design_requirements"]
202 assert req[
"W_MOM"] == pytest.approx(75.0), f
206 a = yaml.safe_load((REPO /
"configs/default.yaml").read_text())[
"design_requirements"]
207 b = yaml.safe_load((REPO /
"configs/canonical/impinging.yaml").read_text())[
"design_requirements"]
208 for k
in (
"W_MOM",
"W_SMD",
"layer1_W_OF",
"layer1_W_THRUST",
"impinging_momentum_R_min",
209 "impinging_momentum_R_max",
"layer1_stagnation_pressure_frac_min",
210 "layer1_stagnation_pressure_frac_max",
"layer1_impinging_angle_deg_max",
211 "layer1_impinging_n_doublets_max",
"injector_dp_ratio_O_min",
"injector_dp_ratio_O_max",
212 "injector_dp_ratio_F_min",
"injector_dp_ratio_F_max") + GUARDS:
213 assert a[k] == b[k], k
218 for f
in (
"configs/default.yaml",
"configs/canonical/impinging.yaml"):
219 req = yaml.safe_load((REPO / f).read_text())[
"design_requirements"]
220 assert req[
"layer1_impinging_angle_deg_max"] <= FACE_HEATING_INCLUDED_DEG, f
224 """DEF-10: the loader no longer has to rewrite default.yaml's band to make it the one used."""
225 raw = yaml.safe_load((REPO /
"configs/default.yaml").read_text())[
"design_requirements"]
226 cfg = load_config(str(REPO /
"configs/default.yaml")).design_requirements
227 for k
in (
"injector_dp_ratio_O_min",
"injector_dp_ratio_O_max",
228 "injector_dp_ratio_F_min",
"injector_dp_ratio_F_max"):
229 assert getattr(cfg, k) == pytest.approx(raw[k]), k
234@pytest.mark.parametrize(
"field, code_default", [
235 (
"layer1_stagnation_pressure_frac_min",
"0.35"),
236 (
"layer1_stagnation_pressure_frac_max",
"1.0"),
237 (
"layer1_momentum_log_deadband_rel",
"0.0"),
241 desc = DesignRequirementsConfig.model_fields[field].description
242 assert f
"unset: {code_default}" in desc
or f
"is {code_default} when unset" in desc, desc
246 """Fields added tonight by the flight and Layer 1 owners must say what they do."""
247 for k
in (
"layer1_W_ISP",
"layer1_W_MASS",
"max_apogee_m",
"max_apogee_datum",
248 "min_rail_exit_velocity_m_s",
"min_static_margin_cal",
"max_static_margin_cal",
249 "regulator_supply_pressure_effect",
"regulator_supply_pressure_effect_source",
250 "regulator_min_differential_psi",
"layer1_injector_min_back_web_m"):
251 desc = DesignRequirementsConfig.model_fields[k].description
or ""
252 assert len(desc) > 40, k
256 """DEF-10 (partial): until the schema and optimizer rewrites are removed together, the rewrite
257 of a typed 0.15-0.35 band must at least be reported."""
259 with caplog.at_level(logging.WARNING, logger=
"engine.pipeline.config_schemas"):
261 assert any(
"0.15-0.35" in r.getMessage()
for r
in caplog.records)
265 """CEACache deletes and rebuilds a table whose grid differs from the request; a dropdown
266 toggle must not trigger that (minutes of CEA, and the shipped table gone)."""
272 def __init__(self, *a, **k):
274 raise RuntimeError(
"would have opened the table")
277 monkeypatch.setattr(cc,
"CEACache", _Spy)
278 d[
"combustion"][
"cea"][
"n_points"] = 20
279 before = dict(d[
"spray"][
"smd"])
280 out = derive_chamber_gas(d)
282 assert out[
"spray"][
"smd"][
"chamber_gas_T"] == before[
"chamber_gas_T"]
283 d[
"combustion"][
"cea"][
"n_points"] = 34
284 derive_chamber_gas(d)
285 assert len(opened) == 1
test_preset_lox_properties_are_coolprop_oxygen(preset)
test_new_ethalox_doublet_gets_the_ethalox_design_point()
test_new_design_lox_is_the_presets()
test_session_start_and_canonical_doublet_agree_on_the_objective()
dict _ui_switch(dict cfg, **body)
test_ethalox_round_trip_through_pintle_stays_ethalox()
test_momentum_weight_is_the_code_default_not_a_ban()
test_legacy_dp_band_rewrite_is_not_silent(caplog)
test_every_new_requirement_field_is_documented()
test_chamber_gas_derivation_never_opens_a_table_it_would_rebuild(monkeypatch)
dict _new_ethalox_doublet()
test_shipped_dp_band_is_written_as_used()
test_session_start_config_names_its_propellant_and_the_switch_flags_the_seed()
test_schema_description_states_the_code_fallback(field, code_default)
test_chamber_gas_follows_the_o_f_target_from_cea()
test_included_angle_band_stays_inside_the_face_heating_limit()
test_a_measured_cd_survives_propellant_switches()
test_custom_propellant_survives_an_injector_swap()
test_a_preset_less_config_is_recognised_from_its_fluids()
test_new_doublet_has_the_injector_face_guards(build)
test_doublet_keeps_the_chosen_propellant_and_the_users_requirements()
test_reselecting_the_live_propellant_keeps_the_users_o_f()
dict _blank_via_canonical()