STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_flight_launch_and_report.py
Go to the documentation of this file.
1"""Launch rail, angle and heading from the config; rail exit and static margin reported (FLT-8);
2pressure thrust with altitude (FLT-10).
3
4The flight flew Flight(rail_length=3.35, inclination=90, heading=0) from literals and reported
5neither the rail-exit velocity nor the static margin, though RocketPy computes both. The thrust
6curve reached RocketPy without a reference pressure, so (p_ref - p(z)) A_exit was zero all the
7way up.
8
9Independent checks: a hand integration of the rail run, a hand Barrowman CP and the audit's hand
10static margin (CG 2.693 m, CP 1.541 m, 7.35 cal), a rail-then-gravity-turn point mass, and the
11isothermal barometric formula.
12"""
13
14from __future__ import annotations
15
16import math
17
18import numpy as np
19import pytest
20
21rocketpy = pytest.importorskip("rocketpy")
22
23from flight_cases import fly, header_curve, shipped # noqa: E402
24
25G0 = 9.80665
26
27
28@pytest.fixture(scope="module")
30 return fly(shipped())
31
32
34 import ui.flight_sim as fs
35
36 seen = {}
37 real = fs.Flight
38
39 def spy(*a, **k):
40 seen.update(k)
41 return real(*a, **k)
42
43 monkeypatch.setattr(fs, "Flight", spy)
44 cfg = shipped()
45 cfg.environment.rail_length_m = 6.1
46 cfg.environment.launch_inclination_deg = 84.0
47 cfg.environment.launch_heading_deg = 45.0
48 fly(cfg)
49 assert (seen["rail_length"], seen["inclination"], seen["heading"]) == (6.1, 84.0, 45.0)
50
51
53 """Along a vertical rail: dv/dt = F/m - g - D/m, dm/dt = -mdot, to s = rail length."""
54 cfg = shipped()
55 t_c, F, mO, mF = header_curve()
56 m = shipped_flight["flight_report"]["wet_mass_kg"]
57 A, rho = math.pi * cfg.rocket.radius**2, 1.1175 # ISA at 626.67 m, kg/m3
58 s = v = t = 0.0
59 dt = 1e-5
60 while s < cfg.environment.rail_length_m:
61 Fk = np.interp(t, t_c, F)
62 a = Fk / m - G0 - 0.5 * rho * v * v * 0.85 * A / m
63 v += a * dt
64 s += v * dt
65 m -= np.interp(t, t_c, mO + mF) * dt
66 t += dt
67 rep = shipped_flight["flight_report"]["launch"]
68 assert rep["rail_exit_velocity_m_s"] == pytest.approx(v, rel=0.01)
69 assert rep["rail_exit_velocity_m_s"] == pytest.approx(21.7, abs=0.2) # the audit's number
70 assert rep["rail_buttons_declared"] is False and rep["effective_rail_length_m"] == 3.35
71
72
73def _barrowman_cp(cfg, nose_tip, nose_len):
74 """Barrowman (TIR-33): nose CN_a = 2 at L - V/A_base from the tip (L/2 for von Karman);
75 trapezoidal fins with body interference 1 + R/(s + R)."""
76 R, fins = cfg.rocket.radius, cfg.rocket.fins
77 d = 2 * R
78 cr, ct, s, n = fins.root_chord, fins.tip_chord, fins.fin_span, fins.no_fins
79 sweep = cr - ct # leading edge swept, trailing edge square to the body
80 l_f = math.hypot(s, sweep + ct / 2 - cr / 2) # mid-chord line
81 cn_f = (1 + R / (s + R)) * 4 * n * (s / d) ** 2 / (1 + math.sqrt(1 + (2 * l_f / (cr + ct)) ** 2))
82 x_f_from_le = sweep * (cr + 2 * ct) / (3 * (cr + ct)) + (cr + ct - cr * ct / (cr + ct)) / 6
83 x_fins = fins.fin_position - x_f_from_le # tail_to_nose coordinates
84 x_nose = nose_tip - nose_len / 2
85 return (2.0 * x_nose + cn_f * x_fins) / (2.0 + cn_f)
86
87
89 cfg = shipped()
90 rocket = shipped_flight["flight"].rocket
91 rep = shipped_flight["flight_report"]
92 nose_len = cfg.rocket.nose_fineness_ratio * 2 * cfg.rocket.radius
93 cp = _barrowman_cp(cfg, rep["stack_length_m"], nose_len)
94 cg = float(rocket.center_of_mass(0.0))
95 assert rep["stability"]["static_margin_liftoff_cal"] == pytest.approx((cg - cp) / (2 * cfg.rocket.radius), abs=0.15)
96 assert rep["stability"]["static_margin_liftoff_cal"] == pytest.approx(7.35, abs=0.3) # the audit's hand number
97 assert rep["stability"]["static_margin_burnout_cal"] > rep["stability"]["static_margin_rail_exit_cal"]
98
99
101 assert shipped_flight["flight_report"]["checks"] == []
102 cfg = shipped()
103 cfg.design_requirements.min_rail_exit_velocity_m_s = 30.48 # 100 ft/s
104 cfg.design_requirements.min_static_margin_cal = 1.5
105 rep = fly(cfg)["flight_report"]
106 by = {c["name"]: c for c in rep["checks"]}
107 assert by["rail_exit_velocity"]["passed"] is False
108 assert by["static_margin_rail_exit"]["passed"] is True
109 assert any("rail_exit_velocity" in w for w in rep["warnings"])
110
111
112def _gravity_turn_loss(cfg, drag, incl_deg):
113 """Point-mass 2-DOF: along the rail at incl_deg to its end, then thrust along the velocity
114 (a gravity turn, what an overstable vehicle does in still air). Returns h(90) - h(incl)."""
115 from fluids.atmosphere import ATMOSPHERE_1976
116 from scipy.integrate import solve_ivp
117
118 t_c, F, mO, mF = header_curve()
119 md = mO + mF
120 A = math.pi * cfg.rocket.radius**2
121 m0 = 81.706
122 elev, rail = cfg.environment.elevation, cfg.environment.rail_length_m
123
124 def apogee(incl):
125 th0 = math.radians(incl)
126
127 def forces(t, h, V, m):
128 burning = t < t_c[-1]
129 atm = ATMOSPHERE_1976(elev + max(h, 0.0))
130 T = np.interp(t, t_c, F) if burning else 0.0
131 D = 0.5 * atm.rho * V * V * drag.at(V / atm.v_sonic, burning) * A
132 return T - D, (np.interp(t, t_c, md) if burning else 0.0)
133
134 def on_rail(t, y):
135 s_, v, m = y
136 f, mdot = forces(t, s_ * math.sin(th0), v, m)
137 return [v, f / m - G0 * math.sin(th0), -mdot]
138
139 off = lambda t, y: y[0] - rail # noqa: E731
140 off.terminal = True
141 r = solve_ivp(on_rail, (0, 5), [0, 0, m0], events=off, max_step=1e-3, rtol=1e-9)
142 t1, (s1, v1, m1) = r.t_events[0][0], r.y_events[0][0]
143
144 def flight(t, y):
145 x, h, vx, vh, m = y
146 V = math.hypot(vx, vh)
147 f, mdot = forces(t, h, V, m)
148 return [vx, vh, f * vx / V / m, f * vh / V / m - G0, -mdot]
149
150 top = lambda t, y: y[3] # noqa: E731
151 top.terminal, top.direction = True, -1
152 y0 = [s1 * math.cos(th0), s1 * math.sin(th0), v1 * math.cos(th0), v1 * math.sin(th0), m1]
153 sol = solve_ivp(flight, (t1, 80), y0, events=top, max_step=0.05, rtol=1e-8)
154 return sol.y_events[0][0][1]
155
156 return apogee(90.0) - apogee(incl_deg)
157
158
160 pytest.importorskip("fluids")
161 from engine.pipeline.vehicle_drag import DragCurves
162
163 cfg = shipped()
164 cfg.environment.launch_inclination_deg = 84.0
165 h84 = fly(cfg)["apogee"]
166 loss = shipped_flight["apogee"] - h84
167 d = shipped_flight["flight_report"]["drag"]
168 drag = DragCurves(tuple(d["mach"]), tuple(d["cd_power_off"]), tuple(d["cd_power_on"]), d["model"], d["source"])
169 bound = _gravity_turn_loss(cfg, drag, 84.0)
170 assert 0.0 < loss < 1.2 * bound, (loss, bound)
171
172
173# ---- FLT-10: pressure thrust
174
175
177 """The time-varying solver computes the curve at the pad's ISA 1976 pressure (test_ambient_isa.py)."""
178 from engine.core.runner import compute_ambient_pressure_from_elevation
179
180 elev = shipped().environment.elevation
181 p_ref = compute_ambient_pressure_from_elevation(elev)
182 motor = shipped_flight["flight"].rocket.motor
183 assert motor.reference_pressure == pytest.approx(p_ref, rel=1e-9)
184 assert shipped_flight["flight_report"]["reference_pressure_pa"] == pytest.approx(p_ref, rel=1e-9)
185
186
188 """F(z) = F_curve + (p_ref - p(z)) A_exit, Sutton & Biblarz eq. 3-21: ~55 N at 1.2 km MSL."""
189 from engine.pipeline.config_schemas import ensure_chamber_geometry
190
191 cfg = shipped()
192 fl = shipped_flight["flight"]
193 A_e = ensure_chamber_geometry(cfg).A_exit
194 p = fl.env.pressure(1200.0)
195 dF = fl.rocket.motor.pressure_thrust(p)
196 assert dF == pytest.approx((fl.rocket.motor.reference_pressure - p) * A_e, abs=0.01)
197 assert dF == pytest.approx(54.6, abs=1.0)
#define max(a, b)
Definition Arduino.h:43
static void fly(world_t *w)
Definition flight_e2e.c:369