phytrace_sdk/models/domains/
simulation.rs

1//! Simulation Domain - Simulator and scenario information.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use validator::Validate;
6
7use crate::models::enums::{SimulationFidelity, SimulatorType};
8
9/// Simulation domain containing simulator and scenario information.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct SimulationDomain {
12    /// Whether this is simulated data
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub is_simulated: Option<bool>,
15
16    /// Simulator information
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub simulator: Option<SimulatorInfo>,
19
20    /// Scenario information
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub scenario: Option<ScenarioInfo>,
23
24    /// Digital twin information
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub digital_twin: Option<DigitalTwinInfo>,
27
28    /// Simulation time (may differ from wall clock)
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub sim_time: Option<DateTime<Utc>>,
31
32    /// Time scale factor (1.0 = real-time)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub time_scale: Option<f64>,
35
36    /// Simulation step count
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub step_count: Option<u64>,
39}
40
41/// Simulator information.
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43pub struct SimulatorInfo {
44    /// Simulator type
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub simulator_type: Option<SimulatorType>,
47
48    /// Simulator name
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub name: Option<String>,
51
52    /// Simulator version
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub version: Option<String>,
55
56    /// Simulation fidelity
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub fidelity: Option<SimulationFidelity>,
59
60    /// Physics engine
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub physics_engine: Option<String>,
63
64    /// Rendering enabled
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub rendering_enabled: Option<bool>,
67
68    /// Real-time factor achieved
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub real_time_factor: Option<f64>,
71
72    /// Step rate (Hz)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub step_rate_hz: Option<f64>,
75}
76
77/// Scenario information.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct ScenarioInfo {
80    /// Scenario ID
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub scenario_id: Option<String>,
83
84    /// Scenario name
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub name: Option<String>,
87
88    /// Scenario version
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub version: Option<String>,
91
92    /// Scenario description
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub description: Option<String>,
95
96    /// World/map name
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub world_name: Option<String>,
99
100    /// Scenario start time
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub start_time: Option<DateTime<Utc>>,
103
104    /// Scenario duration (seconds)
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub duration_sec: Option<f64>,
107
108    /// Current scenario time (seconds)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub current_time_sec: Option<f64>,
111
112    /// Scenario parameters
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub parameters: Option<std::collections::HashMap<String, String>>,
115}
116
117/// Digital twin information.
118#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
119pub struct DigitalTwinInfo {
120    /// Digital twin ID
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub twin_id: Option<String>,
123
124    /// Physical asset ID
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub physical_asset_id: Option<String>,
127
128    /// Synchronization status
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub sync_status: Option<String>,
131
132    /// Last sync time
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub last_sync: Option<DateTime<Utc>>,
135
136    /// Sync lag (ms)
137    #[serde(skip_serializing_if = "Option::is_none")]
138    #[validate(range(min = 0.0))]
139    pub sync_lag_ms: Option<f64>,
140
141    /// Deviation from physical (if tracked)
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub deviation_m: Option<f64>,
144}
145
146impl SimulationDomain {
147    /// Create simulation domain (marking as simulated).
148    pub fn simulated() -> Self {
149        Self {
150            is_simulated: Some(true),
151            ..Default::default()
152        }
153    }
154
155    /// Create with simulator info.
156    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    /// Add scenario info.
169    pub fn with_scenario(mut self, scenario: ScenarioInfo) -> Self {
170        self.scenario = Some(scenario);
171        self
172    }
173
174    /// Add digital twin info.
175    pub fn with_digital_twin(mut self, twin: DigitalTwinInfo) -> Self {
176        self.digital_twin = Some(twin);
177        self
178    }
179
180    /// Set time scale.
181    pub fn with_time_scale(mut self, scale: f64) -> Self {
182        self.time_scale = Some(scale);
183        self
184    }
185}
186
187impl ScenarioInfo {
188    /// Create a new scenario.
189    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    // ==========================================================================
213    // SimulationDomain Additional Tests
214    // ==========================================================================
215
216    #[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    // ==========================================================================
296    // SimulatorInfo Tests
297    // ==========================================================================
298
299    #[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    // ==========================================================================
330    // ScenarioInfo Tests
331    // ==========================================================================
332
333    #[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    // ==========================================================================
379    // DigitalTwinInfo Tests
380    // ==========================================================================
381
382    #[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    // ==========================================================================
410    // Serialization Roundtrip Tests
411    // ==========================================================================
412
413    #[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}