STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_robust_ddp_constraints.py
Go to the documentation of this file.
1"""Unit tests for robust DDP controller constraints."""
2
3import unittest
4import numpy as np
5
7 is_safe,
8 constraint_values,
9 get_constraint_summary,
10)
11from engine.control.robust_ddp.data_models import ControllerConfig
12from engine.control.robust_ddp.engine_wrapper import EngineEstimate
14 IDX_P_COPV,
15 IDX_P_REG,
16 IDX_P_U_F,
17 IDX_P_U_O,
18 IDX_P_D_F,
19 IDX_P_D_O,
20)
21
22
23class TestConstraints(unittest.TestCase):
24 """Test constraint checking."""
25
26 def setUp(self):
27 """Set up test fixtures."""
29 P_copv_min=1e6, # 1 MPa minimum
30 P_u_max=10e6, # 10 MPa maximum
31 MR_min=1.5,
32 MR_max=3.0,
33 eps_i=1e-3, # 0.1% minimum injector pressure drop fraction
34 headroom_dp_min=0.05e6, # 0.05 MPa minimum headroom
35 )
36
37 def create_state(self, **kwargs) -> np.ndarray:
38 """Create test state vector with optional overrides."""
39 from engine.control.robust_ddp.dynamics import IDX_V_U_F, IDX_V_U_O
40
41 defaults = {
42 IDX_P_COPV: 30e6, # 30 MPa COPV
43 IDX_P_REG: 24e6, # 24 MPa regulator
44 IDX_P_U_F: 3e6, # 3 MPa fuel ullage
45 IDX_P_U_O: 3.5e6, # 3.5 MPa oxidizer ullage
46 IDX_P_D_F: 2.5e6, # 2.5 MPa fuel feed
47 IDX_P_D_O: 3e6, # 3 MPa oxidizer feed
48 IDX_V_U_F: 0.01, # V_u_F
49 IDX_V_U_O: 0.01, # V_u_O
50 }
51 defaults.update(kwargs)
52 x = np.zeros(8)
53 for idx, val in defaults.items():
54 x[idx] = val
55 return x
56
57 def create_estimate(self, **kwargs) -> EngineEstimate:
58 """Create test engine estimate with optional overrides."""
59 defaults = {
60 "P_ch": 2e6, # 2 MPa chamber pressure
61 "F": 1000.0, # 1 kN thrust
62 "mdot_F": 0.5, # 0.5 kg/s fuel
63 "mdot_O": 1.0, # 1.0 kg/s oxidizer
64 "MR": 2.0, # O/F = 2.0
65 "injector_dp_F": 0.5e6, # 0.5 MPa injector drop
66 "injector_dp_O": 1.0e6, # 1.0 MPa injector drop
67 }
68 defaults.update(kwargs)
69 return EngineEstimate(**defaults)
70
72 """Test COPV minimum pressure constraint."""
73 # Safe case: P_copv > P_copv_min
74 x = self.create_state(**{IDX_P_COPV: 5e6}) # 5 MPa > 1 MPa min
75 eng_est = self.create_estimate()
76
77 constraints = constraint_values(x, eng_est, self.config)
78 self.assertLess(constraints["copv_min"], 0) # Negative = satisfied
79 self.assertGreater(constraints["copv_margin"], 0) # Positive margin
80
81 self.assertTrue(is_safe(x, eng_est, self.config))
82
83 # Violation case: P_copv < P_copv_min
84 x = self.create_state(**{IDX_P_COPV: 0.5e6}) # 0.5 MPa < 1 MPa min
85 constraints = constraint_values(x, eng_est, self.config)
86 self.assertGreater(constraints["copv_min"], 0) # Positive = violated
87 self.assertLess(constraints["copv_margin"], 0) # Negative margin
88
89 self.assertFalse(is_safe(x, eng_est, self.config))
90
92 """Test ullage maximum pressure constraints."""
93 # Safe case: P_u < P_u_max
94 x = self.create_state(**{IDX_P_U_F: 5e6, IDX_P_U_O: 6e6}) # Both < 10 MPa max
95 eng_est = self.create_estimate()
96
97 constraints = constraint_values(x, eng_est, self.config)
98 self.assertLess(constraints["ullage_max_F"], 0) # Negative = satisfied
99 self.assertLess(constraints["ullage_max_O"], 0)
100 self.assertGreater(constraints["ullage_margin_F"], 0) # Positive margin
101 self.assertGreater(constraints["ullage_margin_O"], 0)
102
103 self.assertTrue(is_safe(x, eng_est, self.config))
104
105 # Violation case: P_u > P_u_max
106 x = self.create_state(**{IDX_P_U_F: 12e6, IDX_P_U_O: 11e6}) # Both > 10 MPa max
107 constraints = constraint_values(x, eng_est, self.config)
108 self.assertGreater(constraints["ullage_max_F"], 0) # Positive = violated
109 self.assertGreater(constraints["ullage_max_O"], 0)
110 self.assertLess(constraints["ullage_margin_F"], 0) # Negative margin
111 self.assertLess(constraints["ullage_margin_O"], 0)
112
113 self.assertFalse(is_safe(x, eng_est, self.config))
114
116 """Test mixture ratio constraints."""
117 # Safe case: MR_min <= MR <= MR_max
118 x = self.create_state()
119 eng_est = self.create_estimate(MR=2.0) # 1.5 <= 2.0 <= 3.0
120
121 constraints = constraint_values(x, eng_est, self.config)
122 self.assertLess(constraints["MR_min"], 0) # Negative = satisfied
123 self.assertLess(constraints["MR_max"], 0)
124 self.assertGreater(constraints["MR_margin_low"], 0) # Positive margin
125 self.assertGreater(constraints["MR_margin_high"], 0)
126
127 self.assertTrue(is_safe(x, eng_est, self.config))
128
129 # Violation case: MR < MR_min
130 eng_est = self.create_estimate(MR=1.0) # 1.0 < 1.5 min
131 constraints = constraint_values(x, eng_est, self.config)
132 self.assertGreater(constraints["MR_min"], 0) # Positive = violated
133 self.assertLess(constraints["MR_margin_low"], 0) # Negative margin
134
135 self.assertFalse(is_safe(x, eng_est, self.config))
136
137 # Violation case: MR > MR_max
138 eng_est = self.create_estimate(MR=4.0) # 4.0 > 3.0 max
139 constraints = constraint_values(x, eng_est, self.config)
140 self.assertGreater(constraints["MR_max"], 0) # Positive = violated
141 self.assertLess(constraints["MR_margin_high"], 0) # Negative margin
142
143 self.assertFalse(is_safe(x, eng_est, self.config))
144
146 """Test injector stiffness constraints."""
147 # Constraint: (P_d_i - P_ch) >= eps_i * P_ch
148 # With eps_i = 1e-3 and P_ch = 2e6, required dp = 2e3 Pa
149
150 # Safe case: injector_dp >= required
151 x = self.create_state(**{IDX_P_D_F: 2.5e6, IDX_P_D_O: 3e6})
152 eng_est = self.create_estimate(
153 P_ch=2e6,
154 injector_dp_F=0.5e6, # 0.5 MPa > 2e3 Pa required
155 injector_dp_O=1.0e6, # 1.0 MPa > 2e3 Pa required
156 )
157
158 constraints = constraint_values(x, eng_est, self.config)
159 self.assertLess(constraints["injector_stiffness_F"], 0) # Negative = satisfied
160 self.assertLess(constraints["injector_stiffness_O"], 0)
161 self.assertGreater(constraints["injector_stiffness_margin_F"], 0)
162 self.assertGreater(constraints["injector_stiffness_margin_O"], 0)
163
164 self.assertTrue(is_safe(x, eng_est, self.config))
165
166 # Violation case: injector_dp < required
167 eng_est = self.create_estimate(
168 P_ch=2e6,
169 injector_dp_F=1e3, # 1e3 Pa < 2e3 Pa required
170 injector_dp_O=1e3, # 1e3 Pa < 2e3 Pa required
171 )
172 constraints = constraint_values(x, eng_est, self.config)
173 self.assertGreater(constraints["injector_stiffness_F"], 0) # Positive = violated
174 self.assertGreater(constraints["injector_stiffness_O"], 0)
175 self.assertLess(constraints["injector_stiffness_margin_F"], 0)
176 self.assertLess(constraints["injector_stiffness_margin_O"], 0)
177
178 self.assertFalse(is_safe(x, eng_est, self.config))
179
181 """Test headroom constraints for actuation effectiveness."""
182 # Constraint: (P_reg - P_u_i) >= dp_min for effective pressurization
183
184 # Sufficient headroom case
185 x = self.create_state(
186 **{IDX_P_REG: 5e6, IDX_P_U_F: 4e6, IDX_P_U_O: 4.5e6}
187 ) # Headroom: 1 MPa and 0.5 MPa > 0.05 MPa min
188 eng_est = self.create_estimate()
189
190 constraints = constraint_values(x, eng_est, self.config)
191 self.assertEqual(constraints["headroom_insufficient_F"], 0.0) # Sufficient
192 self.assertEqual(constraints["headroom_insufficient_O"], 0.0)
193 self.assertGreater(constraints["headroom_margin_F"], 0) # Positive margin
194 self.assertGreater(constraints["headroom_margin_O"], 0)
195
196 # Insufficient headroom case
197 x = self.create_state(
198 **{IDX_P_REG: 3.01e6, IDX_P_U_F: 3e6, IDX_P_U_O: 3e6}
199 ) # Headroom: 0.01 MPa < 0.05 MPa min
200 constraints = constraint_values(x, eng_est, self.config)
201 self.assertEqual(constraints["headroom_insufficient_F"], 1.0) # Insufficient
202 self.assertEqual(constraints["headroom_insufficient_O"], 1.0)
203 self.assertLess(constraints["headroom_margin_F"], 0) # Negative margin
204 self.assertLess(constraints["headroom_margin_O"], 0)
205
207 """Test that all constraints can be satisfied simultaneously."""
208 x = self.create_state(
209 **{
210 IDX_P_COPV: 30e6, # > 1 MPa min
211 IDX_P_REG: 24e6, # High regulator
212 IDX_P_U_F: 3e6, # < 10 MPa max
213 IDX_P_U_O: 3.5e6, # < 10 MPa max
214 IDX_P_D_F: 2.5e6, # Sufficient for injector stiffness
215 IDX_P_D_O: 3e6, # Sufficient for injector stiffness
216 }
217 )
218 eng_est = self.create_estimate(
219 P_ch=2e6,
220 MR=2.0, # Within [1.5, 3.0]
221 injector_dp_F=0.5e6, # > eps_i * P_ch
222 injector_dp_O=1.0e6, # > eps_i * P_ch
223 )
224
225 self.assertTrue(is_safe(x, eng_est, self.config))
226
227 constraints = constraint_values(x, eng_est, self.config)
228 summary = get_constraint_summary(constraints)
229 self.assertTrue(summary["safe"])
230 self.assertEqual(len(summary["violations"]), 0)
231
233 """Test detection of multiple constraint violations."""
234 x = self.create_state(
235 **{
236 IDX_P_COPV: 0.5e6, # < 1 MPa min (violation)
237 IDX_P_U_F: 12e6, # > 10 MPa max (violation)
238 IDX_P_U_O: 11e6, # > 10 MPa max (violation)
239 }
240 )
241 eng_est = self.create_estimate(
242 MR=0.5, # < 1.5 min (violation)
243 injector_dp_F=1e3, # < required (violation)
244 )
245
246 self.assertFalse(is_safe(x, eng_est, self.config))
247
248 constraints = constraint_values(x, eng_est, self.config)
249 summary = get_constraint_summary(constraints)
250 self.assertFalse(summary["safe"])
251 self.assertGreater(len(summary["violations"]), 1)
252
254 """Test handling of NaN values in engine estimate."""
255 x = self.create_state()
256 eng_est = self.create_estimate(
257 P_ch=np.nan,
258 MR=np.nan,
259 injector_dp_F=np.nan,
260 injector_dp_O=np.nan,
261 )
262
263 constraints = constraint_values(x, eng_est, self.config)
264
265 # MR constraints should be infinite (violation)
266 self.assertTrue(np.isinf(constraints["MR_min"]))
267 self.assertTrue(np.isinf(constraints["MR_max"]))
268
269 # Injector stiffness should be infinite (violation)
270 self.assertTrue(np.isinf(constraints["injector_stiffness_F"]))
271 self.assertTrue(np.isinf(constraints["injector_stiffness_O"]))
272
273 self.assertFalse(is_safe(x, eng_est, self.config))
274
276 """Test constraint summary generation."""
277 # Safe case
278 x = self.create_state()
279 eng_est = self.create_estimate()
280
281 constraints = constraint_values(x, eng_est, self.config)
282 summary = get_constraint_summary(constraints)
283
284 self.assertTrue(summary["safe"])
285 self.assertEqual(len(summary["violations"]), 0)
286 self.assertIn("margins", summary)
287 self.assertIn("headroom_flags", summary)
288 self.assertFalse(summary["headroom_flags"]["insufficient_F"])
289 self.assertFalse(summary["headroom_flags"]["insufficient_O"])
290
291 # Violation case
292 x = self.create_state(**{IDX_P_COPV: 0.5e6})
293 constraints = constraint_values(x, eng_est, self.config)
294 summary = get_constraint_summary(constraints)
295
296 self.assertFalse(summary["safe"])
297 self.assertIn("copv_min", summary["violations"])
298
300 """Test edge cases (boundary conditions)."""
301 # Exactly at COPV minimum
302 x = self.create_state(**{IDX_P_COPV: 1e6}) # Exactly at minimum
303 eng_est = self.create_estimate()
304 constraints = constraint_values(x, eng_est, self.config)
305 self.assertAlmostEqual(constraints["copv_min"], 0.0, places=3)
306 self.assertAlmostEqual(constraints["copv_margin"], 0.0, places=3)
307 self.assertTrue(is_safe(x, eng_est, self.config)) # Boundary is safe
308
309 # Exactly at ullage maximum
310 x = self.create_state(**{IDX_P_U_F: 10e6}) # Exactly at maximum
311 constraints = constraint_values(x, eng_est, self.config)
312 self.assertAlmostEqual(constraints["ullage_max_F"], 0.0, places=3)
313 self.assertAlmostEqual(constraints["ullage_margin_F"], 0.0, places=3)
314 self.assertTrue(is_safe(x, eng_est, self.config)) # Boundary is safe
315
316 # Exactly at MR bounds
317 eng_est = self.create_estimate(MR=1.5) # Exactly at minimum
318 constraints = constraint_values(x, eng_est, self.config)
319 self.assertAlmostEqual(constraints["MR_min"], 0.0, places=3)
320 self.assertAlmostEqual(constraints["MR_margin_low"], 0.0, places=3)
321 self.assertTrue(is_safe(x, eng_est, self.config))
322
323 eng_est = self.create_estimate(MR=3.0) # Exactly at maximum
324 constraints = constraint_values(x, eng_est, self.config)
325 self.assertAlmostEqual(constraints["MR_max"], 0.0, places=3)
326 self.assertAlmostEqual(constraints["MR_margin_high"], 0.0, places=3)
327 self.assertTrue(is_safe(x, eng_est, self.config))
328
329 # Exactly at headroom minimum
330 x = self.create_state(
331 **{IDX_P_REG: 3.05e6, IDX_P_U_F: 3e6}
332 ) # Headroom = 0.05 MPa (exactly at minimum)
333 constraints = constraint_values(x, eng_est, self.config)
334 self.assertAlmostEqual(constraints["headroom_margin_F"], 0.0, places=3)
335 self.assertEqual(constraints["headroom_insufficient_F"], 0.0) # Sufficient (>=)
336
337
338if __name__ == '__main__':
339 unittest.main()
340
void setUp(void)
Definition test_main.cpp:17