STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_robust_ddp_controller_integration.py
Go to the documentation of this file.
1"""Integration test for robust DDP controller.
2
3Runs controller for 200 steps with simulated dynamics and verifies:
4- Constraints never violated
5- COPV pressure decreases but stays above minimum
6- Thrust tracks reference within tolerance
7"""
8
9import unittest
10import numpy as np
11from unittest.mock import Mock
12
13from engine.control.robust_ddp.controller import RobustDDPController
15 ControllerConfig,
16 Measurement,
17 NavState,
18 Command,
19 CommandType,
20)
22 step,
23 DynamicsParams,
24 IDX_P_COPV,
25 IDX_P_REG,
26 IDX_P_U_F,
27 IDX_P_U_O,
28 IDX_P_D_F,
29 IDX_P_D_O,
30 IDX_V_U_F,
31 IDX_V_U_O,
32)
33from engine.control.robust_ddp.engine_wrapper import EngineEstimate, EngineWrapper
35from engine.pipeline.config_schemas import PintleEngineConfig
36
37
38class TestControllerIntegration(unittest.TestCase):
39 """Integration test for robust DDP controller."""
40
41 def setUp(self):
42 """Set up test fixtures."""
43 # Create controller config
45 N=10, # Short horizon for faster test
46 dt=0.01, # 10 ms
47 dwell_time=0.05,
48 duty_quantization=0.1, # 10% steps
49 qF=1.0,
50 qMR=10.0,
51 qGas=0.1,
52 qSwitch=0.01,
53 MR_min=1.5,
54 MR_max=3.0,
55 P_u_max=10e6,
56 P_copv_min=1e6,
57 max_iterations=3, # Few iterations for speed
58 convergence_tol=1e-3,
59 )
60
61 # Create minimal engine config
63
64 # Create controller
66
67 # Initial state
68 self.x0 = np.array([
69 30e6, # P_copv
70 24e6, # P_reg
71 3e6, # P_u_F
72 3.5e6, # P_u_O
73 2.5e6, # P_d_F
74 3e6, # P_d_O
75 0.01, # V_u_F
76 0.01, # V_u_O
77 ])
78
79 # Reference thrust
80 self.F_ref = 5000.0 # 5 kN
81
82 # Dynamics params
83 self.dynamics_params = DynamicsParams.from_config(self.cfg)
84
85 def _create_minimal_engine_config(self) -> PintleEngineConfig:
86 """Create minimal engine config for testing."""
88 PintleEngineConfig,
89 PintleInjectorConfig,
90 PintleInjectorGeometry,
91 FuelGeometry,
92 OxidizerGeometry,
93 FluidConfig,
94 FeedSystemConfig,
95 DischargeConfig,
96 CombustionConfig,
97 CEAConfig,
98 ChamberGeometryConfig,
99 )
100
101 return PintleEngineConfig(
102 injector=PintleInjectorConfig(
103 type="pintle",
104 geometry=PintleInjectorGeometry(
105 fuel=FuelGeometry(
106 d_pintle_tip=0.01,
107 n_holes=8,
108 d_hole=0.001,
109 ),
110 oxidizer=OxidizerGeometry(
111 d_annulus_inner=0.008,
112 d_annulus_outer=0.012,
113 ),
114 ),
115 ),
116 fluids={
117 "fuel": FluidConfig(
118 name="RP-1",
119 density=800.0,
120 viscosity=2e-3,
121 surface_tension=0.025,
122 ),
123 "oxidizer": FluidConfig(
124 name="LOX",
125 density=1140.0,
126 viscosity=0.2e-3,
127 surface_tension=0.013,
128 ),
129 },
130 feed_system={
131 "fuel": FeedSystemConfig(
132 line_length=1.0,
133 line_diameter=0.02,
134 roughness=1e-6,
135 ),
136 "oxidizer": FeedSystemConfig(
137 line_length=1.0,
138 line_diameter=0.02,
139 roughness=1e-6,
140 ),
141 },
142 discharge={
143 "fuel": DischargeConfig(
144 Cd=0.6,
145 A=1e-4,
146 ),
147 "oxidizer": DischargeConfig(
148 Cd=0.6,
149 A=1e-4,
150 ),
151 },
152 combustion=CombustionConfig(
153 cea=CEAConfig(),
154 ),
155 chamber_geometry=ChamberGeometryConfig(
156 A_throat=1e-4,
157 A_exit=5e-4,
158 L_star=1.0,
159 ),
160 )
161
163 self,
164 x: np.ndarray,
165 u: np.ndarray,
166 engine_wrapper: EngineWrapper,
167 ) -> np.ndarray:
168 """
169 Simulate one step of dynamics.
170
171 Parameters:
172 -----------
173 x : np.ndarray
174 Current state
175 u : np.ndarray
176 Control action
177 engine_wrapper : EngineWrapper
178 Engine wrapper for mass flow estimation
179
180 Returns:
181 --------
182 x_next : np.ndarray
183 Next state
184 """
185 # Estimate mass flows
186 try:
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
190 except Exception:
191 mdot_F = 0.0
192 mdot_O = 0.0
193
194 # Step dynamics
195 x_next = step(x, u, self.cfg.dt, self.dynamics_params, mdot_F, mdot_O)
196
197 return x_next
198
199 def _measurement_from_state(self, x: np.ndarray) -> Measurement:
200 """Create measurement from state."""
201 return Measurement(
202 P_copv=x[IDX_P_COPV],
203 P_reg=x[IDX_P_REG],
204 P_u_fuel=x[IDX_P_U_F],
205 P_u_ox=x[IDX_P_U_O],
206 P_d_fuel=x[IDX_P_D_F],
207 P_d_ox=x[IDX_P_D_O],
208 )
209
211 """Test controller integration over 200 steps."""
212 # Reset controller
213 self.controller.reset()
214
215 # Initial state
216 x = self.x0.copy()
217
218 # Track history
219 history = {
220 "x": [],
221 "u": [],
222 "F": [],
223 "P_copv": [],
224 "constraint_violations": [],
225 }
226
227 # Create command
228 cmd = Command(
229 command_type=CommandType.THRUST_DESIRED,
230 thrust_desired=self.F_ref,
231 )
232
233 # Navigation state (constant for this test)
234 nav = NavState(h=0.0, vz=0.0, theta=0.0, mass_estimate=100.0)
235
236 # Run for 200 steps
237 num_steps = 200
238 for k in range(num_steps):
239 # Create measurement from current state
240 meas = self._measurement_from_state(x)
241
242 # Controller step
243 actuation_cmd, diagnostics = self.controller.step(meas, nav, cmd)
244
245 # Extract control
246 u = np.array([actuation_cmd.duty_F, actuation_cmd.duty_O])
247
248 # Simulate dynamics
249 x_next = self._simulate_dynamics(x, u, self.controller.engine_wrapper)
250
251 # Check constraints
252 eng_est = self.controller.engine_wrapper.estimate_from_pressures(
253 x[IDX_P_D_F], x[IDX_P_D_O]
254 )
255 safe = is_safe(x, eng_est, self.cfg)
256
257 # Store history
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)
263
264 # Update state
265 x = x_next
266
267 # Convert to arrays
268 P_copv_history = np.array(history["P_copv"])
269 F_history = np.array(history["F"])
270 constraint_violations = np.array(history["constraint_violations"])
271
272 # Check 1: Constraints never violated
273 self.assertFalse(
274 np.any(constraint_violations),
275 f"Constraints violated at steps: {np.where(constraint_violations)[0]}"
276 )
277
278 # Check 2: COPV pressure decreases but stays above minimum
279 P_copv_final = P_copv_history[-1]
280 P_copv_initial = P_copv_history[0]
281
282 self.assertLess(
283 P_copv_final, P_copv_initial,
284 "COPV pressure should decrease over time"
285 )
286
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)"
290 )
291
292 # Check 3: Thrust tracks reference within reasonable tolerance
293 # Allow 20% tolerance for model uncertainty
294 F_tolerance = 0.2 * self.F_ref
295 F_mean = np.mean(F_history[50:]) # Skip initial transient
296
297 self.assertLess(
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"
300 )
301
302 # Additional checks
303 # Check that control is reasonable (not all zeros or all ones)
304 u_history = np.array(history["u"])
305 u_mean = np.mean(u_history, axis=0)
306
307 self.assertTrue(
308 np.any(u_mean > 0.1) and np.any(u_mean < 0.9),
309 "Control should vary (not stuck at boundaries)"
310 )
311
312 # Check that state stays bounded
313 x_history = np.array(history["x"])
314 self.assertTrue(
315 np.all(np.isfinite(x_history)),
316 "State should remain finite"
317 )
318
319 # Check ullage pressures stay below maximum
320 P_u_F_history = x_history[:, IDX_P_U_F]
321 P_u_O_history = x_history[:, IDX_P_U_O]
322
323 self.assertLessEqual(
324 np.max(P_u_F_history), self.cfg.P_u_max * 1.1, # 10% margin for numerical errors
325 f"Fuel ullage pressure should stay below maximum"
326 )
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"
330 )
331
332
333if __name__ == "__main__":
334 unittest.main()
335
np.ndarray _simulate_dynamics(self, np.ndarray x, np.ndarray u, EngineWrapper engine_wrapper)
void setUp(void)
Definition test_main.cpp:17