1use 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#[derive(Debug, Clone)]
15pub enum SensorType {
16 Temperature,
18 Humidity,
20 Pressure,
22 Light,
24 Motion,
26 Accelerometer,
28 GPS,
30 Sound,
32 AirQuality,
34 Energy,
36}
37
38#[derive(Debug, Clone)]
40pub struct DataQuality {
41 pub missing_percentage: f64,
43 pub outlier_percentage: f64,
45 pub snr: f64,
47 pub consistency_score: f64,
49}
50
51pub struct EnvironmentalSensorAnalysis {
53 pub temperature: Option<Array1<f64>>,
55 pub humidity: Option<Array1<f64>>,
57 pub pressure: Option<Array1<f64>>,
59 pub light: Option<Array1<f64>>,
61 pub timestamps: Array1<i64>,
63 pub sampling_interval: f64,
65}
66
67impl EnvironmentalSensorAnalysis {
68 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 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 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 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 pub fn detect_sensor_malfunctions(&self) -> Result<HashMap<String, Vec<usize>>> {
119 let mut malfunctions = HashMap::new();
120
121 if let Some(ref temp_data) = self.temperature {
123 let mut temp_issues = Vec::new();
124
125 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 for j in (i - consecutive_count)..i {
134 temp_issues.push(j);
135 }
136 }
137 consecutive_count = 1;
138 }
139 }
140
141 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 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 if let Some(ref humidity_data) = self.humidity {
160 let mut humidity_issues = Vec::new();
161
162 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 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 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 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[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 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 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
262pub struct MotionSensorAnalysis {
264 pub acceleration: Option<Array2<f64>>,
266 pub motion: Option<Array1<f64>>,
268 pub gps: Option<Array2<f64>>,
270 pub timestamps: Array1<i64>,
272 pub fs: f64,
274}
275
276impl MotionSensorAnalysis {
277 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 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 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 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 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; 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; }
351
352 let window = accel_data.slice(scirs2_core::ndarray::s![start..end, ..]);
353
354 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 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 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 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; total_distance += distance;
412 }
413
414 Ok(total_distance)
415 }
416
417 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 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 if magnitude > threshold {
431 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 fall_events.push(i);
445 }
446 }
447 }
448 }
449
450 Ok(fall_events)
451 }
452
453 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 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 let mean_magnitude = magnitude.clone().mean();
467 let filtered: Array1<f64> = magnitude.iter().map(|&x| x - mean_magnitude).collect();
468
469 let mut steps = 0;
471 let min_peak_height = 1.0; let min_peak_distance = (0.5 * self.fs) as usize; 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
490pub struct IoTDataQualityAnalysis {
492 pub data: Array1<f64>,
494 pub sensor_type: SensorType,
496 pub timestamps: Array1<i64>,
498}
499
500impl IoTDataQualityAnalysis {
501 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 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 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 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 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 fn calculate_consistency_score(&self) -> Result<f64> {
602 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 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
637pub struct IoTAnalysis {
639 pub environmental: Option<EnvironmentalSensorAnalysis>,
641 pub motion: Option<MotionSensorAnalysis>,
643 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 pub fn new() -> Self {
656 Self {
657 environmental: None,
658 motion: None,
659 quality_assessments: HashMap::new(),
660 }
661 }
662
663 pub fn with_environmental(mut self, analysis: EnvironmentalSensorAnalysis) -> Self {
665 self.environmental = Some(analysis);
666 self
667 }
668
669 pub fn with_motion(mut self, analysis: MotionSensorAnalysis) -> Self {
671 self.motion = Some(analysis);
672 self
673 }
674
675 pub fn add_quality_assessment(&mut self, sensorname: String, quality: DataQuality) {
677 self.quality_assessments.insert(sensorname, quality);
678 }
679
680 pub fn system_health_report(&self) -> Result<HashMap<String, String>> {
682 let mut report = HashMap::new();
683
684 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 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 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 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 pub fn predictive_maintenance(&self) -> Result<Vec<String>> {
746 let mut maintenance_alerts = Vec::new();
747
748 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 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 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]); 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 let accel_data = arr2(&[
854 [1.0, 2.0, 9.8], [1.0, 2.0, 9.8], [10.0, 15.0, 20.0], [0.5, 0.5, 0.5], [0.5, 0.5, 0.5], ]);
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); }
870}