STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_layerx_eng.py
Go to the documentation of this file.
1"""A Layer X burn exported as a RASP .eng thrust curve for OpenRocket (engine/layerx/eng.py).
2
3Expected values are independent of the exporter:
4 * the impulse of a step-held curve is the sum of thrust x step, by hand;
5 * the 6.8 kN engine's diameter is its drawn stack, 5.000 in bore + 2 x (0.5 in ablative + 0.25 in
6 steel) = 6.5 in = 165.1 mm; its length is the face-to-throat of the chamber plus a Rao 80 % bell,
7 recomputed here from A_t, L*, the bore and epsilon.
8"""
9import math
10import os
11import sys
12
13import pytest
14
15sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
17from engine.layerx.eng import EDGE_S, check, motor_header, to_eng # noqa: E402
18from engine.pipeline.io import load_config # noqa: E402
19
20ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21DT = 0.02
22N = 175 # 3.5 s
23
24
25def _result(thrust=lambda k: 6800.0 - 2.0 * k, motor=True):
26 # The hand-off shape: a sample at Fire carrying the first step, then one per step end.
27 f = [thrust(0)] + [thrust(k) for k in range(N)]
28 t = [0.0] + [DT * (k + 1) for k in range(N)]
29 r = {
30 "timeseries": {"data": {"time": t, "thrust_kN": [v * 1e-3 for v in f]},
31 "summary": {"lox_propellant_kg": 6.35, "fuel_propellant_kg": 4.40}},
32 "provenance": {"drawing": {"name": "stand"}, "settings": {}, "derived": {"target_lockup_psia": 578.0}},
33 }
34 if motor:
35 r["motor"] = {"diameter_mm": 165.1, "length_mm": 309.9, "dry_kg": 14.54, "basis": {}}
36 return r, sum(thrust(k) * DT for k in range(N))
37
38
40 r, impulse = _result()
41 eng = check(to_eng(r, run_id="abc"))
42 assert eng["increasing"]
43 assert eng["time"][0] == 0.0 and eng["thrust"][0] == 0.0 # OpenRocket's start
44 assert eng["thrust"][-1] == 0.0 # and its required end
45 assert eng["time"][1] == pytest.approx(EDGE_S)
46 assert eng["time"][-2] == pytest.approx(N * DT) and eng["time"][-1] == pytest.approx(N * DT + EDGE_S)
47 assert eng["impulse"] == pytest.approx(impulse, rel=2e-4)
48 # the ramps in and out cancel to half an edge of (last - first) thrust
49 t, f = r["timeseries"]["data"]["time"], [v * 1e3 for v in r["timeseries"]["data"]["thrust_kN"]]
50 curve = sum(0.5 * (f[i] + f[i + 1]) * (t[i + 1] - t[i]) for i in range(len(t) - 1))
51 assert abs(eng["impulse"] - curve) < 0.5
52 assert eng["delays"] == "P"
53 assert eng["propellant_kg"] == pytest.approx(10.75)
54 assert eng["total_kg"] == pytest.approx(10.75 + 14.54)
55 assert (eng["diameter_mm"], eng["length_mm"]) == (165.1, 309.9)
56 assert eng["name"].startswith("STAR-") and " " not in eng["name"]
57
58
60 """One impulse, the run's: each sample after Fire carries the step that ends there, so the run
61 totals a step-held curve (sum of thrust x step, by hand); the header quoted the trapezoid, a third
62 figure (LE4 (3): 23743.9 against the run's 23737.9 N-s). T-0 in psia at a tenth (ADR 0007): it read
63 'tanks 544.5709487755134 psia, bottle 3750.0 psig'."""
64 r, impulse = _result()
65 head = [line for line in to_eng(r).splitlines() if line.startswith(";")]
66 assert f"total impulse {impulse:.1f} N-s" in head[2]
67 assert f"mean thrust {impulse / (N * DT):.1f} N" in head[2]
68 assert "tanks 578.0 psia, bottle - psia (as set)" in head[1]
69 r["summary"] = {"ox": {"t0_psia": 544.5709487755134}, "fuel": {"t0_psia": 544.5709487755134},
70 "copv_t0_psia": 3764.6959487755134}
71 head = [line for line in to_eng(r).splitlines() if line.startswith(";")]
72 assert "tanks 544.6 psia, bottle 3764.7 psia at T-0" in head[1] and "psig" not in head[1]
73
74
76 """The file's name and the motor's name in it carry the same mean thrust, the run's (step-held).
77 The header moved to the run's mean on 2026-11-02 while the download kept the trapezoid's: this
78 falling curve (6800 N less 20 N a step) read STAR-5070N on the file and STAR-5060N inside it.
79 By hand: 0.02 s x (175 x 6800 - 20 x 15225) N = 17710 N s over 3.5 s = 5060 N."""
80 from engine.layerx.eng import filename
81
82 r, impulse = _result(thrust=lambda k: 6800.0 - 20.0 * k)
83 mean = impulse / (N * DT)
84 t, f = [0.0] + [DT * (k + 1) for k in range(N)], [6800.0] + [6800.0 - 20.0 * k for k in range(N)]
85 trapezoid = sum(0.5 * (f[i] + f[i + 1]) * (t[i + 1] - t[i]) for i in range(N)) / (N * DT)
86 assert round(trapezoid) != round(mean) # the case tells the two apart
87 motor = check(to_eng(r))["name"]
88 assert motor == f"STAR-{round(mean)}N"
89 assert filename(r, "abc") == f"{motor}-abc.eng"
90
91
93 r, _ = _result()
94 eng = check(to_eng(r, diameter_mm=152.4, length_mm=500.0, dry_kg=0.0))
95 assert (eng["diameter_mm"], eng["length_mm"]) == (152.4, 500.0)
96 assert eng["total_kg"] == pytest.approx(eng["propellant_kg"])
97
98
100 r, _ = _result(motor=False)
101 with pytest.raises(ValueError, match="did not record the engine diameter or length"):
102 to_eng(r)
103
104
106 cfg = load_config(os.path.join(ROOT, "configs", "ethalox_6800N.yaml"))
107 m = motor_header(cfg)
108 # The 6.5 in chamber is wider than the 156.7 mm airframe: the file carries the body diameter,
109 # which is what OpenRocket's motor mount accepts, and keeps the chamber's OD alongside.
110 assert m["chamber_od_mm"] == pytest.approx(6.5 * 25.4, abs=0.05)
111 assert m["diameter_mm"] == pytest.approx(2.0 * cfg.rocket.radius * 1e3, abs=0.05)
112 cg = cfg.chamber_geometry
113 R_t = math.sqrt(cg.A_throat / math.pi)
114 theta = math.radians(cfg.design_requirements.layer1_contraction_half_angle_deg or 45.0)
115 # chamber volume = cylinder + cone frustum to the entrance-arc tangency, then the arc to the throat
116 r_c, r_tan = cg.chamber_diameter / 2.0, R_t * (1.0 + 1.5 * (1.0 - math.cos(theta)))
117 L_cone = (r_c - r_tan) / math.tan(theta)
118 V_cone = math.pi * L_cone / 3.0 * (r_c ** 2 + r_c * r_tan + r_tan ** 2)
119 L_cyl = (cg.Lstar * cg.A_throat - V_cone) / (math.pi * r_c ** 2)
120 bell = 0.8 * (math.sqrt(cg.expansion_ratio) - 1.0) * R_t / math.tan(math.radians(15.0))
121 expected = (L_cyl + L_cone + 1.5 * R_t * math.sin(theta) + bell) * 1e3
122 assert m["length_mm"] == pytest.approx(expected, rel=0.02)
123 assert m["dry_kg"] == pytest.approx(cfg.rocket.engine_mass)
test_the_header_quotes_the_runs_impulse_and_t0_in_psia()
_result(thrust=lambda k:6800.0 - 2.0 *k, motor=True)
test_given_dimensions_override_the_run()
test_the_file_reads_back_as_the_burn()
test_the_download_is_named_for_the_motor_inside_it()
static void check(bool ok, const std::string &what)