phytrace_sdk/models/domains/
ai.rs

1//! AI Domain - Models, decisions, and anomalies.
2//!
3//! Contains AI/ML model information, decisions, and anomaly detection.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::models::enums::{AnomalySeverity, InferenceDevice, ModelType};
10
11/// AI domain containing model and decision information.
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub struct AiDomain {
14    /// Active AI models
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub models: Option<Vec<AiModel>>,
17
18    /// Recent AI decisions
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub decisions: Option<Vec<AiDecision>>,
21
22    /// Detected anomalies
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub anomalies: Option<Vec<Anomaly>>,
25
26    /// Total inference count (session)
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub total_inferences: Option<u64>,
29
30    /// Average inference latency (ms)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub avg_inference_ms: Option<f64>,
33}
34
35/// AI model information.
36#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
37pub struct AiModel {
38    /// Model ID
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub model_id: Option<String>,
41
42    /// Model name
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub name: Option<String>,
45
46    /// Model type
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub model_type: Option<ModelType>,
49
50    /// Model version
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub version: Option<String>,
53
54    /// Framework (pytorch, tensorflow, onnx, etc.)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub framework: Option<String>,
57
58    /// Inference device
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub device: Option<InferenceDevice>,
61
62    /// Whether model is loaded
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub is_loaded: Option<bool>,
65
66    /// Inference count (session)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub inference_count: Option<u64>,
69
70    /// Average inference time (ms)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    #[validate(range(min = 0.0))]
73    pub avg_inference_ms: Option<f64>,
74
75    /// Last inference time
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub last_inference: Option<DateTime<Utc>>,
78
79    /// Model accuracy (if tracked)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    #[validate(range(min = 0.0, max = 1.0))]
82    pub accuracy: Option<f64>,
83
84    /// Memory usage (bytes)
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub memory_bytes: Option<u64>,
87}
88
89/// AI decision record.
90#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
91pub struct AiDecision {
92    /// Decision ID
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub decision_id: Option<String>,
95
96    /// Model that made the decision
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub model_id: Option<String>,
99
100    /// Decision type/category
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub decision_type: Option<String>,
103
104    /// Decision outcome/value
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub outcome: Option<String>,
107
108    /// Confidence score (0-1)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    #[validate(range(min = 0.0, max = 1.0))]
111    pub confidence: Option<f64>,
112
113    /// Decision timestamp
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub timestamp: Option<DateTime<Utc>>,
116
117    /// Whether decision was executed
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub executed: Option<bool>,
120
121    /// Latency to make decision (ms)
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub latency_ms: Option<f64>,
124
125    /// Input features summary
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub input_summary: Option<String>,
128}
129
130/// Detected anomaly.
131#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
132pub struct Anomaly {
133    /// Anomaly ID
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub anomaly_id: Option<String>,
136
137    /// Anomaly type/category
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub anomaly_type: Option<String>,
140
141    /// Severity
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub severity: Option<AnomalySeverity>,
144
145    /// Description
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub description: Option<String>,
148
149    /// Confidence score (0-1)
150    #[serde(skip_serializing_if = "Option::is_none")]
151    #[validate(range(min = 0.0, max = 1.0))]
152    pub confidence: Option<f64>,
153
154    /// Anomaly score (higher = more anomalous)
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub anomaly_score: Option<f64>,
157
158    /// Detection timestamp
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub timestamp: Option<DateTime<Utc>>,
161
162    /// Source/detector
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub source: Option<String>,
165
166    /// Affected component
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub component: Option<String>,
169
170    /// Whether anomaly is still active
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub is_active: Option<bool>,
173
174    /// Recommended action
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub recommended_action: Option<String>,
177}
178
179impl AiDomain {
180    /// Add a model.
181    pub fn with_model(mut self, model: AiModel) -> Self {
182        let models = self.models.get_or_insert_with(Vec::new);
183        models.push(model);
184        self
185    }
186
187    /// Add a decision.
188    pub fn with_decision(mut self, decision: AiDecision) -> Self {
189        let decisions = self.decisions.get_or_insert_with(Vec::new);
190        decisions.push(decision);
191        self
192    }
193
194    /// Add an anomaly.
195    pub fn with_anomaly(mut self, anomaly: Anomaly) -> Self {
196        let anomalies = self.anomalies.get_or_insert_with(Vec::new);
197        anomalies.push(anomaly);
198        self
199    }
200}
201
202impl AiModel {
203    /// Create a new model.
204    pub fn new(name: impl Into<String>, model_type: ModelType) -> Self {
205        Self {
206            name: Some(name.into()),
207            model_type: Some(model_type),
208            is_loaded: Some(true),
209            ..Default::default()
210        }
211    }
212}
213
214impl Anomaly {
215    /// Create a new anomaly.
216    pub fn new(
217        anomaly_type: impl Into<String>,
218        severity: AnomalySeverity,
219        description: impl Into<String>,
220    ) -> Self {
221        Self {
222            anomaly_id: Some(uuid::Uuid::new_v4().to_string()),
223            anomaly_type: Some(anomaly_type.into()),
224            severity: Some(severity),
225            description: Some(description.into()),
226            timestamp: Some(Utc::now()),
227            is_active: Some(true),
228            ..Default::default()
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_ai_model() {
239        let model = AiModel::new("yolo_v8", ModelType::ObjectDetection);
240        assert_eq!(model.model_type, Some(ModelType::ObjectDetection));
241    }
242
243    #[test]
244    fn test_anomaly() {
245        let anomaly = Anomaly::new(
246            "sensor_drift",
247            AnomalySeverity::Medium,
248            "LiDAR readings drifting",
249        );
250        assert!(anomaly.timestamp.is_some());
251        assert_eq!(anomaly.is_active, Some(true));
252    }
253
254    // ==========================================================================
255    // AiDomain Tests
256    // ==========================================================================
257
258    #[test]
259    fn test_ai_domain_default() {
260        let ai = AiDomain::default();
261        assert!(ai.models.is_none());
262        assert!(ai.decisions.is_none());
263        assert!(ai.anomalies.is_none());
264        assert!(ai.total_inferences.is_none());
265        assert!(ai.avg_inference_ms.is_none());
266    }
267
268    #[test]
269    fn test_ai_domain_with_model() {
270        let model = AiModel::new("resnet50", ModelType::ObjectDetection);
271        let ai = AiDomain::default().with_model(model);
272
273        assert!(ai.models.is_some());
274        assert_eq!(ai.models.as_ref().unwrap().len(), 1);
275        assert_eq!(
276            ai.models.as_ref().unwrap()[0].name,
277            Some("resnet50".to_string())
278        );
279    }
280
281    #[test]
282    fn test_ai_domain_with_multiple_models() {
283        let model1 = AiModel::new("yolo_v8", ModelType::ObjectDetection);
284        let model2 = AiModel::new("bert", ModelType::Nlu);
285
286        let ai = AiDomain::default().with_model(model1).with_model(model2);
287
288        assert_eq!(ai.models.as_ref().unwrap().len(), 2);
289    }
290
291    #[test]
292    fn test_ai_domain_with_decision() {
293        let decision = AiDecision {
294            decision_id: Some("dec-001".to_string()),
295            decision_type: Some("navigation".to_string()),
296            outcome: Some("turn_left".to_string()),
297            confidence: Some(0.95),
298            timestamp: Some(Utc::now()),
299            ..Default::default()
300        };
301
302        let ai = AiDomain::default().with_decision(decision);
303        assert!(ai.decisions.is_some());
304        assert_eq!(
305            ai.decisions.as_ref().unwrap()[0].outcome,
306            Some("turn_left".to_string())
307        );
308    }
309
310    #[test]
311    fn test_ai_domain_with_anomaly() {
312        let anomaly = Anomaly::new(
313            "temperature_spike",
314            AnomalySeverity::High,
315            "Motor overheating",
316        );
317        let ai = AiDomain::default().with_anomaly(anomaly);
318
319        assert!(ai.anomalies.is_some());
320        assert_eq!(
321            ai.anomalies.as_ref().unwrap()[0].severity,
322            Some(AnomalySeverity::High)
323        );
324    }
325
326    #[test]
327    fn test_ai_domain_chained_builders() {
328        let ai = AiDomain::default()
329            .with_model(AiModel::new("model1", ModelType::ObjectDetection))
330            .with_decision(AiDecision::default())
331            .with_anomaly(Anomaly::new("test", AnomalySeverity::Low, "desc"));
332
333        assert!(ai.models.is_some());
334        assert!(ai.decisions.is_some());
335        assert!(ai.anomalies.is_some());
336    }
337
338    // ==========================================================================
339    // AiModel Tests
340    // ==========================================================================
341
342    #[test]
343    fn test_ai_model_new() {
344        let model = AiModel::new("efficientnet", ModelType::ObjectDetection);
345
346        assert_eq!(model.name, Some("efficientnet".to_string()));
347        assert_eq!(model.model_type, Some(ModelType::ObjectDetection));
348        assert_eq!(model.is_loaded, Some(true));
349    }
350
351    #[test]
352    fn test_ai_model_default() {
353        let model = AiModel::default();
354        assert!(model.model_id.is_none());
355        assert!(model.name.is_none());
356        assert!(model.model_type.is_none());
357        assert!(model.version.is_none());
358        assert!(model.framework.is_none());
359        assert!(model.device.is_none());
360        assert!(model.is_loaded.is_none());
361        assert!(model.inference_count.is_none());
362        assert!(model.avg_inference_ms.is_none());
363        assert!(model.last_inference.is_none());
364        assert!(model.accuracy.is_none());
365        assert!(model.memory_bytes.is_none());
366    }
367
368    #[test]
369    fn test_ai_model_full() {
370        let model = AiModel {
371            model_id: Some("model-001".to_string()),
372            name: Some("yolo_v8_nano".to_string()),
373            model_type: Some(ModelType::ObjectDetection),
374            version: Some("8.0.1".to_string()),
375            framework: Some("pytorch".to_string()),
376            device: Some(InferenceDevice::Gpu),
377            is_loaded: Some(true),
378            inference_count: Some(1000),
379            avg_inference_ms: Some(15.5),
380            last_inference: Some(Utc::now()),
381            accuracy: Some(0.92),
382            memory_bytes: Some(50_000_000),
383        };
384
385        assert_eq!(model.framework, Some("pytorch".to_string()));
386        assert_eq!(model.device, Some(InferenceDevice::Gpu));
387        assert_eq!(model.accuracy, Some(0.92));
388    }
389
390    #[test]
391    fn test_ai_model_serialization() {
392        let model = AiModel::new("test_model", ModelType::SemanticSegmentation);
393        let json = serde_json::to_string(&model).unwrap();
394        let deserialized: AiModel = serde_json::from_str(&json).unwrap();
395
396        assert_eq!(deserialized.name, Some("test_model".to_string()));
397    }
398
399    // ==========================================================================
400    // AiDecision Tests
401    // ==========================================================================
402
403    #[test]
404    fn test_ai_decision_default() {
405        let decision = AiDecision::default();
406        assert!(decision.decision_id.is_none());
407        assert!(decision.model_id.is_none());
408        assert!(decision.decision_type.is_none());
409        assert!(decision.outcome.is_none());
410        assert!(decision.confidence.is_none());
411        assert!(decision.timestamp.is_none());
412        assert!(decision.executed.is_none());
413        assert!(decision.latency_ms.is_none());
414        assert!(decision.input_summary.is_none());
415    }
416
417    #[test]
418    fn test_ai_decision_full() {
419        let decision = AiDecision {
420            decision_id: Some("dec-123".to_string()),
421            model_id: Some("model-001".to_string()),
422            decision_type: Some("obstacle_avoidance".to_string()),
423            outcome: Some("stop".to_string()),
424            confidence: Some(0.98),
425            timestamp: Some(Utc::now()),
426            executed: Some(true),
427            latency_ms: Some(12.5),
428            input_summary: Some("Obstacle detected at 2m".to_string()),
429        };
430
431        assert_eq!(decision.outcome, Some("stop".to_string()));
432        assert_eq!(decision.executed, Some(true));
433        assert_eq!(decision.latency_ms, Some(12.5));
434    }
435
436    #[test]
437    fn test_ai_decision_serialization() {
438        let decision = AiDecision {
439            decision_type: Some("navigation".to_string()),
440            confidence: Some(0.85),
441            ..Default::default()
442        };
443
444        let json = serde_json::to_string(&decision).unwrap();
445        let deserialized: AiDecision = serde_json::from_str(&json).unwrap();
446        assert_eq!(deserialized.confidence, Some(0.85));
447    }
448
449    // ==========================================================================
450    // Anomaly Tests
451    // ==========================================================================
452
453    #[test]
454    fn test_anomaly_new() {
455        let anomaly = Anomaly::new(
456            "battery_degradation",
457            AnomalySeverity::Critical,
458            "Battery capacity below threshold",
459        );
460
461        assert!(anomaly.anomaly_id.is_some());
462        assert_eq!(
463            anomaly.anomaly_type,
464            Some("battery_degradation".to_string())
465        );
466        assert_eq!(anomaly.severity, Some(AnomalySeverity::Critical));
467        assert_eq!(
468            anomaly.description,
469            Some("Battery capacity below threshold".to_string())
470        );
471        assert!(anomaly.timestamp.is_some());
472        assert_eq!(anomaly.is_active, Some(true));
473    }
474
475    #[test]
476    fn test_anomaly_default() {
477        let anomaly = Anomaly::default();
478        assert!(anomaly.anomaly_id.is_none());
479        assert!(anomaly.anomaly_type.is_none());
480        assert!(anomaly.severity.is_none());
481        assert!(anomaly.description.is_none());
482        assert!(anomaly.confidence.is_none());
483        assert!(anomaly.anomaly_score.is_none());
484        assert!(anomaly.timestamp.is_none());
485        assert!(anomaly.source.is_none());
486        assert!(anomaly.component.is_none());
487        assert!(anomaly.is_active.is_none());
488        assert!(anomaly.recommended_action.is_none());
489    }
490
491    #[test]
492    fn test_anomaly_full() {
493        let anomaly = Anomaly {
494            anomaly_id: Some("anom-001".to_string()),
495            anomaly_type: Some("vibration".to_string()),
496            severity: Some(AnomalySeverity::Medium),
497            description: Some("Unusual vibration pattern".to_string()),
498            confidence: Some(0.87),
499            anomaly_score: Some(3.5),
500            timestamp: Some(Utc::now()),
501            source: Some("accelerometer".to_string()),
502            component: Some("motor_left".to_string()),
503            is_active: Some(true),
504            recommended_action: Some("Schedule inspection".to_string()),
505        };
506
507        assert_eq!(anomaly.anomaly_score, Some(3.5));
508        assert_eq!(anomaly.source, Some("accelerometer".to_string()));
509        assert_eq!(
510            anomaly.recommended_action,
511            Some("Schedule inspection".to_string())
512        );
513    }
514
515    #[test]
516    fn test_anomaly_serialization() {
517        let anomaly = Anomaly::new("test_anomaly", AnomalySeverity::Low, "Test description");
518        let json = serde_json::to_string(&anomaly).unwrap();
519        let deserialized: Anomaly = serde_json::from_str(&json).unwrap();
520
521        assert_eq!(deserialized.anomaly_type, Some("test_anomaly".to_string()));
522        assert_eq!(deserialized.severity, Some(AnomalySeverity::Low));
523    }
524
525    // ==========================================================================
526    // AiDomain Serialization Tests
527    // ==========================================================================
528
529    #[test]
530    fn test_ai_domain_serialization_roundtrip() {
531        let ai = AiDomain {
532            models: Some(vec![AiModel::new("model1", ModelType::ObjectDetection)]),
533            decisions: Some(vec![AiDecision {
534                confidence: Some(0.9),
535                ..Default::default()
536            }]),
537            anomalies: Some(vec![Anomaly::new("test", AnomalySeverity::Low, "desc")]),
538            total_inferences: Some(5000),
539            avg_inference_ms: Some(25.0),
540        };
541
542        let json = serde_json::to_string(&ai).unwrap();
543        let deserialized: AiDomain = serde_json::from_str(&json).unwrap();
544
545        assert_eq!(deserialized.total_inferences, Some(5000));
546        assert_eq!(deserialized.avg_inference_ms, Some(25.0));
547        assert!(deserialized.models.is_some());
548        assert!(deserialized.decisions.is_some());
549        assert!(deserialized.anomalies.is_some());
550    }
551}