1use crate::error::{PhysicsError, PhysicsResult};
21use crate::simulation::SimulationParameters;
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::time::{SystemTime, UNIX_EPOCH};
25use uuid::Uuid;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct TwinId(String);
34
35impl TwinId {
36 pub fn new() -> Self {
38 Self(Uuid::new_v4().to_string())
39 }
40
41 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45}
46
47impl Default for TwinId {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl std::fmt::Display for TwinId {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}", self.0)
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SensorReading {
66 pub id: String,
68 pub quantity: String,
70 pub value: f64,
72 pub unit: String,
74 pub timestamp_ms: u64,
76 pub uncertainty: Option<f64>,
78}
79
80impl SensorReading {
81 pub fn new(
83 id: impl Into<String>,
84 quantity: impl Into<String>,
85 value: f64,
86 unit: impl Into<String>,
87 ) -> Self {
88 let timestamp_ms = SystemTime::now()
89 .duration_since(UNIX_EPOCH)
90 .map(|d| d.as_millis() as u64)
91 .unwrap_or(0);
92 Self {
93 id: id.into(),
94 quantity: quantity.into(),
95 value,
96 unit: unit.into(),
97 timestamp_ms,
98 uncertainty: None,
99 }
100 }
101
102 pub fn with_uncertainty(mut self, sigma: f64) -> Self {
104 self.uncertainty = Some(sigma);
105 self
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct ActuatorCommand {
112 pub id: String,
114 pub target_quantity: String,
116 pub target_value: f64,
118 pub applied: bool,
120}
121
122impl ActuatorCommand {
123 pub fn new(
125 id: impl Into<String>,
126 target_quantity: impl Into<String>,
127 target_value: f64,
128 ) -> Self {
129 Self {
130 id: id.into(),
131 target_quantity: target_quantity.into(),
132 target_value,
133 applied: false,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct QuantityDeviation {
145 pub quantity: String,
147 pub physical_value: f64,
149 pub digital_value: f64,
151 pub deviation: f64,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SyncAnomaly {
158 pub quantity: String,
160 pub description: String,
162 pub severity: f64,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SyncReport {
169 pub synced_quantities: Vec<String>,
171 pub deviations: Vec<QuantityDeviation>,
173 pub anomalies: Vec<SyncAnomaly>,
175 pub sync_time_ms: u64,
177}
178
179impl SyncReport {
180 fn new_now() -> Self {
181 let sync_time_ms = SystemTime::now()
182 .duration_since(UNIX_EPOCH)
183 .map(|d| d.as_millis() as u64)
184 .unwrap_or(0);
185 Self {
186 synced_quantities: Vec::new(),
187 deviations: Vec::new(),
188 anomalies: Vec::new(),
189 sync_time_ms,
190 }
191 }
192
193 pub fn is_healthy(&self, threshold: f64) -> bool {
195 self.anomalies.iter().all(|a| a.severity < threshold)
196 }
197}
198
199const ANOMALY_DEVIATION_THRESHOLD: f64 = 0.15;
206
207pub struct DigitalTwin {
213 pub entity_iri: String,
215 pub twin_type: String,
217 pub model_state: HashMap<String, f64>,
220 pub state: HashMap<String, f64>,
222 pub sensors: Vec<SensorReading>,
224 pub actuators: Vec<ActuatorCommand>,
226 pub last_sync: SystemTime,
228}
229
230impl DigitalTwin {
231 pub fn new(entity_iri: impl Into<String>, twin_type: impl Into<String>) -> Self {
233 Self {
234 entity_iri: entity_iri.into(),
235 twin_type: twin_type.into(),
236 model_state: HashMap::new(),
237 state: HashMap::new(),
238 sensors: Vec::new(),
239 actuators: Vec::new(),
240 last_sync: UNIX_EPOCH,
241 }
242 }
243
244 pub fn entity_iri(&self) -> &str {
246 &self.entity_iri
247 }
248
249 pub fn set_model_value(&mut self, quantity: impl Into<String>, value: f64) {
251 let qty = quantity.into();
252 self.model_state.insert(qty.clone(), value);
253 self.state.insert(qty, value);
254 }
255
256 pub fn buffer_sensor(&mut self, reading: SensorReading) {
263 self.sensors.retain(|s| s.id != reading.id);
264 self.sensors.push(reading);
265 }
266
267 pub fn apply_sensor_to_state(&mut self) {
270 for sensor in &self.sensors {
271 self.state.insert(sensor.quantity.clone(), sensor.value);
272 }
273 }
274
275 pub fn queue_actuator(&mut self, cmd: ActuatorCommand) {
278 self.model_state
279 .insert(cmd.target_quantity.clone(), cmd.target_value);
280 self.actuators.retain(|a| a.id != cmd.id);
281 self.actuators.push(cmd);
282 }
283
284 pub fn apply_pending_actuators(&mut self) {
287 for cmd in &mut self.actuators {
288 if !cmd.applied {
289 self.model_state
290 .insert(cmd.target_quantity.clone(), cmd.target_value);
291 self.state
292 .insert(cmd.target_quantity.clone(), cmd.target_value);
293 cmd.applied = true;
294 }
295 }
296 }
297
298 pub async fn extract_simulation_params(&self) -> PhysicsResult<SimulationParameters> {
300 let initial_conditions = self
301 .model_state
302 .iter()
303 .map(|(k, v)| {
304 (
305 k.clone(),
306 crate::simulation::parameter_extraction::PhysicalQuantity {
307 value: *v,
308 unit: String::from("SI"),
309 uncertainty: None,
310 },
311 )
312 })
313 .collect();
314
315 Ok(SimulationParameters {
316 entity_iri: self.entity_iri.clone(),
317 simulation_type: self.twin_type.clone(),
318 initial_conditions,
319 boundary_conditions: Vec::new(),
320 time_span: (0.0, 100.0),
321 time_steps: 100,
322 material_properties: HashMap::new(),
323 constraints: Vec::new(),
324 })
325 }
326}
327
328pub struct DigitalTwinManager {
334 twins: HashMap<TwinId, DigitalTwin>,
335 anomaly_threshold: f64,
337}
338
339impl DigitalTwinManager {
340 pub fn new() -> Self {
342 Self {
343 twins: HashMap::new(),
344 anomaly_threshold: ANOMALY_DEVIATION_THRESHOLD,
345 }
346 }
347
348 pub fn with_anomaly_threshold(mut self, threshold: f64) -> Self {
350 self.anomaly_threshold = threshold;
351 self
352 }
353
354 pub fn register(
359 &mut self,
360 name: impl Into<String>,
361 model_uri: impl Into<String>,
362 ) -> PhysicsResult<TwinId> {
363 let id = TwinId::new();
364 let twin = DigitalTwin::new(model_uri, name);
365 self.twins.insert(id.clone(), twin);
366 Ok(id)
367 }
368
369 pub fn update_from_sensor(
374 &mut self,
375 twin_id: &TwinId,
376 reading: SensorReading,
377 ) -> PhysicsResult<()> {
378 let twin = self
379 .twins
380 .get_mut(twin_id)
381 .ok_or_else(|| PhysicsError::Internal(format!("twin not found: {twin_id}")))?;
382 twin.buffer_sensor(reading);
383 Ok(())
384 }
385
386 pub fn get_state(&self, twin_id: &TwinId) -> PhysicsResult<HashMap<String, f64>> {
388 let twin = self
389 .twins
390 .get(twin_id)
391 .ok_or_else(|| PhysicsError::Internal(format!("twin not found: {twin_id}")))?;
392 Ok(twin.state.clone())
393 }
394
395 pub fn synchronize(&mut self, twin_id: &TwinId) -> PhysicsResult<SyncReport> {
404 let threshold = self.anomaly_threshold;
405 let twin = self
406 .twins
407 .get_mut(twin_id)
408 .ok_or_else(|| PhysicsError::Internal(format!("twin not found: {twin_id}")))?;
409
410 twin.apply_pending_actuators();
412
413 let mut report = SyncReport::new_now();
415
416 for sensor in &twin.sensors {
417 let digital_value = *twin.model_state.get(&sensor.quantity).unwrap_or(&0.0);
419 let deviation = (sensor.value - digital_value).abs();
420 let relative = if digital_value.abs() > 1e-12 {
421 deviation / digital_value.abs()
422 } else {
423 deviation
425 };
426
427 report.synced_quantities.push(sensor.quantity.clone());
428 report.deviations.push(QuantityDeviation {
429 quantity: sensor.quantity.clone(),
430 physical_value: sensor.value,
431 digital_value,
432 deviation,
433 });
434
435 if relative > threshold {
436 let severity = (relative / (threshold + f64::EPSILON)).clamp(0.0, 1.0);
437 report.anomalies.push(SyncAnomaly {
438 quantity: sensor.quantity.clone(),
439 description: format!(
440 "Deviation {deviation:.4} ({:.1}%) exceeds threshold {:.1}%",
441 relative * 100.0,
442 threshold * 100.0
443 ),
444 severity,
445 });
446 }
447 }
448
449 twin.apply_sensor_to_state();
451
452 twin.last_sync = SystemTime::now();
454
455 Ok(report)
456 }
457
458 pub fn get_twin(&self, twin_id: &TwinId) -> Option<&DigitalTwin> {
460 self.twins.get(twin_id)
461 }
462
463 pub fn get_twin_mut(&mut self, twin_id: &TwinId) -> Option<&mut DigitalTwin> {
465 self.twins.get_mut(twin_id)
466 }
467
468 pub fn len(&self) -> usize {
470 self.twins.len()
471 }
472
473 pub fn is_empty(&self) -> bool {
475 self.twins.is_empty()
476 }
477}
478
479impl Default for DigitalTwinManager {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct DtdlTelemetry {
492 pub name: String,
494 pub schema: String,
496 pub unit: Option<String>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct DtdlProperty {
503 pub name: String,
504 pub schema: String,
505 pub writable: bool,
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct DtdlComponent {
511 pub name: String,
512 pub schema: String,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct DtdlRelationship {
518 pub name: String,
519 pub target: Option<String>,
520 pub max_multiplicity: Option<u32>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct DtdlModel {
526 pub id: String,
528 pub display_name: String,
530 pub telemetry: Vec<DtdlTelemetry>,
532 pub properties: Vec<DtdlProperty>,
534 pub components: Vec<DtdlComponent>,
536 pub relationships: Vec<DtdlRelationship>,
538}
539
540impl DtdlModel {
541 pub fn new(id: impl Into<String>, display_name: impl Into<String>) -> Self {
543 Self {
544 id: id.into(),
545 display_name: display_name.into(),
546 telemetry: Vec::new(),
547 properties: Vec::new(),
548 components: Vec::new(),
549 relationships: Vec::new(),
550 }
551 }
552}
553
554pub fn parse_dtdl_json(json: &str) -> PhysicsResult<DtdlModel> {
570 let root: serde_json::Value = serde_json::from_str(json)
571 .map_err(|e| PhysicsError::Internal(format!("DTDL JSON parse error: {e}")))?;
572
573 let id = root
574 .get("@id")
575 .and_then(|v| v.as_str())
576 .unwrap_or("")
577 .to_string();
578
579 let display_name = root
580 .get("displayName")
581 .and_then(|v| v.as_str())
582 .unwrap_or(&id)
583 .to_string();
584
585 let mut model = DtdlModel::new(id, display_name);
586
587 if let Some(contents) = root.get("contents").and_then(|v| v.as_array()) {
588 for item in contents {
589 let type_tag = item
590 .get("@type")
591 .and_then(|v| v.as_str())
592 .unwrap_or("")
593 .to_lowercase();
594 let name = item
595 .get("name")
596 .and_then(|v| v.as_str())
597 .unwrap_or("")
598 .to_string();
599 let schema = item
600 .get("schema")
601 .and_then(|v| v.as_str())
602 .unwrap_or("double")
603 .to_string();
604
605 match type_tag.as_str() {
606 "telemetry" => {
607 let unit = item.get("unit").and_then(|v| v.as_str()).map(String::from);
608 model.telemetry.push(DtdlTelemetry { name, schema, unit });
609 }
610 "property" => {
611 let writable = item
612 .get("writable")
613 .and_then(|v| v.as_bool())
614 .unwrap_or(false);
615 model.properties.push(DtdlProperty {
616 name,
617 schema,
618 writable,
619 });
620 }
621 "component" => {
622 model.components.push(DtdlComponent { name, schema });
623 }
624 "relationship" => {
625 let target = item
626 .get("target")
627 .and_then(|v| v.as_str())
628 .map(String::from);
629 let max_multiplicity = item
630 .get("maxMultiplicity")
631 .and_then(|v| v.as_u64())
632 .map(|n| n as u32);
633 model.relationships.push(DtdlRelationship {
634 name,
635 target,
636 max_multiplicity,
637 });
638 }
639 _ => {
640 }
642 }
643 }
644 }
645
646 Ok(model)
647}
648
649pub fn model_to_digital_twin(model: &DtdlModel) -> DigitalTwin {
654 let mut twin = DigitalTwin::new(model.id.clone(), model.display_name.clone());
655
656 for tel in &model.telemetry {
657 twin.set_model_value(tel.name.clone(), 0.0);
658 }
659 for prop in &model.properties {
660 twin.set_model_value(prop.name.clone(), 0.0);
661 }
662
663 twin
664}
665
666#[cfg(test)]
671mod tests {
672 use super::*;
673
674 fn make_reading(id: &str, qty: &str, value: f64) -> SensorReading {
675 SensorReading::new(id, qty, value, "SI")
676 }
677
678 #[test]
681 fn twin_id_uniqueness() {
682 let a = TwinId::new();
683 let b = TwinId::new();
684 assert_ne!(a, b, "each TwinId must be unique");
685 }
686
687 #[test]
688 fn twin_id_display() {
689 let id = TwinId::new();
690 assert!(!id.to_string().is_empty());
691 }
692
693 #[test]
696 fn digital_twin_creation() {
697 let twin = DigitalTwin::new("urn:example:motor:1", "ElectricMotor");
698 assert_eq!(twin.entity_iri(), "urn:example:motor:1");
699 assert_eq!(twin.twin_type, "ElectricMotor");
700 assert!(twin.state.is_empty());
701 }
702
703 #[test]
704 fn digital_twin_sensor_buffered() {
705 let mut twin = DigitalTwin::new("urn:example:motor:1", "ElectricMotor");
706 twin.buffer_sensor(make_reading("s1", "temperature", 350.0));
707
708 assert!(!twin.state.contains_key("temperature"));
710 assert_eq!(twin.sensors.len(), 1);
711 }
712
713 #[test]
714 fn digital_twin_sensor_apply_to_state() {
715 let mut twin = DigitalTwin::new("urn:example:motor:1", "ElectricMotor");
716 twin.buffer_sensor(make_reading("s1", "temperature", 350.0));
717 twin.apply_sensor_to_state();
718
719 assert_eq!(twin.state["temperature"], 350.0);
720 }
721
722 #[test]
723 fn digital_twin_sensor_deduplication() {
724 let mut twin = DigitalTwin::new("urn:example:motor:1", "ElectricMotor");
725 twin.buffer_sensor(make_reading("s1", "temperature", 350.0));
726 twin.buffer_sensor(make_reading("s1", "temperature", 370.0));
727
728 assert_eq!(twin.sensors.len(), 1);
730 assert_eq!(twin.sensors[0].value, 370.0);
731 }
732
733 #[test]
734 fn digital_twin_actuator_command() {
735 let mut twin = DigitalTwin::new("urn:example:motor:1", "ElectricMotor");
736 twin.set_model_value("set_temperature", 300.0);
737 twin.queue_actuator(ActuatorCommand::new("a1", "set_temperature", 400.0));
738 twin.apply_pending_actuators();
739
740 assert_eq!(twin.model_state["set_temperature"], 400.0);
741 assert_eq!(twin.state["set_temperature"], 400.0);
742 assert!(twin.actuators[0].applied);
743 }
744
745 #[test]
748 fn manager_register_and_get_state() {
749 let mut mgr = DigitalTwinManager::new();
750 let id = mgr
751 .register("Thermostat", "urn:example:thermostat:1")
752 .expect("register failed");
753
754 assert_eq!(mgr.len(), 1);
755
756 mgr.get_twin_mut(&id)
758 .expect("should succeed")
759 .set_model_value("temperature", 298.15);
760
761 mgr.update_from_sensor(&id, SensorReading::new("t1", "temperature", 298.15, "K"))
763 .expect("sensor update failed");
764
765 mgr.synchronize(&id).expect("sync failed");
767
768 let state = mgr.get_state(&id).expect("get_state failed");
769 assert!((state["temperature"] - 298.15).abs() < 1e-9);
770 }
771
772 #[test]
773 fn manager_synchronise_no_anomaly() {
774 let mut mgr = DigitalTwinManager::new();
775 let id = mgr
776 .register("Motor", "urn:example:motor:1")
777 .expect("should succeed");
778
779 mgr.get_twin_mut(&id)
781 .expect("should succeed")
782 .set_model_value("speed", 1500.0);
783
784 mgr.update_from_sensor(&id, SensorReading::new("s1", "speed", 1500.0, "rpm"))
786 .expect("should succeed");
787
788 let report = mgr.synchronize(&id).expect("sync failed");
789 assert_eq!(report.synced_quantities.len(), 1);
790 assert!(
791 report.anomalies.is_empty(),
792 "no anomaly expected when sensor matches model"
793 );
794 }
795
796 #[test]
797 fn manager_synchronise_with_anomaly() {
798 let mut mgr = DigitalTwinManager::new().with_anomaly_threshold(0.05);
799 let id = mgr
800 .register("Sensor", "urn:example:s:1")
801 .expect("should succeed");
802
803 mgr.get_twin_mut(&id)
805 .expect("should succeed")
806 .set_model_value("temperature", 300.0);
807
808 mgr.update_from_sensor(&id, SensorReading::new("s1", "temperature", 400.0, "K"))
810 .expect("should succeed");
811
812 let report = mgr.synchronize(&id).expect("should succeed");
813 assert!(
814 !report.anomalies.is_empty(),
815 "expected anomaly to be detected (model=300, sensor=400)"
816 );
817 assert_eq!(report.deviations[0].digital_value, 300.0);
818 assert_eq!(report.deviations[0].physical_value, 400.0);
819 }
820
821 #[test]
824 fn dtdl_parse_minimal() {
825 let json = r#"{
826 "@context": "dtmi:dtdl:context;2",
827 "@type": "Interface",
828 "@id": "dtmi:com:example:Thermostat;1",
829 "displayName": "Thermostat",
830 "contents": [
831 { "@type": "Telemetry", "name": "temperature", "schema": "double", "unit": "degreeCelsius" },
832 { "@type": "Property", "name": "targetTemperature", "schema": "double", "writable": true }
833 ]
834 }"#;
835
836 let model = parse_dtdl_json(json).expect("parse failed");
837 assert_eq!(model.id, "dtmi:com:example:Thermostat;1");
838 assert_eq!(model.display_name, "Thermostat");
839 assert_eq!(model.telemetry.len(), 1);
840 assert_eq!(model.telemetry[0].name, "temperature");
841 assert_eq!(model.telemetry[0].unit.as_deref(), Some("degreeCelsius"));
842 assert_eq!(model.properties.len(), 1);
843 assert!(model.properties[0].writable);
844 }
845
846 #[test]
847 fn dtdl_parse_with_relationship() {
848 let json = r#"{
849 "@id": "dtmi:com:example:Room;1",
850 "displayName": "Room",
851 "contents": [
852 { "@type": "Relationship", "name": "contains", "target": "dtmi:com:example:Device;1", "maxMultiplicity": 10 }
853 ]
854 }"#;
855
856 let model = parse_dtdl_json(json).expect("parse failed");
857 assert_eq!(model.relationships.len(), 1);
858 assert_eq!(model.relationships[0].name, "contains");
859 assert_eq!(model.relationships[0].max_multiplicity, Some(10));
860 }
861
862 #[test]
863 fn dtdl_model_to_twin_state_initialised() {
864 let json = r#"{
865 "@id": "dtmi:com:example:Device;1",
866 "displayName": "Device",
867 "contents": [
868 { "@type": "Telemetry", "name": "voltage", "schema": "double" },
869 { "@type": "Property", "name": "setPoint", "schema": "double", "writable": false }
870 ]
871 }"#;
872
873 let model = parse_dtdl_json(json).expect("should succeed");
874 let twin = model_to_digital_twin(&model);
875
876 assert!(twin.model_state.contains_key("voltage"));
877 assert!(twin.model_state.contains_key("setPoint"));
878 assert!(twin.state.contains_key("voltage"));
879 assert_eq!(twin.entity_iri(), "dtmi:com:example:Device;1");
880 }
881
882 #[test]
883 fn dtdl_parse_invalid_json_returns_error() {
884 let result = parse_dtdl_json("{ not valid json }");
885 assert!(result.is_err());
886 }
887}