phytrace_sdk/models/domains/
simulation.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use validator::Validate;
6
7use crate::models::enums::{SimulationFidelity, SimulatorType};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct SimulationDomain {
12 #[serde(skip_serializing_if = "Option::is_none")]
14 pub is_simulated: Option<bool>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub simulator: Option<SimulatorInfo>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub scenario: Option<ScenarioInfo>,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub digital_twin: Option<DigitalTwinInfo>,
27
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub sim_time: Option<DateTime<Utc>>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub time_scale: Option<f64>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub step_count: Option<u64>,
39}
40
41#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct SimulatorInfo {
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub simulator_type: Option<SimulatorType>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub name: Option<String>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub version: Option<String>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub fidelity: Option<SimulationFidelity>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub physics_engine: Option<String>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub rendering_enabled: Option<bool>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub real_time_factor: Option<f64>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub step_rate_hz: Option<f64>,
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct ScenarioInfo {
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub scenario_id: Option<String>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub name: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub version: Option<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub world_name: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub start_time: Option<DateTime<Utc>>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub duration_sec: Option<f64>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub current_time_sec: Option<f64>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub parameters: Option<std::collections::HashMap<String, String>>,
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
119pub struct DigitalTwinInfo {
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub twin_id: Option<String>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub physical_asset_id: Option<String>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub sync_status: Option<String>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub last_sync: Option<DateTime<Utc>>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 #[validate(range(min = 0.0))]
139 pub sync_lag_ms: Option<f64>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub deviation_m: Option<f64>,
144}
145
146impl SimulationDomain {
147 pub fn simulated() -> Self {
149 Self {
150 is_simulated: Some(true),
151 ..Default::default()
152 }
153 }
154
155 pub fn with_simulator(simulator_type: SimulatorType, fidelity: SimulationFidelity) -> Self {
157 Self {
158 is_simulated: Some(true),
159 simulator: Some(SimulatorInfo {
160 simulator_type: Some(simulator_type),
161 fidelity: Some(fidelity),
162 ..Default::default()
163 }),
164 ..Default::default()
165 }
166 }
167
168 pub fn with_scenario(mut self, scenario: ScenarioInfo) -> Self {
170 self.scenario = Some(scenario);
171 self
172 }
173
174 pub fn with_digital_twin(mut self, twin: DigitalTwinInfo) -> Self {
176 self.digital_twin = Some(twin);
177 self
178 }
179
180 pub fn with_time_scale(mut self, scale: f64) -> Self {
182 self.time_scale = Some(scale);
183 self
184 }
185}
186
187impl ScenarioInfo {
188 pub fn new(scenario_id: impl Into<String>, name: impl Into<String>) -> Self {
190 Self {
191 scenario_id: Some(scenario_id.into()),
192 name: Some(name.into()),
193 start_time: Some(Utc::now()),
194 ..Default::default()
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_simulation_domain() {
205 let sim = SimulationDomain::with_simulator(SimulatorType::Gazebo, SimulationFidelity::High)
206 .with_scenario(ScenarioInfo::new("scenario-001", "Warehouse Test"));
207
208 assert_eq!(sim.is_simulated, Some(true));
209 assert!(sim.scenario.is_some());
210 }
211
212 #[test]
217 fn test_simulation_domain_default() {
218 let sim = SimulationDomain::default();
219 assert!(sim.is_simulated.is_none());
220 assert!(sim.simulator.is_none());
221 assert!(sim.scenario.is_none());
222 assert!(sim.digital_twin.is_none());
223 assert!(sim.sim_time.is_none());
224 assert!(sim.time_scale.is_none());
225 assert!(sim.step_count.is_none());
226 }
227
228 #[test]
229 fn test_simulation_domain_simulated() {
230 let sim = SimulationDomain::simulated();
231 assert_eq!(sim.is_simulated, Some(true));
232 }
233
234 #[test]
235 fn test_simulation_domain_with_simulator() {
236 let sim =
237 SimulationDomain::with_simulator(SimulatorType::IsaacSim, SimulationFidelity::Medium);
238
239 assert_eq!(sim.is_simulated, Some(true));
240 assert!(sim.simulator.is_some());
241 assert_eq!(
242 sim.simulator.as_ref().unwrap().simulator_type,
243 Some(SimulatorType::IsaacSim)
244 );
245 assert_eq!(
246 sim.simulator.as_ref().unwrap().fidelity,
247 Some(SimulationFidelity::Medium)
248 );
249 }
250
251 #[test]
252 fn test_simulation_domain_with_scenario() {
253 let scenario = ScenarioInfo::new("scn-001", "Navigation Test");
254 let sim = SimulationDomain::simulated().with_scenario(scenario);
255
256 assert!(sim.scenario.is_some());
257 assert_eq!(
258 sim.scenario.as_ref().unwrap().name,
259 Some("Navigation Test".to_string())
260 );
261 }
262
263 #[test]
264 fn test_simulation_domain_with_digital_twin() {
265 let twin = DigitalTwinInfo {
266 twin_id: Some("twin-001".to_string()),
267 physical_asset_id: Some("robot-001".to_string()),
268 sync_status: Some("synchronized".to_string()),
269 ..Default::default()
270 };
271
272 let sim = SimulationDomain::simulated().with_digital_twin(twin);
273 assert!(sim.digital_twin.is_some());
274 }
275
276 #[test]
277 fn test_simulation_domain_with_time_scale() {
278 let sim = SimulationDomain::simulated().with_time_scale(2.0);
279 assert_eq!(sim.time_scale, Some(2.0));
280 }
281
282 #[test]
283 fn test_simulation_domain_chained_builders() {
284 let sim = SimulationDomain::with_simulator(SimulatorType::Gazebo, SimulationFidelity::Low)
285 .with_scenario(ScenarioInfo::new("scn", "test"))
286 .with_digital_twin(DigitalTwinInfo::default())
287 .with_time_scale(1.5);
288
289 assert!(sim.simulator.is_some());
290 assert!(sim.scenario.is_some());
291 assert!(sim.digital_twin.is_some());
292 assert!(sim.time_scale.is_some());
293 }
294
295 #[test]
300 fn test_simulator_info_default() {
301 let info = SimulatorInfo::default();
302 assert!(info.simulator_type.is_none());
303 assert!(info.name.is_none());
304 assert!(info.version.is_none());
305 assert!(info.fidelity.is_none());
306 assert!(info.physics_engine.is_none());
307 assert!(info.rendering_enabled.is_none());
308 assert!(info.real_time_factor.is_none());
309 assert!(info.step_rate_hz.is_none());
310 }
311
312 #[test]
313 fn test_simulator_info_full() {
314 let info = SimulatorInfo {
315 simulator_type: Some(SimulatorType::Gazebo),
316 name: Some("Gazebo Fortress".to_string()),
317 version: Some("11.0".to_string()),
318 fidelity: Some(SimulationFidelity::High),
319 physics_engine: Some("ODE".to_string()),
320 rendering_enabled: Some(true),
321 real_time_factor: Some(0.95),
322 step_rate_hz: Some(1000.0),
323 };
324
325 assert_eq!(info.name, Some("Gazebo Fortress".to_string()));
326 assert_eq!(info.real_time_factor, Some(0.95));
327 }
328
329 #[test]
334 fn test_scenario_info_new() {
335 let scenario = ScenarioInfo::new("scn-001", "Warehouse Navigation");
336
337 assert_eq!(scenario.scenario_id, Some("scn-001".to_string()));
338 assert_eq!(scenario.name, Some("Warehouse Navigation".to_string()));
339 assert!(scenario.start_time.is_some());
340 }
341
342 #[test]
343 fn test_scenario_info_default() {
344 let scenario = ScenarioInfo::default();
345 assert!(scenario.scenario_id.is_none());
346 assert!(scenario.name.is_none());
347 assert!(scenario.version.is_none());
348 assert!(scenario.description.is_none());
349 assert!(scenario.world_name.is_none());
350 assert!(scenario.start_time.is_none());
351 assert!(scenario.duration_sec.is_none());
352 assert!(scenario.current_time_sec.is_none());
353 assert!(scenario.parameters.is_none());
354 }
355
356 #[test]
357 fn test_scenario_info_full() {
358 let mut params = std::collections::HashMap::new();
359 params.insert("robot_count".to_string(), "5".to_string());
360 params.insert("obstacle_density".to_string(), "medium".to_string());
361
362 let scenario = ScenarioInfo {
363 scenario_id: Some("scn-100".to_string()),
364 name: Some("Multi-Robot Test".to_string()),
365 version: Some("1.0".to_string()),
366 description: Some("Test with multiple robots".to_string()),
367 world_name: Some("warehouse_large".to_string()),
368 start_time: Some(Utc::now()),
369 duration_sec: Some(3600.0),
370 current_time_sec: Some(120.5),
371 parameters: Some(params),
372 };
373
374 assert_eq!(scenario.duration_sec, Some(3600.0));
375 assert!(scenario.parameters.is_some());
376 }
377
378 #[test]
383 fn test_digital_twin_info_default() {
384 let twin = DigitalTwinInfo::default();
385 assert!(twin.twin_id.is_none());
386 assert!(twin.physical_asset_id.is_none());
387 assert!(twin.sync_status.is_none());
388 assert!(twin.last_sync.is_none());
389 assert!(twin.sync_lag_ms.is_none());
390 assert!(twin.deviation_m.is_none());
391 }
392
393 #[test]
394 fn test_digital_twin_info_full() {
395 let twin = DigitalTwinInfo {
396 twin_id: Some("dt-001".to_string()),
397 physical_asset_id: Some("robot-physical-001".to_string()),
398 sync_status: Some("synchronized".to_string()),
399 last_sync: Some(Utc::now()),
400 sync_lag_ms: Some(15.0),
401 deviation_m: Some(0.02),
402 };
403
404 assert_eq!(twin.twin_id, Some("dt-001".to_string()));
405 assert_eq!(twin.sync_lag_ms, Some(15.0));
406 assert_eq!(twin.deviation_m, Some(0.02));
407 }
408
409 #[test]
414 fn test_simulation_domain_serialization_roundtrip() {
415 let sim = SimulationDomain::with_simulator(SimulatorType::Gazebo, SimulationFidelity::High)
416 .with_scenario(ScenarioInfo::new("scn-001", "Test"))
417 .with_time_scale(1.0);
418
419 let json = serde_json::to_string(&sim).unwrap();
420 let deserialized: SimulationDomain = serde_json::from_str(&json).unwrap();
421
422 assert_eq!(deserialized.is_simulated, Some(true));
423 assert!(deserialized.simulator.is_some());
424 assert!(deserialized.scenario.is_some());
425 }
426}