1"""Unit tests for robust DDP controller data models."""
6from pathlib
import Path
8from datetime
import datetime
26 """Test Measurement dataclass."""
29 """Test creating a valid measurement."""
37 timestamp=1234567890.0
39 self.assertEqual(meas.P_copv, 5e6)
40 self.assertEqual(meas.timestamp, 1234567890.0)
43 """Test measurement with datetime timestamp."""
55 self.assertIsInstance(meas.timestamp, float)
58 """Test that negative pressures raise ValueError."""
59 with self.assertRaises(ValueError):
70 """Test that infinite pressures raise ValueError."""
71 with self.assertRaises(ValueError):
82 """Test measurement serialization to/from dict."""
90 timestamp=1234567890.0
93 meas2 = Measurement.from_dict(data)
94 self.assertEqual(meas.P_copv, meas2.P_copv)
95 self.assertEqual(meas.timestamp, meas2.timestamp)
99 """Test NavState dataclass."""
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)
109 """Test nav state with quaternion."""
110 quat = [1.0, 0.0, 0.0, 0.0]
111 nav =
NavState(h=100.0, vz=10.0, quaternion=quat)
112 self.assertEqual(nav.quaternion, quat)
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)
120 """Test that negative altitude raises ValueError."""
121 with self.assertRaises(ValueError):
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])
129 with self.assertRaises(ValueError):
130 NavState(h=100.0, vz=10.0, quaternion=[2.0, 0.0, 0.0, 0.0])
133 """Test nav state serialization."""
134 nav =
NavState(h=100.0, vz=10.0, theta=0.1, mass_estimate=500.0)
136 nav2 = NavState.from_dict(data)
137 self.assertEqual(nav.h, nav2.h)
138 self.assertEqual(nav.mass_estimate, nav2.mass_estimate)
142 """Test Command dataclass."""
145 """Test command with constant thrust."""
147 command_type=CommandType.THRUST_DESIRED,
148 thrust_desired=1000.0
150 self.assertEqual(cmd.command_type, CommandType.THRUST_DESIRED)
151 self.assertEqual(cmd.thrust_desired, 1000.0)
154 """Test command with time-varying thrust profile."""
155 profile = [(0.0, 1000.0), (1.0, 1500.0), (2.0, 2000.0)]
157 command_type=CommandType.THRUST_DESIRED,
158 thrust_desired=profile
160 self.assertEqual(cmd.thrust_desired, profile)
163 """Test command with altitude goal."""
165 command_type=CommandType.ALTITUDE_GOAL,
168 self.assertEqual(cmd.command_type, CommandType.ALTITUDE_GOAL)
169 self.assertEqual(cmd.altitude_goal, 1000.0)
172 """Test that missing thrust raises ValueError."""
173 with self.assertRaises(ValueError):
175 command_type=CommandType.THRUST_DESIRED,
180 """Test that missing altitude raises ValueError."""
181 with self.assertRaises(ValueError):
183 command_type=CommandType.ALTITUDE_GOAL,
188 """Test that negative thrust raises ValueError."""
189 with self.assertRaises(ValueError):
191 command_type=CommandType.THRUST_DESIRED,
192 thrust_desired=-100.0
196 """Test command serialization."""
198 command_type=CommandType.THRUST_DESIRED,
199 thrust_desired=1000.0
202 cmd2 = Command.from_dict(data)
203 self.assertEqual(cmd.command_type, cmd2.command_type)
204 self.assertEqual(cmd.thrust_desired, cmd2.thrust_desired)
208 """Test ControllerConfig dataclass."""
211 """Test creating a valid config."""
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)
219 """Test config validation."""
220 with self.assertRaises(ValueError):
223 with self.assertRaises(ValueError):
226 with self.assertRaises(ValueError):
230 """Test config serialization."""
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)
238 """Test config JSON save/load."""
240 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.json', delete=
False)
as f:
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)
249 Path(filepath).unlink()
252 """Test config YAML save/load."""
254 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.yaml', delete=
False)
as f:
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)
263 Path(filepath).unlink()
267 """Test ControllerState dataclass."""
270 """Test creating a valid state."""
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)
277 """Test state reset behavior."""
279 state.u_prev[
"P_u_fuel"] = 3e6
281 state.iteration_count = 5
282 state.last_cost = 100.0
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'))
292 """Test state serialization."""
294 state.u_prev[
"P_u_fuel"] = 3e6
297 data = state.to_dict()
298 state2 = ControllerState.from_dict(data)
300 self.assertEqual(state.u_prev[
"P_u_fuel"], state2.u_prev[
"P_u_fuel"])
301 self.assertEqual(state.beta, state2.beta)
305 """Test config loader functions."""
308 """Test getting default config."""
309 config = get_default_config()
310 self.assertIsInstance(config, ControllerConfig)
311 self.assertEqual(config.N, 50)
314 """Test loading None returns default."""
315 config = load_config(
None)
316 self.assertIsInstance(config, ControllerConfig)
319 """Test loading config from JSON."""
321 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.json', delete=
False)
as f:
325 save_config(config, filepath)
326 config2 = load_config(filepath)
327 self.assertEqual(config.N, config2.N)
329 Path(filepath).unlink()
332 """Test loading config from YAML."""
334 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.yaml', delete=
False)
as f:
338 save_config(config, filepath)
339 config2 = load_config(filepath)
340 self.assertEqual(config.N, config2.N)
342 Path(filepath).unlink()
345 """Test loading config with invalid format raises error."""
346 with tempfile.NamedTemporaryFile(mode=
'w', suffix=
'.txt', delete=
False)
as f:
350 with self.assertRaises(ValueError):
351 load_config(filepath)
353 Path(filepath).unlink()
356 """Test loading missing file raises FileNotFoundError."""
357 with self.assertRaises(FileNotFoundError):
358 load_config(
"/nonexistent/path/config.json")
361if __name__ ==
'__main__':
test_command_thrust_profile(self)
test_command_altitude_goal(self)
test_command_serialization(self)
test_command_missing_altitude(self)
test_command_missing_thrust(self)
test_command_negative_thrust(self)
test_command_thrust_constant(self)
test_load_config_missing_file(self)
test_load_config_json(self)
test_load_config_invalid_format(self)
test_load_config_yaml(self)
test_load_config_none(self)
test_get_default_config(self)
test_config_yaml_io(self)
test_config_validation(self)
test_config_creation(self)
test_config_json_io(self)
test_config_serialization(self)
test_state_serialization(self)
test_state_creation(self)
test_measurement_serialization(self)
test_measurement_negative_pressure(self)
test_measurement_datetime(self)
test_measurement_infinite_pressure(self)
test_measurement_creation(self)
test_navstate_with_mass(self)
test_navstate_invalid_quaternion(self)
test_navstate_negative_altitude(self)
test_navstate_serialization(self)
test_navstate_with_quaternion(self)
test_navstate_creation(self)