1"""Tests for the cold gas thruster analysis module."""
14 """Helper to build a config with sensible test defaults."""
16 throat_diameter=0.003,
18 inlet_pressure=500_000,
19 chamber_diameter=0.010,
22 defaults.update(overrides)
30 """Verify the isentropic analysis gives physically reasonable results."""
34 assert result.thrust > 0,
"Thrust must be positive"
38 assert result.mass_flow > 0
41 """Expansion ratio > 1 should give M > 1."""
43 assert result.exit_mach > 1.0
46 """Isentropic expansion must cool the gas."""
49 assert result.exit_temperature < cfg.inlet_temperature
52 """CO₂ cold gas Isp should be roughly 40–80 s."""
54 assert 30 < result.specific_impulse < 100, (
55 f
"Isp = {result.specific_impulse:.1f} s — outside expected CO₂ range"
59 """F should equal ṁ·v + (Pe-Pa)·Ae."""
62 result.mass_flow * result.exit_velocity
63 + (result.exit_pressure -
_make_config().ambient_pressure) * result.exit_area
65 np.testing.assert_allclose(result.thrust, reconstructed, rtol=1e-10)
68 """F should equal ṁ · Isp · g₀."""
70 np.testing.assert_allclose(
71 result.thrust, result.mass_flow * result.specific_impulse * G0, rtol=1e-10
82 assert result.torque > 0
87 expected = result.thrust * cfg.moment_arm
88 np.testing.assert_allclose(result.torque, expected, rtol=1e-12)
92 assert result.angular_accel > 0
96 expected = result.torque / result.moi_estimate
97 np.testing.assert_allclose(result.angular_accel, expected, rtol=1e-12)
108 assert r_hi.thrust > r_lo.thrust
113 assert r_large.mass_flow > r_small.mass_flow
123 pts = cgt.generate_nozzle_contour(do_plot=
False)
125 assert pts.shape[1] == 2
129 """Nozzle throat should have smaller radius than the chamber."""
131 pts = cgt.generate_nozzle_contour(do_plot=
False)
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}"
140 """Last contour point should be near the exit radius."""
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)
148 """First segment should be at the chamber radius and length."""
151 pts = cgt.generate_nozzle_contour(do_plot=
False)
152 r_chamber = cfg.chamber_diameter / 2.0
154 np.testing.assert_allclose(pts[0, 1], r_chamber, rtol=1e-10)
158 x_range = np.max(pts[:, 0]) - np.min(pts[:, 0])
159 assert x_range > cfg.chamber_length
169 expected = (cfg.exit_diameter / cfg.throat_diameter) ** 2
170 np.testing.assert_allclose(cfg.expansion_ratio, expected, rtol=1e-12)
173 cfg =
_make_config(rocket_length=10.0, cg_from_tail=4.0)
174 assert cfg.moment_arm == 6.0
178 expected = 8.314462618 / 0.04401
179 np.testing.assert_allclose(cfg.R_specific, expected, rtol=1e-10)
183 assert cfg._use_solved_plot
is False
187 assert cfg._use_solved_plot
is True
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)
197 assert cfg._effective_volume_chamber == 5e-6
204 """Config with volume_chamber set so the solved_chamber_plot path is used."""
206 throat_diameter=0.003,
208 chamber_diameter=0.010,
209 chamber_length=0.020,
211 volume_chamber=np.pi / 4.0 * 0.010 ** 2 * 0.020,
212 inlet_pressure=500_000,
214 base.update(overrides)
219 """Verify generate_nozzle_contour() works correctly via solved_chamber_plot."""
223 assert cfg._use_solved_plot
is True
227 pts = cgt.generate_nozzle_contour(do_plot=
False)
229 assert pts.shape[1] == 2
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)
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)
245 """Minimum radius in the contour should be ~= throat radius."""
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)
255 pts = cgt.generate_nozzle_contour(do_plot=
False)
256 assert np.min(pts[:, 1]) < pts[0, 1]
259 """If Lstar=0, it should be computed from volume/A_throat without error."""
262 pts = cgt.generate_nozzle_contour(do_plot=
False)
266 """Providing an explicit Lstar should also work without error."""
269 pts = cgt.generate_nozzle_contour(do_plot=
False)
277 """Confirm the legacy hand-rolled path still works when volume_chamber=0."""
281 assert cfg._use_solved_plot
is False
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)
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)
test_exit_temperature_below_inlet(self)
test_mass_flow_positive(self)
test_isp_in_expected_range(self)
test_exit_mach_supersonic(self)
test_mass_flow_isp_thrust_identity(self)
test_thrust_positive(self)
test_thrust_identity(self)
test_effective_volume_chamber_uses_explicit(self)
test_use_solved_plot_true_when_volume_set(self)
test_expansion_ratio_computed(self)
test_use_solved_plot_false_by_default(self)
test_effective_volume_chamber_auto_estimate(self)
test_fallback_starts_at_chamber_radius(self)
test_fallback_ends_at_exit_radius(self)
test_fallback_path_selected(self)
test_contour_ends_at_exit_radius(self)
test_contour_starts_converging(self)
test_contour_includes_chamber(self)
test_larger_throat_more_flow(self)
test_higher_pressure_more_thrust(self)
test_solved_path_selected(self)
test_contour_ends_at_exit_radius(self)
test_lstar_explicit(self)
test_contour_throat_smaller_than_chamber(self)
test_lstar_auto_computed(self)
test_contour_has_throat(self)
test_contour_starts_at_chamber_radius(self)
test_torque_positive(self)
test_angular_accel_equals_torque_over_moi(self)
test_angular_accel_positive(self)
test_torque_equals_f_times_arm(self)
ColdGasThrusterConfig _make_solved_config(**overrides)
ColdGasThrusterConfig _make_config(**overrides)