1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use validator::Validate;
8
9use crate::models::enums::{AnomalySeverity, InferenceDevice, ModelType};
10
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub struct AiDomain {
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub models: Option<Vec<AiModel>>,
17
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub decisions: Option<Vec<AiDecision>>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub anomalies: Option<Vec<Anomaly>>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub total_inferences: Option<u64>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub avg_inference_ms: Option<f64>,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
37pub struct AiModel {
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub model_id: Option<String>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub name: Option<String>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub model_type: Option<ModelType>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub version: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub framework: Option<String>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub device: Option<InferenceDevice>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub is_loaded: Option<bool>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub inference_count: Option<u64>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 #[validate(range(min = 0.0))]
73 pub avg_inference_ms: Option<f64>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub last_inference: Option<DateTime<Utc>>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 #[validate(range(min = 0.0, max = 1.0))]
82 pub accuracy: Option<f64>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub memory_bytes: Option<u64>,
87}
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
91pub struct AiDecision {
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub decision_id: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub model_id: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub decision_type: Option<String>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub outcome: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 #[validate(range(min = 0.0, max = 1.0))]
111 pub confidence: Option<f64>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub timestamp: Option<DateTime<Utc>>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub executed: Option<bool>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub latency_ms: Option<f64>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub input_summary: Option<String>,
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize, Validate)]
132pub struct Anomaly {
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub anomaly_id: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub anomaly_type: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub severity: Option<AnomalySeverity>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub description: Option<String>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 #[validate(range(min = 0.0, max = 1.0))]
152 pub confidence: Option<f64>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub anomaly_score: Option<f64>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub timestamp: Option<DateTime<Utc>>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub source: Option<String>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub component: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub is_active: Option<bool>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
176 pub recommended_action: Option<String>,
177}
178
179impl AiDomain {
180 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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}