STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_robust_ddp_actuation.py
Go to the documentation of this file.
1"""Unit tests for actuation module."""
2
3import unittest
4import numpy as np
5
7 compute_actuation,
8 ActuationCommand,
9 ExecutionBackend,
10 quantize_duty,
11 enforce_dwell,
12 binary_actuation,
13 update_state_dwell_timers,
14 create_duty_grid,
15)
16from engine.control.robust_ddp.data_models import ControllerConfig, ControllerState
17
18
19class TestActuation(unittest.TestCase):
20 """Test actuation command generation."""
21
22 def setUp(self):
23 """Set up test fixtures."""
25 N=10,
26 dt=0.01,
27 dwell_time=0.05, # 50 ms minimum dwell
28 duty_quantization=0.01, # 1% steps
29 )
30
32 u_prev={"P_u_fuel": 0.5, "P_u_ox": 0.5},
33 dwell_timers={"P_u_fuel": 0.1, "P_u_ox": 0.1}, # Already satisfied
34 )
35
37 """Test duty quantization."""
38 # Test exact grid points
39 self.assertAlmostEqual(quantize_duty(0.0, 0.01), 0.0)
40 self.assertAlmostEqual(quantize_duty(0.5, 0.01), 0.5)
41 self.assertAlmostEqual(quantize_duty(1.0, 0.01), 1.0)
42
43 # Test rounding
44 self.assertAlmostEqual(quantize_duty(0.005, 0.01), 0.0) # Rounds down
45 self.assertAlmostEqual(quantize_duty(0.015, 0.01), 0.02) # Rounds up
46 self.assertAlmostEqual(quantize_duty(0.495, 0.01), 0.5) # Rounds up
47
48 # Test clamping
49 self.assertAlmostEqual(quantize_duty(-0.1, 0.01), 0.0)
50 self.assertAlmostEqual(quantize_duty(1.1, 0.01), 1.0)
51
52 # Test different quantization steps
53 self.assertAlmostEqual(quantize_duty(0.25, 0.1), 0.2) # 10% steps
54 self.assertAlmostEqual(quantize_duty(0.75, 0.1), 0.8)
55
57 """Test dwell enforcement when dwell time is satisfied."""
58 # Dwell time satisfied, control changed -> allow change
59 u_new = 0.7
60 u_prev = 0.5
61 dwell_timer = 0.1 # > dwell_time (0.05)
62 dwell_time = 0.05
63 dt = 0.01
64
65 u_output, dwell_timer_new = enforce_dwell(
66 u_new, u_prev, dwell_timer, dwell_time, dt
67 )
68
69 # Should allow change and reset timer
70 self.assertAlmostEqual(u_output, u_new)
71 self.assertAlmostEqual(dwell_timer_new, 0.0)
72
74 """Test dwell enforcement when dwell time is not satisfied."""
75 # Dwell time not satisfied, control changed -> keep previous
76 u_new = 0.7
77 u_prev = 0.5
78 dwell_timer = 0.02 # < dwell_time (0.05)
79 dwell_time = 0.05
80 dt = 0.01
81
82 u_output, dwell_timer_new = enforce_dwell(
83 u_new, u_prev, dwell_timer, dwell_time, dt
84 )
85
86 # Should keep previous control and increment timer
87 self.assertAlmostEqual(u_output, u_prev)
88 self.assertAlmostEqual(dwell_timer_new, dwell_timer + dt)
89
91 """Test dwell enforcement when control unchanged."""
92 # Control unchanged -> increment timer
93 u_new = 0.5
94 u_prev = 0.5
95 dwell_timer = 0.1
96 dwell_time = 0.05
97 dt = 0.01
98
99 u_output, dwell_timer_new = enforce_dwell(
100 u_new, u_prev, dwell_timer, dwell_time, dt
101 )
102
103 # Should allow (no change) and increment timer
104 self.assertAlmostEqual(u_output, u_new)
105 self.assertAlmostEqual(dwell_timer_new, dwell_timer + dt)
106
108 """Test binary actuation with threshold method."""
109 # Threshold method (no sigma-delta)
110 duty_output, onoff = binary_actuation(0.3, 0.01, self.cfg, None)
111 self.assertFalse(onoff)
112 self.assertEqual(duty_output, 0.0)
113
114 duty_output, onoff = binary_actuation(0.7, 0.01, self.cfg, None)
115 self.assertTrue(onoff)
116 self.assertEqual(duty_output, 1.0)
117
119 """Test binary actuation with sigma-delta modulation."""
120 # Sigma-delta: accumulator starts at 0
121 accumulator = 0.0
122
123 # Test with duty = 0.3 (should average to 30% over time)
124 # First few steps
125 outputs = []
126 for _ in range(10):
127 duty_output, onoff = binary_actuation(0.3, 0.01, self.cfg, accumulator)
128 outputs.append(duty_output)
129 # Update accumulator (simulate what would happen in state)
130 accumulator = accumulator + (0.3 - duty_output)
131
132 # Should have some 1s and some 0s (not all 0s or all 1s for 0.3 duty)
133 # Average should be close to 0.3
134 avg_duty = np.mean(outputs)
135 self.assertGreater(avg_duty, 0.0)
136 self.assertLess(avg_duty, 1.0)
137
139 """Test compute_actuation with PWM backend."""
140 u_relaxed = np.array([0.75, 0.45])
141
142 cmd = compute_actuation(
143 u_relaxed=u_relaxed,
144 state=self.state,
145 cfg=self.cfg,
146 backend=ExecutionBackend.PWM,
147 )
148
149 # Check structure
150 self.assertIsInstance(cmd, ActuationCommand)
151 self.assertEqual(cmd.backend, ExecutionBackend.PWM)
152
153 # Check quantization
154 self.assertAlmostEqual(cmd.u_F_quantized, 0.75)
155 self.assertAlmostEqual(cmd.u_O_quantized, 0.45)
156
157 # Check duty cycles (should match quantized values after dwell)
158 self.assertGreaterEqual(cmd.duty_F, 0.0)
159 self.assertLessEqual(cmd.duty_F, 1.0)
160 self.assertGreaterEqual(cmd.duty_O, 0.0)
161 self.assertLessEqual(cmd.duty_O, 1.0)
162
163 # Check on/off (should match duty > 0)
164 self.assertEqual(cmd.u_F_onoff, cmd.duty_F > 0.0)
165 self.assertEqual(cmd.u_O_onoff, cmd.duty_O > 0.0)
166
168 """Test compute_actuation with binary backend."""
169 u_relaxed = np.array([0.75, 0.25])
170
171 cmd = compute_actuation(
172 u_relaxed=u_relaxed,
173 state=self.state,
174 cfg=self.cfg,
175 backend=ExecutionBackend.BINARY,
176 )
177
178 # Check structure
179 self.assertIsInstance(cmd, ActuationCommand)
180 self.assertEqual(cmd.backend, ExecutionBackend.BINARY)
181
182 # Check duty cycles are binary (0 or 1)
183 self.assertIn(cmd.duty_F, [0.0, 1.0])
184 self.assertIn(cmd.duty_O, [0.0, 1.0])
185
186 # Check on/off matches duty
187 self.assertEqual(cmd.u_F_onoff, cmd.duty_F > 0.0)
188 self.assertEqual(cmd.u_O_onoff, cmd.duty_O > 0.0)
189
191 """Test compute_actuation enforces dwell time."""
192 # Set state with short dwell timer (not satisfied)
193 state_short_dwell = ControllerState(
194 u_prev={"P_u_fuel": 0.5, "P_u_ox": 0.5},
195 dwell_timers={"P_u_fuel": 0.02, "P_u_ox": 0.02}, # < dwell_time
196 )
197
198 # Try to change control
199 u_relaxed = np.array([0.8, 0.3]) # Different from 0.5
200
201 cmd = compute_actuation(
202 u_relaxed=u_relaxed,
203 state=state_short_dwell,
204 cfg=self.cfg,
205 backend=ExecutionBackend.PWM,
206 )
207
208 # Should keep previous control (0.5) because dwell not satisfied
209 # Note: This depends on how u_prev is interpreted
210 # For now, check that dwell timer is incremented
211 self.assertGreater(cmd.dwell_timer_F, state_short_dwell.dwell_timers["P_u_fuel"])
212
214 """Test compute_actuation allows change when dwell satisfied."""
215 # Set state with satisfied dwell timer
216 state_satisfied = ControllerState(
217 u_prev={"P_u_fuel": 0.5, "P_u_ox": 0.5},
218 dwell_timers={"P_u_fuel": 0.1, "P_u_ox": 0.1}, # > dwell_time
219 )
220
221 # Try to change control
222 u_relaxed = np.array([0.8, 0.3])
223
224 cmd = compute_actuation(
225 u_relaxed=u_relaxed,
226 state=state_satisfied,
227 cfg=self.cfg,
228 backend=ExecutionBackend.PWM,
229 )
230
231 # Should allow change and reset timer
232 self.assertAlmostEqual(cmd.dwell_timer_F, 0.0)
233 self.assertAlmostEqual(cmd.dwell_timer_O, 0.0)
234
236 """Test updating dwell timers in state."""
237 state = ControllerState(
238 u_prev={"P_u_fuel": 0.0, "P_u_ox": 0.0},
239 dwell_timers={"P_u_fuel": 0.0, "P_u_ox": 0.0},
240 )
241
242 cmd = ActuationCommand(
243 u_F_onoff=True,
244 u_O_onoff=False,
245 duty_F=0.75,
246 duty_O=0.25,
247 u_F_quantized=0.75,
248 u_O_quantized=0.25,
249 backend=ExecutionBackend.PWM,
250 dwell_timer_F=0.05,
251 dwell_timer_O=0.03,
252 )
253
254 update_state_dwell_timers(state, cmd)
255
256 # Check timers updated
257 self.assertAlmostEqual(state.dwell_timers["P_u_fuel"], 0.05)
258 self.assertAlmostEqual(state.dwell_timers["P_u_ox"], 0.03)
259
260 # Check u_prev updated
261 self.assertAlmostEqual(state.u_prev["P_u_fuel"], 0.75)
262 self.assertAlmostEqual(state.u_prev["P_u_ox"], 0.25)
263
265 """Test duty grid creation."""
266 grid = create_duty_grid(0.1)
267
268 # Should have 11 points: 0, 0.1, 0.2, ..., 1.0
269 self.assertEqual(len(grid), 11)
270 self.assertAlmostEqual(grid[0], 0.0)
271 self.assertAlmostEqual(grid[-1], 1.0)
272 self.assertAlmostEqual(grid[5], 0.5)
273
274 # Test smaller step
275 grid = create_duty_grid(0.01)
276 self.assertEqual(len(grid), 101)
277 self.assertAlmostEqual(grid[0], 0.0)
278 self.assertAlmostEqual(grid[-1], 1.0)
279
281 """Test duty quantization edge cases."""
282 # Very small quantization
283 self.assertAlmostEqual(quantize_duty(0.123456, 0.001), 0.123, places=3)
284
285 # Large quantization
286 self.assertAlmostEqual(quantize_duty(0.75, 0.5), 1.0) # Rounds to 1.0
287
288 # Zero quantization (should handle gracefully)
289 # This would cause division by zero, but duty_quantization should be > 0
290 # Just test that it works with very small values
291 self.assertAlmostEqual(quantize_duty(0.5, 1e-6), 0.5, places=6)
292
294 """Test compute_actuation clamps inputs."""
295 # Test values outside [0, 1]
296 u_relaxed = np.array([-0.1, 1.5])
297
298 cmd = compute_actuation(
299 u_relaxed=u_relaxed,
300 state=self.state,
301 cfg=self.cfg,
302 backend=ExecutionBackend.PWM,
303 )
304
305 # Should clamp to [0, 1]
306 self.assertGreaterEqual(cmd.duty_F, 0.0)
307 self.assertLessEqual(cmd.duty_F, 1.0)
308 self.assertGreaterEqual(cmd.duty_O, 0.0)
309 self.assertLessEqual(cmd.duty_O, 1.0)
310
311
312if __name__ == "__main__":
313 unittest.main()
314
void setUp(void)
Definition test_main.cpp:17