STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_flight_router.py
Go to the documentation of this file.
1"""The Flight tab's router: the apogee ceiling (FLT-5), the masses it forwards (FLT-6), and the
2thrust curve's reference pressure (FLT-10).
3
4FLT-5: the only altitude requirement was a lower target; a 15,000 ft MSL waiver appeared only in
5a config comment, with no datum. FLT-6: engine and tank-structure masses edited on the Flight
6tab were written only into propulsion_dry_mass, which the flight never reads once engine_mass is
7set, so every such edit flew the config's masses. FLT-10: the time series runs at the SESSION
8config's pad, so the thrust curve's reference is that elevation, not one the tab overrides.
9"""
10
11from __future__ import annotations
12
13import asyncio
14import contextlib
15import io
16import math
17import types
18
19import pytest
20
21pytest.importorskip("rocketpy")
22pytest.importorskip("stardesign", reason="backend.session needs lib/stardesign on PYTHONPATH")
23
24from flight_cases import header_curve, shipped # noqa: E402
25
26
27def _request(cfg, **over):
28 from backend.routers.flight import FlightSimRequest
29
30 t, F, mO, mF = header_curve()
31 body = dict(time_array=t.tolist(), thrust_array=F.tolist(), mdot_O_array=mO.tolist(), mdot_F_array=mF.tolist(),
32 lox_mass_kg=cfg.lox_tank.mass, fuel_mass_kg=cfg.fuel_tank.mass)
33 body.update(over)
34 return FlightSimRequest(**body)
35
36
37def _simulate(cfg, request):
38 from backend.routers.flight import _execute_flight_simulation
39
40 with contextlib.redirect_stdout(io.StringIO()):
41 return _execute_flight_simulation(cfg, request)
42
43
44# ---- FLT-5
45
46
48 from engine.pipeline.config_schemas import DesignRequirementsConfig
49
50 base = shipped().design_requirements.model_dump()
51 with pytest.raises(ValueError, match="max_apogee_datum"):
52 DesignRequirementsConfig(**{**base, "max_apogee_m": 4572.0})
53 assert DesignRequirementsConfig(**base).max_apogee_m is None # unset by default: nothing is invented
54
55
57 from backend.routers.flight import ceiling_agl_m
58
59 assert ceiling_agl_m({"max_apogee_m": 15000 * 0.3048, "max_apogee_datum": "MSL"}, 626.67) == pytest.approx(3945.33, abs=0.01)
60 assert ceiling_agl_m({"max_apogee_m": 3000.0, "max_apogee_datum": "AGL"}, 626.67) == pytest.approx(3000.0)
61 assert ceiling_agl_m({"max_apogee_m": None}, 626.67) is None
62
63
65 cfg = shipped()
66 r = _simulate(cfg, _request(cfg))
67 assert r.status == "success" and r.ceiling is None
68
69
71 cfg = shipped()
72 cfg.design_requirements.max_apogee_m = 15000 * 0.3048
73 cfg.design_requirements.max_apogee_datum = "MSL"
74 r = _simulate(cfg, _request(cfg))
75 c = r.ceiling
76 assert r.status == "success"
77 assert c["ceiling_agl_m"] == pytest.approx(3945.33, abs=0.01)
78 assert c["nominal_apogee_agl_m"] == pytest.approx(r.apogee_m)
79 assert "surface_roughness_m 0" in c["corner"]
80 assert c["corner_apogee_agl_m"] > c["nominal_apogee_agl_m"] + 100.0, "a smooth skin flies higher"
81 assert c["corner_margin_m"] == pytest.approx(c["ceiling_agl_m"] - c["corner_apogee_agl_m"])
82 assert c["violated"] is False
83 assert r.apogee_msl_m == pytest.approx(r.apogee_m + 626.67)
84
85
87 cfg = shipped()
88 cfg.design_requirements.max_apogee_m = 3000.0
89 cfg.design_requirements.max_apogee_datum = "AGL"
90 r = _simulate(cfg, _request(cfg))
91 assert r.status == "success" and r.ceiling["violated"] is True
92 assert any("ceiling exceeded" in w for w in r.propellant.warnings)
93
94
96 from backend.routers.flight import FlightOptimizeRequest, optimize_flight_altitude
97
98 cfg = shipped()
99 cfg.design_requirements.max_apogee_m = 3000.0
100 cfg.design_requirements.max_apogee_datum = "AGL"
101 t, F, mO, mF = header_curve()
102 req = FlightOptimizeRequest(time_array=t.tolist(), thrust_array=F.tolist(), mdot_O_array=mO.tolist(),
103 mdot_F_array=mF.tolist(), target_apogee_m=3500.0, apogee_tolerance_m=15.0)
104 session = types.SimpleNamespace(app_state=types.SimpleNamespace(has_config=lambda: True, config=cfg,
105 snapshot=lambda: (cfg, None)))
106 out = asyncio.run(optimize_flight_altitude(req, session))
107 assert out.success is False and "ceiling" in out.infeasible_reason and out.simulations_run == 0
108
109
110# ---- FLT-6
111
112
114 from backend.routers.flight import RocketConfig, build_flight_config
115 from engine.pipeline.config_schemas import PintleEngineConfig
116
117 cfg = shipped()
118 rk = RocketConfig(airframe_mass=43.43, engine_mass=24.54, lox_tank_structure_mass=4.1, fuel_tank_structure_mass=5.0,
119 copv_dry_mass=3.2, radius=0.078359, inertia=[8.0, 8.0, 0.5])
120 d = build_flight_config(cfg, _request(cfg, rocket=rk))
121 PintleEngineConfig(**d)
122 r = d["rocket"]
123 assert (r["engine_mass"], r["lox_tank_structure_mass"], r["fuel_tank_structure_mass"], r["copv_dry_mass"]) == (24.54, 4.1, 5.0, 3.2)
124 assert r["propulsion_dry_mass"] == pytest.approx(24.54 + 4.1 + 5.0 + 3.2)
125 assert r["nose_kind"] == cfg.rocket.nose_kind, "a field the request omits keeps the config's value"
126
127
129 """+10 kg of engine: the 1-DOF (engine/pipeline/flight_1dof, checked against Sutton) predicts
130 the loss with the same drag; the flight must move by about as much, not by zero."""
131 from backend.routers.flight import RocketConfig
132 from engine.pipeline.flight_1dof import vertical_apogee_agl
133 from engine.pipeline.vehicle_drag import DragCurves
134
135 cfg = shipped()
136 rk = dict(airframe_mass=cfg.rocket.airframe_mass, lox_tank_structure_mass=cfg.rocket.lox_tank_structure_mass,
137 fuel_tank_structure_mass=cfg.rocket.fuel_tank_structure_mass, copv_dry_mass=cfg.rocket.copv_dry_mass,
138 radius=cfg.rocket.radius, inertia=list(cfg.rocket.inertia), fins=cfg.rocket.fins.model_dump())
139 light = _simulate(cfg, _request(cfg, rocket=RocketConfig(engine_mass=cfg.rocket.engine_mass, **rk)))
140 heavy = _simulate(cfg, _request(cfg, rocket=RocketConfig(engine_mass=cfg.rocket.engine_mass + 10.0, **rk)))
141 d = light.report["drag"]
142 drag = DragCurves(tuple(d["mach"]), tuple(d["cd_power_off"]), tuple(d["cd_power_on"]), d["model"], d["source"])
143 t, F, mO, mF = header_curve()
144 m0 = light.report["wet_mass_kg"]
145 kw = dict(area_m2=math.pi * cfg.rocket.radius**2, drag=drag, elevation_m=626.67)
146 predicted = vertical_apogee_agl(t, F, mO + mF, m0, **kw) - vertical_apogee_agl(t, F, mO + mF, m0 + 10.0, **kw)
147 moved = light.apogee_m - heavy.apogee_m
148 assert heavy.report["wet_mass_kg"] == pytest.approx(m0 + 10.0, abs=1e-6)
149 assert moved == pytest.approx(predicted, rel=0.1), (moved, predicted)
150
151
152# ---- FLT-10
153
154
156 from backend.routers.flight import EnvironmentConfig
157
158 cfg = shipped()
159 env = EnvironmentConfig(latitude=35.35, longitude=-117.81, elevation=1500.0, date=[2026, 1, 30, 18])
160 r = _simulate(cfg, _request(cfg, environment=env))
161 from engine.core.runner import compute_ambient_pressure_from_elevation
162
163 # the engine solver's ambient at the SESSION pad (ISA 1976, checked by hand in test_ambient_isa.py)
164 p_pad = compute_ambient_pressure_from_elevation(cfg.environment.elevation)
165 assert r.report["reference_pressure_pa"] == pytest.approx(p_pad, rel=1e-9)
166 stated = _simulate(cfg, _request(cfg, reference_pressure_pa=90000.0))
167 assert stated.report["reference_pressure_pa"] == 90000.0
test_ceiling_is_checked_at_the_high_apogee_corner()
test_reference_pressure_is_the_session_pad_not_the_tabs()
test_optimize_refuses_a_target_above_the_ceiling()