STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_cold_gas_thruster.py
Go to the documentation of this file.
1"""Tests for the cold gas thruster analysis module."""
2
3import numpy as np
4import pytest
6 ColdGasThruster,
7 ColdGasThrusterConfig,
8 ColdGasThrusterResult,
9 G0,
10)
11
12
13def _make_config(**overrides) -> ColdGasThrusterConfig:
14 """Helper to build a config with sensible test defaults."""
15 defaults = dict(
16 throat_diameter=0.003, # 3 mm
17 exit_diameter=0.006, # 6 mm → ε ≈ 4
18 inlet_pressure=500_000, # 500 kPa
19 chamber_diameter=0.010, # 10 mm
20 chamber_length=0.015, # 15 mm
21 )
22 defaults.update(overrides)
23 return ColdGasThrusterConfig(**defaults)
24
25
26# -------------------------------------------------------------------
27# Basic physics sanity checks
28# -------------------------------------------------------------------
30 """Verify the isentropic analysis gives physically reasonable results."""
31
33 result = ColdGasThruster(_make_config()).compute()
34 assert result.thrust > 0, "Thrust must be positive"
35
37 result = ColdGasThruster(_make_config()).compute()
38 assert result.mass_flow > 0
39
41 """Expansion ratio > 1 should give M > 1."""
42 result = ColdGasThruster(_make_config()).compute()
43 assert result.exit_mach > 1.0
44
46 """Isentropic expansion must cool the gas."""
47 cfg = _make_config()
48 result = ColdGasThruster(cfg).compute()
49 assert result.exit_temperature < cfg.inlet_temperature
50
52 """CO₂ cold gas Isp should be roughly 40–80 s."""
53 result = ColdGasThruster(_make_config()).compute()
54 assert 30 < result.specific_impulse < 100, (
55 f"Isp = {result.specific_impulse:.1f} s — outside expected CO₂ range"
56 )
57
59 """F should equal ṁ·v + (Pe-Pa)·Ae."""
60 result = ColdGasThruster(_make_config()).compute()
61 reconstructed = (
62 result.mass_flow * result.exit_velocity
63 + (result.exit_pressure - _make_config().ambient_pressure) * result.exit_area
64 )
65 np.testing.assert_allclose(result.thrust, reconstructed, rtol=1e-10)
66
68 """F should equal ṁ · Isp · g₀."""
69 result = ColdGasThruster(_make_config()).compute()
70 np.testing.assert_allclose(
71 result.thrust, result.mass_flow * result.specific_impulse * G0, rtol=1e-10
72 )
73
74
75# -------------------------------------------------------------------
76# Torque and angular dynamics
77# -------------------------------------------------------------------
79
81 result = ColdGasThruster(_make_config()).compute()
82 assert result.torque > 0
83
85 cfg = _make_config()
86 result = ColdGasThruster(cfg).compute()
87 expected = result.thrust * cfg.moment_arm
88 np.testing.assert_allclose(result.torque, expected, rtol=1e-12)
89
91 result = ColdGasThruster(_make_config()).compute()
92 assert result.angular_accel > 0
93
95 result = ColdGasThruster(_make_config()).compute()
96 expected = result.torque / result.moi_estimate
97 np.testing.assert_allclose(result.angular_accel, expected, rtol=1e-12)
98
99
100# -------------------------------------------------------------------
101# Scaling behaviour
102# -------------------------------------------------------------------
104
106 r_lo = ColdGasThruster(_make_config(inlet_pressure=200_000)).compute()
107 r_hi = ColdGasThruster(_make_config(inlet_pressure=800_000)).compute()
108 assert r_hi.thrust > r_lo.thrust
109
111 r_small = ColdGasThruster(_make_config(throat_diameter=0.002)).compute()
112 r_large = ColdGasThruster(_make_config(throat_diameter=0.005)).compute()
113 assert r_large.mass_flow > r_small.mass_flow
114
115
116# -------------------------------------------------------------------
117# Nozzle contour
118# -------------------------------------------------------------------
120
123 pts = cgt.generate_nozzle_contour(do_plot=False)
124 assert pts.ndim == 2
125 assert pts.shape[1] == 2
126 assert len(pts) > 10
127
129 """Nozzle throat should have smaller radius than the chamber."""
131 pts = cgt.generate_nozzle_contour(do_plot=False)
132 # Chamber radius is pts[0, 1]
133 # Throat radius should be the minimum radius in the contour
134 r_min = np.min(pts[:, 1])
135 assert r_min < pts[0, 1], (
136 f"Throat radius {r_min:.3f} should be smaller than chamber radius {pts[0, 1]:.3f}"
137 )
138
140 """Last contour point should be near the exit radius."""
141 cfg = _make_config()
142 cgt = ColdGasThruster(cfg)
143 pts = cgt.generate_nozzle_contour(do_plot=False)
144 r_exit = cfg.exit_diameter / 2.0
145 np.testing.assert_allclose(pts[-1, 1], r_exit, rtol=0.01)
146
148 """First segment should be at the chamber radius and length."""
149 cfg = _make_config()
150 cgt = ColdGasThruster(cfg)
151 pts = cgt.generate_nozzle_contour(do_plot=False)
152 r_chamber = cfg.chamber_diameter / 2.0
153 # Check first point matches chamber radius
154 np.testing.assert_allclose(pts[0, 1], r_chamber, rtol=1e-10)
155 # Check total length includes chamber length
156 # (rao contour nozzle length is approx L_noz + x_entrance)
157 # We just check the x-range is at least as large as chamber_length
158 x_range = np.max(pts[:, 0]) - np.min(pts[:, 0])
159 assert x_range > cfg.chamber_length
160
161
162# -------------------------------------------------------------------
163# Config validation
164# -------------------------------------------------------------------
166
168 cfg = _make_config()
169 expected = (cfg.exit_diameter / cfg.throat_diameter) ** 2
170 np.testing.assert_allclose(cfg.expansion_ratio, expected, rtol=1e-12)
171
173 cfg = _make_config(rocket_length=10.0, cg_from_tail=4.0)
174 assert cfg.moment_arm == 6.0
175
177 cfg = _make_config()
178 expected = 8.314462618 / 0.04401
179 np.testing.assert_allclose(cfg.R_specific, expected, rtol=1e-10)
180
182 cfg = _make_config()
183 assert cfg._use_solved_plot is False
184
186 cfg = _make_config(volume_chamber=1e-6)
187 assert cfg._use_solved_plot is True
188
190 """When volume_chamber=0, should estimate from A_chamber * chamber_length."""
191 cfg = _make_config(chamber_diameter=0.010, chamber_length=0.015)
192 expected = np.pi / 4.0 * 0.010 ** 2 * 0.015
193 np.testing.assert_allclose(cfg._effective_volume_chamber, expected, rtol=1e-12)
194
196 cfg = _make_config(volume_chamber=5e-6)
197 assert cfg._effective_volume_chamber == 5e-6
198
199
200# -------------------------------------------------------------------
201# Solved chamber plot path
202# -------------------------------------------------------------------
203def _make_solved_config(**overrides) -> ColdGasThrusterConfig:
204 """Config with volume_chamber set so the solved_chamber_plot path is used."""
205 base = dict(
206 throat_diameter=0.003,
207 exit_diameter=0.006,
208 chamber_diameter=0.010,
209 chamber_length=0.020,
210 # chamber volume = A_chamber * length as a realistic value
211 volume_chamber=np.pi / 4.0 * 0.010 ** 2 * 0.020,
212 inlet_pressure=500_000,
213 )
214 base.update(overrides)
215 return ColdGasThrusterConfig(**base)
216
217
219 """Verify generate_nozzle_contour() works correctly via solved_chamber_plot."""
220
222 cfg = _make_solved_config()
223 assert cfg._use_solved_plot is True
224
227 pts = cgt.generate_nozzle_contour(do_plot=False)
228 assert pts.ndim == 2
229 assert pts.shape[1] == 2
230 assert len(pts) > 10
231
233 cfg = _make_solved_config()
234 cgt = ColdGasThruster(cfg)
235 pts = cgt.generate_nozzle_contour(do_plot=False)
236 np.testing.assert_allclose(pts[0, 1], cfg.chamber_diameter / 2.0, rtol=1e-8)
237
239 cfg = _make_solved_config()
240 cgt = ColdGasThruster(cfg)
241 pts = cgt.generate_nozzle_contour(do_plot=False)
242 np.testing.assert_allclose(pts[-1, 1], cfg.exit_diameter / 2.0, rtol=0.02)
243
245 """Minimum radius in the contour should be ~= throat radius."""
246 cfg = _make_solved_config()
247 cgt = ColdGasThruster(cfg)
248 pts = cgt.generate_nozzle_contour(do_plot=False)
249 r_min = np.min(pts[:, 1])
250 np.testing.assert_allclose(r_min, cfg.throat_diameter / 2.0, rtol=0.05)
251
253 cfg = _make_solved_config()
254 cgt = ColdGasThruster(cfg)
255 pts = cgt.generate_nozzle_contour(do_plot=False)
256 assert np.min(pts[:, 1]) < pts[0, 1]
257
259 """If Lstar=0, it should be computed from volume/A_throat without error."""
260 cfg = _make_solved_config(Lstar=0.0)
261 cgt = ColdGasThruster(cfg)
262 pts = cgt.generate_nozzle_contour(do_plot=False)
263 assert len(pts) > 10
264
266 """Providing an explicit Lstar should also work without error."""
267 cfg = _make_solved_config(Lstar=1.5)
268 cgt = ColdGasThruster(cfg)
269 pts = cgt.generate_nozzle_contour(do_plot=False)
270 assert len(pts) > 10
271
272
273# -------------------------------------------------------------------
274# Fallback path unchanged
275# -------------------------------------------------------------------
277 """Confirm the legacy hand-rolled path still works when volume_chamber=0."""
278
280 cfg = _make_config()
281 assert cfg._use_solved_plot is False
282
284 cfg = _make_config()
285 cgt = ColdGasThruster(cfg)
286 pts = cgt.generate_nozzle_contour(do_plot=False)
287 np.testing.assert_allclose(pts[0, 1], cfg.chamber_diameter / 2.0, rtol=1e-10)
288
290 cfg = _make_config()
291 cgt = ColdGasThruster(cfg)
292 pts = cgt.generate_nozzle_contour(do_plot=False)
293 np.testing.assert_allclose(pts[-1, 1], cfg.exit_diameter / 2.0, rtol=0.01)
ColdGasThrusterConfig _make_solved_config(**overrides)
ColdGasThrusterConfig _make_config(**overrides)