STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layerx_flight_inline.py
Go to the documentation of this file.
1"""The inline 1-DOF ascent (D5-C): the specific force a burn pass can read at each twin step
2without flying the whole burn first.
3
4Checked against a closed form (Tsiolkovsky with gravity, no air), against RocketPy on LE4's saved
5delivered curve (AUDIT 9.3 5: 0.002 %), and on the pad (exactly one g0 while the vehicle is held).
6"""
7
8from __future__ import annotations
9
10import math
11
12import numpy as np
13import pytest
14
15from engine.layerx import flight as flt
16from test_layerx_flight import SITE_PA, built_flight, le4_arrays, le4_config, le4_loads, le4_payload
17
18G0 = 9.80665
19
20
21@pytest.fixture(scope="module")
22def config():
23 return le4_config()
24
25
26@pytest.fixture(scope="module")
27def rocketpy_le4(config):
28 """LE4's curve flown in RocketPy with the config's own COPV: the reference."""
29 _, res = built_flight(config, le4_payload(), le4_loads())
30 return res["flight"]
31
32
33# ---------------------------------------------------------------------- closed form
34
35
37 """No drag, no pressure thrust, constant F and mdot: the specific force is F/m(t) exactly, and
38 v = c ln(m0/m) - g0 t, z = c [t - (m/mdot) ln(m0/m)] - g0 t^2/2 with c = F/mdot (Sutton &
39 Biblarz, Rocket Propulsion Elements, 9th ed., ch. 4, vertical flight without drag)."""
40 m0, F, md = 80.0, 7000.0, 3.0
41 t = np.linspace(0.0, 3.5, 71)
42 ascent = flt.InlineAscent(mass_kg=m0, reference_area_m2=0.0193, nozzle_exit_area_m2=0.0, elevation_m=626.67,
43 reference_pressure_pa=None, drag=None, dt_max=0.01)
44 c = F / md
45 for tk in t:
46 a = ascent.advance(float(tk), F, md)
47 m = m0 - md * tk
48 assert a == pytest.approx(F / m, rel=1e-12)
49 assert ascent.m == pytest.approx(m, rel=1e-12)
50 assert ascent.v == pytest.approx(c * math.log(m0 / m) - G0 * tk, abs=1e-6)
51 assert ascent.z == pytest.approx(c * (tk - m / md * math.log(m0 / m)) - 0.5 * G0 * tk * tk, abs=1e-6)
52 assert ascent.liftoff_time_s == 0.0
53
54
55# ---------------------------------------------------------------------- against RocketPy
56
57
59 """LE4's saved delivered curve, the same vehicle (mass, drag, nozzle, pad): the inline ascent's
60 specific force against RocketPy's at every firing sample while RocketPy's motor burns."""
61 t, F, mO, mF = le4_arrays()
62 fl = rocketpy_le4
63 mass = flt.liftoff_mass(config, le4_loads(), ambient_pa=SITE_PA)
64 assert mass["value"] == pytest.approx(float(fl.rocket.total_mass(0.0)), abs=1e-3)
65 out = flt.inline_specific_force(config, F, mO + mF, t, mass["value"], SITE_PA)
66 tb = float(fl.rocket.motor.burn_out_time)
67 burning = t <= tb
68 assert burning.sum() == len(t) - 1 # the last sample is 1.2 ms past RocketPy's cutoff
69 ours = np.asarray(out["accel_m_s2"])[burning]
70 theirs = np.array([flt.axial_acceleration(fl, float(x)) for x in t[burning]])
71 assert np.max(np.abs(ours / theirs - 1.0)) < 1e-4 # measured 0.0023 %
72 assert out["held"] == [False] * len(t) and out["liftoff_time_s"] == 0.0
73 # The state too: RocketPy's altitude and speed at the last burning sample.
74 k = int(np.flatnonzero(burning)[-1])
75 assert out["altitude_m"][k] == pytest.approx(float(fl.z(t[k])) - config.environment.elevation, rel=2e-3)
76 assert out["velocity_m_s"][k] == pytest.approx(float(fl.vz(t[k])), rel=1e-3)
77
78
80 """RocketPy's ``Flight.R3`` is a cubic spline over its solution nodes, and the drag steps at
81 burnout (power-on to power-off Cd). Across the step the spline rings: on the LE4 helium flight it
82 read -423 N 36 ms before cutoff, between -646 N and -747 N while the speed rose, so RocketPy's
83 specific force jumped 3.4 % and the inline check (vs_rocketpy 0.0338 against 0.005) failed under
84 rocketpy 1.11.0 and 1.13.0 alike. Read linearly between its nodes, the drag is bounded by them."""
85 from types import SimpleNamespace
86
87 from rocketpy import Function
88
89 tb = 3.4864
90 x = list(np.arange(3.30, 3.44, 0.02)) + [3.45, tb, tb + 1e-5, 3.49, 3.51, 3.53]
91 y = [-600.0 - 300.0 * (t - 3.30) for t in x[:-5]] + [-648.0, -722.2, -722.0, -721.5, -721.0]
92 r3 = Function(np.column_stack([x, y]), interpolation="spline")
93 assert abs(float(r3(3.46)) - float(np.interp(3.46, x, y))) > 100.0 # the spline rings here
94 motor = SimpleNamespace(nozzle_radius=0.0, reference_pressure=None, burn_start_time=0.0, burn_out_time=tb,
95 thrust=lambda t: 7200.0 if t <= tb else 0.0)
96 flight = SimpleNamespace(rocket=SimpleNamespace(motor=motor, total_mass=lambda t: 70.0), R3=r3,
97 out_of_rail_time=0.5, inclination=90.0)
98 for t in (3.43, 3.46, 3.47, 3.48):
99 drag = float(np.interp(t, x, y))
100 assert -722.2 <= drag <= -600.0
101 assert flt.axial_acceleration(flight, t) == pytest.approx((7200.0 + drag) / 70.0, rel=1e-12)
102
103
105 """``{t, accel_m_s2}`` on the burn's clock, as ``fly``'s schedule: the outer loop's own measure
106 of change between them is far inside its tolerance."""
107 t, F, mO, mF = le4_arrays()
108 flown = flt.fly(config, le4_payload(), le4_loads(), SITE_PA)
109 assert flown["ok"], flown.get("error")
110 out = flt.inline_specific_force(config, F, mO + mF, t, flown["liftoff_mass_kg"], SITE_PA)
111 assert out["t"] == flown["schedule"]["t"]
112 assert flt.schedule_change(flown["schedule"], out) < 0.02 * flt.ACCEL_TOLERANCE # measured 2.3e-5
113
114
115# ---------------------------------------------------------------------- the pad
116
117
119 """Before Fire (zero thrust) and while thrust is below the weight the stand carries the vehicle:
120 it does not move and its columns feel exactly g0, the pad's ``Setup.body_acceleration``. The
121 first sample whose thrust beats the weight releases it."""
122 m0 = 80.0
123 w = m0 * G0
124 t = [-0.5, -0.25, 0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30]
125 F = [0.0, 0.0, 0.0, 0.3 * w, 0.6 * w, 0.9 * w, 1.5 * w, 8.0 * w, 8.0 * w]
126 md = [0.0, 0.0, 0.0, 0.5, 1.0, 1.5, 2.5, 3.0, 3.0]
127 out = flt.inline_specific_force(config, F, md, t, m0, SITE_PA)
128 for k in range(6):
129 assert out["accel_m_s2"][k] == G0 and out["held"][k], k
130 assert out["altitude_m"][k] == 0.0 and out["velocity_m_s"][k] == 0.0, k
131 # Mass leaves while it is held: the propellant flows whether or not the vehicle moves.
132 assert out["mass_kg"][5] == pytest.approx(m0 - (0.25 + 0.75 + 1.25) * 0.05, rel=1e-12)
133 assert not out["held"][7] and out["accel_m_s2"][7] > 7.0 * G0
134 assert 0.15 <= out["liftoff_time_s"] <= 0.20
135 assert out["velocity_m_s"][8] > 0.0 and out["altitude_m"][8] > 0.0
136
137
139 """Causal: a burn pass that drives the ascent step by step gets what the batch call returns."""
140 t, F, mO, mF = le4_arrays()
141 m0 = 80.0
142 batch = flt.inline_specific_force(config, F, mO + mF, t, m0, SITE_PA)
143 ascent = flt.InlineAscent.from_config(config, m0, SITE_PA)
144 stepped = [ascent.advance(float(a), float(b), float(c)) for a, b, c in zip(t, F, mO + mF)]
145 assert stepped == batch["accel_m_s2"]
146
147
149 t, F, mO, mF = le4_arrays()
150 out = flt.inline_specific_force(config, F, mO + mF, t, 80.0, SITE_PA)
151 model = out["model"]
152 assert model["name"] == "inline_vertical_1dof" and "Sutton" in model["source"] and model["assumptions"]
153 for name, row in model["inputs"].items():
154 assert set(row) == {"value", "unit", "provenance"} and row["provenance"], name
155 assert "schema default" in model["inputs"]["surface_roughness_m"]["provenance"]
156
157
159 with pytest.raises(ValueError):
160 flt.inline_specific_force(config, [1.0, 2.0], [1.0], [0.0, 1.0], 80.0, SITE_PA)
161 with pytest.raises(ValueError):
162 flt.inline_specific_force(config, [1.0, 2.0], [1.0, 1.0], [1.0, 0.0], 80.0, SITE_PA)
163 with pytest.raises(ValueError):
164 flt.InlineAscent(mass_kg=0.0, reference_area_m2=0.0, nozzle_exit_area_m2=0.0, elevation_m=0.0,
165 reference_pressure_pa=None)
test_inline_matches_rocketpy_through_the_burn(config, rocketpy_le4)