86 """Create minimal engine config for testing."""
90 PintleInjectorGeometry,
98 ChamberGeometryConfig,
102 injector=PintleInjectorConfig(
104 geometry=PintleInjectorGeometry(
110 oxidizer=OxidizerGeometry(
111 d_annulus_inner=0.008,
112 d_annulus_outer=0.012,
121 surface_tension=0.025,
123 "oxidizer": FluidConfig(
127 surface_tension=0.013,
131 "fuel": FeedSystemConfig(
136 "oxidizer": FeedSystemConfig(
143 "fuel": DischargeConfig(
147 "oxidizer": DischargeConfig(
152 combustion=CombustionConfig(
155 chamber_geometry=ChamberGeometryConfig(
166 engine_wrapper: EngineWrapper,
169 Simulate one step of dynamics.
177 engine_wrapper : EngineWrapper
178 Engine wrapper for mass flow estimation
187 eng_est = engine_wrapper.estimate_from_pressures(x[IDX_P_D_F], x[IDX_P_D_O])
188 mdot_F = eng_est.mdot_F
if np.isfinite(eng_est.mdot_F)
else 0.0
189 mdot_O = eng_est.mdot_O
if np.isfinite(eng_est.mdot_O)
else 0.0
211 """Test controller integration over 200 steps."""
224 "constraint_violations": [],
229 command_type=CommandType.THRUST_DESIRED,
230 thrust_desired=self.
F_ref,
234 nav =
NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
238 for k
in range(num_steps):
243 actuation_cmd, diagnostics = self.
controller.step(meas, nav, cmd)
246 u = np.array([actuation_cmd.duty_F, actuation_cmd.duty_O])
252 eng_est = self.
controller.engine_wrapper.estimate_from_pressures(
253 x[IDX_P_D_F], x[IDX_P_D_O]
255 safe = is_safe(x, eng_est, self.
cfg)
258 history[
"x"].append(x.copy())
259 history[
"u"].append(u.copy())
260 history[
"F"].append(eng_est.F)
261 history[
"P_copv"].append(x[IDX_P_COPV])
262 history[
"constraint_violations"].append(
not safe)
268 P_copv_history = np.array(history[
"P_copv"])
269 F_history = np.array(history[
"F"])
270 constraint_violations = np.array(history[
"constraint_violations"])
274 np.any(constraint_violations),
275 f
"Constraints violated at steps: {np.where(constraint_violations)[0]}"
279 P_copv_final = P_copv_history[-1]
280 P_copv_initial = P_copv_history[0]
283 P_copv_final, P_copv_initial,
284 "COPV pressure should decrease over time"
287 self.assertGreaterEqual(
288 P_copv_final, self.
cfg.P_copv_min,
289 f
"COPV pressure ({P_copv_final/1e6:.2f} MPa) should stay above minimum ({self.cfg.P_copv_min/1e6:.2f} MPa)"
294 F_tolerance = 0.2 * self.
F_ref
295 F_mean = np.mean(F_history[50:])
298 abs(F_mean - self.
F_ref), F_tolerance,
299 f
"Thrust ({F_mean:.1f} N) should track reference ({self.F_ref:.1f} N) within {F_tolerance:.1f} N"
304 u_history = np.array(history[
"u"])
305 u_mean = np.mean(u_history, axis=0)
308 np.any(u_mean > 0.1)
and np.any(u_mean < 0.9),
309 "Control should vary (not stuck at boundaries)"
313 x_history = np.array(history[
"x"])
315 np.all(np.isfinite(x_history)),
316 "State should remain finite"
320 P_u_F_history = x_history[:, IDX_P_U_F]
321 P_u_O_history = x_history[:, IDX_P_U_O]
323 self.assertLessEqual(
324 np.max(P_u_F_history), self.
cfg.P_u_max * 1.1,
325 f
"Fuel ullage pressure should stay below maximum"
327 self.assertLessEqual(
328 np.max(P_u_O_history), self.
cfg.P_u_max * 1.1,
329 f
"Oxidizer ullage pressure should stay below maximum"