STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_robust_ddp_data_models.py
Go to the documentation of this file.
1"""Unit tests for robust DDP controller data models."""
2
3import unittest
4import json
5import tempfile
6from pathlib import Path
7import numpy as np
8from datetime import datetime
9
11 Measurement,
12 NavState,
13 Command,
14 CommandType,
15 ControllerConfig,
16 ControllerState,
17)
19 load_config,
20 save_config,
21 get_default_config,
22)
23
24
25class TestMeasurement(unittest.TestCase):
26 """Test Measurement dataclass."""
27
29 """Test creating a valid measurement."""
30 meas = Measurement(
31 P_copv=5e6,
32 P_reg=4e6,
33 P_u_fuel=3e6,
34 P_u_ox=3.5e6,
35 P_d_fuel=2e6,
36 P_d_ox=2.5e6,
37 timestamp=1234567890.0
38 )
39 self.assertEqual(meas.P_copv, 5e6)
40 self.assertEqual(meas.timestamp, 1234567890.0)
41
43 """Test measurement with datetime timestamp."""
44 dt = datetime.now()
45 meas = Measurement(
46 P_copv=5e6,
47 P_reg=4e6,
48 P_u_fuel=3e6,
49 P_u_ox=3.5e6,
50 P_d_fuel=2e6,
51 P_d_ox=2.5e6,
52 timestamp=dt
53 )
54 # Should be converted to float
55 self.assertIsInstance(meas.timestamp, float)
56
58 """Test that negative pressures raise ValueError."""
59 with self.assertRaises(ValueError):
61 P_copv=-1e6, # Negative!
62 P_reg=4e6,
63 P_u_fuel=3e6,
64 P_u_ox=3.5e6,
65 P_d_fuel=2e6,
66 P_d_ox=2.5e6
67 )
68
70 """Test that infinite pressures raise ValueError."""
71 with self.assertRaises(ValueError):
73 P_copv=np.inf,
74 P_reg=4e6,
75 P_u_fuel=3e6,
76 P_u_ox=3.5e6,
77 P_d_fuel=2e6,
78 P_d_ox=2.5e6
79 )
80
82 """Test measurement serialization to/from dict."""
83 meas = Measurement(
84 P_copv=5e6,
85 P_reg=4e6,
86 P_u_fuel=3e6,
87 P_u_ox=3.5e6,
88 P_d_fuel=2e6,
89 P_d_ox=2.5e6,
90 timestamp=1234567890.0
91 )
92 data = meas.to_dict()
93 meas2 = Measurement.from_dict(data)
94 self.assertEqual(meas.P_copv, meas2.P_copv)
95 self.assertEqual(meas.timestamp, meas2.timestamp)
96
97
98class TestNavState(unittest.TestCase):
99 """Test NavState dataclass."""
100
102 """Test creating a valid nav state."""
103 nav = NavState(h=100.0, vz=10.0, theta=0.1)
104 self.assertEqual(nav.h, 100.0)
105 self.assertEqual(nav.vz, 10.0)
106 self.assertEqual(nav.theta, 0.1)
107
109 """Test nav state with quaternion."""
110 quat = [1.0, 0.0, 0.0, 0.0] # Identity quaternion
111 nav = NavState(h=100.0, vz=10.0, quaternion=quat)
112 self.assertEqual(nav.quaternion, quat)
113
115 """Test nav state with mass estimate."""
116 nav = NavState(h=100.0, vz=10.0, mass_estimate=500.0)
117 self.assertEqual(nav.mass_estimate, 500.0)
118
120 """Test that negative altitude raises ValueError."""
121 with self.assertRaises(ValueError):
122 NavState(h=-10.0, vz=10.0)
123
125 """Test that invalid quaternion raises ValueError."""
126 with self.assertRaises(ValueError):
127 NavState(h=100.0, vz=10.0, quaternion=[1.0, 0.0]) # Wrong length
128
129 with self.assertRaises(ValueError):
130 NavState(h=100.0, vz=10.0, quaternion=[2.0, 0.0, 0.0, 0.0]) # Not normalized
131
133 """Test nav state serialization."""
134 nav = NavState(h=100.0, vz=10.0, theta=0.1, mass_estimate=500.0)
135 data = nav.to_dict()
136 nav2 = NavState.from_dict(data)
137 self.assertEqual(nav.h, nav2.h)
138 self.assertEqual(nav.mass_estimate, nav2.mass_estimate)
139
140
141class TestCommand(unittest.TestCase):
142 """Test Command dataclass."""
143
145 """Test command with constant thrust."""
146 cmd = Command(
147 command_type=CommandType.THRUST_DESIRED,
148 thrust_desired=1000.0
149 )
150 self.assertEqual(cmd.command_type, CommandType.THRUST_DESIRED)
151 self.assertEqual(cmd.thrust_desired, 1000.0)
152
154 """Test command with time-varying thrust profile."""
155 profile = [(0.0, 1000.0), (1.0, 1500.0), (2.0, 2000.0)]
156 cmd = Command(
157 command_type=CommandType.THRUST_DESIRED,
158 thrust_desired=profile
159 )
160 self.assertEqual(cmd.thrust_desired, profile)
161
163 """Test command with altitude goal."""
164 cmd = Command(
165 command_type=CommandType.ALTITUDE_GOAL,
166 altitude_goal=1000.0
167 )
168 self.assertEqual(cmd.command_type, CommandType.ALTITUDE_GOAL)
169 self.assertEqual(cmd.altitude_goal, 1000.0)
170
172 """Test that missing thrust raises ValueError."""
173 with self.assertRaises(ValueError):
174 Command(
175 command_type=CommandType.THRUST_DESIRED,
176 thrust_desired=None
177 )
178
180 """Test that missing altitude raises ValueError."""
181 with self.assertRaises(ValueError):
182 Command(
183 command_type=CommandType.ALTITUDE_GOAL,
184 altitude_goal=None
185 )
186
188 """Test that negative thrust raises ValueError."""
189 with self.assertRaises(ValueError):
190 Command(
191 command_type=CommandType.THRUST_DESIRED,
192 thrust_desired=-100.0
193 )
194
196 """Test command serialization."""
197 cmd = Command(
198 command_type=CommandType.THRUST_DESIRED,
199 thrust_desired=1000.0
200 )
201 data = cmd.to_dict()
202 cmd2 = Command.from_dict(data)
203 self.assertEqual(cmd.command_type, cmd2.command_type)
204 self.assertEqual(cmd.thrust_desired, cmd2.thrust_desired)
205
206
207class TestControllerConfig(unittest.TestCase):
208 """Test ControllerConfig dataclass."""
209
211 """Test creating a valid config."""
212 config = ControllerConfig()
213 self.assertEqual(config.N, 50)
214 self.assertEqual(config.dt, 0.01)
215 self.assertEqual(config.MR_min, 1.5)
216 self.assertEqual(config.MR_max, 3.0)
217
219 """Test config validation."""
220 with self.assertRaises(ValueError):
221 ControllerConfig(N=-1) # Negative N
222
223 with self.assertRaises(ValueError):
224 ControllerConfig(dt=-0.01) # Negative dt
225
226 with self.assertRaises(ValueError):
227 ControllerConfig(MR_min=3.0, MR_max=1.5) # min >= max
228
230 """Test config serialization."""
231 config = ControllerConfig(N=100, dt=0.02)
232 data = config.to_dict()
233 config2 = ControllerConfig.from_dict(data)
234 self.assertEqual(config.N, config2.N)
235 self.assertEqual(config.dt, config2.dt)
236
238 """Test config JSON save/load."""
239 config = ControllerConfig(N=75, dt=0.015)
240 with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
241 filepath = f.name
242
243 try:
244 config.to_json(filepath)
245 config2 = ControllerConfig.from_json(filepath)
246 self.assertEqual(config.N, config2.N)
247 self.assertEqual(config.dt, config2.dt)
248 finally:
249 Path(filepath).unlink()
250
252 """Test config YAML save/load."""
253 config = ControllerConfig(N=75, dt=0.015)
254 with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
255 filepath = f.name
256
257 try:
258 config.to_yaml(filepath)
259 config2 = ControllerConfig.from_yaml(filepath)
260 self.assertEqual(config.N, config2.N)
261 self.assertEqual(config.dt, config2.dt)
262 finally:
263 Path(filepath).unlink()
264
265
266class TestControllerState(unittest.TestCase):
267 """Test ControllerState dataclass."""
268
270 """Test creating a valid state."""
271 state = ControllerState()
272 self.assertEqual(state.u_prev["P_u_fuel"], 0.0)
273 self.assertEqual(state.beta, 0.0)
274 self.assertEqual(state.iteration_count, 0)
275
277 """Test state reset behavior."""
278 state = ControllerState()
279 state.u_prev["P_u_fuel"] = 3e6
280 state.beta = 0.5
281 state.iteration_count = 5
282 state.last_cost = 100.0
283
284 state.reset()
285
286 self.assertEqual(state.u_prev["P_u_fuel"], 0.0)
287 self.assertEqual(state.beta, 0.0)
288 self.assertEqual(state.iteration_count, 0)
289 self.assertEqual(state.last_cost, float('inf'))
290
292 """Test state serialization."""
293 state = ControllerState()
294 state.u_prev["P_u_fuel"] = 3e6
295 state.beta = 0.5
296
297 data = state.to_dict()
298 state2 = ControllerState.from_dict(data)
299
300 self.assertEqual(state.u_prev["P_u_fuel"], state2.u_prev["P_u_fuel"])
301 self.assertEqual(state.beta, state2.beta)
302
303
304class TestConfigLoader(unittest.TestCase):
305 """Test config loader functions."""
306
308 """Test getting default config."""
309 config = get_default_config()
310 self.assertIsInstance(config, ControllerConfig)
311 self.assertEqual(config.N, 50)
312
314 """Test loading None returns default."""
315 config = load_config(None)
316 self.assertIsInstance(config, ControllerConfig)
317
319 """Test loading config from JSON."""
320 config = ControllerConfig(N=100, dt=0.02)
321 with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
322 filepath = f.name
323
324 try:
325 save_config(config, filepath)
326 config2 = load_config(filepath)
327 self.assertEqual(config.N, config2.N)
328 finally:
329 Path(filepath).unlink()
330
332 """Test loading config from YAML."""
333 config = ControllerConfig(N=100, dt=0.02)
334 with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
335 filepath = f.name
336
337 try:
338 save_config(config, filepath)
339 config2 = load_config(filepath)
340 self.assertEqual(config.N, config2.N)
341 finally:
342 Path(filepath).unlink()
343
345 """Test loading config with invalid format raises error."""
346 with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
347 filepath = f.name
348
349 try:
350 with self.assertRaises(ValueError):
351 load_config(filepath)
352 finally:
353 Path(filepath).unlink()
354
356 """Test loading missing file raises FileNotFoundError."""
357 with self.assertRaises(FileNotFoundError):
358 load_config("/nonexistent/path/config.json")
359
360
361if __name__ == '__main__':
362 unittest.main()
363