scirs2_series/
iot_sensors.rs

1//! IoT sensor data analysis for time series
2//!
3//! This module provides specialized functionality for analyzing time series data
4//! from Internet of Things (IoT) sensors including temperature, humidity, motion,
5//! GPS, accelerometer, and other sensor data streams.
6
7use crate::error::{Result, TimeSeriesError};
8use scirs2_core::ndarray::{Array1, Array2};
9use scirs2_core::validation::check_positive;
10use statrs::statistics::Statistics;
11use std::collections::HashMap;
12
13/// IoT sensor data types
14#[derive(Debug, Clone)]
15pub enum SensorType {
16    /// Temperature sensor (°C)
17    Temperature,
18    /// Humidity sensor (%)
19    Humidity,
20    /// Pressure sensor (hPa)
21    Pressure,
22    /// Light intensity sensor (lux)
23    Light,
24    /// Motion/PIR sensor (binary)
25    Motion,
26    /// Accelerometer (3-axis: x, y, z in m/s²)
27    Accelerometer,
28    /// GPS coordinates (latitude, longitude)
29    GPS,
30    /// Sound level sensor (dB)
31    Sound,
32    /// Air quality sensor (AQI or ppm)
33    AirQuality,
34    /// Energy consumption (W or kWh)
35    Energy,
36}
37
38/// Sensor data quality assessment
39#[derive(Debug, Clone)]
40pub struct DataQuality {
41    /// Missing data percentage
42    pub missing_percentage: f64,
43    /// Outlier percentage
44    pub outlier_percentage: f64,
45    /// Signal-to-noise ratio
46    pub snr: f64,
47    /// Data consistency score
48    pub consistency_score: f64,
49}
50
51/// Environmental sensor data analysis
52pub struct EnvironmentalSensorAnalysis {
53    /// Temperature readings
54    pub temperature: Option<Array1<f64>>,
55    /// Humidity readings
56    pub humidity: Option<Array1<f64>>,
57    /// Pressure readings
58    pub pressure: Option<Array1<f64>>,
59    /// Light readings
60    pub light: Option<Array1<f64>>,
61    /// Time stamps
62    pub timestamps: Array1<i64>,
63    /// Sampling interval in seconds
64    pub sampling_interval: f64,
65}
66
67impl EnvironmentalSensorAnalysis {
68    /// Create new environmental sensor analysis
69    pub fn new(_timestamps: Array1<i64>, samplinginterval: f64) -> Result<Self> {
70        check_positive(samplinginterval, "sampling_interval")?;
71
72        Ok(Self {
73            temperature: None,
74            humidity: None,
75            pressure: None,
76            light: None,
77            timestamps: _timestamps,
78            sampling_interval: samplinginterval,
79        })
80    }
81
82    /// Add temperature data
83    pub fn with_temperature(mut self, data: Array1<f64>) -> Result<Self> {
84        if data.iter().any(|x| !x.is_finite()) {
85            return Err(TimeSeriesError::InvalidInput(
86                "Temperature data contains non-finite values".to_string(),
87            ));
88        }
89        if data.len() != self.timestamps.len() {
90            return Err(TimeSeriesError::InvalidInput(
91                "Temperature data length must match timestamps".to_string(),
92            ));
93        }
94        self.temperature = Some(data);
95        Ok(self)
96    }
97
98    /// Add humidity data
99    pub fn with_humidity(mut self, data: Array1<f64>) -> Result<Self> {
100        if data.iter().any(|x| !x.is_finite()) {
101            return Err(TimeSeriesError::InvalidInput(
102                "Humidity data contains non-finite values".to_string(),
103            ));
104        }
105
106        // Validate humidity range (0-100%)
107        if data.iter().any(|&x| !(0.0..=100.0).contains(&x)) {
108            return Err(TimeSeriesError::InvalidInput(
109                "Humidity values must be between 0 and 100%".to_string(),
110            ));
111        }
112
113        self.humidity = Some(data);
114        Ok(self)
115    }
116
117    /// Detect sensor malfunctions using multiple criteria
118    pub fn detect_sensor_malfunctions(&self) -> Result<HashMap<String, Vec<usize>>> {
119        let mut malfunctions = HashMap::new();
120
121        // Temperature sensor malfunction detection
122        if let Some(ref temp_data) = self.temperature {
123            let mut temp_issues = Vec::new();
124
125            // Stuck sensor (same value for extended period)
126            let mut consecutive_count = 1;
127            for i in 1..temp_data.len() {
128                if (temp_data[i] - temp_data[i - 1]).abs() < 0.01 {
129                    consecutive_count += 1;
130                } else {
131                    if consecutive_count > 20 {
132                        // 20 consecutive identical readings
133                        for j in (i - consecutive_count)..i {
134                            temp_issues.push(j);
135                        }
136                    }
137                    consecutive_count = 1;
138                }
139            }
140
141            // Impossible values (temperature outside reasonable range)
142            for (i, &temp) in temp_data.iter().enumerate() {
143                if !(-50.0..=100.0).contains(&temp) {
144                    temp_issues.push(i);
145                }
146            }
147
148            // Sudden jumps (> 10°C change in one reading)
149            for i in 1..temp_data.len() {
150                if (temp_data[i] - temp_data[i - 1]).abs() > 10.0 {
151                    temp_issues.push(i);
152                }
153            }
154
155            malfunctions.insert("Temperature".to_string(), temp_issues);
156        }
157
158        // Humidity sensor malfunction detection
159        if let Some(ref humidity_data) = self.humidity {
160            let mut humidity_issues = Vec::new();
161
162            // Stuck at 0% or 100%
163            for (i, &humidity) in humidity_data.iter().enumerate() {
164                if humidity == 0.0 || humidity == 100.0 {
165                    humidity_issues.push(i);
166                }
167            }
168
169            // Sudden changes > 20%
170            for i in 1..humidity_data.len() {
171                if (humidity_data[i] - humidity_data[i - 1]).abs() > 20.0 {
172                    humidity_issues.push(i);
173                }
174            }
175
176            malfunctions.insert("Humidity".to_string(), humidity_issues);
177        }
178
179        Ok(malfunctions)
180    }
181
182    /// Calculate comfort index from temperature and humidity
183    pub fn comfort_index(&self) -> Result<Array1<f64>> {
184        let temp_data = self.temperature.as_ref().ok_or_else(|| {
185            TimeSeriesError::InvalidInput("Temperature data required".to_string())
186        })?;
187        let humidity_data = self
188            .humidity
189            .as_ref()
190            .ok_or_else(|| TimeSeriesError::InvalidInput("Humidity data required".to_string()))?;
191
192        let mut comfort = Array1::zeros(temp_data.len());
193
194        for i in 0..comfort.len() {
195            let temp = temp_data[i];
196            let rh = humidity_data[i];
197
198            // Heat Index calculation (simplified)
199            let heat_index = if temp >= 27.0 && rh >= 40.0 {
200                -42.379 + 2.04901523 * temp + 10.14333127 * rh
201                    - 0.22475541 * temp * rh
202                    - 0.00683783 * temp * temp
203                    - 0.05481717 * rh * rh
204                    + 0.00122874 * temp * temp * rh
205                    + 0.00085282 * temp * rh * rh
206                    - 0.00000199 * temp * temp * rh * rh
207            } else {
208                temp
209            };
210
211            // Comfort score (0-100, higher is more comfortable)
212            comfort[i] = if heat_index <= 27.0 && (30.0..=60.0).contains(&rh) {
213                100.0 - (heat_index - 22.0).abs() * 5.0 - (rh - 45.0).abs() * 0.5
214            } else {
215                50.0 - (heat_index - 22.0).abs() * 2.0 - (rh - 45.0).abs() * 0.3
216            }
217            .clamp(0.0, 100.0);
218        }
219
220        Ok(comfort)
221    }
222
223    /// Energy optimization recommendations based on environmental data
224    pub fn energy_optimization_recommendations(&self) -> Result<Vec<String>> {
225        let mut recommendations = Vec::new();
226
227        if let Some(ref temp_data) = self.temperature {
228            let avg_temp = temp_data.mean().unwrap();
229
230            if avg_temp > 25.0 {
231                recommendations
232                    .push("Consider increasing cooling setpoint during peak hours".to_string());
233            } else if avg_temp < 18.0 {
234                recommendations
235                    .push("Consider decreasing heating setpoint during off-peak hours".to_string());
236            }
237
238            // Temperature stability analysis
239            let temp_std = temp_data.std(0.0);
240            if temp_std > 3.0 {
241                recommendations.push(
242                    "High temperature variation detected - check HVAC efficiency".to_string(),
243                );
244            }
245        }
246
247        if let Some(ref humidity_data) = self.humidity {
248            let avg_humidity = humidity_data.mean().unwrap();
249
250            if avg_humidity > 70.0 {
251                recommendations
252                    .push("High humidity detected - consider dehumidification".to_string());
253            } else if avg_humidity < 30.0 {
254                recommendations.push("Low humidity detected - consider humidification".to_string());
255            }
256        }
257
258        Ok(recommendations)
259    }
260}
261
262/// Motion and acceleration sensor analysis
263pub struct MotionSensorAnalysis {
264    /// Accelerometer data (3-axis)
265    pub acceleration: Option<Array2<f64>>,
266    /// Motion detection data (binary)
267    pub motion: Option<Array1<f64>>,
268    /// GPS coordinates [latitude, longitude]
269    pub gps: Option<Array2<f64>>,
270    /// Time stamps
271    pub timestamps: Array1<i64>,
272    /// Sampling frequency (Hz)
273    pub fs: f64,
274}
275
276impl MotionSensorAnalysis {
277    /// Create new motion sensor analysis
278    pub fn new(timestamps: Array1<i64>, fs: f64) -> Result<Self> {
279        check_positive(fs, "sampling_frequency")?;
280
281        Ok(Self {
282            acceleration: None,
283            motion: None,
284            gps: None,
285            timestamps,
286            fs,
287        })
288    }
289
290    /// Add accelerometer data (3-axis: x, y, z)
291    pub fn with_accelerometer(mut self, data: Array2<f64>) -> Result<Self> {
292        if data.iter().any(|x| !x.is_finite()) {
293            return Err(TimeSeriesError::InvalidInput(
294                "Acceleration data contains non-finite values".to_string(),
295            ));
296        }
297
298        if data.ncols() != 3 {
299            return Err(TimeSeriesError::InvalidInput(
300                "Accelerometer data must have 3 columns (x, y, z)".to_string(),
301            ));
302        }
303
304        self.acceleration = Some(data);
305        Ok(self)
306    }
307
308    /// Add GPS data
309    pub fn with_gps(mut self, data: Array2<f64>) -> Result<Self> {
310        if data.iter().any(|x| !x.is_finite()) {
311            return Err(TimeSeriesError::InvalidInput(
312                "GPS data contains non-finite values".to_string(),
313            ));
314        }
315
316        if data.ncols() != 2 {
317            return Err(TimeSeriesError::InvalidInput(
318                "GPS data must have 2 columns (latitude, longitude)".to_string(),
319            ));
320        }
321
322        // Validate GPS coordinates
323        for row in data.outer_iter() {
324            let lat = row[0];
325            let lon = row[1];
326            if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
327                return Err(TimeSeriesError::InvalidInput(
328                    "Invalid GPS coordinates".to_string(),
329                ));
330            }
331        }
332
333        self.gps = Some(data);
334        Ok(self)
335    }
336
337    /// Detect different activity types from accelerometer data
338    pub fn activity_recognition(&self) -> Result<Vec<(usize, String)>> {
339        let accel_data = self.acceleration.as_ref().ok_or_else(|| {
340            TimeSeriesError::InvalidInput("Accelerometer data required".to_string())
341        })?;
342
343        let window_size = (2.0 * self.fs) as usize; // 2-second windows
344        let mut activities = Vec::new();
345
346        for start in (0..accel_data.nrows()).step_by(window_size) {
347            let end = (start + window_size).min(accel_data.nrows());
348            if end - start < window_size / 2 {
349                break; // Skip incomplete windows
350            }
351
352            let window = accel_data.slice(scirs2_core::ndarray::s![start..end, ..]);
353
354            // Calculate features
355            let magnitude: Array1<f64> = window
356                .outer_iter()
357                .map(|row| (row[0] * row[0] + row[1] * row[1] + row[2] * row[2]).sqrt())
358                .collect();
359
360            let mean_magnitude = magnitude.clone().mean();
361            let std_magnitude = magnitude.std(0.0);
362            let max_magnitude = magnitude.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
363
364            // Simple activity classification based on acceleration patterns
365            let activity = if std_magnitude > 3.0 && max_magnitude > 15.0 {
366                "Running"
367            } else if std_magnitude > 1.5 && mean_magnitude > 10.0 {
368                "Walking"
369            } else if std_magnitude < 0.5 && mean_magnitude < 10.5 {
370                "Stationary"
371            } else if max_magnitude > 20.0 {
372                "High Impact Activity"
373            } else {
374                "Light Activity"
375            };
376
377            activities.push((start, activity.to_string()));
378        }
379
380        Ok(activities)
381    }
382
383    /// Calculate distance traveled from GPS data
384    pub fn distance_traveled(&self) -> Result<f64> {
385        let gps_data = self
386            .gps
387            .as_ref()
388            .ok_or_else(|| TimeSeriesError::InvalidInput("GPS data required".to_string()))?;
389
390        if gps_data.nrows() < 2 {
391            return Ok(0.0);
392        }
393
394        let mut total_distance = 0.0;
395
396        for i in 1..gps_data.nrows() {
397            let lat1 = gps_data[[i - 1, 0]].to_radians();
398            let lon1 = gps_data[[i - 1, 1]].to_radians();
399            let lat2 = gps_data[[i, 0]].to_radians();
400            let lon2 = gps_data[[i, 1]].to_radians();
401
402            // Haversine formula for distance calculation
403            let dlat = lat2 - lat1;
404            let dlon = lon2 - lon1;
405
406            let a =
407                (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
408            let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
409            let distance = 6371000.0 * c; // Earth radius in meters
410
411            total_distance += distance;
412        }
413
414        Ok(total_distance)
415    }
416
417    /// Detect falls using accelerometer data
418    pub fn fall_detection(&self, threshold: f64) -> Result<Vec<usize>> {
419        let accel_data = self.acceleration.as_ref().ok_or_else(|| {
420            TimeSeriesError::InvalidInput("Accelerometer data required".to_string())
421        })?;
422
423        let mut fall_events = Vec::new();
424
425        // Calculate magnitude of acceleration vector
426        for (i, row) in accel_data.outer_iter().enumerate() {
427            let magnitude = (row[0] * row[0] + row[1] * row[1] + row[2] * row[2]).sqrt();
428
429            // Fall detection: sudden increase followed by near-zero acceleration
430            if magnitude > threshold {
431                // Check for low acceleration in the next few samples (impact followed by stillness)
432                let check_samples = ((0.5 * self.fs) as usize).min(accel_data.nrows() - i - 1);
433                if check_samples > 0 {
434                    let future_window = accel_data
435                        .slice(scirs2_core::ndarray::s![i + 1..i + 1 + check_samples, ..]);
436                    let future_magnitudes: Array1<f64> = future_window
437                        .outer_iter()
438                        .map(|row| (row[0] * row[0] + row[1] * row[1] + row[2] * row[2]).sqrt())
439                        .collect();
440
441                    let mean_future = future_magnitudes.mean();
442                    if mean_future < 2.0 {
443                        // Low acceleration following high impact
444                        fall_events.push(i);
445                    }
446                }
447            }
448        }
449
450        Ok(fall_events)
451    }
452
453    /// Calculate step count from accelerometer data
454    pub fn step_count(&self) -> Result<usize> {
455        let accel_data = self.acceleration.as_ref().ok_or_else(|| {
456            TimeSeriesError::InvalidInput("Accelerometer data required".to_string())
457        })?;
458
459        // Calculate magnitude of acceleration
460        let magnitude: Array1<f64> = accel_data
461            .outer_iter()
462            .map(|row| (row[0] * row[0] + row[1] * row[1] + row[2] * row[2]).sqrt())
463            .collect();
464
465        // Apply high-pass filter to remove gravity component
466        let mean_magnitude = magnitude.clone().mean();
467        let filtered: Array1<f64> = magnitude.iter().map(|&x| x - mean_magnitude).collect();
468
469        // Simple peak detection for step counting
470        let mut steps = 0;
471        let min_peak_height = 1.0; // Threshold for step detection
472        let min_peak_distance = (0.5 * self.fs) as usize; // Minimum 0.5 seconds between steps
473        let mut last_peak = 0;
474
475        for i in 1..filtered.len() - 1 {
476            if filtered[i] > filtered[i - 1]
477                && filtered[i] > filtered[i + 1]
478                && filtered[i] > min_peak_height
479                && (i - last_peak) > min_peak_distance
480            {
481                steps += 1;
482                last_peak = i;
483            }
484        }
485
486        Ok(steps)
487    }
488}
489
490/// General IoT sensor data quality assessment
491pub struct IoTDataQualityAnalysis {
492    /// Sensor data array
493    pub data: Array1<f64>,
494    /// Sensor type
495    pub sensor_type: SensorType,
496    /// Time stamps
497    pub timestamps: Array1<i64>,
498}
499
500impl IoTDataQualityAnalysis {
501    /// Create new data quality analysis
502    pub fn new(
503        data: Array1<f64>,
504        sensor_type: SensorType,
505        timestamps: Array1<i64>,
506    ) -> Result<Self> {
507        if data.len() != timestamps.len() {
508            return Err(TimeSeriesError::InvalidInput(
509                "Data and timestamp arrays must have same length".to_string(),
510            ));
511        }
512
513        Ok(Self {
514            data,
515            sensor_type,
516            timestamps,
517        })
518    }
519
520    /// Assess overall data quality
521    pub fn assess_quality(&self) -> Result<DataQuality> {
522        let missing_percentage = self.calculate_missing_percentage()?;
523        let outlier_percentage = self.calculate_outlier_percentage()?;
524        let snr = self.calculate_snr()?;
525        let consistency_score = self.calculate_consistency_score()?;
526
527        Ok(DataQuality {
528            missing_percentage,
529            outlier_percentage,
530            snr,
531            consistency_score,
532        })
533    }
534
535    /// Calculate percentage of missing data points
536    fn calculate_missing_percentage(&self) -> Result<f64> {
537        let missing_count = self.data.iter().filter(|&&x| x.is_nan()).count();
538        Ok((missing_count as f64 / self.data.len() as f64) * 100.0)
539    }
540
541    /// Calculate percentage of outliers using IQR method
542    fn calculate_outlier_percentage(&self) -> Result<f64> {
543        let mut sorted_data: Vec<f64> = self
544            .data
545            .iter()
546            .filter(|&&x| !x.is_nan())
547            .cloned()
548            .collect();
549        sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap());
550
551        if sorted_data.len() < 4 {
552            return Ok(0.0);
553        }
554
555        let q1_idx = sorted_data.len() / 4;
556        let q3_idx = 3 * sorted_data.len() / 4;
557        let q1 = sorted_data[q1_idx];
558        let q3 = sorted_data[q3_idx];
559        let iqr = q3 - q1;
560
561        let lower_bound = q1 - 1.5 * iqr;
562        let upper_bound = q3 + 1.5 * iqr;
563
564        let outlier_count = self
565            .data
566            .iter()
567            .filter(|&&x| !x.is_nan() && (x < lower_bound || x > upper_bound))
568            .count();
569
570        Ok((outlier_count as f64 / sorted_data.len() as f64) * 100.0)
571    }
572
573    /// Calculate signal-to-noise ratio
574    fn calculate_snr(&self) -> Result<f64> {
575        let valid_data: Vec<f64> = self
576            .data
577            .iter()
578            .filter(|&&x| !x.is_nan())
579            .cloned()
580            .collect();
581
582        if valid_data.is_empty() {
583            return Ok(0.0);
584        }
585
586        let mean = valid_data.iter().sum::<f64>() / valid_data.len() as f64;
587        let variance =
588            valid_data.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / valid_data.len() as f64;
589
590        let signal_power = mean.abs();
591        let noise_power = variance.sqrt();
592
593        if noise_power == 0.0 {
594            Ok(f64::INFINITY)
595        } else {
596            Ok(20.0 * (signal_power / noise_power).log10())
597        }
598    }
599
600    /// Calculate data consistency score based on expected patterns
601    fn calculate_consistency_score(&self) -> Result<f64> {
602        // Check for reasonable sampling rate consistency
603        if self.timestamps.len() < 2 {
604            return Ok(100.0);
605        }
606
607        let mut intervals = Vec::new();
608        for i in 1..self.timestamps.len() {
609            intervals.push(self.timestamps[i] - self.timestamps[i - 1]);
610        }
611
612        if intervals.is_empty() {
613            return Ok(100.0);
614        }
615
616        let mean_interval = intervals.iter().sum::<i64>() as f64 / intervals.len() as f64;
617        let std_interval = {
618            let variance = intervals
619                .iter()
620                .map(|&x| (x as f64 - mean_interval).powi(2))
621                .sum::<f64>()
622                / intervals.len() as f64;
623            variance.sqrt()
624        };
625
626        // Consistency score based on interval regularity (0-100)
627        let consistency = if mean_interval == 0.0 {
628            0.0
629        } else {
630            100.0 - (std_interval / mean_interval * 100.0).min(100.0)
631        };
632
633        Ok(consistency.max(0.0))
634    }
635}
636
637/// Comprehensive IoT sensor analysis
638pub struct IoTAnalysis {
639    /// Environmental sensor analysis
640    pub environmental: Option<EnvironmentalSensorAnalysis>,
641    /// Motion sensor analysis
642    pub motion: Option<MotionSensorAnalysis>,
643    /// Data quality assessments
644    pub quality_assessments: HashMap<String, DataQuality>,
645}
646
647impl Default for IoTAnalysis {
648    fn default() -> Self {
649        Self::new()
650    }
651}
652
653impl IoTAnalysis {
654    /// Create new IoT analysis
655    pub fn new() -> Self {
656        Self {
657            environmental: None,
658            motion: None,
659            quality_assessments: HashMap::new(),
660        }
661    }
662
663    /// Add environmental sensor analysis
664    pub fn with_environmental(mut self, analysis: EnvironmentalSensorAnalysis) -> Self {
665        self.environmental = Some(analysis);
666        self
667    }
668
669    /// Add motion sensor analysis
670    pub fn with_motion(mut self, analysis: MotionSensorAnalysis) -> Self {
671        self.motion = Some(analysis);
672        self
673    }
674
675    /// Add data quality assessment for a sensor
676    pub fn add_quality_assessment(&mut self, sensorname: String, quality: DataQuality) {
677        self.quality_assessments.insert(sensorname, quality);
678    }
679
680    /// Generate comprehensive IoT system health report
681    pub fn system_health_report(&self) -> Result<HashMap<String, String>> {
682        let mut report = HashMap::new();
683
684        // Environmental system health
685        if let Some(ref env) = self.environmental {
686            let malfunctions = env.detect_sensor_malfunctions()?;
687            let total_issues: usize = malfunctions.values().map(|v| v.len()).sum();
688
689            let env_status = if total_issues == 0 {
690                "All environmental sensors functioning normally".to_string()
691            } else {
692                format!("{total_issues} environmental sensor issues detected")
693            };
694            report.insert("Environmental_Status".to_string(), env_status);
695
696            // Energy recommendations
697            let recommendations = env.energy_optimization_recommendations()?;
698            if !recommendations.is_empty() {
699                report.insert(
700                    "Energy_Recommendations".to_string(),
701                    recommendations.join("; "),
702                );
703            }
704        }
705
706        // Motion system health
707        if let Some(ref motion) = self.motion {
708            if motion.acceleration.is_some() {
709                let activities = motion.activity_recognition()?;
710                let activity_summary = format!("{} activity periods detected", activities.len());
711                report.insert("Activity_Status".to_string(), activity_summary);
712            }
713
714            if motion.gps.is_some() {
715                let distance = motion.distance_traveled()?;
716                report.insert(
717                    "Distance_Traveled".to_string(),
718                    format!("{distance:.2} meters"),
719                );
720            }
721        }
722
723        // Overall data quality
724        let mut quality_issues = 0;
725        for quality in self.quality_assessments.values() {
726            if quality.missing_percentage > 10.0
727                || quality.outlier_percentage > 5.0
728                || quality.snr < 10.0
729            {
730                quality_issues += 1;
731            }
732        }
733
734        let quality_status = if quality_issues == 0 {
735            "All sensors showing good data quality".to_string()
736        } else {
737            format!("{quality_issues} sensors showing data quality issues")
738        };
739        report.insert("Data_Quality_Status".to_string(), quality_status);
740
741        Ok(report)
742    }
743
744    /// Predict maintenance needs based on sensor data patterns
745    pub fn predictive_maintenance(&self) -> Result<Vec<String>> {
746        let mut maintenance_alerts = Vec::new();
747
748        // Check data quality degradation
749        for (sensor_name, quality) in &self.quality_assessments {
750            if quality.missing_percentage > 20.0 {
751                maintenance_alerts.push(format!(
752                    "Sensor '{sensor_name}' may need replacement - high missing data rate"
753                ));
754            }
755
756            if quality.outlier_percentage > 15.0 {
757                maintenance_alerts.push(format!(
758                    "Sensor '{sensor_name}' may need calibration - high outlier rate"
759                ));
760            }
761
762            if quality.snr < 5.0 {
763                maintenance_alerts.push(format!(
764                    "Sensor '{sensor_name}' may have connectivity issues - low signal quality"
765                ));
766            }
767        }
768
769        // Environmental sensor specific maintenance
770        if let Some(ref env) = self.environmental {
771            let malfunctions = env.detect_sensor_malfunctions()?;
772
773            for (sensor_type, issues) in malfunctions {
774                if issues.len() > 100 {
775                    maintenance_alerts.push(format!(
776                        "{sensor_type} sensor requires immediate attention - multiple malfunctions detected"
777                    ));
778                }
779            }
780        }
781
782        Ok(maintenance_alerts)
783    }
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789    use scirs2_core::ndarray::{arr1, arr2};
790
791    #[test]
792    fn test_environmental_sensor_analysis() {
793        let timestamps = arr1(&[1, 2, 3, 4, 5]);
794        let temperatures = arr1(&[20.0, 21.0, 22.0, 23.0, 24.0]);
795        let humidity = arr1(&[45.0, 46.0, 47.0, 48.0, 49.0]);
796
797        let analysis = EnvironmentalSensorAnalysis::new(timestamps, 1.0)
798            .unwrap()
799            .with_temperature(temperatures)
800            .unwrap()
801            .with_humidity(humidity)
802            .unwrap();
803
804        let comfort = analysis.comfort_index().unwrap();
805        assert_eq!(comfort.len(), 5);
806        assert!(comfort.iter().all(|&x| (0.0..=100.0).contains(&x)));
807
808        let malfunctions = analysis.detect_sensor_malfunctions().unwrap();
809        assert!(malfunctions.contains_key("Temperature"));
810        assert!(malfunctions.contains_key("Humidity"));
811    }
812
813    #[test]
814    fn test_motion_sensor_analysis() {
815        let timestamps = arr1(&[1, 2, 3, 4, 5]);
816        let accel_data = arr2(&[
817            [1.0, 2.0, 9.8],
818            [1.1, 2.1, 9.9],
819            [1.2, 2.2, 10.0],
820            [1.3, 2.3, 10.1],
821            [1.4, 2.4, 10.2],
822        ]);
823
824        // Use a lower sampling frequency so the window size fits our data
825        let analysis = MotionSensorAnalysis::new(timestamps, 2.0)
826            .unwrap()
827            .with_accelerometer(accel_data)
828            .unwrap();
829
830        let activities = analysis.activity_recognition().unwrap();
831        assert!(!activities.is_empty());
832
833        let _steps = analysis.step_count().unwrap();
834    }
835
836    #[test]
837    fn test_iot_data_quality() {
838        let data = arr1(&[1.0, 2.0, 3.0, 100.0, 5.0]); // Contains outlier
839        let timestamps = arr1(&[1, 2, 3, 4, 5]);
840
841        let quality_analysis =
842            IoTDataQualityAnalysis::new(data, SensorType::Temperature, timestamps).unwrap();
843        let quality = quality_analysis.assess_quality().unwrap();
844
845        assert!(quality.outlier_percentage > 0.0);
846        assert!(quality.consistency_score >= 0.0 && quality.consistency_score <= 100.0);
847    }
848
849    #[test]
850    fn test_fall_detection() {
851        let timestamps = arr1(&[1, 2, 3, 4, 5]);
852        // Simulate fall: normal acceleration followed by high impact then stillness
853        let accel_data = arr2(&[
854            [1.0, 2.0, 9.8],    // Normal
855            [1.0, 2.0, 9.8],    // Normal
856            [10.0, 15.0, 20.0], // High impact (fall)
857            [0.5, 0.5, 0.5],    // Near stillness after fall
858            [0.5, 0.5, 0.5],    // Continued stillness
859        ]);
860
861        let analysis = MotionSensorAnalysis::new(timestamps, 100.0)
862            .unwrap()
863            .with_accelerometer(accel_data)
864            .unwrap();
865
866        let falls = analysis.fall_detection(25.0).unwrap();
867        assert!(!falls.is_empty());
868        assert_eq!(falls[0], 2); // Fall detected at index 2
869    }
870}