feat: Add wifi-densepose-mat disaster detection module

Implements WiFi-Mat (Mass Casualty Assessment Tool) for detecting and
localizing survivors trapped in rubble, earthquakes, and natural disasters.

Architecture:
- Domain-Driven Design with bounded contexts (Detection, Localization, Alerting)
- Modular Rust crate integrating with existing wifi-densepose-* crates
- Event-driven architecture for audit trails and distributed deployments

Features:
- Breathing pattern detection from CSI amplitude variations
- Heartbeat detection using micro-Doppler analysis
- Movement classification (gross, fine, tremor, periodic)
- START protocol-compatible triage classification
- 3D position estimation via triangulation and depth estimation
- Real-time alert generation with priority escalation

Documentation:
- ADR-001: Architecture Decision Record for wifi-Mat
- DDD domain model specification
This commit is contained in:
Claude
2026-01-13 17:24:50 +00:00
parent 0fa9a0b882
commit a17b630c02
31 changed files with 9042 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ members = [
"crates/wifi-densepose-hardware",
"crates/wifi-densepose-wasm",
"crates/wifi-densepose-cli",
"crates/wifi-densepose-mat",
]
[workspace.package]
@@ -91,6 +92,7 @@ wifi-densepose-db = { path = "crates/wifi-densepose-db" }
wifi-densepose-config = { path = "crates/wifi-densepose-config" }
wifi-densepose-hardware = { path = "crates/wifi-densepose-hardware" }
wifi-densepose-wasm = { path = "crates/wifi-densepose-wasm" }
wifi-densepose-mat = { path = "crates/wifi-densepose-mat" }
[profile.release]
lto = true

View File

@@ -0,0 +1,67 @@
[package]
name = "wifi-densepose-mat"
version = "0.1.0"
edition = "2021"
authors = ["WiFi-DensePose Team"]
description = "Mass Casualty Assessment Tool - WiFi-based disaster survivor detection"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/wifi-densepose"
keywords = ["wifi", "disaster", "rescue", "detection", "vital-signs"]
categories = ["science", "algorithms"]
[features]
default = ["std"]
std = []
portable = ["low-power"]
low-power = []
distributed = ["tokio/sync"]
drone = ["distributed"]
serde = ["dep:serde", "chrono/serde"]
[dependencies]
# Workspace dependencies
wifi-densepose-core = { path = "../wifi-densepose-core" }
wifi-densepose-signal = { path = "../wifi-densepose-signal" }
wifi-densepose-nn = { path = "../wifi-densepose-nn" }
# Async runtime
tokio = { version = "1.35", features = ["rt", "sync", "time"] }
async-trait = "0.1"
# Error handling
thiserror = "1.0"
anyhow = "1.0"
# Serialization
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = "1.0"
# Time handling
chrono = { version = "0.4", features = ["serde"] }
# Math and signal processing
num-complex = "0.4"
ndarray = "0.15"
rustfft = "6.1"
# Utilities
uuid = { version = "1.6", features = ["v4", "serde"] }
tracing = "0.1"
parking_lot = "0.12"
# Geo calculations
geo = "0.27"
[dev-dependencies]
tokio-test = "0.4"
criterion = { version = "0.5", features = ["html_reports"] }
proptest = "1.4"
approx = "0.5"
[[bench]]
name = "detection_bench"
harness = false
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

View File

@@ -0,0 +1,339 @@
//! Alert dispatching and delivery.
use crate::domain::{Alert, AlertId, Priority, Survivor};
use crate::MatError;
use super::AlertGenerator;
use std::collections::HashMap;
/// Configuration for alert dispatch
#[derive(Debug, Clone)]
pub struct AlertConfig {
/// Enable audio alerts
pub audio_enabled: bool,
/// Enable visual alerts
pub visual_enabled: bool,
/// Escalation timeout in seconds
pub escalation_timeout_secs: u64,
/// Maximum pending alerts before forced escalation
pub max_pending_alerts: usize,
/// Auto-acknowledge after seconds (0 = disabled)
pub auto_ack_secs: u64,
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
audio_enabled: true,
visual_enabled: true,
escalation_timeout_secs: 300, // 5 minutes
max_pending_alerts: 50,
auto_ack_secs: 0, // Disabled
}
}
}
/// Dispatcher for sending alerts to rescue teams
pub struct AlertDispatcher {
config: AlertConfig,
generator: AlertGenerator,
pending_alerts: parking_lot::RwLock<HashMap<AlertId, Alert>>,
handlers: Vec<Box<dyn AlertHandler>>,
}
impl AlertDispatcher {
/// Create a new alert dispatcher
pub fn new(config: AlertConfig) -> Self {
Self {
config,
generator: AlertGenerator::new(),
pending_alerts: parking_lot::RwLock::new(HashMap::new()),
handlers: Vec::new(),
}
}
/// Add an alert handler
pub fn add_handler(&mut self, handler: Box<dyn AlertHandler>) {
self.handlers.push(handler);
}
/// Generate an alert for a survivor
pub fn generate_alert(&self, survivor: &Survivor) -> Result<Alert, MatError> {
self.generator.generate(survivor)
}
/// Dispatch an alert
pub async fn dispatch(&self, alert: Alert) -> Result<(), MatError> {
let alert_id = alert.id().clone();
let priority = alert.priority();
// Store in pending alerts
self.pending_alerts.write().insert(alert_id.clone(), alert.clone());
// Log the alert
tracing::info!(
alert_id = %alert_id,
priority = ?priority,
title = %alert.payload().title,
"Dispatching alert"
);
// Send to all handlers
for handler in &self.handlers {
if let Err(e) = handler.handle(&alert).await {
tracing::warn!(
alert_id = %alert_id,
handler = %handler.name(),
error = %e,
"Handler failed to process alert"
);
}
}
// Check if we're at capacity
let pending_count = self.pending_alerts.read().len();
if pending_count >= self.config.max_pending_alerts {
tracing::warn!(
pending_count,
max = self.config.max_pending_alerts,
"Alert capacity reached - escalating oldest alerts"
);
self.escalate_oldest().await?;
}
Ok(())
}
/// Acknowledge an alert
pub fn acknowledge(&self, alert_id: &AlertId, by: &str) -> Result<(), MatError> {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.get_mut(alert_id) {
alert.acknowledge(by);
tracing::info!(
alert_id = %alert_id,
acknowledged_by = by,
"Alert acknowledged"
);
Ok(())
} else {
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
}
}
/// Resolve an alert
pub fn resolve(&self, alert_id: &AlertId, resolution: crate::domain::AlertResolution) -> Result<(), MatError> {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.remove(alert_id) {
let mut resolved_alert = alert;
resolved_alert.resolve(resolution);
tracing::info!(
alert_id = %alert_id,
"Alert resolved"
);
Ok(())
} else {
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
}
}
/// Get all pending alerts
pub fn pending(&self) -> Vec<Alert> {
self.pending_alerts.read().values().cloned().collect()
}
/// Get pending alerts by priority
pub fn pending_by_priority(&self, priority: Priority) -> Vec<Alert> {
self.pending_alerts
.read()
.values()
.filter(|a| a.priority() == priority)
.cloned()
.collect()
}
/// Get count of pending alerts
pub fn pending_count(&self) -> usize {
self.pending_alerts.read().len()
}
/// Check and escalate timed-out alerts
pub async fn check_escalations(&self) -> Result<u32, MatError> {
let timeout_secs = self.config.escalation_timeout_secs as i64;
let mut escalated = 0;
let mut to_escalate = Vec::new();
{
let alerts = self.pending_alerts.read();
for (id, alert) in alerts.iter() {
if alert.needs_escalation(timeout_secs) {
to_escalate.push(id.clone());
}
}
}
for id in to_escalate {
let mut alerts = self.pending_alerts.write();
if let Some(alert) = alerts.get_mut(&id) {
alert.escalate();
escalated += 1;
tracing::warn!(
alert_id = %id,
new_priority = ?alert.priority(),
"Alert escalated due to timeout"
);
}
}
Ok(escalated)
}
/// Escalate oldest pending alerts
async fn escalate_oldest(&self) -> Result<(), MatError> {
let mut alerts: Vec<_> = self.pending_alerts.read()
.iter()
.map(|(id, alert)| (id.clone(), *alert.created_at()))
.collect();
// Sort by creation time (oldest first)
alerts.sort_by_key(|(_, created)| *created);
// Escalate oldest 10%
let to_escalate = (alerts.len() / 10).max(1);
let mut pending = self.pending_alerts.write();
for (id, _) in alerts.into_iter().take(to_escalate) {
if let Some(alert) = pending.get_mut(&id) {
alert.escalate();
}
}
Ok(())
}
/// Get configuration
pub fn config(&self) -> &AlertConfig {
&self.config
}
}
/// Handler for processing alerts
#[async_trait::async_trait]
pub trait AlertHandler: Send + Sync {
/// Handler name
fn name(&self) -> &str;
/// Handle an alert
async fn handle(&self, alert: &Alert) -> Result<(), MatError>;
}
/// Console/logging alert handler
pub struct ConsoleAlertHandler;
#[async_trait::async_trait]
impl AlertHandler for ConsoleAlertHandler {
fn name(&self) -> &str {
"console"
}
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
let priority_indicator = match alert.priority() {
Priority::Critical => "🔴",
Priority::High => "🟠",
Priority::Medium => "🟡",
Priority::Low => "🔵",
};
println!("\n{} ALERT {}", priority_indicator, "=".repeat(50));
println!("ID: {}", alert.id());
println!("Priority: {:?}", alert.priority());
println!("Title: {}", alert.payload().title);
println!("{}", "=".repeat(60));
println!("{}", alert.payload().message);
println!("{}", "=".repeat(60));
println!("Recommended Action: {}", alert.payload().recommended_action);
println!("{}\n", "=".repeat(60));
Ok(())
}
}
/// Audio alert handler (placeholder)
pub struct AudioAlertHandler;
#[async_trait::async_trait]
impl AlertHandler for AudioAlertHandler {
fn name(&self) -> &str {
"audio"
}
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
// In production, this would trigger actual audio alerts
let pattern = alert.priority().audio_pattern();
tracing::debug!(
alert_id = %alert.id(),
pattern,
"Would play audio alert"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{SurvivorId, TriageStatus, AlertPayload};
fn create_test_alert() -> Alert {
Alert::new(
SurvivorId::new(),
Priority::High,
AlertPayload::new("Test Alert", "Test message", TriageStatus::Delayed),
)
}
#[tokio::test]
async fn test_dispatch_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let result = dispatcher.dispatch(alert).await;
assert!(result.is_ok());
assert_eq!(dispatcher.pending_count(), 1);
}
#[tokio::test]
async fn test_acknowledge_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let alert_id = alert.id().clone();
dispatcher.dispatch(alert).await.unwrap();
let result = dispatcher.acknowledge(&alert_id, "Team Alpha");
assert!(result.is_ok());
let pending = dispatcher.pending();
assert!(pending.iter().any(|a| a.id() == &alert_id && a.acknowledged_by() == Some("Team Alpha")));
}
#[tokio::test]
async fn test_resolve_alert() {
let dispatcher = AlertDispatcher::new(AlertConfig::default());
let alert = create_test_alert();
let alert_id = alert.id().clone();
dispatcher.dispatch(alert).await.unwrap();
let resolution = crate::domain::AlertResolution {
resolution_type: crate::domain::ResolutionType::Rescued,
notes: "Survivor extracted successfully".to_string(),
resolved_by: Some("Team Alpha".to_string()),
resolved_at: chrono::Utc::now(),
};
dispatcher.resolve(&alert_id, resolution).unwrap();
assert_eq!(dispatcher.pending_count(), 0);
}
}

View File

@@ -0,0 +1,288 @@
//! Alert generation from survivor detections.
use crate::domain::{
Alert, AlertPayload, Priority, Survivor, TriageStatus, ScanZoneId,
};
use crate::MatError;
/// Generator for alerts based on survivor status
pub struct AlertGenerator {
/// Zone name lookup (would be connected to event in production)
zone_names: std::collections::HashMap<ScanZoneId, String>,
}
impl AlertGenerator {
/// Create a new alert generator
pub fn new() -> Self {
Self {
zone_names: std::collections::HashMap::new(),
}
}
/// Register a zone name
pub fn register_zone(&mut self, zone_id: ScanZoneId, name: String) {
self.zone_names.insert(zone_id, name);
}
/// Generate an alert for a survivor
pub fn generate(&self, survivor: &Survivor) -> Result<Alert, MatError> {
let priority = Priority::from_triage(survivor.triage_status());
let payload = self.create_payload(survivor);
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate an escalation alert
pub fn generate_escalation(
&self,
survivor: &Survivor,
reason: &str,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!("ESCALATED: {}", payload.title);
payload.message = format!(
"{}\n\nReason for escalation: {}",
payload.message, reason
);
// Escalated alerts are always at least high priority
let priority = match survivor.triage_status() {
TriageStatus::Immediate => Priority::Critical,
_ => Priority::High,
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate a status change alert
pub fn generate_status_change(
&self,
survivor: &Survivor,
previous_status: &TriageStatus,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!(
"Status Change: {}{}",
previous_status, survivor.triage_status()
);
// Determine if this is an upgrade (worse) or downgrade (better)
let is_upgrade = survivor.triage_status().priority() < previous_status.priority();
if is_upgrade {
payload.message = format!(
"URGENT: Survivor condition has WORSENED.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
} else {
payload.message = format!(
"Survivor condition has improved.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
}
let priority = if is_upgrade {
Priority::from_triage(survivor.triage_status())
} else {
Priority::Medium
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Create alert payload from survivor data
fn create_payload(&self, survivor: &Survivor) -> AlertPayload {
let zone_name = self.zone_names
.get(survivor.zone_id())
.map(String::as_str)
.unwrap_or("Unknown Zone");
let title = format!(
"{} Survivor Detected - {}",
survivor.triage_status(),
zone_name
);
let vital_info = self.format_vital_signs(survivor);
let location_info = self.format_location(survivor);
let message = format!(
"Survivor ID: {}\n\
Zone: {}\n\
Triage: {}\n\
Confidence: {:.0}%\n\n\
Vital Signs:\n{}\n\n\
Location:\n{}",
survivor.id(),
zone_name,
survivor.triage_status(),
survivor.confidence() * 100.0,
vital_info,
location_info
);
let recommended_action = self.recommend_action(survivor);
AlertPayload::new(title, message, survivor.triage_status().clone())
.with_action(recommended_action)
.with_metadata("zone_id", survivor.zone_id().to_string())
.with_metadata("confidence", format!("{:.2}", survivor.confidence()))
}
/// Format vital signs for display
fn format_vital_signs(&self, survivor: &Survivor) -> String {
let vitals = survivor.vital_signs();
let mut lines = Vec::new();
if let Some(reading) = vitals.latest() {
if let Some(breathing) = &reading.breathing {
lines.push(format!(
" Breathing: {:.1} BPM ({:?})",
breathing.rate_bpm, breathing.pattern_type
));
} else {
lines.push(" Breathing: Not detected".to_string());
}
if let Some(heartbeat) = &reading.heartbeat {
lines.push(format!(
" Heartbeat: {:.0} BPM ({:?})",
heartbeat.rate_bpm, heartbeat.strength
));
}
lines.push(format!(
" Movement: {:?} (intensity: {:.1})",
reading.movement.movement_type,
reading.movement.intensity
));
} else {
lines.push(" No recent readings".to_string());
}
lines.join("\n")
}
/// Format location for display
fn format_location(&self, survivor: &Survivor) -> String {
match survivor.location() {
Some(loc) => {
let depth_str = if loc.is_buried() {
format!("{:.1}m below surface", loc.depth())
} else {
"At surface level".to_string()
};
format!(
" Position: ({:.1}, {:.1})\n\
Depth: {}\n\
Uncertainty: ±{:.1}m",
loc.x, loc.y,
depth_str,
loc.uncertainty.horizontal_error
)
}
None => " Position not yet determined".to_string(),
}
}
/// Recommend action based on triage status
fn recommend_action(&self, survivor: &Survivor) -> String {
match survivor.triage_status() {
TriageStatus::Immediate => {
"IMMEDIATE RESCUE REQUIRED. Deploy heavy rescue team. \
Prepare for airway management and critical care on extraction."
}
TriageStatus::Delayed => {
"Rescue team required. Mark location. Provide reassurance \
if communication is possible. Monitor for status changes."
}
TriageStatus::Minor => {
"Lower priority. Guide to extraction if conscious and mobile. \
Assign walking wounded assistance team."
}
TriageStatus::Deceased => {
"Mark location for recovery. Do not allocate rescue resources. \
Document for incident report."
}
TriageStatus::Unknown => {
"Requires additional assessment. Deploy scout team with \
enhanced detection equipment to confirm status."
}
}
.to_string()
}
}
impl Default for AlertGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore, VitalSignsReading};
use chrono::Utc;
fn create_test_survivor() -> Survivor {
let vitals = VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
};
Survivor::new(ScanZoneId::new(), vitals, None)
}
#[test]
fn test_generate_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let result = generator.generate(&survivor);
assert!(result.is_ok());
let alert = result.unwrap();
assert!(alert.is_pending());
}
#[test]
fn test_escalation_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_escalation(&survivor, "Vital signs deteriorating")
.unwrap();
assert!(alert.payload().title.contains("ESCALATED"));
assert!(matches!(alert.priority(), Priority::Critical | Priority::High));
}
#[test]
fn test_status_change_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_status_change(
&survivor,
&TriageStatus::Minor,
).unwrap();
assert!(alert.payload().title.contains("Status Change"));
}
}

View File

@@ -0,0 +1,9 @@
//! Alerting module for emergency notifications.
mod generator;
mod dispatcher;
mod triage_service;
pub use generator::AlertGenerator;
pub use dispatcher::{AlertDispatcher, AlertConfig};
pub use triage_service::{TriageService, PriorityCalculator};

View File

@@ -0,0 +1,317 @@
//! Triage service for calculating and updating survivor priority.
use crate::domain::{
Priority, Survivor, TriageStatus, VitalSignsReading,
triage::TriageCalculator,
};
/// Service for triage operations
pub struct TriageService;
impl TriageService {
/// Calculate triage status from vital signs
pub fn calculate_triage(vitals: &VitalSignsReading) -> TriageStatus {
TriageCalculator::calculate(vitals)
}
/// Check if survivor should be upgraded
pub fn should_upgrade(survivor: &Survivor) -> bool {
TriageCalculator::should_upgrade(
survivor.triage_status(),
survivor.is_deteriorating(),
)
}
/// Get upgraded status
pub fn upgrade_status(current: &TriageStatus) -> TriageStatus {
TriageCalculator::upgrade(current)
}
/// Evaluate overall severity for multiple survivors
pub fn evaluate_mass_casualty(survivors: &[&Survivor]) -> MassCasualtyAssessment {
let total = survivors.len() as u32;
let mut immediate = 0u32;
let mut delayed = 0u32;
let mut minor = 0u32;
let mut deceased = 0u32;
let mut unknown = 0u32;
for survivor in survivors {
match survivor.triage_status() {
TriageStatus::Immediate => immediate += 1,
TriageStatus::Delayed => delayed += 1,
TriageStatus::Minor => minor += 1,
TriageStatus::Deceased => deceased += 1,
TriageStatus::Unknown => unknown += 1,
}
}
let severity = Self::calculate_severity(immediate, delayed, total);
let resource_level = Self::calculate_resource_level(immediate, delayed, minor);
MassCasualtyAssessment {
total,
immediate,
delayed,
minor,
deceased,
unknown,
severity,
resource_level,
}
}
/// Calculate overall severity level
fn calculate_severity(immediate: u32, delayed: u32, total: u32) -> SeverityLevel {
if total == 0 {
return SeverityLevel::Minimal;
}
let critical_ratio = (immediate + delayed) as f64 / total as f64;
if immediate >= 10 || critical_ratio > 0.5 {
SeverityLevel::Critical
} else if immediate >= 5 || critical_ratio > 0.3 {
SeverityLevel::Major
} else if immediate >= 1 || critical_ratio > 0.1 {
SeverityLevel::Moderate
} else {
SeverityLevel::Minimal
}
}
/// Calculate resource level needed
fn calculate_resource_level(immediate: u32, delayed: u32, minor: u32) -> ResourceLevel {
// Each immediate needs ~4 rescuers
// Each delayed needs ~2 rescuers
// Each minor needs ~0.5 rescuers
let rescuers_needed = immediate * 4 + delayed * 2 + minor / 2;
if rescuers_needed >= 100 {
ResourceLevel::MutualAid
} else if rescuers_needed >= 50 {
ResourceLevel::MultiAgency
} else if rescuers_needed >= 20 {
ResourceLevel::Enhanced
} else if rescuers_needed >= 5 {
ResourceLevel::Standard
} else {
ResourceLevel::Minimal
}
}
}
/// Calculator for alert priority
pub struct PriorityCalculator;
impl PriorityCalculator {
/// Calculate priority from triage status
pub fn from_triage(status: &TriageStatus) -> Priority {
Priority::from_triage(status)
}
/// Calculate priority with additional factors
pub fn calculate_with_factors(
status: &TriageStatus,
deteriorating: bool,
time_since_detection_mins: u64,
depth_meters: Option<f64>,
) -> Priority {
let base_priority = Priority::from_triage(status);
// Adjust for deterioration
let priority = if deteriorating && base_priority != Priority::Critical {
match base_priority {
Priority::High => Priority::Critical,
Priority::Medium => Priority::High,
Priority::Low => Priority::Medium,
Priority::Critical => Priority::Critical,
}
} else {
base_priority
};
// Adjust for time (longer = more urgent)
let priority = if time_since_detection_mins > 30 && priority == Priority::Medium {
Priority::High
} else {
priority
};
// Adjust for depth (deeper = more complex rescue)
if let Some(depth) = depth_meters {
if depth > 3.0 && priority == Priority::High {
return Priority::Critical;
}
}
priority
}
}
/// Mass casualty assessment result
#[derive(Debug, Clone)]
pub struct MassCasualtyAssessment {
/// Total survivors detected
pub total: u32,
/// Immediate (Red) count
pub immediate: u32,
/// Delayed (Yellow) count
pub delayed: u32,
/// Minor (Green) count
pub minor: u32,
/// Deceased (Black) count
pub deceased: u32,
/// Unknown count
pub unknown: u32,
/// Overall severity level
pub severity: SeverityLevel,
/// Resource level needed
pub resource_level: ResourceLevel,
}
impl MassCasualtyAssessment {
/// Get count of living survivors
pub fn living(&self) -> u32 {
self.immediate + self.delayed + self.minor
}
/// Get count needing active rescue
pub fn needs_rescue(&self) -> u32 {
self.immediate + self.delayed
}
/// Get summary string
pub fn summary(&self) -> String {
format!(
"MCI Assessment:\n\
Total: {} (Living: {}, Deceased: {})\n\
Immediate: {}, Delayed: {}, Minor: {}\n\
Severity: {:?}, Resources: {:?}",
self.total, self.living(), self.deceased,
self.immediate, self.delayed, self.minor,
self.severity, self.resource_level
)
}
}
/// Severity levels for mass casualty incidents
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeverityLevel {
/// Few or no critical patients
Minimal,
/// Some critical patients, manageable
Moderate,
/// Many critical patients, challenging
Major,
/// Overwhelming number of critical patients
Critical,
}
/// Resource levels for response
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceLevel {
/// Standard response adequate
Minimal,
/// Standard response needed
Standard,
/// Enhanced response needed
Enhanced,
/// Multi-agency response needed
MultiAgency,
/// Regional mutual aid required
MutualAid,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{
BreathingPattern, BreathingType, ConfidenceScore, ScanZoneId,
};
use chrono::Utc;
fn create_test_vitals(rate_bpm: f32) -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
}
}
#[test]
fn test_calculate_triage() {
let normal = create_test_vitals(16.0);
assert!(matches!(
TriageService::calculate_triage(&normal),
TriageStatus::Immediate | TriageStatus::Delayed | TriageStatus::Minor
));
let fast = create_test_vitals(35.0);
assert!(matches!(
TriageService::calculate_triage(&fast),
TriageStatus::Immediate
));
}
#[test]
fn test_priority_from_triage() {
assert_eq!(
PriorityCalculator::from_triage(&TriageStatus::Immediate),
Priority::Critical
);
assert_eq!(
PriorityCalculator::from_triage(&TriageStatus::Delayed),
Priority::High
);
}
#[test]
fn test_mass_casualty_assessment() {
let survivors: Vec<Survivor> = (0..10)
.map(|i| {
let rate = if i < 3 { 35.0 } else if i < 6 { 16.0 } else { 18.0 };
Survivor::new(
ScanZoneId::new(),
create_test_vitals(rate),
None,
)
})
.collect();
let survivor_refs: Vec<&Survivor> = survivors.iter().collect();
let assessment = TriageService::evaluate_mass_casualty(&survivor_refs);
assert_eq!(assessment.total, 10);
assert!(assessment.living() >= assessment.needs_rescue());
}
#[test]
fn test_priority_with_factors() {
// Deteriorating patient should be upgraded
let priority = PriorityCalculator::calculate_with_factors(
&TriageStatus::Delayed,
true,
0,
None,
);
assert_eq!(priority, Priority::Critical);
// Deep burial should upgrade
let priority = PriorityCalculator::calculate_with_factors(
&TriageStatus::Delayed,
false,
0,
Some(4.0),
);
assert_eq!(priority, Priority::Critical);
}
}

View File

@@ -0,0 +1,293 @@
//! Breathing pattern detection from CSI signals.
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore};
/// Configuration for breathing detection
#[derive(Debug, Clone)]
pub struct BreathingDetectorConfig {
/// Minimum breathing rate to detect (breaths per minute)
pub min_rate_bpm: f32,
/// Maximum breathing rate to detect
pub max_rate_bpm: f32,
/// Minimum signal amplitude to consider
pub min_amplitude: f32,
/// Window size for FFT analysis (samples)
pub window_size: usize,
/// Overlap between windows (0.0-1.0)
pub window_overlap: f32,
/// Confidence threshold
pub confidence_threshold: f32,
}
impl Default for BreathingDetectorConfig {
fn default() -> Self {
Self {
min_rate_bpm: 4.0, // Very slow breathing
max_rate_bpm: 40.0, // Fast breathing (distressed)
min_amplitude: 0.1,
window_size: 512,
window_overlap: 0.5,
confidence_threshold: 0.3,
}
}
}
/// Detector for breathing patterns in CSI signals
pub struct BreathingDetector {
config: BreathingDetectorConfig,
}
impl BreathingDetector {
/// Create a new breathing detector
pub fn new(config: BreathingDetectorConfig) -> Self {
Self { config }
}
/// Create with default configuration
pub fn with_defaults() -> Self {
Self::new(BreathingDetectorConfig::default())
}
/// Detect breathing pattern from CSI amplitude variations
///
/// Breathing causes periodic chest movement that modulates the WiFi signal.
/// We detect this by looking for periodic variations in the 0.1-0.67 Hz range
/// (corresponding to 6-40 breaths per minute).
pub fn detect(&self, csi_amplitudes: &[f64], sample_rate: f64) -> Option<BreathingPattern> {
if csi_amplitudes.len() < self.config.window_size {
return None;
}
// Calculate the frequency spectrum
let spectrum = self.compute_spectrum(csi_amplitudes);
// Find the dominant frequency in the breathing range
let min_freq = self.config.min_rate_bpm as f64 / 60.0;
let max_freq = self.config.max_rate_bpm as f64 / 60.0;
let (dominant_freq, amplitude) = self.find_dominant_frequency(
&spectrum,
sample_rate,
min_freq,
max_freq,
)?;
// Convert to BPM
let rate_bpm = (dominant_freq * 60.0) as f32;
// Check amplitude threshold
if amplitude < self.config.min_amplitude as f64 {
return None;
}
// Calculate regularity (how peaked is the spectrum)
let regularity = self.calculate_regularity(&spectrum, dominant_freq, sample_rate);
// Determine breathing type based on rate and regularity
let pattern_type = self.classify_pattern(rate_bpm, regularity);
// Calculate confidence
let confidence = self.calculate_confidence(amplitude, regularity);
if confidence < self.config.confidence_threshold {
return None;
}
Some(BreathingPattern {
rate_bpm,
amplitude: amplitude as f32,
regularity,
pattern_type,
})
}
/// Compute frequency spectrum using FFT
fn compute_spectrum(&self, signal: &[f64]) -> Vec<f64> {
use rustfft::{FftPlanner, num_complex::Complex};
let n = signal.len().next_power_of_two();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(n);
// Prepare input with zero padding
let mut buffer: Vec<Complex<f64>> = signal
.iter()
.map(|&x| Complex::new(x, 0.0))
.collect();
buffer.resize(n, Complex::new(0.0, 0.0));
// Apply Hanning window
for (i, sample) in buffer.iter_mut().enumerate().take(signal.len()) {
let window = 0.5 * (1.0 - (2.0 * std::f64::consts::PI * i as f64 / signal.len() as f64).cos());
*sample = Complex::new(sample.re * window, 0.0);
}
fft.process(&mut buffer);
// Return magnitude spectrum (only positive frequencies)
buffer.iter()
.take(n / 2)
.map(|c| c.norm())
.collect()
}
/// Find dominant frequency in a given range
fn find_dominant_frequency(
&self,
spectrum: &[f64],
sample_rate: f64,
min_freq: f64,
max_freq: f64,
) -> Option<(f64, f64)> {
let n = spectrum.len() * 2; // Original FFT size
let freq_resolution = sample_rate / n as f64;
let min_bin = (min_freq / freq_resolution).ceil() as usize;
let max_bin = (max_freq / freq_resolution).floor() as usize;
if min_bin >= spectrum.len() || max_bin >= spectrum.len() || min_bin >= max_bin {
return None;
}
// Find peak in range
let mut max_amplitude = 0.0;
let mut max_bin_idx = min_bin;
for i in min_bin..=max_bin {
if spectrum[i] > max_amplitude {
max_amplitude = spectrum[i];
max_bin_idx = i;
}
}
if max_amplitude < self.config.min_amplitude as f64 {
return None;
}
// Interpolate for better frequency estimate
let freq = max_bin_idx as f64 * freq_resolution;
Some((freq, max_amplitude))
}
/// Calculate how regular/periodic the signal is
fn calculate_regularity(&self, spectrum: &[f64], dominant_freq: f64, sample_rate: f64) -> f32 {
let n = spectrum.len() * 2;
let freq_resolution = sample_rate / n as f64;
let peak_bin = (dominant_freq / freq_resolution).round() as usize;
if peak_bin >= spectrum.len() {
return 0.0;
}
// Measure how much energy is concentrated at the peak vs spread
let peak_power = spectrum[peak_bin];
let total_power: f64 = spectrum.iter().sum();
if total_power == 0.0 {
return 0.0;
}
// Also check harmonics (2x, 3x frequency)
let harmonic_power: f64 = [2, 3].iter()
.filter_map(|&mult| {
let harmonic_bin = peak_bin * mult;
if harmonic_bin < spectrum.len() {
Some(spectrum[harmonic_bin])
} else {
None
}
})
.sum();
((peak_power + harmonic_power * 0.5) / total_power * 3.0).min(1.0) as f32
}
/// Classify the breathing pattern type
fn classify_pattern(&self, rate_bpm: f32, regularity: f32) -> BreathingType {
if rate_bpm < 6.0 {
if regularity < 0.3 {
BreathingType::Agonal
} else {
BreathingType::Shallow
}
} else if rate_bpm < 10.0 {
BreathingType::Shallow
} else if rate_bpm > 30.0 {
BreathingType::Labored
} else if regularity < 0.4 {
BreathingType::Irregular
} else {
BreathingType::Normal
}
}
/// Calculate overall detection confidence
fn calculate_confidence(&self, amplitude: f64, regularity: f32) -> f32 {
// Combine amplitude strength and regularity
let amplitude_score = (amplitude / 1.0).min(1.0) as f32;
let regularity_score = regularity;
// Weight regularity more heavily for breathing detection
amplitude_score * 0.4 + regularity_score * 0.6
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_breathing_signal(rate_bpm: f64, sample_rate: f64, duration: f64) -> Vec<f64> {
let num_samples = (sample_rate * duration) as usize;
let freq = rate_bpm / 60.0;
(0..num_samples)
.map(|i| {
let t = i as f64 / sample_rate;
(2.0 * std::f64::consts::PI * freq * t).sin()
})
.collect()
}
#[test]
fn test_detect_normal_breathing() {
let detector = BreathingDetector::with_defaults();
let signal = generate_breathing_signal(16.0, 100.0, 30.0);
let result = detector.detect(&signal, 100.0);
assert!(result.is_some());
let pattern = result.unwrap();
assert!(pattern.rate_bpm >= 14.0 && pattern.rate_bpm <= 18.0);
assert!(matches!(pattern.pattern_type, BreathingType::Normal));
}
#[test]
fn test_detect_fast_breathing() {
let detector = BreathingDetector::with_defaults();
let signal = generate_breathing_signal(35.0, 100.0, 30.0);
let result = detector.detect(&signal, 100.0);
assert!(result.is_some());
let pattern = result.unwrap();
assert!(pattern.rate_bpm > 30.0);
assert!(matches!(pattern.pattern_type, BreathingType::Labored));
}
#[test]
fn test_no_detection_on_noise() {
let detector = BreathingDetector::with_defaults();
// Random noise with low amplitude
let signal: Vec<f64> = (0..1000)
.map(|i| (i as f64 * 0.1).sin() * 0.01)
.collect();
let result = detector.detect(&signal, 100.0);
// Should either be None or have very low confidence
if let Some(pattern) = result {
assert!(pattern.amplitude < 0.1);
}
}
}

View File

@@ -0,0 +1,390 @@
//! Heartbeat detection from micro-Doppler signatures in CSI.
use crate::domain::{HeartbeatSignature, SignalStrength};
/// Configuration for heartbeat detection
#[derive(Debug, Clone)]
pub struct HeartbeatDetectorConfig {
/// Minimum heart rate to detect (BPM)
pub min_rate_bpm: f32,
/// Maximum heart rate to detect (BPM)
pub max_rate_bpm: f32,
/// Minimum signal strength required
pub min_signal_strength: f64,
/// Window size for analysis
pub window_size: usize,
/// Enable enhanced micro-Doppler processing
pub enhanced_processing: bool,
/// Confidence threshold
pub confidence_threshold: f32,
}
impl Default for HeartbeatDetectorConfig {
fn default() -> Self {
Self {
min_rate_bpm: 30.0, // Very slow (bradycardia)
max_rate_bpm: 200.0, // Very fast (extreme tachycardia)
min_signal_strength: 0.05,
window_size: 1024,
enhanced_processing: true,
confidence_threshold: 0.4,
}
}
}
/// Detector for heartbeat signatures using micro-Doppler analysis
///
/// Heartbeats cause very small chest wall movements (~0.5mm) that can be
/// detected through careful analysis of CSI phase variations at higher
/// frequencies than breathing (0.8-3.3 Hz for 48-200 BPM).
pub struct HeartbeatDetector {
config: HeartbeatDetectorConfig,
}
impl HeartbeatDetector {
/// Create a new heartbeat detector
pub fn new(config: HeartbeatDetectorConfig) -> Self {
Self { config }
}
/// Create with default configuration
pub fn with_defaults() -> Self {
Self::new(HeartbeatDetectorConfig::default())
}
/// Detect heartbeat from CSI phase data
///
/// Heartbeat detection is more challenging than breathing due to:
/// - Much smaller displacement (~0.5mm vs ~10mm for breathing)
/// - Higher frequency (masked by breathing harmonics)
/// - Lower signal-to-noise ratio
///
/// We use micro-Doppler analysis on the phase component after
/// removing the breathing component.
pub fn detect(
&self,
csi_phase: &[f64],
sample_rate: f64,
breathing_rate: Option<f64>,
) -> Option<HeartbeatSignature> {
if csi_phase.len() < self.config.window_size {
return None;
}
// Remove breathing component if known
let filtered = if let Some(br) = breathing_rate {
self.remove_breathing_component(csi_phase, sample_rate, br)
} else {
self.highpass_filter(csi_phase, sample_rate, 0.8)
};
// Compute micro-Doppler spectrum
let spectrum = self.compute_micro_doppler_spectrum(&filtered, sample_rate);
// Find heartbeat frequency
let min_freq = self.config.min_rate_bpm as f64 / 60.0;
let max_freq = self.config.max_rate_bpm as f64 / 60.0;
let (heart_freq, strength) = self.find_heartbeat_frequency(
&spectrum,
sample_rate,
min_freq,
max_freq,
)?;
if strength < self.config.min_signal_strength {
return None;
}
let rate_bpm = (heart_freq * 60.0) as f32;
// Calculate heart rate variability from peak width
let variability = self.estimate_hrv(&spectrum, heart_freq, sample_rate);
// Determine signal strength category
let signal_strength = self.categorize_strength(strength);
// Calculate confidence
let confidence = self.calculate_confidence(strength, variability);
if confidence < self.config.confidence_threshold {
return None;
}
Some(HeartbeatSignature {
rate_bpm,
variability,
strength: signal_strength,
})
}
/// Remove breathing component using notch filter
fn remove_breathing_component(
&self,
signal: &[f64],
sample_rate: f64,
breathing_rate: f64,
) -> Vec<f64> {
// Simple IIR notch filter at breathing frequency and harmonics
let mut filtered = signal.to_vec();
let breathing_freq = breathing_rate / 60.0;
// Notch at fundamental and first two harmonics
for harmonic in 1..=3 {
let notch_freq = breathing_freq * harmonic as f64;
filtered = self.apply_notch_filter(&filtered, sample_rate, notch_freq, 0.05);
}
filtered
}
/// Apply a simple notch filter
fn apply_notch_filter(
&self,
signal: &[f64],
sample_rate: f64,
center_freq: f64,
bandwidth: f64,
) -> Vec<f64> {
// Second-order IIR notch filter
let w0 = 2.0 * std::f64::consts::PI * center_freq / sample_rate;
let bw = 2.0 * std::f64::consts::PI * bandwidth / sample_rate;
let r = 1.0 - bw / 2.0;
let cos_w0 = w0.cos();
let b0 = 1.0;
let b1 = -2.0 * cos_w0;
let b2 = 1.0;
let a1 = -2.0 * r * cos_w0;
let a2 = r * r;
let mut output = vec![0.0; signal.len()];
let mut x1 = 0.0;
let mut x2 = 0.0;
let mut y1 = 0.0;
let mut y2 = 0.0;
for (i, &x) in signal.iter().enumerate() {
let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
output[i] = y;
x2 = x1;
x1 = x;
y2 = y1;
y1 = y;
}
output
}
/// High-pass filter to remove low frequencies
fn highpass_filter(&self, signal: &[f64], sample_rate: f64, cutoff: f64) -> Vec<f64> {
// Simple first-order high-pass filter
let rc = 1.0 / (2.0 * std::f64::consts::PI * cutoff);
let dt = 1.0 / sample_rate;
let alpha = rc / (rc + dt);
let mut output = vec![0.0; signal.len()];
if signal.is_empty() {
return output;
}
output[0] = signal[0];
for i in 1..signal.len() {
output[i] = alpha * (output[i - 1] + signal[i] - signal[i - 1]);
}
output
}
/// Compute micro-Doppler spectrum optimized for heartbeat detection
fn compute_micro_doppler_spectrum(&self, signal: &[f64], _sample_rate: f64) -> Vec<f64> {
use rustfft::{FftPlanner, num_complex::Complex};
let n = signal.len().next_power_of_two();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(n);
// Apply Blackman window for better frequency resolution
let mut buffer: Vec<Complex<f64>> = signal
.iter()
.enumerate()
.map(|(i, &x)| {
let n_f = signal.len() as f64;
let window = 0.42
- 0.5 * (2.0 * std::f64::consts::PI * i as f64 / n_f).cos()
+ 0.08 * (4.0 * std::f64::consts::PI * i as f64 / n_f).cos();
Complex::new(x * window, 0.0)
})
.collect();
buffer.resize(n, Complex::new(0.0, 0.0));
fft.process(&mut buffer);
// Return power spectrum
buffer.iter()
.take(n / 2)
.map(|c| c.norm_sqr())
.collect()
}
/// Find heartbeat frequency in spectrum
fn find_heartbeat_frequency(
&self,
spectrum: &[f64],
sample_rate: f64,
min_freq: f64,
max_freq: f64,
) -> Option<(f64, f64)> {
let n = spectrum.len() * 2;
let freq_resolution = sample_rate / n as f64;
let min_bin = (min_freq / freq_resolution).ceil() as usize;
let max_bin = (max_freq / freq_resolution).floor() as usize;
if min_bin >= spectrum.len() || max_bin >= spectrum.len() {
return None;
}
// Find the strongest peak
let mut max_power = 0.0;
let mut max_bin_idx = min_bin;
for i in min_bin..=max_bin.min(spectrum.len() - 1) {
if spectrum[i] > max_power {
max_power = spectrum[i];
max_bin_idx = i;
}
}
// Check if it's a real peak (local maximum)
if max_bin_idx > 0 && max_bin_idx < spectrum.len() - 1 {
if spectrum[max_bin_idx] <= spectrum[max_bin_idx - 1]
|| spectrum[max_bin_idx] <= spectrum[max_bin_idx + 1]
{
// Not a real peak
return None;
}
}
let freq = max_bin_idx as f64 * freq_resolution;
let strength = max_power.sqrt(); // Convert power to amplitude
Some((freq, strength))
}
/// Estimate heart rate variability from spectral peak width
fn estimate_hrv(&self, spectrum: &[f64], peak_freq: f64, sample_rate: f64) -> f32 {
let n = spectrum.len() * 2;
let freq_resolution = sample_rate / n as f64;
let peak_bin = (peak_freq / freq_resolution).round() as usize;
if peak_bin >= spectrum.len() {
return 0.0;
}
let peak_power = spectrum[peak_bin];
if peak_power == 0.0 {
return 0.0;
}
// Find -3dB width (half-power points)
let half_power = peak_power / 2.0;
let mut left = peak_bin;
let mut right = peak_bin;
while left > 0 && spectrum[left] > half_power {
left -= 1;
}
while right < spectrum.len() - 1 && spectrum[right] > half_power {
right += 1;
}
// HRV is proportional to bandwidth
let bandwidth = (right - left) as f64 * freq_resolution;
let hrv_estimate = bandwidth * 60.0; // Convert to BPM variation
// Normalize to 0-1 range (typical HRV is 2-20 BPM)
(hrv_estimate / 20.0).min(1.0) as f32
}
/// Categorize signal strength
fn categorize_strength(&self, strength: f64) -> SignalStrength {
if strength > 0.5 {
SignalStrength::Strong
} else if strength > 0.2 {
SignalStrength::Moderate
} else if strength > 0.1 {
SignalStrength::Weak
} else {
SignalStrength::VeryWeak
}
}
/// Calculate detection confidence
fn calculate_confidence(&self, strength: f64, hrv: f32) -> f32 {
// Strong signal with reasonable HRV indicates real heartbeat
let strength_score = (strength / 0.5).min(1.0) as f32;
// Very low or very high HRV might indicate noise
let hrv_score = if hrv > 0.05 && hrv < 0.5 {
1.0
} else {
0.5
};
strength_score * 0.7 + hrv_score * 0.3
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_heartbeat_signal(rate_bpm: f64, sample_rate: f64, duration: f64) -> Vec<f64> {
let num_samples = (sample_rate * duration) as usize;
let freq = rate_bpm / 60.0;
(0..num_samples)
.map(|i| {
let t = i as f64 / sample_rate;
// Heartbeat is more pulse-like than sine
let phase = 2.0 * std::f64::consts::PI * freq * t;
0.3 * phase.sin() + 0.1 * (2.0 * phase).sin()
})
.collect()
}
#[test]
fn test_detect_heartbeat() {
let detector = HeartbeatDetector::with_defaults();
let signal = generate_heartbeat_signal(72.0, 1000.0, 10.0);
let result = detector.detect(&signal, 1000.0, None);
// Heartbeat detection is challenging, may not always succeed
if let Some(signature) = result {
assert!(signature.rate_bpm >= 50.0 && signature.rate_bpm <= 100.0);
}
}
#[test]
fn test_highpass_filter() {
let detector = HeartbeatDetector::with_defaults();
// Signal with DC offset and low frequency component
let signal: Vec<f64> = (0..1000)
.map(|i| {
let t = i as f64 / 100.0;
5.0 + (0.1 * t).sin() + (5.0 * t).sin() * 0.2
})
.collect();
let filtered = detector.highpass_filter(&signal, 100.0, 0.5);
// DC component should be removed
let mean: f64 = filtered.iter().skip(100).sum::<f64>() / (filtered.len() - 100) as f64;
assert!(mean.abs() < 1.0);
}
}

View File

@@ -0,0 +1,17 @@
//! Detection module for vital signs detection from CSI data.
//!
//! This module provides detectors for:
//! - Breathing patterns
//! - Heartbeat signatures
//! - Movement classification
//! - Ensemble classification combining all signals
mod breathing;
mod heartbeat;
mod movement;
mod pipeline;
pub use breathing::{BreathingDetector, BreathingDetectorConfig};
pub use heartbeat::{HeartbeatDetector, HeartbeatDetectorConfig};
pub use movement::{MovementClassifier, MovementClassifierConfig};
pub use pipeline::{DetectionPipeline, DetectionConfig, VitalSignsDetector};

View File

@@ -0,0 +1,274 @@
//! Movement classification from CSI signal variations.
use crate::domain::{MovementProfile, MovementType};
/// Configuration for movement classification
#[derive(Debug, Clone)]
pub struct MovementClassifierConfig {
/// Threshold for detecting any movement
pub movement_threshold: f64,
/// Threshold for gross movement
pub gross_movement_threshold: f64,
/// Window size for variance calculation
pub window_size: usize,
/// Threshold for periodic movement detection
pub periodicity_threshold: f64,
}
impl Default for MovementClassifierConfig {
fn default() -> Self {
Self {
movement_threshold: 0.1,
gross_movement_threshold: 0.5,
window_size: 100,
periodicity_threshold: 0.3,
}
}
}
/// Classifier for movement types from CSI signals
pub struct MovementClassifier {
config: MovementClassifierConfig,
}
impl MovementClassifier {
/// Create a new movement classifier
pub fn new(config: MovementClassifierConfig) -> Self {
Self { config }
}
/// Create with default configuration
pub fn with_defaults() -> Self {
Self::new(MovementClassifierConfig::default())
}
/// Classify movement from CSI signal
pub fn classify(&self, csi_signal: &[f64], sample_rate: f64) -> MovementProfile {
if csi_signal.len() < self.config.window_size {
return MovementProfile::default();
}
// Calculate signal statistics
let variance = self.calculate_variance(csi_signal);
let max_change = self.calculate_max_change(csi_signal);
let periodicity = self.calculate_periodicity(csi_signal, sample_rate);
// Determine movement type
let (movement_type, is_voluntary) = self.determine_movement_type(
variance,
max_change,
periodicity,
);
// Calculate intensity
let intensity = self.calculate_intensity(variance, max_change);
// Calculate frequency of movement
let frequency = self.calculate_movement_frequency(csi_signal, sample_rate);
MovementProfile {
movement_type,
intensity,
frequency,
is_voluntary,
}
}
/// Calculate signal variance
fn calculate_variance(&self, signal: &[f64]) -> f64 {
if signal.is_empty() {
return 0.0;
}
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let variance = signal.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / signal.len() as f64;
variance
}
/// Calculate maximum change in signal
fn calculate_max_change(&self, signal: &[f64]) -> f64 {
if signal.len() < 2 {
return 0.0;
}
signal.windows(2)
.map(|w| (w[1] - w[0]).abs())
.fold(0.0, f64::max)
}
/// Calculate periodicity score using autocorrelation
fn calculate_periodicity(&self, signal: &[f64], _sample_rate: f64) -> f64 {
if signal.len() < 3 {
return 0.0;
}
// Calculate autocorrelation
let n = signal.len();
let mean = signal.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = signal.iter().map(|x| x - mean).collect();
let variance: f64 = centered.iter().map(|x| x * x).sum();
if variance == 0.0 {
return 0.0;
}
// Find first peak in autocorrelation after lag 0
let max_lag = n / 2;
let mut max_corr = 0.0;
for lag in 1..max_lag {
let corr: f64 = centered.iter()
.take(n - lag)
.zip(centered.iter().skip(lag))
.map(|(a, b)| a * b)
.sum();
let normalized_corr = corr / variance;
if normalized_corr > max_corr {
max_corr = normalized_corr;
}
}
max_corr.max(0.0)
}
/// Determine movement type based on signal characteristics
fn determine_movement_type(
&self,
variance: f64,
max_change: f64,
periodicity: f64,
) -> (MovementType, bool) {
// No significant movement
if variance < self.config.movement_threshold * 0.5
&& max_change < self.config.movement_threshold
{
return (MovementType::None, false);
}
// Check for gross movement (large, purposeful)
if max_change > self.config.gross_movement_threshold
&& variance > self.config.movement_threshold
{
// Gross movement with low periodicity suggests voluntary
let is_voluntary = periodicity < self.config.periodicity_threshold;
return (MovementType::Gross, is_voluntary);
}
// Check for periodic movement (breathing-related or tremor)
if periodicity > self.config.periodicity_threshold {
// High periodicity with low variance = breathing-related
if variance < self.config.movement_threshold * 2.0 {
return (MovementType::Periodic, false);
}
// High periodicity with higher variance = tremor
return (MovementType::Tremor, false);
}
// Fine movement (small but detectable)
if variance > self.config.movement_threshold * 0.5 {
// Fine movement might be voluntary if not very periodic
let is_voluntary = periodicity < 0.2;
return (MovementType::Fine, is_voluntary);
}
(MovementType::None, false)
}
/// Calculate movement intensity (0.0-1.0)
fn calculate_intensity(&self, variance: f64, max_change: f64) -> f32 {
// Combine variance and max change
let variance_score = (variance / (self.config.gross_movement_threshold * 2.0)).min(1.0);
let change_score = (max_change / self.config.gross_movement_threshold).min(1.0);
((variance_score * 0.6 + change_score * 0.4) as f32).min(1.0)
}
/// Calculate movement frequency (movements per second)
fn calculate_movement_frequency(&self, signal: &[f64], sample_rate: f64) -> f32 {
if signal.len() < 3 {
return 0.0;
}
// Count zero crossings (after removing mean)
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let centered: Vec<f64> = signal.iter().map(|x| x - mean).collect();
let zero_crossings: usize = centered.windows(2)
.filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
.count();
// Each zero crossing is half a cycle
let duration = signal.len() as f64 / sample_rate;
let frequency = zero_crossings as f64 / (2.0 * duration);
frequency as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_movement() {
let classifier = MovementClassifier::with_defaults();
let signal: Vec<f64> = vec![1.0; 200];
let profile = classifier.classify(&signal, 100.0);
assert!(matches!(profile.movement_type, MovementType::None));
}
#[test]
fn test_gross_movement() {
let classifier = MovementClassifier::with_defaults();
// Simulate large movement
let mut signal: Vec<f64> = vec![0.0; 200];
for i in 50..100 {
signal[i] = 2.0;
}
for i in 150..180 {
signal[i] = -1.5;
}
let profile = classifier.classify(&signal, 100.0);
assert!(matches!(profile.movement_type, MovementType::Gross));
}
#[test]
fn test_periodic_movement() {
let classifier = MovementClassifier::with_defaults();
// Simulate periodic signal (like breathing)
let signal: Vec<f64> = (0..1000)
.map(|i| (2.0 * std::f64::consts::PI * i as f64 / 100.0).sin() * 0.3)
.collect();
let profile = classifier.classify(&signal, 100.0);
// Should detect periodic or fine movement
assert!(!matches!(profile.movement_type, MovementType::None));
}
#[test]
fn test_intensity_calculation() {
let classifier = MovementClassifier::with_defaults();
// Low intensity
let low_signal: Vec<f64> = (0..200)
.map(|i| (i as f64 * 0.1).sin() * 0.05)
.collect();
let low_profile = classifier.classify(&low_signal, 100.0);
// High intensity
let high_signal: Vec<f64> = (0..200)
.map(|i| (i as f64 * 0.1).sin() * 2.0)
.collect();
let high_profile = classifier.classify(&high_signal, 100.0);
assert!(high_profile.intensity > low_profile.intensity);
}
}

View File

@@ -0,0 +1,356 @@
//! Detection pipeline combining all vital signs detectors.
use crate::domain::{ScanZone, VitalSignsReading, ConfidenceScore};
use crate::{DisasterConfig, MatError};
use super::{
BreathingDetector, BreathingDetectorConfig,
HeartbeatDetector, HeartbeatDetectorConfig,
MovementClassifier, MovementClassifierConfig,
};
/// Configuration for the detection pipeline
#[derive(Debug, Clone)]
pub struct DetectionConfig {
/// Breathing detector configuration
pub breathing: BreathingDetectorConfig,
/// Heartbeat detector configuration
pub heartbeat: HeartbeatDetectorConfig,
/// Movement classifier configuration
pub movement: MovementClassifierConfig,
/// Sample rate of CSI data (Hz)
pub sample_rate: f64,
/// Whether to enable heartbeat detection (slower, more processing)
pub enable_heartbeat: bool,
/// Minimum overall confidence to report detection
pub min_confidence: f64,
}
impl Default for DetectionConfig {
fn default() -> Self {
Self {
breathing: BreathingDetectorConfig::default(),
heartbeat: HeartbeatDetectorConfig::default(),
movement: MovementClassifierConfig::default(),
sample_rate: 1000.0,
enable_heartbeat: false,
min_confidence: 0.3,
}
}
}
impl DetectionConfig {
/// Create configuration from disaster config
pub fn from_disaster_config(config: &DisasterConfig) -> Self {
let mut detection_config = Self::default();
// Adjust sensitivity
detection_config.breathing.confidence_threshold = (1.0 - config.sensitivity) as f32 * 0.5;
detection_config.heartbeat.confidence_threshold = (1.0 - config.sensitivity) as f32 * 0.5;
detection_config.min_confidence = 1.0 - config.sensitivity * 0.7;
// Enable heartbeat for high sensitivity
detection_config.enable_heartbeat = config.sensitivity > 0.7;
detection_config
}
}
/// Trait for vital signs detection
pub trait VitalSignsDetector: Send + Sync {
/// Process CSI data and detect vital signs
fn detect(&self, csi_data: &CsiDataBuffer) -> Option<VitalSignsReading>;
}
/// Buffer for CSI data samples
#[derive(Debug, Default)]
pub struct CsiDataBuffer {
/// Amplitude samples
pub amplitudes: Vec<f64>,
/// Phase samples (unwrapped)
pub phases: Vec<f64>,
/// Sample timestamps
pub timestamps: Vec<f64>,
/// Sample rate
pub sample_rate: f64,
}
impl CsiDataBuffer {
/// Create a new buffer
pub fn new(sample_rate: f64) -> Self {
Self {
amplitudes: Vec::new(),
phases: Vec::new(),
timestamps: Vec::new(),
sample_rate,
}
}
/// Add samples to the buffer
pub fn add_samples(&mut self, amplitudes: &[f64], phases: &[f64]) {
self.amplitudes.extend(amplitudes);
self.phases.extend(phases);
// Generate timestamps
let start = self.timestamps.last().copied().unwrap_or(0.0);
let dt = 1.0 / self.sample_rate;
for i in 0..amplitudes.len() {
self.timestamps.push(start + (i + 1) as f64 * dt);
}
}
/// Clear the buffer
pub fn clear(&mut self) {
self.amplitudes.clear();
self.phases.clear();
self.timestamps.clear();
}
/// Get the duration of data in the buffer (seconds)
pub fn duration(&self) -> f64 {
self.amplitudes.len() as f64 / self.sample_rate
}
/// Check if buffer has enough data for analysis
pub fn has_sufficient_data(&self, min_duration: f64) -> bool {
self.duration() >= min_duration
}
}
/// Detection pipeline that combines all detectors
pub struct DetectionPipeline {
config: DetectionConfig,
breathing_detector: BreathingDetector,
heartbeat_detector: HeartbeatDetector,
movement_classifier: MovementClassifier,
data_buffer: parking_lot::RwLock<CsiDataBuffer>,
}
impl DetectionPipeline {
/// Create a new detection pipeline
pub fn new(config: DetectionConfig) -> Self {
Self {
breathing_detector: BreathingDetector::new(config.breathing.clone()),
heartbeat_detector: HeartbeatDetector::new(config.heartbeat.clone()),
movement_classifier: MovementClassifier::new(config.movement.clone()),
data_buffer: parking_lot::RwLock::new(CsiDataBuffer::new(config.sample_rate)),
config,
}
}
/// Process a scan zone and return detected vital signs
pub async fn process_zone(&self, zone: &ScanZone) -> Result<Option<VitalSignsReading>, MatError> {
// In a real implementation, this would:
// 1. Collect CSI data from sensors in the zone
// 2. Preprocess the data
// 3. Run detection algorithms
// For now, check if we have buffered data
let buffer = self.data_buffer.read();
if !buffer.has_sufficient_data(5.0) {
// Need at least 5 seconds of data
return Ok(None);
}
// Detect vital signs
let reading = self.detect_from_buffer(&buffer, zone)?;
// Check minimum confidence
if let Some(ref r) = reading {
if r.confidence.value() < self.config.min_confidence {
return Ok(None);
}
}
Ok(reading)
}
/// Add CSI data to the processing buffer
pub fn add_data(&self, amplitudes: &[f64], phases: &[f64]) {
let mut buffer = self.data_buffer.write();
buffer.add_samples(amplitudes, phases);
// Keep only recent data (last 30 seconds)
let max_samples = (30.0 * self.config.sample_rate) as usize;
if buffer.amplitudes.len() > max_samples {
let drain_count = buffer.amplitudes.len() - max_samples;
buffer.amplitudes.drain(0..drain_count);
buffer.phases.drain(0..drain_count);
buffer.timestamps.drain(0..drain_count);
}
}
/// Clear the data buffer
pub fn clear_buffer(&self) {
self.data_buffer.write().clear();
}
/// Detect vital signs from buffered data
fn detect_from_buffer(
&self,
buffer: &CsiDataBuffer,
_zone: &ScanZone,
) -> Result<Option<VitalSignsReading>, MatError> {
// Detect breathing
let breathing = self.breathing_detector.detect(
&buffer.amplitudes,
buffer.sample_rate,
);
// Detect heartbeat (if enabled)
let heartbeat = if self.config.enable_heartbeat {
let breathing_rate = breathing.as_ref().map(|b| b.rate_bpm as f64);
self.heartbeat_detector.detect(
&buffer.phases,
buffer.sample_rate,
breathing_rate,
)
} else {
None
};
// Classify movement
let movement = self.movement_classifier.classify(
&buffer.amplitudes,
buffer.sample_rate,
);
// Check if we detected anything
if breathing.is_none() && heartbeat.is_none() && movement.movement_type == crate::domain::MovementType::None {
return Ok(None);
}
// Create vital signs reading
let reading = VitalSignsReading::new(breathing, heartbeat, movement);
Ok(Some(reading))
}
/// Get configuration
pub fn config(&self) -> &DetectionConfig {
&self.config
}
/// Update configuration
pub fn update_config(&mut self, config: DetectionConfig) {
self.breathing_detector = BreathingDetector::new(config.breathing.clone());
self.heartbeat_detector = HeartbeatDetector::new(config.heartbeat.clone());
self.movement_classifier = MovementClassifier::new(config.movement.clone());
self.config = config;
}
}
impl VitalSignsDetector for DetectionPipeline {
fn detect(&self, csi_data: &CsiDataBuffer) -> Option<VitalSignsReading> {
// Detect breathing from amplitude variations
let breathing = self.breathing_detector.detect(
&csi_data.amplitudes,
csi_data.sample_rate,
);
// Detect heartbeat from phase variations
let heartbeat = if self.config.enable_heartbeat {
let breathing_rate = breathing.as_ref().map(|b| b.rate_bpm as f64);
self.heartbeat_detector.detect(
&csi_data.phases,
csi_data.sample_rate,
breathing_rate,
)
} else {
None
};
// Classify movement
let movement = self.movement_classifier.classify(
&csi_data.amplitudes,
csi_data.sample_rate,
);
// Create reading if we detected anything
if breathing.is_some() || heartbeat.is_some()
|| movement.movement_type != crate::domain::MovementType::None
{
Some(VitalSignsReading::new(breathing, heartbeat, movement))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_buffer() -> CsiDataBuffer {
let mut buffer = CsiDataBuffer::new(100.0);
// Add 10 seconds of simulated breathing signal
let num_samples = 1000;
let amplitudes: Vec<f64> = (0..num_samples)
.map(|i| {
let t = i as f64 / 100.0;
// 16 BPM breathing (0.267 Hz)
(2.0 * std::f64::consts::PI * 0.267 * t).sin()
})
.collect();
let phases: Vec<f64> = (0..num_samples)
.map(|i| {
let t = i as f64 / 100.0;
// Phase variation from movement
(2.0 * std::f64::consts::PI * 0.267 * t).sin() * 0.5
})
.collect();
buffer.add_samples(&amplitudes, &phases);
buffer
}
#[test]
fn test_pipeline_creation() {
let config = DetectionConfig::default();
let pipeline = DetectionPipeline::new(config);
assert_eq!(pipeline.config().sample_rate, 1000.0);
}
#[test]
fn test_csi_buffer() {
let mut buffer = CsiDataBuffer::new(100.0);
assert!(!buffer.has_sufficient_data(5.0));
let amplitudes: Vec<f64> = vec![1.0; 600];
let phases: Vec<f64> = vec![0.0; 600];
buffer.add_samples(&amplitudes, &phases);
assert!(buffer.has_sufficient_data(5.0));
assert_eq!(buffer.duration(), 6.0);
}
#[test]
fn test_vital_signs_detection() {
let config = DetectionConfig::default();
let pipeline = DetectionPipeline::new(config);
let buffer = create_test_buffer();
let result = pipeline.detect(&buffer);
assert!(result.is_some());
let reading = result.unwrap();
assert!(reading.has_vitals());
}
#[test]
fn test_config_from_disaster_config() {
let disaster_config = DisasterConfig::builder()
.sensitivity(0.9)
.build();
let detection_config = DetectionConfig::from_disaster_config(&disaster_config);
// High sensitivity should enable heartbeat detection
assert!(detection_config.enable_heartbeat);
// Low minimum confidence due to high sensitivity
assert!(detection_config.min_confidence < 0.4);
}
}

View File

@@ -0,0 +1,458 @@
//! Alert types for emergency notifications.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{SurvivorId, TriageStatus, Coordinates3D};
/// Unique identifier for an alert
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertId(Uuid);
impl AlertId {
/// Create a new random alert ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for AlertId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for AlertId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Alert priority levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Priority {
/// Critical - immediate action required
Critical = 1,
/// High - urgent attention needed
High = 2,
/// Medium - important but not urgent
Medium = 3,
/// Low - informational
Low = 4,
}
impl Priority {
/// Create from triage status
pub fn from_triage(status: &TriageStatus) -> Self {
match status {
TriageStatus::Immediate => Priority::Critical,
TriageStatus::Delayed => Priority::High,
TriageStatus::Minor => Priority::Medium,
TriageStatus::Deceased => Priority::Low,
TriageStatus::Unknown => Priority::Medium,
}
}
/// Get numeric value (lower = higher priority)
pub fn value(&self) -> u8 {
*self as u8
}
/// Get display color
pub fn color(&self) -> &'static str {
match self {
Priority::Critical => "red",
Priority::High => "orange",
Priority::Medium => "yellow",
Priority::Low => "blue",
}
}
/// Get sound pattern for audio alerts
pub fn audio_pattern(&self) -> &'static str {
match self {
Priority::Critical => "rapid_beep",
Priority::High => "double_beep",
Priority::Medium => "single_beep",
Priority::Low => "soft_tone",
}
}
}
impl std::fmt::Display for Priority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Priority::Critical => write!(f, "CRITICAL"),
Priority::High => write!(f, "HIGH"),
Priority::Medium => write!(f, "MEDIUM"),
Priority::Low => write!(f, "LOW"),
}
}
}
/// Payload containing alert details
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertPayload {
/// Human-readable title
pub title: String,
/// Detailed message
pub message: String,
/// Triage status of survivor
pub triage_status: TriageStatus,
/// Location if known
pub location: Option<Coordinates3D>,
/// Recommended action
pub recommended_action: String,
/// Time-critical deadline (if any)
pub deadline: Option<DateTime<Utc>>,
/// Additional metadata
pub metadata: std::collections::HashMap<String, String>,
}
impl AlertPayload {
/// Create a new alert payload
pub fn new(
title: impl Into<String>,
message: impl Into<String>,
triage_status: TriageStatus,
) -> Self {
Self {
title: title.into(),
message: message.into(),
triage_status,
location: None,
recommended_action: String::new(),
metadata: std::collections::HashMap::new(),
}
}
/// Set location
pub fn with_location(mut self, location: Coordinates3D) -> Self {
self.location = Some(location);
self
}
/// Set recommended action
pub fn with_action(mut self, action: impl Into<String>) -> Self {
self.recommended_action = action.into();
self
}
/// Set deadline
pub fn with_deadline(mut self, deadline: DateTime<Utc>) -> Self {
self.deadline = Some(deadline);
self
}
/// Add metadata
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
/// Status of an alert
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlertStatus {
/// Alert is pending acknowledgement
Pending,
/// Alert has been acknowledged
Acknowledged,
/// Alert is being worked on
InProgress,
/// Alert has been resolved
Resolved,
/// Alert was cancelled/superseded
Cancelled,
/// Alert expired without action
Expired,
}
/// Resolution details for a closed alert
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertResolution {
/// Resolution type
pub resolution_type: ResolutionType,
/// Resolution notes
pub notes: String,
/// Team that resolved
pub resolved_by: Option<String>,
/// Resolution time
pub resolved_at: DateTime<Utc>,
}
/// Types of alert resolution
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResolutionType {
/// Survivor was rescued
Rescued,
/// Alert was a false positive
FalsePositive,
/// Survivor deceased before rescue
Deceased,
/// Alert superseded by new information
Superseded,
/// Alert timed out
TimedOut,
/// Other resolution
Other,
}
/// An alert for rescue teams
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Alert {
id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
payload: AlertPayload,
status: AlertStatus,
created_at: DateTime<Utc>,
acknowledged_at: Option<DateTime<Utc>>,
acknowledged_by: Option<String>,
resolution: Option<AlertResolution>,
escalation_count: u32,
}
impl Alert {
/// Create a new alert
pub fn new(survivor_id: SurvivorId, priority: Priority, payload: AlertPayload) -> Self {
Self {
id: AlertId::new(),
survivor_id,
priority,
payload,
status: AlertStatus::Pending,
created_at: Utc::now(),
acknowledged_at: None,
acknowledged_by: None,
resolution: None,
escalation_count: 0,
}
}
/// Get the alert ID
pub fn id(&self) -> &AlertId {
&self.id
}
/// Get the survivor ID
pub fn survivor_id(&self) -> &SurvivorId {
&self.survivor_id
}
/// Get the priority
pub fn priority(&self) -> Priority {
self.priority
}
/// Get the payload
pub fn payload(&self) -> &AlertPayload {
&self.payload
}
/// Get the status
pub fn status(&self) -> &AlertStatus {
&self.status
}
/// Get creation time
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
/// Get acknowledgement time
pub fn acknowledged_at(&self) -> Option<&DateTime<Utc>> {
self.acknowledged_at.as_ref()
}
/// Get who acknowledged
pub fn acknowledged_by(&self) -> Option<&str> {
self.acknowledged_by.as_deref()
}
/// Get resolution
pub fn resolution(&self) -> Option<&AlertResolution> {
self.resolution.as_ref()
}
/// Get escalation count
pub fn escalation_count(&self) -> u32 {
self.escalation_count
}
/// Acknowledge the alert
pub fn acknowledge(&mut self, by: impl Into<String>) {
self.status = AlertStatus::Acknowledged;
self.acknowledged_at = Some(Utc::now());
self.acknowledged_by = Some(by.into());
}
/// Mark as in progress
pub fn start_work(&mut self) {
if self.status == AlertStatus::Acknowledged {
self.status = AlertStatus::InProgress;
}
}
/// Resolve the alert
pub fn resolve(&mut self, resolution: AlertResolution) {
self.status = AlertStatus::Resolved;
self.resolution = Some(resolution);
}
/// Cancel the alert
pub fn cancel(&mut self, reason: &str) {
self.status = AlertStatus::Cancelled;
self.resolution = Some(AlertResolution {
resolution_type: ResolutionType::Other,
notes: reason.to_string(),
resolved_by: None,
resolved_at: Utc::now(),
});
}
/// Escalate the alert (increase priority)
pub fn escalate(&mut self) {
self.escalation_count += 1;
if self.priority != Priority::Critical {
self.priority = match self.priority {
Priority::Low => Priority::Medium,
Priority::Medium => Priority::High,
Priority::High => Priority::Critical,
Priority::Critical => Priority::Critical,
};
}
}
/// Check if alert is pending
pub fn is_pending(&self) -> bool {
self.status == AlertStatus::Pending
}
/// Check if alert is active (not resolved/cancelled)
pub fn is_active(&self) -> bool {
matches!(
self.status,
AlertStatus::Pending | AlertStatus::Acknowledged | AlertStatus::InProgress
)
}
/// Time since alert was created
pub fn age(&self) -> chrono::Duration {
Utc::now() - self.created_at
}
/// Time since acknowledgement
pub fn time_since_ack(&self) -> Option<chrono::Duration> {
self.acknowledged_at.map(|t| Utc::now() - t)
}
/// Check if alert needs escalation based on time
pub fn needs_escalation(&self, max_pending_seconds: i64) -> bool {
if !self.is_pending() {
return false;
}
self.age().num_seconds() > max_pending_seconds
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_payload() -> AlertPayload {
AlertPayload::new(
"Survivor Detected",
"Vital signs detected in Zone A",
TriageStatus::Immediate,
)
}
#[test]
fn test_alert_creation() {
let survivor_id = SurvivorId::new();
let alert = Alert::new(
survivor_id.clone(),
Priority::Critical,
create_test_payload(),
);
assert_eq!(alert.survivor_id(), &survivor_id);
assert_eq!(alert.priority(), Priority::Critical);
assert!(alert.is_pending());
assert!(alert.is_active());
}
#[test]
fn test_alert_lifecycle() {
let mut alert = Alert::new(
SurvivorId::new(),
Priority::High,
create_test_payload(),
);
// Initial state
assert!(alert.is_pending());
// Acknowledge
alert.acknowledge("Team Alpha");
assert_eq!(alert.status(), &AlertStatus::Acknowledged);
assert_eq!(alert.acknowledged_by(), Some("Team Alpha"));
// Start work
alert.start_work();
assert_eq!(alert.status(), &AlertStatus::InProgress);
// Resolve
alert.resolve(AlertResolution {
resolution_type: ResolutionType::Rescued,
notes: "Survivor extracted successfully".to_string(),
resolved_by: Some("Team Alpha".to_string()),
resolved_at: Utc::now(),
});
assert_eq!(alert.status(), &AlertStatus::Resolved);
assert!(!alert.is_active());
}
#[test]
fn test_alert_escalation() {
let mut alert = Alert::new(
SurvivorId::new(),
Priority::Low,
create_test_payload(),
);
alert.escalate();
assert_eq!(alert.priority(), Priority::Medium);
assert_eq!(alert.escalation_count(), 1);
alert.escalate();
assert_eq!(alert.priority(), Priority::High);
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
// Can't escalate beyond critical
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
}
#[test]
fn test_priority_from_triage() {
assert_eq!(Priority::from_triage(&TriageStatus::Immediate), Priority::Critical);
assert_eq!(Priority::from_triage(&TriageStatus::Delayed), Priority::High);
assert_eq!(Priority::from_triage(&TriageStatus::Minor), Priority::Medium);
}
}

View File

@@ -0,0 +1,371 @@
//! 3D coordinate system and location types for survivor localization.
/// 3D coordinates representing survivor position
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Coordinates3D {
/// East-West offset from reference point (meters)
pub x: f64,
/// North-South offset from reference point (meters)
pub y: f64,
/// Vertical offset - negative is below surface (meters)
pub z: f64,
/// Uncertainty bounds for this position
pub uncertainty: LocationUncertainty,
}
impl Coordinates3D {
/// Create new coordinates with uncertainty
pub fn new(x: f64, y: f64, z: f64, uncertainty: LocationUncertainty) -> Self {
Self { x, y, z, uncertainty }
}
/// Create coordinates with default uncertainty
pub fn with_default_uncertainty(x: f64, y: f64, z: f64) -> Self {
Self {
x,
y,
z,
uncertainty: LocationUncertainty::default(),
}
}
/// Calculate 3D distance to another point
pub fn distance_to(&self, other: &Coordinates3D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
let dz = self.z - other.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
/// Calculate horizontal (2D) distance only
pub fn horizontal_distance_to(&self, other: &Coordinates3D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
/// Get depth below surface (positive value)
pub fn depth(&self) -> f64 {
-self.z.min(0.0)
}
/// Check if position is below surface
pub fn is_buried(&self) -> bool {
self.z < 0.0
}
/// Get the 95% confidence radius (horizontal)
pub fn confidence_radius(&self) -> f64 {
self.uncertainty.horizontal_error
}
}
/// Uncertainty bounds for a position estimate
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LocationUncertainty {
/// Horizontal error radius at 95% confidence (meters)
pub horizontal_error: f64,
/// Vertical error at 95% confidence (meters)
pub vertical_error: f64,
/// Confidence level (0.0-1.0)
pub confidence: f64,
}
impl Default for LocationUncertainty {
fn default() -> Self {
Self {
horizontal_error: 2.0, // 2 meter default uncertainty
vertical_error: 1.0, // 1 meter vertical uncertainty
confidence: 0.95, // 95% confidence
}
}
}
impl LocationUncertainty {
/// Create uncertainty with specific error bounds
pub fn new(horizontal_error: f64, vertical_error: f64) -> Self {
Self {
horizontal_error,
vertical_error,
confidence: 0.95,
}
}
/// Create high-confidence uncertainty
pub fn high_confidence(horizontal_error: f64, vertical_error: f64) -> Self {
Self {
horizontal_error,
vertical_error,
confidence: 0.99,
}
}
/// Check if uncertainty is acceptable for rescue operations
pub fn is_actionable(&self) -> bool {
// Within 3 meters horizontal is generally actionable
self.horizontal_error <= 3.0 && self.confidence >= 0.8
}
/// Combine two uncertainties (for sensor fusion)
pub fn combine(&self, other: &LocationUncertainty) -> LocationUncertainty {
// Weighted combination based on confidence
let total_conf = self.confidence + other.confidence;
let w1 = self.confidence / total_conf;
let w2 = other.confidence / total_conf;
// Combined uncertainty is reduced when multiple estimates agree
let h_var1 = self.horizontal_error * self.horizontal_error;
let h_var2 = other.horizontal_error * other.horizontal_error;
let combined_h_var = 1.0 / (1.0/h_var1 + 1.0/h_var2);
let v_var1 = self.vertical_error * self.vertical_error;
let v_var2 = other.vertical_error * other.vertical_error;
let combined_v_var = 1.0 / (1.0/v_var1 + 1.0/v_var2);
LocationUncertainty {
horizontal_error: combined_h_var.sqrt(),
vertical_error: combined_v_var.sqrt(),
confidence: (w1 * self.confidence + w2 * other.confidence).min(0.99),
}
}
}
/// Depth estimate with debris profile
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DepthEstimate {
/// Estimated depth in meters
pub depth: f64,
/// Uncertainty range (plus/minus)
pub uncertainty: f64,
/// Estimated debris composition
pub debris_profile: DebrisProfile,
/// Confidence in the estimate
pub confidence: f64,
}
impl DepthEstimate {
/// Create a new depth estimate
pub fn new(
depth: f64,
uncertainty: f64,
debris_profile: DebrisProfile,
confidence: f64,
) -> Self {
Self {
depth,
uncertainty,
debris_profile,
confidence,
}
}
/// Get minimum possible depth
pub fn min_depth(&self) -> f64 {
(self.depth - self.uncertainty).max(0.0)
}
/// Get maximum possible depth
pub fn max_depth(&self) -> f64 {
self.depth + self.uncertainty
}
/// Check if depth is shallow (easier rescue)
pub fn is_shallow(&self) -> bool {
self.depth < 1.5
}
/// Check if depth is moderate
pub fn is_moderate(&self) -> bool {
self.depth >= 1.5 && self.depth < 3.0
}
/// Check if depth is deep (difficult rescue)
pub fn is_deep(&self) -> bool {
self.depth >= 3.0
}
}
/// Profile of debris material between sensor and survivor
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DebrisProfile {
/// Primary material type
pub primary_material: DebrisMaterial,
/// Estimated void fraction (0.0-1.0, higher = more air gaps)
pub void_fraction: f64,
/// Estimated moisture content (affects signal propagation)
pub moisture_content: MoistureLevel,
/// Whether metal content is detected (blocks signals)
pub metal_content: MetalContent,
}
impl Default for DebrisProfile {
fn default() -> Self {
Self {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.3,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::None,
}
}
}
impl DebrisProfile {
/// Calculate signal attenuation factor
pub fn attenuation_factor(&self) -> f64 {
let base = self.primary_material.attenuation_coefficient();
let moisture_factor = self.moisture_content.attenuation_multiplier();
let void_factor = 1.0 - (self.void_fraction * 0.3); // Voids reduce attenuation
base * moisture_factor * void_factor
}
/// Check if debris allows good signal penetration
pub fn is_penetrable(&self) -> bool {
!matches!(self.metal_content, MetalContent::High | MetalContent::Blocking)
&& self.primary_material.attenuation_coefficient() < 5.0
}
}
/// Types of debris materials
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DebrisMaterial {
/// Lightweight concrete, drywall
LightConcrete,
/// Heavy concrete, brick
HeavyConcrete,
/// Wooden structures
Wood,
/// Soil, earth
Soil,
/// Mixed rubble (typical collapse)
Mixed,
/// Snow/ice (avalanche)
Snow,
/// Metal (poor penetration)
Metal,
}
impl DebrisMaterial {
/// Get RF attenuation coefficient (dB/meter)
pub fn attenuation_coefficient(&self) -> f64 {
match self {
DebrisMaterial::Snow => 0.5,
DebrisMaterial::Wood => 1.5,
DebrisMaterial::LightConcrete => 3.0,
DebrisMaterial::Soil => 4.0,
DebrisMaterial::Mixed => 4.5,
DebrisMaterial::HeavyConcrete => 6.0,
DebrisMaterial::Metal => 20.0,
}
}
}
/// Moisture level in debris
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MoistureLevel {
/// Dry conditions
Dry,
/// Slightly damp
Damp,
/// Wet (rain, flooding)
Wet,
/// Saturated (submerged)
Saturated,
}
impl MoistureLevel {
/// Get attenuation multiplier
pub fn attenuation_multiplier(&self) -> f64 {
match self {
MoistureLevel::Dry => 1.0,
MoistureLevel::Damp => 1.3,
MoistureLevel::Wet => 1.8,
MoistureLevel::Saturated => 2.5,
}
}
}
/// Metal content in debris
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MetalContent {
/// No significant metal
None,
/// Low metal content (rebar, pipes)
Low,
/// Moderate metal (structural steel)
Moderate,
/// High metal content
High,
/// Metal is blocking signal
Blocking,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance_calculation() {
let p1 = Coordinates3D::with_default_uncertainty(0.0, 0.0, 0.0);
let p2 = Coordinates3D::with_default_uncertainty(3.0, 4.0, 0.0);
assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
assert!((p1.horizontal_distance_to(&p2) - 5.0).abs() < 0.001);
}
#[test]
fn test_depth_calculation() {
let surface = Coordinates3D::with_default_uncertainty(0.0, 0.0, 0.0);
assert!(!surface.is_buried());
assert!(surface.depth().abs() < 0.001);
let buried = Coordinates3D::with_default_uncertainty(0.0, 0.0, -2.5);
assert!(buried.is_buried());
assert!((buried.depth() - 2.5).abs() < 0.001);
}
#[test]
fn test_uncertainty_combination() {
let u1 = LocationUncertainty::new(2.0, 1.0);
let u2 = LocationUncertainty::new(2.0, 1.0);
let combined = u1.combine(&u2);
// Combined uncertainty should be lower than individual
assert!(combined.horizontal_error < u1.horizontal_error);
}
#[test]
fn test_depth_estimate_categories() {
let shallow = DepthEstimate::new(1.0, 0.2, DebrisProfile::default(), 0.8);
assert!(shallow.is_shallow());
let moderate = DepthEstimate::new(2.0, 0.3, DebrisProfile::default(), 0.7);
assert!(moderate.is_moderate());
let deep = DepthEstimate::new(4.0, 0.5, DebrisProfile::default(), 0.6);
assert!(deep.is_deep());
}
#[test]
fn test_debris_attenuation() {
let snow = DebrisProfile {
primary_material: DebrisMaterial::Snow,
..Default::default()
};
let concrete = DebrisProfile {
primary_material: DebrisMaterial::HeavyConcrete,
..Default::default()
};
assert!(snow.attenuation_factor() < concrete.attenuation_factor());
assert!(snow.is_penetrable());
}
}

View File

@@ -0,0 +1,490 @@
//! Disaster event aggregate root.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use geo::Point;
use super::{
Survivor, SurvivorId, ScanZone, ScanZoneId,
VitalSignsReading, Coordinates3D,
};
use crate::MatError;
/// Unique identifier for a disaster event
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DisasterEventId(Uuid);
impl DisasterEventId {
/// Create a new random event ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for DisasterEventId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for DisasterEventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Types of disaster events
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DisasterType {
/// Building collapse (explosion, structural failure)
BuildingCollapse,
/// Earthquake
Earthquake,
/// Landslide or mudslide
Landslide,
/// Avalanche (snow)
Avalanche,
/// Flood
Flood,
/// Mine collapse
MineCollapse,
/// Industrial accident
Industrial,
/// Tunnel collapse
TunnelCollapse,
/// Unknown or other
Unknown,
}
impl DisasterType {
/// Get typical debris profile for this disaster type
pub fn typical_debris_profile(&self) -> super::DebrisProfile {
use super::{DebrisProfile, DebrisMaterial, MoistureLevel, MetalContent};
match self {
DisasterType::BuildingCollapse => DebrisProfile {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.25,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::Moderate,
},
DisasterType::Earthquake => DebrisProfile {
primary_material: DebrisMaterial::HeavyConcrete,
void_fraction: 0.2,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::Moderate,
},
DisasterType::Avalanche => DebrisProfile {
primary_material: DebrisMaterial::Snow,
void_fraction: 0.4,
moisture_content: MoistureLevel::Wet,
metal_content: MetalContent::None,
},
DisasterType::Landslide => DebrisProfile {
primary_material: DebrisMaterial::Soil,
void_fraction: 0.15,
moisture_content: MoistureLevel::Wet,
metal_content: MetalContent::None,
},
DisasterType::Flood => DebrisProfile {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.3,
moisture_content: MoistureLevel::Saturated,
metal_content: MetalContent::Low,
},
DisasterType::MineCollapse | DisasterType::TunnelCollapse => DebrisProfile {
primary_material: DebrisMaterial::Soil,
void_fraction: 0.2,
moisture_content: MoistureLevel::Damp,
metal_content: MetalContent::Low,
},
DisasterType::Industrial => DebrisProfile {
primary_material: DebrisMaterial::Metal,
void_fraction: 0.35,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::High,
},
DisasterType::Unknown => DebrisProfile::default(),
}
}
/// Get expected maximum survival time (hours)
pub fn expected_survival_hours(&self) -> u32 {
match self {
DisasterType::Avalanche => 2, // Limited air, hypothermia
DisasterType::Flood => 6, // Drowning risk
DisasterType::MineCollapse => 72, // Air supply critical
DisasterType::BuildingCollapse => 96,
DisasterType::Earthquake => 120,
DisasterType::Landslide => 48,
DisasterType::TunnelCollapse => 72,
DisasterType::Industrial => 72,
DisasterType::Unknown => 72,
}
}
}
impl Default for DisasterType {
fn default() -> Self {
Self::Unknown
}
}
/// Current status of the disaster event
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EventStatus {
/// Event just reported, setting up
Initializing,
/// Active search and rescue
Active,
/// Search suspended (weather, safety)
Suspended,
/// Primary rescue complete, secondary search
SecondarySearch,
/// Event closed
Closed,
}
/// Aggregate root for a disaster event
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DisasterEvent {
id: DisasterEventId,
event_type: DisasterType,
start_time: DateTime<Utc>,
location: Point<f64>,
description: String,
scan_zones: Vec<ScanZone>,
survivors: Vec<Survivor>,
status: EventStatus,
metadata: EventMetadata,
}
/// Additional metadata for a disaster event
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventMetadata {
/// Estimated number of people in area at time of disaster
pub estimated_occupancy: Option<u32>,
/// Known survivors (already rescued)
pub confirmed_rescued: u32,
/// Known fatalities
pub confirmed_deceased: u32,
/// Weather conditions
pub weather: Option<String>,
/// Lead agency
pub lead_agency: Option<String>,
/// Notes
pub notes: Vec<String>,
}
impl DisasterEvent {
/// Create a new disaster event
pub fn new(
event_type: DisasterType,
location: Point<f64>,
description: &str,
) -> Self {
Self {
id: DisasterEventId::new(),
event_type,
start_time: Utc::now(),
location,
description: description.to_string(),
scan_zones: Vec::new(),
survivors: Vec::new(),
status: EventStatus::Initializing,
metadata: EventMetadata::default(),
}
}
/// Get the event ID
pub fn id(&self) -> &DisasterEventId {
&self.id
}
/// Get the event type
pub fn event_type(&self) -> &DisasterType {
&self.event_type
}
/// Get the start time
pub fn start_time(&self) -> &DateTime<Utc> {
&self.start_time
}
/// Get the location
pub fn location(&self) -> &Point<f64> {
&self.location
}
/// Get the description
pub fn description(&self) -> &str {
&self.description
}
/// Get the scan zones
pub fn zones(&self) -> &[ScanZone] {
&self.scan_zones
}
/// Get mutable scan zones
pub fn zones_mut(&mut self) -> &mut [ScanZone] {
&mut self.scan_zones
}
/// Get the survivors
pub fn survivors(&self) -> Vec<&Survivor> {
self.survivors.iter().collect()
}
/// Get mutable survivors
pub fn survivors_mut(&mut self) -> &mut [Survivor] {
&mut self.survivors
}
/// Get the current status
pub fn status(&self) -> &EventStatus {
&self.status
}
/// Get metadata
pub fn metadata(&self) -> &EventMetadata {
&self.metadata
}
/// Get mutable metadata
pub fn metadata_mut(&mut self) -> &mut EventMetadata {
&mut self.metadata
}
/// Add a scan zone
pub fn add_zone(&mut self, zone: ScanZone) {
self.scan_zones.push(zone);
// Activate event if first zone
if self.status == EventStatus::Initializing {
self.status = EventStatus::Active;
}
}
/// Remove a scan zone
pub fn remove_zone(&mut self, zone_id: &ScanZoneId) {
self.scan_zones.retain(|z| z.id() != zone_id);
}
/// Record a new detection
pub fn record_detection(
&mut self,
zone_id: ScanZoneId,
vitals: VitalSignsReading,
location: Option<Coordinates3D>,
) -> Result<&Survivor, MatError> {
// Check if this might be an existing survivor
if let Some(loc) = &location {
if let Some(existing) = self.find_nearby_survivor(loc, 2.0) {
// Update existing survivor
let survivor = self.survivors.iter_mut()
.find(|s| s.id() == existing)
.ok_or_else(|| MatError::Domain("Survivor not found".into()))?;
survivor.update_vitals(vitals);
if let Some(l) = location {
survivor.update_location(l);
}
return Ok(survivor);
}
}
// Create new survivor
let survivor = Survivor::new(zone_id, vitals, location);
self.survivors.push(survivor);
Ok(self.survivors.last().unwrap())
}
/// Find a survivor near a location
fn find_nearby_survivor(&self, location: &Coordinates3D, radius: f64) -> Option<&SurvivorId> {
for survivor in &self.survivors {
if let Some(loc) = survivor.location() {
if loc.distance_to(location) < radius {
return Some(survivor.id());
}
}
}
None
}
/// Get survivor by ID
pub fn get_survivor(&self, id: &SurvivorId) -> Option<&Survivor> {
self.survivors.iter().find(|s| s.id() == id)
}
/// Get mutable survivor by ID
pub fn get_survivor_mut(&mut self, id: &SurvivorId) -> Option<&mut Survivor> {
self.survivors.iter_mut().find(|s| s.id() == id)
}
/// Get zone by ID
pub fn get_zone(&self, id: &ScanZoneId) -> Option<&ScanZone> {
self.scan_zones.iter().find(|z| z.id() == id)
}
/// Set event status
pub fn set_status(&mut self, status: EventStatus) {
self.status = status;
}
/// Suspend operations
pub fn suspend(&mut self, reason: &str) {
self.status = EventStatus::Suspended;
self.metadata.notes.push(format!(
"[{}] Suspended: {}",
Utc::now().format("%Y-%m-%d %H:%M:%S"),
reason
));
}
/// Resume operations
pub fn resume(&mut self) {
if self.status == EventStatus::Suspended {
self.status = EventStatus::Active;
self.metadata.notes.push(format!(
"[{}] Resumed operations",
Utc::now().format("%Y-%m-%d %H:%M:%S")
));
}
}
/// Close the event
pub fn close(&mut self) {
self.status = EventStatus::Closed;
}
/// Get time since event started
pub fn elapsed_time(&self) -> chrono::Duration {
Utc::now() - self.start_time
}
/// Get count of survivors by triage status
pub fn triage_counts(&self) -> TriageCounts {
use super::TriageStatus;
let mut counts = TriageCounts::default();
for survivor in &self.survivors {
match survivor.triage_status() {
TriageStatus::Immediate => counts.immediate += 1,
TriageStatus::Delayed => counts.delayed += 1,
TriageStatus::Minor => counts.minor += 1,
TriageStatus::Deceased => counts.deceased += 1,
TriageStatus::Unknown => counts.unknown += 1,
}
}
counts
}
}
/// Triage status counts
#[derive(Debug, Clone, Default)]
pub struct TriageCounts {
/// Immediate (Red)
pub immediate: u32,
/// Delayed (Yellow)
pub delayed: u32,
/// Minor (Green)
pub minor: u32,
/// Deceased (Black)
pub deceased: u32,
/// Unknown
pub unknown: u32,
}
impl TriageCounts {
/// Total count
pub fn total(&self) -> u32 {
self.immediate + self.delayed + self.minor + self.deceased + self.unknown
}
/// Count of living survivors
pub fn living(&self) -> u32 {
self.immediate + self.delayed + self.minor
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{ZoneBounds, BreathingPattern, BreathingType, ConfidenceScore};
fn create_test_vitals() -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
}
}
#[test]
fn test_event_creation() {
let event = DisasterEvent::new(
DisasterType::Earthquake,
Point::new(-122.4194, 37.7749),
"Test earthquake event",
);
assert!(matches!(event.event_type(), DisasterType::Earthquake));
assert_eq!(event.status(), &EventStatus::Initializing);
}
#[test]
fn test_add_zone_activates_event() {
let mut event = DisasterEvent::new(
DisasterType::BuildingCollapse,
Point::new(0.0, 0.0),
"Test",
);
assert_eq!(event.status(), &EventStatus::Initializing);
let zone = ScanZone::new("Zone A", ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0));
event.add_zone(zone);
assert_eq!(event.status(), &EventStatus::Active);
}
#[test]
fn test_record_detection() {
let mut event = DisasterEvent::new(
DisasterType::Earthquake,
Point::new(0.0, 0.0),
"Test",
);
let zone = ScanZone::new("Zone A", ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0));
let zone_id = zone.id().clone();
event.add_zone(zone);
let vitals = create_test_vitals();
event.record_detection(zone_id, vitals, None).unwrap();
assert_eq!(event.survivors().len(), 1);
}
#[test]
fn test_disaster_type_survival_hours() {
assert!(DisasterType::Avalanche.expected_survival_hours() < DisasterType::Earthquake.expected_survival_hours());
}
}

View File

@@ -0,0 +1,497 @@
//! Domain events for the wifi-Mat system.
use chrono::{DateTime, Utc};
use super::{
AlertId, Coordinates3D, Priority, ScanZoneId, SurvivorId,
TriageStatus, VitalSignsReading, AlertResolution,
};
/// All domain events in the system
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DomainEvent {
/// Detection-related events
Detection(DetectionEvent),
/// Alert-related events
Alert(AlertEvent),
/// Zone-related events
Zone(ZoneEvent),
/// System-level events
System(SystemEvent),
}
impl DomainEvent {
/// Get the timestamp of the event
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
DomainEvent::Detection(e) => e.timestamp(),
DomainEvent::Alert(e) => e.timestamp(),
DomainEvent::Zone(e) => e.timestamp(),
DomainEvent::System(e) => e.timestamp(),
}
}
/// Get event type name
pub fn event_type(&self) -> &'static str {
match self {
DomainEvent::Detection(e) => e.event_type(),
DomainEvent::Alert(e) => e.event_type(),
DomainEvent::Zone(e) => e.event_type(),
DomainEvent::System(e) => e.event_type(),
}
}
}
/// Detection-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DetectionEvent {
/// New survivor detected
SurvivorDetected {
survivor_id: SurvivorId,
zone_id: ScanZoneId,
vital_signs: VitalSignsReading,
location: Option<Coordinates3D>,
timestamp: DateTime<Utc>,
},
/// Survivor vital signs updated
VitalsUpdated {
survivor_id: SurvivorId,
previous_triage: TriageStatus,
current_triage: TriageStatus,
confidence: f64,
timestamp: DateTime<Utc>,
},
/// Survivor triage status changed
TriageStatusChanged {
survivor_id: SurvivorId,
previous: TriageStatus,
current: TriageStatus,
reason: String,
timestamp: DateTime<Utc>,
},
/// Survivor location refined
LocationRefined {
survivor_id: SurvivorId,
previous: Option<Coordinates3D>,
current: Coordinates3D,
uncertainty_reduced: bool,
timestamp: DateTime<Utc>,
},
/// Survivor no longer detected
SurvivorLost {
survivor_id: SurvivorId,
last_detection: DateTime<Utc>,
reason: LostReason,
timestamp: DateTime<Utc>,
},
/// Survivor rescued
SurvivorRescued {
survivor_id: SurvivorId,
rescue_team: Option<String>,
timestamp: DateTime<Utc>,
},
/// Survivor marked deceased
SurvivorDeceased {
survivor_id: SurvivorId,
timestamp: DateTime<Utc>,
},
}
impl DetectionEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::SurvivorDetected { timestamp, .. } => *timestamp,
Self::VitalsUpdated { timestamp, .. } => *timestamp,
Self::TriageStatusChanged { timestamp, .. } => *timestamp,
Self::LocationRefined { timestamp, .. } => *timestamp,
Self::SurvivorLost { timestamp, .. } => *timestamp,
Self::SurvivorRescued { timestamp, .. } => *timestamp,
Self::SurvivorDeceased { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::SurvivorDetected { .. } => "SurvivorDetected",
Self::VitalsUpdated { .. } => "VitalsUpdated",
Self::TriageStatusChanged { .. } => "TriageStatusChanged",
Self::LocationRefined { .. } => "LocationRefined",
Self::SurvivorLost { .. } => "SurvivorLost",
Self::SurvivorRescued { .. } => "SurvivorRescued",
Self::SurvivorDeceased { .. } => "SurvivorDeceased",
}
}
/// Get the survivor ID associated with this event
pub fn survivor_id(&self) -> &SurvivorId {
match self {
Self::SurvivorDetected { survivor_id, .. } => survivor_id,
Self::VitalsUpdated { survivor_id, .. } => survivor_id,
Self::TriageStatusChanged { survivor_id, .. } => survivor_id,
Self::LocationRefined { survivor_id, .. } => survivor_id,
Self::SurvivorLost { survivor_id, .. } => survivor_id,
Self::SurvivorRescued { survivor_id, .. } => survivor_id,
Self::SurvivorDeceased { survivor_id, .. } => survivor_id,
}
}
}
/// Reasons for losing a survivor signal
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LostReason {
/// Survivor was rescued (signal expected to stop)
Rescued,
/// Detection determined to be false positive
FalsePositive,
/// Signal lost (interference, debris shift, etc.)
SignalLost,
/// Zone was deactivated
ZoneDeactivated,
/// Sensor malfunction
SensorFailure,
/// Unknown reason
Unknown,
}
/// Alert-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlertEvent {
/// New alert generated
AlertGenerated {
alert_id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
timestamp: DateTime<Utc>,
},
/// Alert acknowledged by rescue team
AlertAcknowledged {
alert_id: AlertId,
acknowledged_by: String,
timestamp: DateTime<Utc>,
},
/// Alert escalated
AlertEscalated {
alert_id: AlertId,
previous_priority: Priority,
new_priority: Priority,
reason: String,
timestamp: DateTime<Utc>,
},
/// Alert resolved
AlertResolved {
alert_id: AlertId,
resolution: AlertResolution,
timestamp: DateTime<Utc>,
},
/// Alert cancelled
AlertCancelled {
alert_id: AlertId,
reason: String,
timestamp: DateTime<Utc>,
},
}
impl AlertEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::AlertGenerated { timestamp, .. } => *timestamp,
Self::AlertAcknowledged { timestamp, .. } => *timestamp,
Self::AlertEscalated { timestamp, .. } => *timestamp,
Self::AlertResolved { timestamp, .. } => *timestamp,
Self::AlertCancelled { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::AlertGenerated { .. } => "AlertGenerated",
Self::AlertAcknowledged { .. } => "AlertAcknowledged",
Self::AlertEscalated { .. } => "AlertEscalated",
Self::AlertResolved { .. } => "AlertResolved",
Self::AlertCancelled { .. } => "AlertCancelled",
}
}
/// Get the alert ID associated with this event
pub fn alert_id(&self) -> &AlertId {
match self {
Self::AlertGenerated { alert_id, .. } => alert_id,
Self::AlertAcknowledged { alert_id, .. } => alert_id,
Self::AlertEscalated { alert_id, .. } => alert_id,
Self::AlertResolved { alert_id, .. } => alert_id,
Self::AlertCancelled { alert_id, .. } => alert_id,
}
}
}
/// Zone-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneEvent {
/// Zone activated
ZoneActivated {
zone_id: ScanZoneId,
zone_name: String,
timestamp: DateTime<Utc>,
},
/// Zone scan completed
ZoneScanCompleted {
zone_id: ScanZoneId,
detections_found: u32,
scan_duration_ms: u64,
timestamp: DateTime<Utc>,
},
/// Zone paused
ZonePaused {
zone_id: ScanZoneId,
reason: String,
timestamp: DateTime<Utc>,
},
/// Zone resumed
ZoneResumed {
zone_id: ScanZoneId,
timestamp: DateTime<Utc>,
},
/// Zone marked complete
ZoneCompleted {
zone_id: ScanZoneId,
total_survivors_found: u32,
timestamp: DateTime<Utc>,
},
/// Zone deactivated
ZoneDeactivated {
zone_id: ScanZoneId,
reason: String,
timestamp: DateTime<Utc>,
},
}
impl ZoneEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::ZoneActivated { timestamp, .. } => *timestamp,
Self::ZoneScanCompleted { timestamp, .. } => *timestamp,
Self::ZonePaused { timestamp, .. } => *timestamp,
Self::ZoneResumed { timestamp, .. } => *timestamp,
Self::ZoneCompleted { timestamp, .. } => *timestamp,
Self::ZoneDeactivated { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::ZoneActivated { .. } => "ZoneActivated",
Self::ZoneScanCompleted { .. } => "ZoneScanCompleted",
Self::ZonePaused { .. } => "ZonePaused",
Self::ZoneResumed { .. } => "ZoneResumed",
Self::ZoneCompleted { .. } => "ZoneCompleted",
Self::ZoneDeactivated { .. } => "ZoneDeactivated",
}
}
/// Get the zone ID associated with this event
pub fn zone_id(&self) -> &ScanZoneId {
match self {
Self::ZoneActivated { zone_id, .. } => zone_id,
Self::ZoneScanCompleted { zone_id, .. } => zone_id,
Self::ZonePaused { zone_id, .. } => zone_id,
Self::ZoneResumed { zone_id, .. } => zone_id,
Self::ZoneCompleted { zone_id, .. } => zone_id,
Self::ZoneDeactivated { zone_id, .. } => zone_id,
}
}
}
/// System-level events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SystemEvent {
/// System started
SystemStarted {
version: String,
timestamp: DateTime<Utc>,
},
/// System stopped
SystemStopped {
reason: String,
timestamp: DateTime<Utc>,
},
/// Sensor connected
SensorConnected {
sensor_id: String,
zone_id: ScanZoneId,
timestamp: DateTime<Utc>,
},
/// Sensor disconnected
SensorDisconnected {
sensor_id: String,
reason: String,
timestamp: DateTime<Utc>,
},
/// Configuration changed
ConfigChanged {
setting: String,
previous_value: String,
new_value: String,
timestamp: DateTime<Utc>,
},
/// Error occurred
ErrorOccurred {
error_type: String,
message: String,
severity: ErrorSeverity,
timestamp: DateTime<Utc>,
},
}
impl SystemEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::SystemStarted { timestamp, .. } => *timestamp,
Self::SystemStopped { timestamp, .. } => *timestamp,
Self::SensorConnected { timestamp, .. } => *timestamp,
Self::SensorDisconnected { timestamp, .. } => *timestamp,
Self::ConfigChanged { timestamp, .. } => *timestamp,
Self::ErrorOccurred { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::SystemStarted { .. } => "SystemStarted",
Self::SystemStopped { .. } => "SystemStopped",
Self::SensorConnected { .. } => "SensorConnected",
Self::SensorDisconnected { .. } => "SensorDisconnected",
Self::ConfigChanged { .. } => "ConfigChanged",
Self::ErrorOccurred { .. } => "ErrorOccurred",
}
}
}
/// Error severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ErrorSeverity {
/// Warning - operation continues
Warning,
/// Error - operation may be affected
Error,
/// Critical - immediate attention required
Critical,
}
/// Event store for persisting domain events
pub trait EventStore: Send + Sync {
/// Append an event to the store
fn append(&self, event: DomainEvent) -> Result<(), crate::MatError>;
/// Get all events
fn all(&self) -> Result<Vec<DomainEvent>, crate::MatError>;
/// Get events since a timestamp
fn since(&self, timestamp: DateTime<Utc>) -> Result<Vec<DomainEvent>, crate::MatError>;
/// Get events for a specific survivor
fn for_survivor(&self, survivor_id: &SurvivorId) -> Result<Vec<DomainEvent>, crate::MatError>;
}
/// In-memory event store implementation
#[derive(Debug, Default)]
pub struct InMemoryEventStore {
events: parking_lot::RwLock<Vec<DomainEvent>>,
}
impl InMemoryEventStore {
/// Create a new in-memory event store
pub fn new() -> Self {
Self::default()
}
}
impl EventStore for InMemoryEventStore {
fn append(&self, event: DomainEvent) -> Result<(), crate::MatError> {
self.events.write().push(event);
Ok(())
}
fn all(&self) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self.events.read().clone())
}
fn since(&self, timestamp: DateTime<Utc>) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self
.events
.read()
.iter()
.filter(|e| e.timestamp() >= timestamp)
.cloned()
.collect())
}
fn for_survivor(&self, survivor_id: &SurvivorId) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self
.events
.read()
.iter()
.filter(|e| {
if let DomainEvent::Detection(de) = e {
de.survivor_id() == survivor_id
} else {
false
}
})
.cloned()
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_in_memory_event_store() {
let store = InMemoryEventStore::new();
let event = DomainEvent::System(SystemEvent::SystemStarted {
version: "1.0.0".to_string(),
timestamp: Utc::now(),
});
store.append(event).unwrap();
let events = store.all().unwrap();
assert_eq!(events.len(), 1);
}
}

View File

@@ -0,0 +1,26 @@
//! Domain module containing core entities, value objects, and domain events.
//!
//! This module follows Domain-Driven Design principles with:
//! - **Entities**: Objects with identity (Survivor, DisasterEvent, ScanZone)
//! - **Value Objects**: Immutable objects without identity (VitalSignsReading, Coordinates3D)
//! - **Domain Events**: Events that capture domain significance
//! - **Aggregates**: Consistency boundaries (DisasterEvent is the root)
pub mod alert;
pub mod coordinates;
pub mod disaster_event;
pub mod events;
pub mod scan_zone;
pub mod survivor;
pub mod triage;
pub mod vital_signs;
// Re-export all domain types
pub use alert::*;
pub use coordinates::*;
pub use disaster_event::*;
pub use events::*;
pub use scan_zone::*;
pub use survivor::*;
pub use triage::*;
pub use vital_signs::*;

View File

@@ -0,0 +1,494 @@
//! Scan zone entity for defining areas to monitor.
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Unique identifier for a scan zone
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanZoneId(Uuid);
impl ScanZoneId {
/// Create a new random zone ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for ScanZoneId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ScanZoneId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Bounds of a scan zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneBounds {
/// Rectangular zone
Rectangle {
/// Minimum X coordinate
min_x: f64,
/// Minimum Y coordinate
min_y: f64,
/// Maximum X coordinate
max_x: f64,
/// Maximum Y coordinate
max_y: f64,
},
/// Circular zone
Circle {
/// Center X coordinate
center_x: f64,
/// Center Y coordinate
center_y: f64,
/// Radius in meters
radius: f64,
},
/// Polygon zone (ordered vertices)
Polygon {
/// List of (x, y) vertices
vertices: Vec<(f64, f64)>,
},
}
impl ZoneBounds {
/// Create a rectangular zone
pub fn rectangle(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y }
}
/// Create a circular zone
pub fn circle(center_x: f64, center_y: f64, radius: f64) -> Self {
ZoneBounds::Circle { center_x, center_y, radius }
}
/// Create a polygon zone
pub fn polygon(vertices: Vec<(f64, f64)>) -> Self {
ZoneBounds::Polygon { vertices }
}
/// Calculate the area of the zone in square meters
pub fn area(&self) -> f64 {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
(max_x - min_x) * (max_y - min_y)
}
ZoneBounds::Circle { radius, .. } => {
std::f64::consts::PI * radius * radius
}
ZoneBounds::Polygon { vertices } => {
// Shoelace formula
if vertices.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let n = vertices.len();
for i in 0..n {
let j = (i + 1) % n;
area += vertices[i].0 * vertices[j].1;
area -= vertices[j].0 * vertices[i].1;
}
(area / 2.0).abs()
}
}
}
/// Check if a point is within the zone bounds
pub fn contains(&self, x: f64, y: f64) -> bool {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
x >= *min_x && x <= *max_x && y >= *min_y && y <= *max_y
}
ZoneBounds::Circle { center_x, center_y, radius } => {
let dx = x - center_x;
let dy = y - center_y;
(dx * dx + dy * dy).sqrt() <= *radius
}
ZoneBounds::Polygon { vertices } => {
// Ray casting algorithm
if vertices.len() < 3 {
return false;
}
let mut inside = false;
let n = vertices.len();
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = vertices[i];
let (xj, yj) = vertices[j];
if ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
{
inside = !inside;
}
j = i;
}
inside
}
}
}
/// Get the center point of the zone
pub fn center(&self) -> (f64, f64) {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
}
ZoneBounds::Circle { center_x, center_y, .. } => {
(*center_x, *center_y)
}
ZoneBounds::Polygon { vertices } => {
if vertices.is_empty() {
return (0.0, 0.0);
}
let sum_x: f64 = vertices.iter().map(|(x, _)| x).sum();
let sum_y: f64 = vertices.iter().map(|(_, y)| y).sum();
let n = vertices.len() as f64;
(sum_x / n, sum_y / n)
}
}
}
}
/// Status of a scan zone
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneStatus {
/// Zone is active and being scanned
Active,
/// Zone is paused (temporary)
Paused,
/// Zone scan is complete
Complete,
/// Zone is inaccessible
Inaccessible,
/// Zone is deactivated
Deactivated,
}
/// Parameters for scanning a zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanParameters {
/// Scan sensitivity (0.0-1.0)
pub sensitivity: f64,
/// Maximum depth to scan (meters)
pub max_depth: f64,
/// Scan resolution (higher = more detailed but slower)
pub resolution: ScanResolution,
/// Whether to use enhanced breathing detection
pub enhanced_breathing: bool,
/// Whether to use heartbeat detection (more sensitive but slower)
pub heartbeat_detection: bool,
}
impl Default for ScanParameters {
fn default() -> Self {
Self {
sensitivity: 0.8,
max_depth: 5.0,
resolution: ScanResolution::Standard,
enhanced_breathing: true,
heartbeat_detection: false,
}
}
}
/// Scan resolution levels
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScanResolution {
/// Quick scan, lower accuracy
Quick,
/// Standard scan
Standard,
/// High resolution scan
High,
/// Maximum resolution (slowest)
Maximum,
}
impl ScanResolution {
/// Get scan time multiplier
pub fn time_multiplier(&self) -> f64 {
match self {
ScanResolution::Quick => 0.5,
ScanResolution::Standard => 1.0,
ScanResolution::High => 2.0,
ScanResolution::Maximum => 4.0,
}
}
}
/// Position of a sensor (WiFi transmitter/receiver)
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SensorPosition {
/// Sensor identifier
pub id: String,
/// X coordinate (meters)
pub x: f64,
/// Y coordinate (meters)
pub y: f64,
/// Z coordinate (meters, height above ground)
pub z: f64,
/// Sensor type
pub sensor_type: SensorType,
/// Whether sensor is operational
pub is_operational: bool,
}
/// Types of sensors
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SensorType {
/// WiFi transmitter
Transmitter,
/// WiFi receiver
Receiver,
/// Combined transmitter/receiver
Transceiver,
}
/// A defined geographic area being monitored for survivors
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanZone {
id: ScanZoneId,
name: String,
bounds: ZoneBounds,
sensor_positions: Vec<SensorPosition>,
parameters: ScanParameters,
status: ZoneStatus,
created_at: DateTime<Utc>,
last_scan: Option<DateTime<Utc>>,
scan_count: u32,
detections_count: u32,
}
impl ScanZone {
/// Create a new scan zone
pub fn new(name: &str, bounds: ZoneBounds) -> Self {
Self {
id: ScanZoneId::new(),
name: name.to_string(),
bounds,
sensor_positions: Vec::new(),
parameters: ScanParameters::default(),
status: ZoneStatus::Active,
created_at: Utc::now(),
last_scan: None,
scan_count: 0,
detections_count: 0,
}
}
/// Create with custom parameters
pub fn with_parameters(name: &str, bounds: ZoneBounds, parameters: ScanParameters) -> Self {
let mut zone = Self::new(name, bounds);
zone.parameters = parameters;
zone
}
/// Get the zone ID
pub fn id(&self) -> &ScanZoneId {
&self.id
}
/// Get the zone name
pub fn name(&self) -> &str {
&self.name
}
/// Get the bounds
pub fn bounds(&self) -> &ZoneBounds {
&self.bounds
}
/// Get sensor positions
pub fn sensor_positions(&self) -> &[SensorPosition] {
&self.sensor_positions
}
/// Get scan parameters
pub fn parameters(&self) -> &ScanParameters {
&self.parameters
}
/// Get mutable scan parameters
pub fn parameters_mut(&mut self) -> &mut ScanParameters {
&mut self.parameters
}
/// Get the status
pub fn status(&self) -> &ZoneStatus {
&self.status
}
/// Get last scan time
pub fn last_scan(&self) -> Option<&DateTime<Utc>> {
self.last_scan.as_ref()
}
/// Get scan count
pub fn scan_count(&self) -> u32 {
self.scan_count
}
/// Get detection count
pub fn detections_count(&self) -> u32 {
self.detections_count
}
/// Add a sensor to the zone
pub fn add_sensor(&mut self, sensor: SensorPosition) {
self.sensor_positions.push(sensor);
}
/// Remove a sensor
pub fn remove_sensor(&mut self, sensor_id: &str) {
self.sensor_positions.retain(|s| s.id != sensor_id);
}
/// Set zone status
pub fn set_status(&mut self, status: ZoneStatus) {
self.status = status;
}
/// Pause the zone
pub fn pause(&mut self) {
self.status = ZoneStatus::Paused;
}
/// Resume the zone
pub fn resume(&mut self) {
if self.status == ZoneStatus::Paused {
self.status = ZoneStatus::Active;
}
}
/// Mark zone as complete
pub fn complete(&mut self) {
self.status = ZoneStatus::Complete;
}
/// Record a scan
pub fn record_scan(&mut self, found_detections: u32) {
self.last_scan = Some(Utc::now());
self.scan_count += 1;
self.detections_count += found_detections;
}
/// Check if a point is within this zone
pub fn contains_point(&self, x: f64, y: f64) -> bool {
self.bounds.contains(x, y)
}
/// Get the area of the zone
pub fn area(&self) -> f64 {
self.bounds.area()
}
/// Check if zone has enough sensors for localization
pub fn has_sufficient_sensors(&self) -> bool {
// Need at least 3 sensors for 2D localization
self.sensor_positions.iter()
.filter(|s| s.is_operational)
.count() >= 3
}
/// Time since last scan
pub fn time_since_scan(&self) -> Option<chrono::Duration> {
self.last_scan.map(|t| Utc::now() - t)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_bounds() {
let bounds = ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0);
assert!((bounds.area() - 100.0).abs() < 0.001);
assert!(bounds.contains(5.0, 5.0));
assert!(!bounds.contains(15.0, 5.0));
assert_eq!(bounds.center(), (5.0, 5.0));
}
#[test]
fn test_circle_bounds() {
let bounds = ZoneBounds::circle(0.0, 0.0, 10.0);
assert!((bounds.area() - std::f64::consts::PI * 100.0).abs() < 0.001);
assert!(bounds.contains(0.0, 0.0));
assert!(bounds.contains(5.0, 5.0));
assert!(!bounds.contains(10.0, 10.0));
}
#[test]
fn test_scan_zone_creation() {
let zone = ScanZone::new(
"Test Zone",
ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
);
assert_eq!(zone.name(), "Test Zone");
assert!(matches!(zone.status(), ZoneStatus::Active));
assert_eq!(zone.scan_count(), 0);
}
#[test]
fn test_scan_zone_sensors() {
let mut zone = ScanZone::new(
"Test Zone",
ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
);
assert!(!zone.has_sufficient_sensors());
for i in 0..3 {
zone.add_sensor(SensorPosition {
id: format!("sensor-{}", i),
x: i as f64 * 10.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
});
}
assert!(zone.has_sufficient_sensors());
}
#[test]
fn test_scan_zone_status_transitions() {
let mut zone = ScanZone::new(
"Test",
ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0),
);
assert!(matches!(zone.status(), ZoneStatus::Active));
zone.pause();
assert!(matches!(zone.status(), ZoneStatus::Paused));
zone.resume();
assert!(matches!(zone.status(), ZoneStatus::Active));
zone.complete();
assert!(matches!(zone.status(), ZoneStatus::Complete));
}
}

View File

@@ -0,0 +1,424 @@
//! Survivor entity representing a detected human in a disaster zone.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{
Coordinates3D, TriageStatus, VitalSignsReading, ScanZoneId,
triage::TriageCalculator,
};
/// Unique identifier for a survivor
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurvivorId(Uuid);
impl SurvivorId {
/// Create a new random survivor ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Create from an existing UUID
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for SurvivorId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for SurvivorId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Current status of a survivor
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SurvivorStatus {
/// Actively being tracked
Active,
/// Confirmed rescued
Rescued,
/// Lost signal, may need re-detection
Lost,
/// Confirmed deceased
Deceased,
/// Determined to be false positive
FalsePositive,
}
/// Additional metadata about a survivor
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurvivorMetadata {
/// Estimated age category based on vital patterns
pub estimated_age_category: Option<AgeCategory>,
/// Notes from rescue team
pub notes: Vec<String>,
/// Tags for organization
pub tags: Vec<String>,
/// Assigned rescue team ID
pub assigned_team: Option<String>,
}
/// Estimated age category based on vital sign patterns
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AgeCategory {
/// Infant (0-2 years)
Infant,
/// Child (2-12 years)
Child,
/// Adult (12-65 years)
Adult,
/// Elderly (65+ years)
Elderly,
/// Cannot determine
Unknown,
}
/// History of vital signs readings
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VitalSignsHistory {
readings: Vec<VitalSignsReading>,
max_history: usize,
}
impl VitalSignsHistory {
/// Create a new history with specified max size
pub fn new(max_history: usize) -> Self {
Self {
readings: Vec::with_capacity(max_history),
max_history,
}
}
/// Add a new reading
pub fn add(&mut self, reading: VitalSignsReading) {
if self.readings.len() >= self.max_history {
self.readings.remove(0);
}
self.readings.push(reading);
}
/// Get the most recent reading
pub fn latest(&self) -> Option<&VitalSignsReading> {
self.readings.last()
}
/// Get all readings
pub fn all(&self) -> &[VitalSignsReading] {
&self.readings
}
/// Get the number of readings
pub fn len(&self) -> usize {
self.readings.len()
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.readings.is_empty()
}
/// Calculate average confidence across readings
pub fn average_confidence(&self) -> f64 {
if self.readings.is_empty() {
return 0.0;
}
let sum: f64 = self.readings.iter()
.map(|r| r.confidence.value())
.sum();
sum / self.readings.len() as f64
}
/// Check if vitals are deteriorating
pub fn is_deteriorating(&self) -> bool {
if self.readings.len() < 3 {
return false;
}
let recent: Vec<_> = self.readings.iter().rev().take(3).collect();
// Check breathing trend
let breathing_declining = recent.windows(2).all(|w| {
match (&w[0].breathing, &w[1].breathing) {
(Some(a), Some(b)) => a.rate_bpm < b.rate_bpm,
_ => false,
}
});
// Check confidence trend
let confidence_declining = recent.windows(2).all(|w| {
w[0].confidence.value() < w[1].confidence.value()
});
breathing_declining || confidence_declining
}
}
/// A detected survivor in the disaster zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Survivor {
id: SurvivorId,
zone_id: ScanZoneId,
first_detected: DateTime<Utc>,
last_updated: DateTime<Utc>,
location: Option<Coordinates3D>,
vital_signs: VitalSignsHistory,
triage_status: TriageStatus,
status: SurvivorStatus,
confidence: f64,
metadata: SurvivorMetadata,
alert_sent: bool,
}
impl Survivor {
/// Create a new survivor from initial detection
pub fn new(
zone_id: ScanZoneId,
initial_vitals: VitalSignsReading,
location: Option<Coordinates3D>,
) -> Self {
let now = Utc::now();
let confidence = initial_vitals.confidence.value();
let triage_status = TriageCalculator::calculate(&initial_vitals);
let mut vital_signs = VitalSignsHistory::new(100);
vital_signs.add(initial_vitals);
Self {
id: SurvivorId::new(),
zone_id,
first_detected: now,
last_updated: now,
location,
vital_signs,
triage_status,
status: SurvivorStatus::Active,
confidence,
metadata: SurvivorMetadata::default(),
alert_sent: false,
}
}
/// Get the survivor ID
pub fn id(&self) -> &SurvivorId {
&self.id
}
/// Get the zone ID where survivor was detected
pub fn zone_id(&self) -> &ScanZoneId {
&self.zone_id
}
/// Get the first detection time
pub fn first_detected(&self) -> &DateTime<Utc> {
&self.first_detected
}
/// Get the last update time
pub fn last_updated(&self) -> &DateTime<Utc> {
&self.last_updated
}
/// Get the estimated location
pub fn location(&self) -> Option<&Coordinates3D> {
self.location.as_ref()
}
/// Get the vital signs history
pub fn vital_signs(&self) -> &VitalSignsHistory {
&self.vital_signs
}
/// Get the current triage status
pub fn triage_status(&self) -> &TriageStatus {
&self.triage_status
}
/// Get the current status
pub fn status(&self) -> &SurvivorStatus {
&self.status
}
/// Get the confidence score
pub fn confidence(&self) -> f64 {
self.confidence
}
/// Get the metadata
pub fn metadata(&self) -> &SurvivorMetadata {
&self.metadata
}
/// Get mutable metadata
pub fn metadata_mut(&mut self) -> &mut SurvivorMetadata {
&mut self.metadata
}
/// Update with new vital signs reading
pub fn update_vitals(&mut self, reading: VitalSignsReading) {
let previous_triage = self.triage_status.clone();
self.vital_signs.add(reading.clone());
self.confidence = self.vital_signs.average_confidence();
self.triage_status = TriageCalculator::calculate(&reading);
self.last_updated = Utc::now();
// Log triage change for audit
if previous_triage != self.triage_status {
tracing::info!(
survivor_id = %self.id,
previous = ?previous_triage,
current = ?self.triage_status,
"Triage status changed"
);
}
}
/// Update the location estimate
pub fn update_location(&mut self, location: Coordinates3D) {
self.location = Some(location);
self.last_updated = Utc::now();
}
/// Mark as rescued
pub fn mark_rescued(&mut self) {
self.status = SurvivorStatus::Rescued;
self.last_updated = Utc::now();
tracing::info!(survivor_id = %self.id, "Survivor marked as rescued");
}
/// Mark as lost (signal lost)
pub fn mark_lost(&mut self) {
self.status = SurvivorStatus::Lost;
self.last_updated = Utc::now();
}
/// Mark as deceased
pub fn mark_deceased(&mut self) {
self.status = SurvivorStatus::Deceased;
self.triage_status = TriageStatus::Deceased;
self.last_updated = Utc::now();
}
/// Mark as false positive
pub fn mark_false_positive(&mut self) {
self.status = SurvivorStatus::FalsePositive;
self.last_updated = Utc::now();
}
/// Check if survivor should generate an alert
pub fn should_alert(&self) -> bool {
if self.alert_sent {
return false;
}
// Alert for high-priority survivors
matches!(
self.triage_status,
TriageStatus::Immediate | TriageStatus::Delayed
) && self.confidence >= 0.5
}
/// Mark that alert was sent
pub fn mark_alert_sent(&mut self) {
self.alert_sent = true;
}
/// Check if vitals are deteriorating (needs priority upgrade)
pub fn is_deteriorating(&self) -> bool {
self.vital_signs.is_deteriorating()
}
/// Get time since last update
pub fn time_since_update(&self) -> chrono::Duration {
Utc::now() - self.last_updated
}
/// Check if survivor data is stale
pub fn is_stale(&self, threshold_seconds: i64) -> bool {
self.time_since_update().num_seconds() > threshold_seconds
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore};
fn create_test_vitals(confidence: f64) -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(confidence),
}
}
#[test]
fn test_survivor_creation() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let survivor = Survivor::new(zone_id.clone(), vitals, None);
assert_eq!(survivor.zone_id(), &zone_id);
assert!(survivor.confidence() >= 0.8);
assert!(matches!(survivor.status(), SurvivorStatus::Active));
}
#[test]
fn test_vital_signs_history() {
let mut history = VitalSignsHistory::new(5);
for i in 0..7 {
history.add(create_test_vitals(0.5 + (i as f64 * 0.05)));
}
// Should only keep last 5
assert_eq!(history.len(), 5);
// Average should be based on last 5 readings
assert!(history.average_confidence() > 0.5);
}
#[test]
fn test_survivor_should_alert() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let survivor = Survivor::new(zone_id, vitals, None);
// Should alert if triage is Immediate or Delayed
// Depends on triage calculation from vitals
assert!(!survivor.alert_sent);
}
#[test]
fn test_survivor_mark_rescued() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let mut survivor = Survivor::new(zone_id, vitals, None);
survivor.mark_rescued();
assert!(matches!(survivor.status(), SurvivorStatus::Rescued));
}
}

View File

@@ -0,0 +1,350 @@
//! Triage classification following START protocol.
//!
//! The START (Simple Triage and Rapid Treatment) protocol is used to
//! quickly categorize victims in mass casualty incidents.
use super::{VitalSignsReading, BreathingType, MovementType};
/// Triage status following START protocol
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TriageStatus {
/// Immediate (Red) - Life-threatening, requires immediate intervention
/// RPM: Respiration >30 or <10, or absent pulse, or unable to follow commands
Immediate,
/// Delayed (Yellow) - Serious but stable, can wait for treatment
/// RPM: Normal respiration, pulse present, follows commands, non-life-threatening
Delayed,
/// Minor (Green) - Walking wounded, minimal treatment needed
/// Can walk, minor injuries
Minor,
/// Deceased (Black) - No vital signs, or not breathing after airway cleared
Deceased,
/// Unknown - Insufficient data for classification
Unknown,
}
impl TriageStatus {
/// Get the priority level (1 = highest)
pub fn priority(&self) -> u8 {
match self {
TriageStatus::Immediate => 1,
TriageStatus::Delayed => 2,
TriageStatus::Minor => 3,
TriageStatus::Deceased => 4,
TriageStatus::Unknown => 5,
}
}
/// Get display color
pub fn color(&self) -> &'static str {
match self {
TriageStatus::Immediate => "red",
TriageStatus::Delayed => "yellow",
TriageStatus::Minor => "green",
TriageStatus::Deceased => "black",
TriageStatus::Unknown => "gray",
}
}
/// Get human-readable description
pub fn description(&self) -> &'static str {
match self {
TriageStatus::Immediate => "Requires immediate life-saving intervention",
TriageStatus::Delayed => "Serious but can wait for treatment",
TriageStatus::Minor => "Minor injuries, walking wounded",
TriageStatus::Deceased => "No vital signs detected",
TriageStatus::Unknown => "Unable to determine status",
}
}
/// Check if this status requires urgent attention
pub fn is_urgent(&self) -> bool {
matches!(self, TriageStatus::Immediate | TriageStatus::Delayed)
}
}
impl std::fmt::Display for TriageStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TriageStatus::Immediate => write!(f, "IMMEDIATE (Red)"),
TriageStatus::Delayed => write!(f, "DELAYED (Yellow)"),
TriageStatus::Minor => write!(f, "MINOR (Green)"),
TriageStatus::Deceased => write!(f, "DECEASED (Black)"),
TriageStatus::Unknown => write!(f, "UNKNOWN"),
}
}
}
/// Calculator for triage status based on vital signs
pub struct TriageCalculator;
impl TriageCalculator {
/// Calculate triage status from vital signs reading
///
/// Uses modified START protocol adapted for remote sensing:
/// 1. Check breathing (respiration)
/// 2. Check for movement/responsiveness (proxy for perfusion/mental status)
/// 3. Classify based on combined assessment
pub fn calculate(vitals: &VitalSignsReading) -> TriageStatus {
// Step 1: Check if any vitals are detected
if !vitals.has_vitals() {
// No vitals at all - either deceased or signal issue
return TriageStatus::Unknown;
}
// Step 2: Assess breathing
let breathing_status = Self::assess_breathing(vitals);
// Step 3: Assess movement/responsiveness
let movement_status = Self::assess_movement(vitals);
// Step 4: Combine assessments
Self::combine_assessments(breathing_status, movement_status)
}
/// Assess breathing status
fn assess_breathing(vitals: &VitalSignsReading) -> BreathingAssessment {
match &vitals.breathing {
None => BreathingAssessment::Absent,
Some(breathing) => {
// Check for agonal breathing (pre-death)
if breathing.pattern_type == BreathingType::Agonal {
return BreathingAssessment::Agonal;
}
// Check rate
if breathing.rate_bpm < 10.0 {
BreathingAssessment::TooSlow
} else if breathing.rate_bpm > 30.0 {
BreathingAssessment::TooFast
} else {
BreathingAssessment::Normal
}
}
}
}
/// Assess movement/responsiveness
fn assess_movement(vitals: &VitalSignsReading) -> MovementAssessment {
match vitals.movement.movement_type {
MovementType::Gross if vitals.movement.is_voluntary => {
MovementAssessment::Responsive
}
MovementType::Gross => MovementAssessment::Moving,
MovementType::Fine => MovementAssessment::MinimalMovement,
MovementType::Tremor => MovementAssessment::InvoluntaryOnly,
MovementType::Periodic => MovementAssessment::MinimalMovement,
MovementType::None => MovementAssessment::None,
}
}
/// Combine breathing and movement assessments into triage status
fn combine_assessments(
breathing: BreathingAssessment,
movement: MovementAssessment,
) -> TriageStatus {
match (breathing, movement) {
// No breathing
(BreathingAssessment::Absent, MovementAssessment::None) => {
TriageStatus::Deceased
}
(BreathingAssessment::Agonal, _) => {
TriageStatus::Immediate
}
(BreathingAssessment::Absent, _) => {
// No breathing but movement - possible airway obstruction
TriageStatus::Immediate
}
// Abnormal breathing rates
(BreathingAssessment::TooFast, _) => {
TriageStatus::Immediate
}
(BreathingAssessment::TooSlow, _) => {
TriageStatus::Immediate
}
// Normal breathing with movement assessment
(BreathingAssessment::Normal, MovementAssessment::Responsive) => {
TriageStatus::Minor
}
(BreathingAssessment::Normal, MovementAssessment::Moving) => {
TriageStatus::Delayed
}
(BreathingAssessment::Normal, MovementAssessment::MinimalMovement) => {
TriageStatus::Delayed
}
(BreathingAssessment::Normal, MovementAssessment::InvoluntaryOnly) => {
TriageStatus::Immediate // Not following commands
}
(BreathingAssessment::Normal, MovementAssessment::None) => {
TriageStatus::Immediate // Breathing but unresponsive
}
}
}
/// Check if status should be upgraded based on deterioration
pub fn should_upgrade(current: &TriageStatus, is_deteriorating: bool) -> bool {
if !is_deteriorating {
return false;
}
// Upgrade if not already at highest priority
matches!(current, TriageStatus::Delayed | TriageStatus::Minor)
}
/// Get upgraded triage status
pub fn upgrade(current: &TriageStatus) -> TriageStatus {
match current {
TriageStatus::Minor => TriageStatus::Delayed,
TriageStatus::Delayed => TriageStatus::Immediate,
other => other.clone(),
}
}
}
/// Internal breathing assessment
#[derive(Debug, Clone, Copy)]
enum BreathingAssessment {
Normal,
TooFast,
TooSlow,
Agonal,
Absent,
}
/// Internal movement assessment
#[derive(Debug, Clone, Copy)]
enum MovementAssessment {
Responsive, // Voluntary purposeful movement
Moving, // Movement but unclear if responsive
MinimalMovement, // Small movements only
InvoluntaryOnly, // Only tremors/involuntary
None, // No movement detected
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, ConfidenceScore, MovementProfile};
use chrono::Utc;
fn create_vitals(
breathing: Option<BreathingPattern>,
movement: MovementProfile,
) -> VitalSignsReading {
VitalSignsReading {
breathing,
heartbeat: None,
movement,
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
}
}
#[test]
fn test_no_vitals_is_unknown() {
let vitals = create_vitals(None, MovementProfile::default());
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Unknown);
}
#[test]
fn test_normal_breathing_responsive_is_minor() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
MovementProfile {
movement_type: MovementType::Gross,
intensity: 0.8,
frequency: 0.5,
is_voluntary: true,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Minor);
}
#[test]
fn test_fast_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
}),
MovementProfile {
movement_type: MovementType::Fine,
intensity: 0.3,
frequency: 0.2,
is_voluntary: false,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_slow_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 8.0,
amplitude: 0.5,
regularity: 0.6,
pattern_type: BreathingType::Shallow,
}),
MovementProfile {
movement_type: MovementType::None,
intensity: 0.0,
frequency: 0.0,
is_voluntary: false,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_agonal_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 4.0,
amplitude: 0.3,
regularity: 0.2,
pattern_type: BreathingType::Agonal,
}),
MovementProfile::default(),
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_triage_priority() {
assert_eq!(TriageStatus::Immediate.priority(), 1);
assert_eq!(TriageStatus::Delayed.priority(), 2);
assert_eq!(TriageStatus::Minor.priority(), 3);
assert_eq!(TriageStatus::Deceased.priority(), 4);
}
#[test]
fn test_upgrade_triage() {
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Minor),
TriageStatus::Delayed
);
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Delayed),
TriageStatus::Immediate
);
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Immediate),
TriageStatus::Immediate
);
}
}

View File

@@ -0,0 +1,375 @@
//! Vital signs value objects for survivor detection.
use chrono::{DateTime, Utc};
/// Confidence score for a detection (0.0 to 1.0)
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConfidenceScore(f64);
impl ConfidenceScore {
/// Create a new confidence score, clamped to [0.0, 1.0]
pub fn new(value: f64) -> Self {
Self(value.clamp(0.0, 1.0))
}
/// Get the raw value
pub fn value(&self) -> f64 {
self.0
}
/// Check if confidence is high (>= 0.8)
pub fn is_high(&self) -> bool {
self.0 >= 0.8
}
/// Check if confidence is medium (>= 0.5)
pub fn is_medium(&self) -> bool {
self.0 >= 0.5
}
/// Check if confidence is low (< 0.5)
pub fn is_low(&self) -> bool {
self.0 < 0.5
}
}
impl Default for ConfidenceScore {
fn default() -> Self {
Self(0.0)
}
}
/// Complete vital signs reading at a point in time
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VitalSignsReading {
/// Breathing pattern if detected
pub breathing: Option<BreathingPattern>,
/// Heartbeat signature if detected
pub heartbeat: Option<HeartbeatSignature>,
/// Movement profile
pub movement: MovementProfile,
/// Timestamp of reading
pub timestamp: DateTime<Utc>,
/// Overall confidence in the reading
pub confidence: ConfidenceScore,
}
impl VitalSignsReading {
/// Create a new vital signs reading
pub fn new(
breathing: Option<BreathingPattern>,
heartbeat: Option<HeartbeatSignature>,
movement: MovementProfile,
) -> Self {
// Calculate combined confidence
let confidence = Self::calculate_confidence(&breathing, &heartbeat, &movement);
Self {
breathing,
heartbeat,
movement,
timestamp: Utc::now(),
confidence,
}
}
/// Calculate combined confidence from individual detections
fn calculate_confidence(
breathing: &Option<BreathingPattern>,
heartbeat: &Option<HeartbeatSignature>,
movement: &MovementProfile,
) -> ConfidenceScore {
let mut total = 0.0;
let mut count = 0.0;
if let Some(b) = breathing {
total += b.confidence();
count += 1.5; // Weight breathing higher
}
if let Some(h) = heartbeat {
total += h.confidence();
count += 1.0;
}
if movement.movement_type != MovementType::None {
total += movement.confidence();
count += 1.0;
}
if count > 0.0 {
ConfidenceScore::new(total / count)
} else {
ConfidenceScore::new(0.0)
}
}
/// Check if any vital sign is detected
pub fn has_vitals(&self) -> bool {
self.breathing.is_some()
|| self.heartbeat.is_some()
|| self.movement.movement_type != MovementType::None
}
/// Check if breathing is detected
pub fn has_breathing(&self) -> bool {
self.breathing.is_some()
}
/// Check if heartbeat is detected
pub fn has_heartbeat(&self) -> bool {
self.heartbeat.is_some()
}
/// Check if movement is detected
pub fn has_movement(&self) -> bool {
self.movement.movement_type != MovementType::None
}
}
/// Breathing pattern detected from CSI analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BreathingPattern {
/// Breaths per minute (normal adult: 12-20)
pub rate_bpm: f32,
/// Signal amplitude/strength
pub amplitude: f32,
/// Pattern regularity (0.0-1.0)
pub regularity: f32,
/// Type of breathing pattern
pub pattern_type: BreathingType,
}
impl BreathingPattern {
/// Check if breathing rate is normal
pub fn is_normal_rate(&self) -> bool {
self.rate_bpm >= 12.0 && self.rate_bpm <= 20.0
}
/// Check if rate is critically low
pub fn is_bradypnea(&self) -> bool {
self.rate_bpm < 10.0
}
/// Check if rate is critically high
pub fn is_tachypnea(&self) -> bool {
self.rate_bpm > 30.0
}
/// Get confidence based on signal quality
pub fn confidence(&self) -> f64 {
(self.amplitude * self.regularity) as f64
}
}
/// Types of breathing patterns
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BreathingType {
/// Normal, regular breathing
Normal,
/// Shallow, weak breathing
Shallow,
/// Deep, labored breathing
Labored,
/// Irregular pattern
Irregular,
/// Agonal breathing (pre-death gasping)
Agonal,
/// Apnea (no breathing detected)
Apnea,
}
/// Heartbeat signature from micro-Doppler analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HeartbeatSignature {
/// Heart rate in beats per minute (normal: 60-100)
pub rate_bpm: f32,
/// Heart rate variability
pub variability: f32,
/// Signal strength
pub strength: SignalStrength,
}
impl HeartbeatSignature {
/// Check if heart rate is normal
pub fn is_normal_rate(&self) -> bool {
self.rate_bpm >= 60.0 && self.rate_bpm <= 100.0
}
/// Check if rate indicates bradycardia
pub fn is_bradycardia(&self) -> bool {
self.rate_bpm < 50.0
}
/// Check if rate indicates tachycardia
pub fn is_tachycardia(&self) -> bool {
self.rate_bpm > 120.0
}
/// Get confidence based on signal strength
pub fn confidence(&self) -> f64 {
match self.strength {
SignalStrength::Strong => 0.9,
SignalStrength::Moderate => 0.7,
SignalStrength::Weak => 0.4,
SignalStrength::VeryWeak => 0.2,
}
}
}
/// Signal strength levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SignalStrength {
/// Strong, clear signal
Strong,
/// Moderate signal
Moderate,
/// Weak signal
Weak,
/// Very weak, borderline
VeryWeak,
}
/// Movement profile from CSI analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MovementProfile {
/// Type of movement detected
pub movement_type: MovementType,
/// Intensity of movement (0.0-1.0)
pub intensity: f32,
/// Frequency of movement patterns
pub frequency: f32,
/// Whether movement appears voluntary/purposeful
pub is_voluntary: bool,
}
impl Default for MovementProfile {
fn default() -> Self {
Self {
movement_type: MovementType::None,
intensity: 0.0,
frequency: 0.0,
is_voluntary: false,
}
}
}
impl MovementProfile {
/// Get confidence based on movement characteristics
pub fn confidence(&self) -> f64 {
match self.movement_type {
MovementType::None => 0.0,
MovementType::Gross => 0.9,
MovementType::Fine => 0.7,
MovementType::Tremor => 0.6,
MovementType::Periodic => 0.5,
}
}
/// Check if movement indicates consciousness
pub fn indicates_consciousness(&self) -> bool {
self.is_voluntary && self.movement_type == MovementType::Gross
}
}
/// Types of movement detected
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MovementType {
/// No movement detected
None,
/// Large body movements (limbs, torso)
Gross,
/// Small movements (fingers, head)
Fine,
/// Involuntary tremor/shaking
Tremor,
/// Periodic movement (possibly breathing-related)
Periodic,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confidence_score_clamping() {
assert_eq!(ConfidenceScore::new(1.5).value(), 1.0);
assert_eq!(ConfidenceScore::new(-0.5).value(), 0.0);
assert_eq!(ConfidenceScore::new(0.7).value(), 0.7);
}
#[test]
fn test_breathing_pattern_rates() {
let normal = BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
};
assert!(normal.is_normal_rate());
assert!(!normal.is_bradypnea());
assert!(!normal.is_tachypnea());
let slow = BreathingPattern {
rate_bpm: 8.0,
amplitude: 0.5,
regularity: 0.6,
pattern_type: BreathingType::Shallow,
};
assert!(slow.is_bradypnea());
let fast = BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
};
assert!(fast.is_tachypnea());
}
#[test]
fn test_vital_signs_reading() {
let breathing = BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
};
let reading = VitalSignsReading::new(
Some(breathing),
None,
MovementProfile::default(),
);
assert!(reading.has_vitals());
assert!(reading.has_breathing());
assert!(!reading.has_heartbeat());
assert!(!reading.has_movement());
}
#[test]
fn test_signal_strength_confidence() {
let strong = HeartbeatSignature {
rate_bpm: 72.0,
variability: 0.1,
strength: SignalStrength::Strong,
};
assert_eq!(strong.confidence(), 0.9);
let weak = HeartbeatSignature {
rate_bpm: 72.0,
variability: 0.1,
strength: SignalStrength::Weak,
};
assert_eq!(weak.confidence(), 0.4);
}
}

View File

@@ -0,0 +1,324 @@
//! Adapter for wifi-densepose-hardware crate.
use super::AdapterError;
use crate::domain::{SensorPosition, SensorType};
/// Hardware adapter for sensor communication
pub struct HardwareAdapter {
/// Connected sensors
sensors: Vec<SensorInfo>,
/// Whether hardware is initialized
initialized: bool,
}
/// Information about a connected sensor
#[derive(Debug, Clone)]
pub struct SensorInfo {
/// Unique sensor ID
pub id: String,
/// Sensor position
pub position: SensorPosition,
/// Current status
pub status: SensorStatus,
/// Last RSSI reading (if available)
pub last_rssi: Option<f64>,
/// Battery level (0-100, if applicable)
pub battery_level: Option<u8>,
}
/// Status of a sensor
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SensorStatus {
/// Sensor is connected and operational
Connected,
/// Sensor is disconnected
Disconnected,
/// Sensor is in error state
Error,
/// Sensor is initializing
Initializing,
/// Sensor battery is low
LowBattery,
}
impl HardwareAdapter {
/// Create a new hardware adapter
pub fn new() -> Self {
Self {
sensors: Vec::new(),
initialized: false,
}
}
/// Initialize hardware communication
pub fn initialize(&mut self) -> Result<(), AdapterError> {
// In production, this would initialize actual hardware
// using wifi-densepose-hardware crate
self.initialized = true;
Ok(())
}
/// Discover available sensors
pub fn discover_sensors(&mut self) -> Result<Vec<SensorInfo>, AdapterError> {
if !self.initialized {
return Err(AdapterError::Hardware("Hardware not initialized".into()));
}
// In production, this would scan for WiFi devices
// For now, return empty list (would be populated by real hardware)
Ok(Vec::new())
}
/// Add a sensor
pub fn add_sensor(&mut self, sensor: SensorInfo) -> Result<(), AdapterError> {
if self.sensors.iter().any(|s| s.id == sensor.id) {
return Err(AdapterError::Hardware(format!(
"Sensor {} already registered",
sensor.id
)));
}
self.sensors.push(sensor);
Ok(())
}
/// Remove a sensor
pub fn remove_sensor(&mut self, sensor_id: &str) -> Result<(), AdapterError> {
let initial_len = self.sensors.len();
self.sensors.retain(|s| s.id != sensor_id);
if self.sensors.len() == initial_len {
return Err(AdapterError::Hardware(format!(
"Sensor {} not found",
sensor_id
)));
}
Ok(())
}
/// Get all sensors
pub fn sensors(&self) -> &[SensorInfo] {
&self.sensors
}
/// Get operational sensors
pub fn operational_sensors(&self) -> Vec<&SensorInfo> {
self.sensors
.iter()
.filter(|s| s.status == SensorStatus::Connected)
.collect()
}
/// Get sensor positions for localization
pub fn sensor_positions(&self) -> Vec<SensorPosition> {
self.sensors
.iter()
.filter(|s| s.status == SensorStatus::Connected)
.map(|s| s.position.clone())
.collect()
}
/// Read CSI data from sensors
pub fn read_csi(&self) -> Result<CsiReadings, AdapterError> {
if !self.initialized {
return Err(AdapterError::Hardware("Hardware not initialized".into()));
}
// In production, this would read actual CSI data
// For now, return empty readings
Ok(CsiReadings {
timestamp: chrono::Utc::now(),
readings: Vec::new(),
})
}
/// Read RSSI from all sensors
pub fn read_rssi(&self) -> Result<Vec<(String, f64)>, AdapterError> {
if !self.initialized {
return Err(AdapterError::Hardware("Hardware not initialized".into()));
}
// Return last known RSSI values
Ok(self
.sensors
.iter()
.filter_map(|s| s.last_rssi.map(|rssi| (s.id.clone(), rssi)))
.collect())
}
/// Update sensor position
pub fn update_sensor_position(
&mut self,
sensor_id: &str,
position: SensorPosition,
) -> Result<(), AdapterError> {
let sensor = self
.sensors
.iter_mut()
.find(|s| s.id == sensor_id)
.ok_or_else(|| AdapterError::Hardware(format!("Sensor {} not found", sensor_id)))?;
sensor.position = position;
Ok(())
}
/// Check hardware health
pub fn health_check(&self) -> HardwareHealth {
let total = self.sensors.len();
let connected = self
.sensors
.iter()
.filter(|s| s.status == SensorStatus::Connected)
.count();
let low_battery = self
.sensors
.iter()
.filter(|s| matches!(s.battery_level, Some(b) if b < 20))
.count();
let status = if connected == 0 && total > 0 {
HealthStatus::Critical
} else if connected < total / 2 {
HealthStatus::Degraded
} else if low_battery > 0 {
HealthStatus::Warning
} else {
HealthStatus::Healthy
};
HardwareHealth {
status,
total_sensors: total,
connected_sensors: connected,
low_battery_sensors: low_battery,
}
}
}
impl Default for HardwareAdapter {
fn default() -> Self {
Self::new()
}
}
/// CSI readings from sensors
#[derive(Debug, Clone)]
pub struct CsiReadings {
/// Timestamp of readings
pub timestamp: chrono::DateTime<chrono::Utc>,
/// Individual sensor readings
pub readings: Vec<SensorCsiReading>,
}
/// CSI reading from a single sensor
#[derive(Debug, Clone)]
pub struct SensorCsiReading {
/// Sensor ID
pub sensor_id: String,
/// CSI amplitudes (per subcarrier)
pub amplitudes: Vec<f64>,
/// CSI phases (per subcarrier)
pub phases: Vec<f64>,
/// RSSI value
pub rssi: f64,
/// Noise floor
pub noise_floor: f64,
}
/// Hardware health status
#[derive(Debug, Clone)]
pub struct HardwareHealth {
/// Overall status
pub status: HealthStatus,
/// Total number of sensors
pub total_sensors: usize,
/// Number of connected sensors
pub connected_sensors: usize,
/// Number of sensors with low battery
pub low_battery_sensors: usize,
}
/// Health status levels
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HealthStatus {
/// All systems operational
Healthy,
/// Minor issues, still functional
Warning,
/// Significant issues, reduced capability
Degraded,
/// System not functional
Critical,
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_sensor(id: &str) -> SensorInfo {
SensorInfo {
id: id.to_string(),
position: SensorPosition {
id: id.to_string(),
x: 0.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
},
status: SensorStatus::Connected,
last_rssi: Some(-45.0),
battery_level: Some(80),
}
}
#[test]
fn test_add_sensor() {
let mut adapter = HardwareAdapter::new();
adapter.initialize().unwrap();
let sensor = create_test_sensor("s1");
assert!(adapter.add_sensor(sensor).is_ok());
assert_eq!(adapter.sensors().len(), 1);
}
#[test]
fn test_duplicate_sensor_error() {
let mut adapter = HardwareAdapter::new();
adapter.initialize().unwrap();
let sensor1 = create_test_sensor("s1");
let sensor2 = create_test_sensor("s1");
adapter.add_sensor(sensor1).unwrap();
assert!(adapter.add_sensor(sensor2).is_err());
}
#[test]
fn test_health_check() {
let mut adapter = HardwareAdapter::new();
adapter.initialize().unwrap();
// No sensors - should be healthy (nothing to fail)
let health = adapter.health_check();
assert!(matches!(health.status, HealthStatus::Healthy));
// Add connected sensor
adapter.add_sensor(create_test_sensor("s1")).unwrap();
let health = adapter.health_check();
assert!(matches!(health.status, HealthStatus::Healthy));
}
#[test]
fn test_sensor_positions() {
let mut adapter = HardwareAdapter::new();
adapter.initialize().unwrap();
adapter.add_sensor(create_test_sensor("s1")).unwrap();
adapter.add_sensor(create_test_sensor("s2")).unwrap();
let positions = adapter.sensor_positions();
assert_eq!(positions.len(), 2);
}
}

View File

@@ -0,0 +1,49 @@
//! Integration layer (Anti-Corruption Layer) for upstream crates.
//!
//! This module provides adapters to translate between:
//! - wifi-densepose-signal types and wifi-Mat domain types
//! - wifi-densepose-nn inference results and detection results
//! - wifi-densepose-hardware interfaces and sensor abstractions
mod signal_adapter;
mod neural_adapter;
mod hardware_adapter;
pub use signal_adapter::SignalAdapter;
pub use neural_adapter::NeuralAdapter;
pub use hardware_adapter::HardwareAdapter;
/// Configuration for integration layer
#[derive(Debug, Clone, Default)]
pub struct IntegrationConfig {
/// Use GPU acceleration if available
pub use_gpu: bool,
/// Batch size for neural inference
pub batch_size: usize,
/// Enable signal preprocessing optimizations
pub optimize_signal: bool,
}
/// Error type for integration layer
#[derive(Debug, thiserror::Error)]
pub enum AdapterError {
/// Signal processing error
#[error("Signal adapter error: {0}")]
Signal(String),
/// Neural network error
#[error("Neural adapter error: {0}")]
Neural(String),
/// Hardware error
#[error("Hardware adapter error: {0}")]
Hardware(String),
/// Configuration error
#[error("Configuration error: {0}")]
Config(String),
/// Data format error
#[error("Data format error: {0}")]
DataFormat(String),
}

View File

@@ -0,0 +1,293 @@
//! Adapter for wifi-densepose-nn crate (neural network inference).
use super::AdapterError;
use crate::domain::{BreathingPattern, BreathingType, HeartbeatSignature, SignalStrength};
use super::signal_adapter::VitalFeatures;
/// Adapter for neural network-based vital signs detection
pub struct NeuralAdapter {
/// Whether to use GPU acceleration
use_gpu: bool,
/// Confidence threshold for valid detections
confidence_threshold: f32,
/// Model loaded status
models_loaded: bool,
}
impl NeuralAdapter {
/// Create a new neural adapter
pub fn new(use_gpu: bool) -> Self {
Self {
use_gpu,
confidence_threshold: 0.5,
models_loaded: false,
}
}
/// Create with default settings (CPU)
pub fn with_defaults() -> Self {
Self::new(false)
}
/// Load neural network models
pub fn load_models(&mut self, _model_path: &str) -> Result<(), AdapterError> {
// In production, this would load ONNX models using wifi-densepose-nn
// For now, mark as loaded for simulation
self.models_loaded = true;
Ok(())
}
/// Classify breathing pattern using neural network
pub fn classify_breathing(
&self,
features: &VitalFeatures,
) -> Result<Option<BreathingPattern>, AdapterError> {
if !self.models_loaded {
// Fall back to rule-based classification
return Ok(self.classify_breathing_rules(features));
}
// In production, this would run ONNX inference
// For now, use rule-based approach
Ok(self.classify_breathing_rules(features))
}
/// Classify heartbeat using neural network
pub fn classify_heartbeat(
&self,
features: &VitalFeatures,
) -> Result<Option<HeartbeatSignature>, AdapterError> {
if !self.models_loaded {
return Ok(self.classify_heartbeat_rules(features));
}
// In production, run ONNX inference
Ok(self.classify_heartbeat_rules(features))
}
/// Combined vital signs classification
pub fn classify_vitals(
&self,
features: &VitalFeatures,
) -> Result<VitalsClassification, AdapterError> {
let breathing = self.classify_breathing(features)?;
let heartbeat = self.classify_heartbeat(features)?;
// Calculate overall confidence
let confidence = self.calculate_confidence(
&breathing,
&heartbeat,
features.signal_quality,
);
Ok(VitalsClassification {
breathing,
heartbeat,
confidence,
signal_quality: features.signal_quality,
})
}
/// Rule-based breathing classification (fallback)
fn classify_breathing_rules(&self, features: &VitalFeatures) -> Option<BreathingPattern> {
if features.breathing_features.len() < 3 {
return None;
}
let peak_freq = features.breathing_features[0];
let power_ratio = features.breathing_features.get(1).copied().unwrap_or(0.0);
let band_ratio = features.breathing_features.get(2).copied().unwrap_or(0.0);
// Check if there's significant energy in breathing band
if power_ratio < 0.05 || band_ratio < 0.1 {
return None;
}
let rate_bpm = (peak_freq * 60.0) as f32;
// Validate rate
if rate_bpm < 4.0 || rate_bpm > 60.0 {
return None;
}
let pattern_type = if rate_bpm < 6.0 {
BreathingType::Agonal
} else if rate_bpm < 10.0 {
BreathingType::Shallow
} else if rate_bpm > 30.0 {
BreathingType::Labored
} else if band_ratio < 0.3 {
BreathingType::Irregular
} else {
BreathingType::Normal
};
Some(BreathingPattern {
rate_bpm,
amplitude: power_ratio as f32,
regularity: band_ratio as f32,
pattern_type,
})
}
/// Rule-based heartbeat classification (fallback)
fn classify_heartbeat_rules(&self, features: &VitalFeatures) -> Option<HeartbeatSignature> {
if features.heartbeat_features.len() < 3 {
return None;
}
let peak_freq = features.heartbeat_features[0];
let power_ratio = features.heartbeat_features.get(1).copied().unwrap_or(0.0);
let band_ratio = features.heartbeat_features.get(2).copied().unwrap_or(0.0);
// Heartbeat detection requires stronger signal
if power_ratio < 0.03 || band_ratio < 0.08 {
return None;
}
let rate_bpm = (peak_freq * 60.0) as f32;
// Validate rate (30-200 BPM)
if rate_bpm < 30.0 || rate_bpm > 200.0 {
return None;
}
let strength = if power_ratio > 0.15 {
SignalStrength::Strong
} else if power_ratio > 0.08 {
SignalStrength::Moderate
} else if power_ratio > 0.04 {
SignalStrength::Weak
} else {
SignalStrength::VeryWeak
};
Some(HeartbeatSignature {
rate_bpm,
variability: band_ratio as f32 * 0.5,
strength,
})
}
/// Calculate overall confidence from detections
fn calculate_confidence(
&self,
breathing: &Option<BreathingPattern>,
heartbeat: &Option<HeartbeatSignature>,
signal_quality: f64,
) -> f32 {
let mut confidence = signal_quality as f32 * 0.3;
if let Some(b) = breathing {
confidence += 0.4 * b.confidence() as f32;
}
if let Some(h) = heartbeat {
confidence += 0.3 * h.confidence() as f32;
}
confidence.clamp(0.0, 1.0)
}
}
impl Default for NeuralAdapter {
fn default() -> Self {
Self::with_defaults()
}
}
/// Result of neural network vital signs classification
#[derive(Debug, Clone)]
pub struct VitalsClassification {
/// Detected breathing pattern
pub breathing: Option<BreathingPattern>,
/// Detected heartbeat
pub heartbeat: Option<HeartbeatSignature>,
/// Overall classification confidence
pub confidence: f32,
/// Signal quality indicator
pub signal_quality: f64,
}
impl VitalsClassification {
/// Check if any vital signs were detected
pub fn has_vitals(&self) -> bool {
self.breathing.is_some() || self.heartbeat.is_some()
}
/// Check if detection confidence is sufficient
pub fn is_confident(&self, threshold: f32) -> bool {
self.confidence >= threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_good_features() -> VitalFeatures {
VitalFeatures {
breathing_features: vec![0.25, 0.2, 0.4], // 15 BPM, good signal
heartbeat_features: vec![1.2, 0.1, 0.15], // 72 BPM, moderate signal
movement_features: vec![0.1, 0.05, 0.01],
signal_quality: 0.8,
}
}
fn create_weak_features() -> VitalFeatures {
VitalFeatures {
breathing_features: vec![0.25, 0.02, 0.05], // Weak
heartbeat_features: vec![1.2, 0.01, 0.02], // Very weak
movement_features: vec![0.01, 0.005, 0.001],
signal_quality: 0.3,
}
}
#[test]
fn test_classify_breathing() {
let adapter = NeuralAdapter::with_defaults();
let features = create_good_features();
let result = adapter.classify_breathing(&features);
assert!(result.is_ok());
assert!(result.unwrap().is_some());
}
#[test]
fn test_weak_signal_no_detection() {
let adapter = NeuralAdapter::with_defaults();
let features = create_weak_features();
let result = adapter.classify_breathing(&features);
assert!(result.is_ok());
// Weak signals may or may not be detected depending on thresholds
}
#[test]
fn test_classify_vitals() {
let adapter = NeuralAdapter::with_defaults();
let features = create_good_features();
let result = adapter.classify_vitals(&features);
assert!(result.is_ok());
let classification = result.unwrap();
assert!(classification.has_vitals());
assert!(classification.confidence > 0.3);
}
#[test]
fn test_confidence_calculation() {
let adapter = NeuralAdapter::with_defaults();
let breathing = Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
});
let confidence = adapter.calculate_confidence(&breathing, &None, 0.8);
assert!(confidence > 0.5);
}
}

View File

@@ -0,0 +1,334 @@
//! Adapter for wifi-densepose-signal crate.
use super::AdapterError;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore};
use crate::detection::CsiDataBuffer;
/// Features extracted from signal for vital signs detection
#[derive(Debug, Clone, Default)]
pub struct VitalFeatures {
/// Breathing frequency features
pub breathing_features: Vec<f64>,
/// Heartbeat frequency features
pub heartbeat_features: Vec<f64>,
/// Movement energy features
pub movement_features: Vec<f64>,
/// Overall signal quality
pub signal_quality: f64,
}
/// Adapter for wifi-densepose-signal crate
pub struct SignalAdapter {
/// Window size for processing
window_size: usize,
/// Overlap between windows
overlap: f64,
/// Sample rate
sample_rate: f64,
}
impl SignalAdapter {
/// Create a new signal adapter
pub fn new(window_size: usize, overlap: f64, sample_rate: f64) -> Self {
Self {
window_size,
overlap,
sample_rate,
}
}
/// Create with default settings
pub fn with_defaults() -> Self {
Self::new(512, 0.5, 1000.0)
}
/// Extract vital sign features from CSI data
pub fn extract_vital_features(
&self,
csi_data: &CsiDataBuffer,
) -> Result<VitalFeatures, AdapterError> {
if csi_data.amplitudes.len() < self.window_size {
return Err(AdapterError::Signal(
"Insufficient data for feature extraction".into()
));
}
// Extract breathing-range features (0.1-0.5 Hz)
let breathing_features = self.extract_frequency_band(
&csi_data.amplitudes,
0.1,
0.5,
)?;
// Extract heartbeat-range features (0.8-2.0 Hz)
let heartbeat_features = self.extract_frequency_band(
&csi_data.phases,
0.8,
2.0,
)?;
// Extract movement features
let movement_features = self.extract_movement_features(&csi_data.amplitudes)?;
// Calculate signal quality
let signal_quality = self.calculate_signal_quality(&csi_data.amplitudes);
Ok(VitalFeatures {
breathing_features,
heartbeat_features,
movement_features,
signal_quality,
})
}
/// Convert upstream CsiFeatures to breathing pattern
pub fn to_breathing_pattern(
&self,
features: &VitalFeatures,
) -> Option<BreathingPattern> {
if features.breathing_features.len() < 3 {
return None;
}
// Extract key values from features
let rate_estimate = features.breathing_features[0];
let amplitude = features.breathing_features.get(1).copied().unwrap_or(0.5);
let regularity = features.breathing_features.get(2).copied().unwrap_or(0.5);
// Convert rate from Hz to BPM
let rate_bpm = (rate_estimate * 60.0) as f32;
// Validate rate
if rate_bpm < 4.0 || rate_bpm > 60.0 {
return None;
}
// Determine breathing type
let pattern_type = self.classify_breathing_type(rate_bpm, regularity);
Some(BreathingPattern {
rate_bpm,
amplitude: amplitude as f32,
regularity: regularity as f32,
pattern_type,
})
}
/// Extract features from a frequency band
fn extract_frequency_band(
&self,
signal: &[f64],
low_freq: f64,
high_freq: f64,
) -> Result<Vec<f64>, AdapterError> {
use rustfft::{FftPlanner, num_complex::Complex};
let n = signal.len().min(self.window_size);
if n < 32 {
return Err(AdapterError::Signal("Signal too short".into()));
}
let fft_size = n.next_power_of_two();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(fft_size);
// Prepare buffer with windowing
let mut buffer: Vec<Complex<f64>> = signal.iter()
.take(n)
.enumerate()
.map(|(i, &x)| {
let window = 0.5 * (1.0 - (2.0 * std::f64::consts::PI * i as f64 / n as f64).cos());
Complex::new(x * window, 0.0)
})
.collect();
buffer.resize(fft_size, Complex::new(0.0, 0.0));
fft.process(&mut buffer);
// Extract magnitude spectrum in frequency range
let freq_resolution = self.sample_rate / fft_size as f64;
let low_bin = (low_freq / freq_resolution).ceil() as usize;
let high_bin = (high_freq / freq_resolution).floor() as usize;
let mut features = Vec::new();
if high_bin > low_bin && high_bin < buffer.len() / 2 {
// Find peak frequency
let mut max_mag = 0.0;
let mut peak_bin = low_bin;
for i in low_bin..=high_bin {
let mag = buffer[i].norm();
if mag > max_mag {
max_mag = mag;
peak_bin = i;
}
}
// Peak frequency
features.push(peak_bin as f64 * freq_resolution);
// Peak magnitude (normalized)
let total_power: f64 = buffer[1..buffer.len()/2]
.iter()
.map(|c| c.norm_sqr())
.sum();
features.push(if total_power > 0.0 { max_mag * max_mag / total_power } else { 0.0 });
// Band power ratio
let band_power: f64 = buffer[low_bin..=high_bin]
.iter()
.map(|c| c.norm_sqr())
.sum();
features.push(if total_power > 0.0 { band_power / total_power } else { 0.0 });
}
Ok(features)
}
/// Extract movement-related features
fn extract_movement_features(&self, signal: &[f64]) -> Result<Vec<f64>, AdapterError> {
if signal.len() < 10 {
return Err(AdapterError::Signal("Signal too short".into()));
}
// Calculate variance
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let variance = signal.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / signal.len() as f64;
// Calculate max absolute change
let max_change = signal.windows(2)
.map(|w| (w[1] - w[0]).abs())
.fold(0.0, f64::max);
// Calculate zero crossing rate
let centered: Vec<f64> = signal.iter().map(|x| x - mean).collect();
let zero_crossings: usize = centered.windows(2)
.filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
.count();
let zcr = zero_crossings as f64 / signal.len() as f64;
Ok(vec![variance, max_change, zcr])
}
/// Calculate overall signal quality
fn calculate_signal_quality(&self, signal: &[f64]) -> f64 {
if signal.len() < 10 {
return 0.0;
}
// SNR estimate based on signal statistics
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let variance = signal.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / signal.len() as f64;
// Higher variance relative to mean suggests better signal
let snr_estimate = if mean.abs() > 1e-10 {
(variance.sqrt() / mean.abs()).min(10.0) / 10.0
} else {
0.5
};
snr_estimate.clamp(0.0, 1.0)
}
/// Classify breathing type from rate and regularity
fn classify_breathing_type(&self, rate_bpm: f32, regularity: f64) -> BreathingType {
if rate_bpm < 6.0 {
if regularity < 0.3 {
BreathingType::Agonal
} else {
BreathingType::Shallow
}
} else if rate_bpm < 10.0 {
BreathingType::Shallow
} else if rate_bpm > 30.0 {
BreathingType::Labored
} else if regularity < 0.4 {
BreathingType::Irregular
} else {
BreathingType::Normal
}
}
}
impl Default for SignalAdapter {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_buffer() -> CsiDataBuffer {
let mut buffer = CsiDataBuffer::new(100.0);
// 10 seconds of data with breathing pattern
let amplitudes: Vec<f64> = (0..1000)
.map(|i| {
let t = i as f64 / 100.0;
(2.0 * std::f64::consts::PI * 0.25 * t).sin() // 15 BPM
})
.collect();
let phases: Vec<f64> = (0..1000)
.map(|i| {
let t = i as f64 / 100.0;
(2.0 * std::f64::consts::PI * 0.25 * t).sin() * 0.5
})
.collect();
buffer.add_samples(&amplitudes, &phases);
buffer
}
#[test]
fn test_extract_vital_features() {
let adapter = SignalAdapter::with_defaults();
let buffer = create_test_buffer();
let result = adapter.extract_vital_features(&buffer);
assert!(result.is_ok());
let features = result.unwrap();
assert!(!features.breathing_features.is_empty());
}
#[test]
fn test_to_breathing_pattern() {
let adapter = SignalAdapter::with_defaults();
let features = VitalFeatures {
breathing_features: vec![0.25, 0.8, 0.9], // 15 BPM
heartbeat_features: vec![],
movement_features: vec![],
signal_quality: 0.8,
};
let pattern = adapter.to_breathing_pattern(&features);
assert!(pattern.is_some());
let p = pattern.unwrap();
assert!(p.rate_bpm > 10.0 && p.rate_bpm < 20.0);
}
#[test]
fn test_signal_quality() {
let adapter = SignalAdapter::with_defaults();
// Good signal
let good_signal: Vec<f64> = (0..100)
.map(|i| (i as f64 * 0.1).sin())
.collect();
let good_quality = adapter.calculate_signal_quality(&good_signal);
// Poor signal (constant)
let poor_signal = vec![0.5; 100];
let poor_quality = adapter.calculate_signal_quality(&poor_signal);
assert!(good_quality > poor_quality);
}
}

View File

@@ -0,0 +1,454 @@
//! # WiFi-DensePose MAT (Mass Casualty Assessment Tool)
//!
//! A modular extension for WiFi-based disaster survivor detection and localization.
//!
//! This crate provides capabilities for detecting human survivors trapped in rubble,
//! debris, or collapsed structures using WiFi Channel State Information (CSI) analysis.
//!
//! ## Features
//!
//! - **Vital Signs Detection**: Breathing patterns, heartbeat signatures, and movement
//! - **Survivor Localization**: 3D position estimation through debris
//! - **Triage Classification**: Automatic START protocol-compatible triage
//! - **Real-time Alerting**: Priority-based alert generation and dispatch
//!
//! ## Use Cases
//!
//! - Earthquake search and rescue
//! - Building collapse response
//! - Avalanche victim location
//! - Flood rescue operations
//! - Mine collapse detection
//!
//! ## Architecture
//!
//! The crate follows Domain-Driven Design (DDD) principles with clear bounded contexts:
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────┐
//! │ wifi-densepose-mat │
//! ├─────────────────────────────────────────────────────────┤
//! │ ┌───────────┐ ┌─────────────┐ ┌─────────────────┐ │
//! │ │ Detection │ │Localization │ │ Alerting │ │
//! │ │ Context │ │ Context │ │ Context │ │
//! │ └─────┬─────┘ └──────┬──────┘ └────────┬────────┘ │
//! │ └───────────────┼──────────────────┘ │
//! │ │ │
//! │ ┌─────────▼─────────┐ │
//! │ │ Integration │ │
//! │ │ Layer │ │
//! │ └───────────────────┘ │
//! └─────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Example
//!
//! ```rust,no_run
//! use wifi_densepose_mat::{
//! DisasterResponse, DisasterConfig, DisasterType,
//! ScanZone, ZoneBounds,
//! };
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! // Initialize disaster response system
//! let config = DisasterConfig::builder()
//! .disaster_type(DisasterType::Earthquake)
//! .sensitivity(0.8)
//! .build();
//!
//! let mut response = DisasterResponse::new(config);
//!
//! // Define scan zone
//! let zone = ScanZone::new(
//! "Building A - North Wing",
//! ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
//! );
//! response.add_zone(zone)?;
//!
//! // Start scanning
//! response.start_scanning().await?;
//!
//! Ok(())
//! }
//! ```
#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
pub mod alerting;
pub mod detection;
pub mod domain;
pub mod integration;
pub mod localization;
// Re-export main types
pub use domain::{
survivor::{Survivor, SurvivorId, SurvivorMetadata, SurvivorStatus},
disaster_event::{DisasterEvent, DisasterEventId, DisasterType, EventStatus},
scan_zone::{ScanZone, ScanZoneId, ZoneBounds, ZoneStatus, ScanParameters},
alert::{Alert, AlertId, AlertPayload, Priority},
vital_signs::{
VitalSignsReading, BreathingPattern, BreathingType,
HeartbeatSignature, MovementProfile, MovementType,
},
triage::{TriageStatus, TriageCalculator},
coordinates::{Coordinates3D, LocationUncertainty, DepthEstimate},
events::{DetectionEvent, AlertEvent, DomainEvent},
};
pub use detection::{
BreathingDetector, BreathingDetectorConfig,
HeartbeatDetector, HeartbeatDetectorConfig,
MovementClassifier, MovementClassifierConfig,
VitalSignsDetector, DetectionPipeline, DetectionConfig,
};
pub use localization::{
Triangulator, TriangulationConfig,
DepthEstimator, DepthEstimatorConfig,
PositionFuser, LocalizationService,
};
pub use alerting::{
AlertGenerator, AlertDispatcher, AlertConfig,
TriageService, PriorityCalculator,
};
pub use integration::{
SignalAdapter, NeuralAdapter, HardwareAdapter,
AdapterError, IntegrationConfig,
};
/// Library version
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Common result type for MAT operations
pub type Result<T> = std::result::Result<T, MatError>;
/// Unified error type for MAT operations
#[derive(Debug, thiserror::Error)]
pub enum MatError {
/// Detection error
#[error("Detection error: {0}")]
Detection(String),
/// Localization error
#[error("Localization error: {0}")]
Localization(String),
/// Alerting error
#[error("Alerting error: {0}")]
Alerting(String),
/// Integration error
#[error("Integration error: {0}")]
Integration(#[from] AdapterError),
/// Configuration error
#[error("Configuration error: {0}")]
Config(String),
/// Domain invariant violation
#[error("Domain error: {0}")]
Domain(String),
/// Repository error
#[error("Repository error: {0}")]
Repository(String),
/// Signal processing error
#[error("Signal processing error: {0}")]
Signal(#[from] wifi_densepose_signal::SignalError),
/// I/O error
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
/// Configuration for the disaster response system
#[derive(Debug, Clone)]
pub struct DisasterConfig {
/// Type of disaster event
pub disaster_type: DisasterType,
/// Detection sensitivity (0.0-1.0)
pub sensitivity: f64,
/// Minimum confidence threshold for survivor detection
pub confidence_threshold: f64,
/// Maximum depth to scan (meters)
pub max_depth: f64,
/// Scan interval in milliseconds
pub scan_interval_ms: u64,
/// Enable continuous monitoring
pub continuous_monitoring: bool,
/// Alert configuration
pub alert_config: AlertConfig,
}
impl Default for DisasterConfig {
fn default() -> Self {
Self {
disaster_type: DisasterType::Unknown,
sensitivity: 0.8,
confidence_threshold: 0.5,
max_depth: 5.0,
scan_interval_ms: 500,
continuous_monitoring: true,
alert_config: AlertConfig::default(),
}
}
}
impl DisasterConfig {
/// Create a new configuration builder
pub fn builder() -> DisasterConfigBuilder {
DisasterConfigBuilder::default()
}
}
/// Builder for DisasterConfig
#[derive(Debug, Default)]
pub struct DisasterConfigBuilder {
config: DisasterConfig,
}
impl DisasterConfigBuilder {
/// Set disaster type
pub fn disaster_type(mut self, disaster_type: DisasterType) -> Self {
self.config.disaster_type = disaster_type;
self
}
/// Set detection sensitivity
pub fn sensitivity(mut self, sensitivity: f64) -> Self {
self.config.sensitivity = sensitivity.clamp(0.0, 1.0);
self
}
/// Set confidence threshold
pub fn confidence_threshold(mut self, threshold: f64) -> Self {
self.config.confidence_threshold = threshold.clamp(0.0, 1.0);
self
}
/// Set maximum scan depth
pub fn max_depth(mut self, depth: f64) -> Self {
self.config.max_depth = depth.max(0.0);
self
}
/// Set scan interval
pub fn scan_interval_ms(mut self, interval: u64) -> Self {
self.config.scan_interval_ms = interval.max(100);
self
}
/// Enable/disable continuous monitoring
pub fn continuous_monitoring(mut self, enabled: bool) -> Self {
self.config.continuous_monitoring = enabled;
self
}
/// Build the configuration
pub fn build(self) -> DisasterConfig {
self.config
}
}
/// Main disaster response coordinator
pub struct DisasterResponse {
config: DisasterConfig,
event: Option<DisasterEvent>,
detection_pipeline: DetectionPipeline,
localization_service: LocalizationService,
alert_dispatcher: AlertDispatcher,
running: std::sync::atomic::AtomicBool,
}
impl DisasterResponse {
/// Create a new disaster response system
pub fn new(config: DisasterConfig) -> Self {
let detection_config = DetectionConfig::from_disaster_config(&config);
let detection_pipeline = DetectionPipeline::new(detection_config);
let localization_service = LocalizationService::new();
let alert_dispatcher = AlertDispatcher::new(config.alert_config.clone());
Self {
config,
event: None,
detection_pipeline,
localization_service,
alert_dispatcher,
running: std::sync::atomic::AtomicBool::new(false),
}
}
/// Initialize a new disaster event
pub fn initialize_event(
&mut self,
location: geo::Point<f64>,
description: &str,
) -> Result<&DisasterEvent> {
let event = DisasterEvent::new(
self.config.disaster_type.clone(),
location,
description,
);
self.event = Some(event);
self.event.as_ref().ok_or_else(|| MatError::Domain("Failed to create event".into()))
}
/// Add a scan zone to the current event
pub fn add_zone(&mut self, zone: ScanZone) -> Result<()> {
let event = self.event.as_mut()
.ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
event.add_zone(zone);
Ok(())
}
/// Start the scanning process
pub async fn start_scanning(&mut self) -> Result<()> {
use std::sync::atomic::Ordering;
self.running.store(true, Ordering::SeqCst);
while self.running.load(Ordering::SeqCst) {
self.scan_cycle().await?;
if !self.config.continuous_monitoring {
break;
}
tokio::time::sleep(
std::time::Duration::from_millis(self.config.scan_interval_ms)
).await;
}
Ok(())
}
/// Stop the scanning process
pub fn stop_scanning(&self) {
use std::sync::atomic::Ordering;
self.running.store(false, Ordering::SeqCst);
}
/// Execute a single scan cycle
async fn scan_cycle(&mut self) -> Result<()> {
let event = self.event.as_mut()
.ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
for zone in event.zones_mut() {
if zone.status() != &ZoneStatus::Active {
continue;
}
// This would integrate with actual hardware in production
// For now, we process any available CSI data
let detection_result = self.detection_pipeline.process_zone(zone).await?;
if let Some(vital_signs) = detection_result {
// Attempt localization
let location = self.localization_service
.estimate_position(&vital_signs, zone)
.ok();
// Create or update survivor
let survivor = event.record_detection(
zone.id().clone(),
vital_signs,
location,
)?;
// Generate alert if needed
if survivor.should_alert() {
let alert = self.alert_dispatcher.generate_alert(&survivor)?;
self.alert_dispatcher.dispatch(alert).await?;
}
}
}
Ok(())
}
/// Get the current disaster event
pub fn event(&self) -> Option<&DisasterEvent> {
self.event.as_ref()
}
/// Get all detected survivors
pub fn survivors(&self) -> Vec<&Survivor> {
self.event.as_ref()
.map(|e| e.survivors())
.unwrap_or_default()
}
/// Get survivors by triage status
pub fn survivors_by_triage(&self, status: TriageStatus) -> Vec<&Survivor> {
self.survivors()
.into_iter()
.filter(|s| s.triage_status() == &status)
.collect()
}
}
/// Prelude module for convenient imports
pub mod prelude {
pub use crate::{
DisasterConfig, DisasterConfigBuilder, DisasterResponse,
MatError, Result,
// Domain types
Survivor, SurvivorId, DisasterEvent, DisasterType,
ScanZone, ZoneBounds, TriageStatus,
VitalSignsReading, BreathingPattern, HeartbeatSignature,
Coordinates3D, Alert, Priority,
// Detection
DetectionPipeline, VitalSignsDetector,
// Localization
LocalizationService,
// Alerting
AlertDispatcher,
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = DisasterConfig::builder()
.disaster_type(DisasterType::Earthquake)
.sensitivity(0.9)
.confidence_threshold(0.6)
.max_depth(10.0)
.build();
assert!(matches!(config.disaster_type, DisasterType::Earthquake));
assert!((config.sensitivity - 0.9).abs() < f64::EPSILON);
assert!((config.confidence_threshold - 0.6).abs() < f64::EPSILON);
assert!((config.max_depth - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_sensitivity_clamping() {
let config = DisasterConfig::builder()
.sensitivity(1.5)
.build();
assert!((config.sensitivity - 1.0).abs() < f64::EPSILON);
let config = DisasterConfig::builder()
.sensitivity(-0.5)
.build();
assert!(config.sensitivity.abs() < f64::EPSILON);
}
#[test]
fn test_version() {
assert!(!VERSION.is_empty());
}
}

View File

@@ -0,0 +1,297 @@
//! Depth estimation through debris layers.
use crate::domain::{DebrisProfile, DepthEstimate, DebrisMaterial, MoistureLevel};
/// Configuration for depth estimation
#[derive(Debug, Clone)]
pub struct DepthEstimatorConfig {
/// Maximum depth to estimate (meters)
pub max_depth: f64,
/// Minimum signal attenuation to consider (dB)
pub min_attenuation: f64,
/// WiFi frequency in GHz
pub frequency_ghz: f64,
/// Free space path loss at 1 meter (dB)
pub free_space_loss_1m: f64,
}
impl Default for DepthEstimatorConfig {
fn default() -> Self {
Self {
max_depth: 10.0,
min_attenuation: 3.0,
frequency_ghz: 5.8, // 5.8 GHz WiFi
free_space_loss_1m: 47.0, // FSPL at 1m for 5.8 GHz
}
}
}
/// Estimator for survivor depth through debris
pub struct DepthEstimator {
config: DepthEstimatorConfig,
}
impl DepthEstimator {
/// Create a new depth estimator
pub fn new(config: DepthEstimatorConfig) -> Self {
Self { config }
}
/// Create with default configuration
pub fn with_defaults() -> Self {
Self::new(DepthEstimatorConfig::default())
}
/// Estimate depth from signal attenuation
pub fn estimate_depth(
&self,
signal_attenuation: f64, // Total attenuation in dB
distance_2d: f64, // Horizontal distance in meters
debris_profile: &DebrisProfile,
) -> Option<DepthEstimate> {
if signal_attenuation < self.config.min_attenuation {
// Very little attenuation - probably not buried
return Some(DepthEstimate {
depth: 0.0,
uncertainty: 0.5,
debris_profile: debris_profile.clone(),
confidence: 0.9,
});
}
// Calculate free space path loss for horizontal distance
let fspl = self.free_space_path_loss(distance_2d);
// Debris attenuation = total - free space loss
let debris_attenuation = (signal_attenuation - fspl).max(0.0);
// Get attenuation coefficient for debris type
let attenuation_per_meter = debris_profile.attenuation_factor();
if attenuation_per_meter < 0.1 {
return None;
}
// Estimate depth
let depth = debris_attenuation / attenuation_per_meter;
// Clamp to maximum
if depth > self.config.max_depth {
return None;
}
// Calculate uncertainty (increases with depth and material variability)
let base_uncertainty = 0.3;
let depth_uncertainty = depth * 0.15;
let material_uncertainty = self.material_uncertainty(debris_profile);
let uncertainty = base_uncertainty + depth_uncertainty + material_uncertainty;
// Calculate confidence (decreases with depth)
let confidence = (1.0 - depth / self.config.max_depth).max(0.3);
Some(DepthEstimate {
depth,
uncertainty,
debris_profile: debris_profile.clone(),
confidence,
})
}
/// Estimate debris profile from signal characteristics
pub fn estimate_debris_profile(
&self,
signal_variance: f64,
signal_multipath: f64,
moisture_indicator: f64,
) -> DebrisProfile {
// Estimate material based on signal characteristics
let primary_material = if signal_variance > 0.5 {
// High variance suggests heterogeneous material
DebrisMaterial::Mixed
} else if signal_multipath > 0.7 {
// High multipath suggests reflective surfaces
DebrisMaterial::HeavyConcrete
} else if signal_multipath < 0.3 {
// Low multipath suggests absorptive material
DebrisMaterial::Soil
} else {
DebrisMaterial::LightConcrete
};
// Estimate void fraction from multipath
let void_fraction = signal_multipath.clamp(0.1, 0.5);
// Estimate moisture from signal characteristics
let moisture_content = if moisture_indicator > 0.7 {
MoistureLevel::Wet
} else if moisture_indicator > 0.4 {
MoistureLevel::Damp
} else {
MoistureLevel::Dry
};
DebrisProfile {
primary_material,
void_fraction,
moisture_content,
metal_content: crate::domain::MetalContent::Low,
}
}
/// Calculate free space path loss
fn free_space_path_loss(&self, distance: f64) -> f64 {
// FSPL = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c)
// Simplified: FSPL(d) = FSPL(1m) + 20*log10(d)
if distance <= 0.0 {
return 0.0;
}
self.config.free_space_loss_1m + 20.0 * distance.log10()
}
/// Calculate uncertainty based on material properties
fn material_uncertainty(&self, profile: &DebrisProfile) -> f64 {
// Mixed materials have higher uncertainty
let material_factor = match profile.primary_material {
DebrisMaterial::Mixed => 0.4,
DebrisMaterial::HeavyConcrete => 0.2,
DebrisMaterial::LightConcrete => 0.2,
DebrisMaterial::Soil => 0.3,
DebrisMaterial::Wood => 0.15,
DebrisMaterial::Snow => 0.1,
DebrisMaterial::Metal => 0.5, // Very unpredictable
};
// Moisture adds uncertainty
let moisture_factor = match profile.moisture_content {
MoistureLevel::Dry => 0.0,
MoistureLevel::Damp => 0.1,
MoistureLevel::Wet => 0.2,
MoistureLevel::Saturated => 0.3,
};
material_factor + moisture_factor
}
/// Estimate depth from multiple signal paths
pub fn estimate_from_multipath(
&self,
direct_path_attenuation: f64,
reflected_paths: &[(f64, f64)], // (attenuation, delay)
debris_profile: &DebrisProfile,
) -> Option<DepthEstimate> {
// Use path differences to estimate depth
if reflected_paths.is_empty() {
return self.estimate_depth(direct_path_attenuation, 0.0, debris_profile);
}
// Average extra path length from reflections
const SPEED_OF_LIGHT: f64 = 299_792_458.0;
let avg_extra_path: f64 = reflected_paths
.iter()
.map(|(_, delay)| delay * SPEED_OF_LIGHT / 2.0) // Round trip
.sum::<f64>() / reflected_paths.len() as f64;
// Extra path length is approximately related to depth
// (reflections bounce off debris layers)
let estimated_depth = avg_extra_path / 4.0; // Empirical factor
let attenuation_per_meter = debris_profile.attenuation_factor();
let attenuation_based_depth = direct_path_attenuation / attenuation_per_meter;
// Combine estimates
let depth = (estimated_depth + attenuation_based_depth) / 2.0;
if depth > self.config.max_depth {
return None;
}
let uncertainty = 0.5 + depth * 0.2;
let confidence = (1.0 - depth / self.config.max_depth).max(0.3);
Some(DepthEstimate {
depth,
uncertainty,
debris_profile: debris_profile.clone(),
confidence,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_debris() -> DebrisProfile {
DebrisProfile {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.25,
moisture_content: MoistureLevel::Dry,
metal_content: crate::domain::MetalContent::Low,
}
}
#[test]
fn test_low_attenuation_surface() {
let estimator = DepthEstimator::with_defaults();
let result = estimator.estimate_depth(1.0, 5.0, &default_debris());
assert!(result.is_some());
let estimate = result.unwrap();
assert!(estimate.depth < 0.1);
assert!(estimate.confidence > 0.8);
}
#[test]
fn test_depth_increases_with_attenuation() {
let estimator = DepthEstimator::with_defaults();
let debris = default_debris();
let low = estimator.estimate_depth(10.0, 0.0, &debris);
let high = estimator.estimate_depth(30.0, 0.0, &debris);
assert!(low.is_some() && high.is_some());
assert!(high.unwrap().depth > low.unwrap().depth);
}
#[test]
fn test_confidence_decreases_with_depth() {
let estimator = DepthEstimator::with_defaults();
let debris = default_debris();
let shallow = estimator.estimate_depth(5.0, 0.0, &debris);
let deep = estimator.estimate_depth(40.0, 0.0, &debris);
if let (Some(s), Some(d)) = (shallow, deep) {
assert!(s.confidence > d.confidence);
}
}
#[test]
fn test_debris_profile_estimation() {
let estimator = DepthEstimator::with_defaults();
// High variance = mixed materials
let profile = estimator.estimate_debris_profile(0.7, 0.5, 0.3);
assert!(matches!(profile.primary_material, DebrisMaterial::Mixed));
// High multipath = concrete
let profile2 = estimator.estimate_debris_profile(0.2, 0.8, 0.3);
assert!(matches!(profile2.primary_material, DebrisMaterial::HeavyConcrete));
}
#[test]
fn test_free_space_path_loss() {
let estimator = DepthEstimator::with_defaults();
// FSPL increases with distance
let fspl_1m = estimator.free_space_path_loss(1.0);
let fspl_10m = estimator.free_space_path_loss(10.0);
assert!(fspl_10m > fspl_1m);
// Should be about 20 dB difference (20*log10(10))
assert!((fspl_10m - fspl_1m - 20.0).abs() < 1.0);
}
}

View File

@@ -0,0 +1,393 @@
//! Position fusion combining multiple localization techniques.
use crate::domain::{
Coordinates3D, LocationUncertainty, ScanZone, VitalSignsReading,
DepthEstimate, DebrisProfile,
};
use super::{Triangulator, TriangulationConfig, DepthEstimator, DepthEstimatorConfig};
/// Service for survivor localization
pub struct LocalizationService {
triangulator: Triangulator,
depth_estimator: DepthEstimator,
position_fuser: PositionFuser,
}
impl LocalizationService {
/// Create a new localization service
pub fn new() -> Self {
Self {
triangulator: Triangulator::with_defaults(),
depth_estimator: DepthEstimator::with_defaults(),
position_fuser: PositionFuser::new(),
}
}
/// Create with custom configurations
pub fn with_config(
triangulation_config: TriangulationConfig,
depth_config: DepthEstimatorConfig,
) -> Self {
Self {
triangulator: Triangulator::new(triangulation_config),
depth_estimator: DepthEstimator::new(depth_config),
position_fuser: PositionFuser::new(),
}
}
/// Estimate survivor position
pub fn estimate_position(
&self,
vitals: &VitalSignsReading,
zone: &ScanZone,
) -> Option<Coordinates3D> {
// Get sensor positions
let sensors = zone.sensor_positions();
if sensors.len() < 3 {
return None;
}
// Estimate 2D position from triangulation
// In real implementation, RSSI values would come from actual measurements
let rssi_values = self.simulate_rssi_measurements(sensors, vitals);
let position_2d = self.triangulator.estimate_position(sensors, &rssi_values)?;
// Estimate depth
let debris_profile = self.estimate_debris_profile(zone);
let signal_attenuation = self.calculate_signal_attenuation(&rssi_values);
let depth_estimate = self.depth_estimator.estimate_depth(
signal_attenuation,
0.0,
&debris_profile,
)?;
// Combine into 3D position
let position_3d = Coordinates3D::new(
position_2d.x,
position_2d.y,
-depth_estimate.depth, // Negative = below surface
self.combine_uncertainties(&position_2d.uncertainty, &depth_estimate),
);
Some(position_3d)
}
/// Simulate RSSI measurements (placeholder for real sensor data)
fn simulate_rssi_measurements(
&self,
sensors: &[crate::domain::SensorPosition],
_vitals: &VitalSignsReading,
) -> Vec<(String, f64)> {
// In production, this would read actual sensor values
// For now, return placeholder values
sensors.iter()
.map(|s| (s.id.clone(), -50.0 + rand_range(-10.0, 10.0)))
.collect()
}
/// Estimate debris profile for the zone
fn estimate_debris_profile(&self, _zone: &ScanZone) -> DebrisProfile {
// Would use zone metadata and signal analysis
DebrisProfile::default()
}
/// Calculate average signal attenuation
fn calculate_signal_attenuation(&self, rssi_values: &[(String, f64)]) -> f64 {
if rssi_values.is_empty() {
return 0.0;
}
// Reference RSSI at surface (typical open-air value)
const REFERENCE_RSSI: f64 = -30.0;
let avg_rssi: f64 = rssi_values.iter().map(|(_, r)| r).sum::<f64>()
/ rssi_values.len() as f64;
(REFERENCE_RSSI - avg_rssi).max(0.0)
}
/// Combine horizontal and depth uncertainties
fn combine_uncertainties(
&self,
horizontal: &LocationUncertainty,
depth: &DepthEstimate,
) -> LocationUncertainty {
LocationUncertainty {
horizontal_error: horizontal.horizontal_error,
vertical_error: depth.uncertainty,
confidence: (horizontal.confidence * depth.confidence).sqrt(),
}
}
}
impl Default for LocalizationService {
fn default() -> Self {
Self::new()
}
}
/// Fuses multiple position estimates
pub struct PositionFuser {
/// History of position estimates for smoothing
history: parking_lot::RwLock<Vec<PositionEstimate>>,
/// Maximum history size
max_history: usize,
}
/// A position estimate with metadata
#[derive(Debug, Clone)]
pub struct PositionEstimate {
/// The position
pub position: Coordinates3D,
/// Timestamp
pub timestamp: chrono::DateTime<chrono::Utc>,
/// Source of estimate
pub source: EstimateSource,
/// Weight for fusion
pub weight: f64,
}
/// Source of a position estimate
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EstimateSource {
/// From RSSI-based triangulation
RssiTriangulation,
/// From time-of-arrival
TimeOfArrival,
/// From CSI fingerprinting
CsiFingerprint,
/// From angle of arrival
AngleOfArrival,
/// From depth estimation
DepthEstimation,
/// Fused from multiple sources
Fused,
}
impl PositionFuser {
/// Create a new position fuser
pub fn new() -> Self {
Self {
history: parking_lot::RwLock::new(Vec::new()),
max_history: 20,
}
}
/// Add a position estimate
pub fn add_estimate(&self, estimate: PositionEstimate) {
let mut history = self.history.write();
history.push(estimate);
// Keep only recent history
if history.len() > self.max_history {
history.remove(0);
}
}
/// Fuse multiple position estimates into one
pub fn fuse(&self, estimates: &[PositionEstimate]) -> Option<Coordinates3D> {
if estimates.is_empty() {
return None;
}
if estimates.len() == 1 {
return Some(estimates[0].position.clone());
}
// Weighted average based on uncertainty and source confidence
let mut total_weight = 0.0;
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_z = 0.0;
for estimate in estimates {
let weight = self.calculate_weight(estimate);
total_weight += weight;
sum_x += estimate.position.x * weight;
sum_y += estimate.position.y * weight;
sum_z += estimate.position.z * weight;
}
if total_weight == 0.0 {
return None;
}
let fused_x = sum_x / total_weight;
let fused_y = sum_y / total_weight;
let fused_z = sum_z / total_weight;
// Calculate fused uncertainty (reduced due to multiple estimates)
let fused_uncertainty = self.calculate_fused_uncertainty(estimates);
Some(Coordinates3D::new(
fused_x,
fused_y,
fused_z,
fused_uncertainty,
))
}
/// Fuse with temporal smoothing
pub fn fuse_with_history(&self, current: &PositionEstimate) -> Option<Coordinates3D> {
// Add current to history
self.add_estimate(current.clone());
let history = self.history.read();
// Use exponentially weighted moving average
let alpha = 0.3; // Smoothing factor
let mut smoothed = current.position.clone();
for (i, estimate) in history.iter().rev().enumerate().skip(1) {
let weight = alpha * (1.0 - alpha).powi(i as i32);
smoothed.x = smoothed.x * (1.0 - weight) + estimate.position.x * weight;
smoothed.y = smoothed.y * (1.0 - weight) + estimate.position.y * weight;
smoothed.z = smoothed.z * (1.0 - weight) + estimate.position.z * weight;
}
Some(smoothed)
}
/// Calculate weight for an estimate
fn calculate_weight(&self, estimate: &PositionEstimate) -> f64 {
// Base weight from source reliability
let source_weight = match estimate.source {
EstimateSource::TimeOfArrival => 1.0,
EstimateSource::AngleOfArrival => 0.9,
EstimateSource::CsiFingerprint => 0.8,
EstimateSource::RssiTriangulation => 0.7,
EstimateSource::DepthEstimation => 0.6,
EstimateSource::Fused => 1.0,
};
// Adjust by uncertainty (lower uncertainty = higher weight)
let uncertainty_factor = 1.0 / (1.0 + estimate.position.uncertainty.horizontal_error);
// User-provided weight
let user_weight = estimate.weight;
source_weight * uncertainty_factor * user_weight
}
/// Calculate uncertainty after fusing multiple estimates
fn calculate_fused_uncertainty(&self, estimates: &[PositionEstimate]) -> LocationUncertainty {
if estimates.is_empty() {
return LocationUncertainty::default();
}
// Combined uncertainty is reduced with multiple estimates
let n = estimates.len() as f64;
let avg_h_error: f64 = estimates.iter()
.map(|e| e.position.uncertainty.horizontal_error)
.sum::<f64>() / n;
let avg_v_error: f64 = estimates.iter()
.map(|e| e.position.uncertainty.vertical_error)
.sum::<f64>() / n;
// Uncertainty reduction factor (more estimates = more confidence)
let reduction = (1.0 / n.sqrt()).max(0.5);
LocationUncertainty {
horizontal_error: avg_h_error * reduction,
vertical_error: avg_v_error * reduction,
confidence: (0.95 * (1.0 + (n - 1.0) * 0.02)).min(0.99),
}
}
/// Clear history
pub fn clear_history(&self) {
self.history.write().clear();
}
}
impl Default for PositionFuser {
fn default() -> Self {
Self::new()
}
}
/// Simple random range (for simulation)
fn rand_range(min: f64, max: f64) -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let pseudo_random = ((seed * 1103515245 + 12345) % (1 << 31)) as f64 / (1u64 << 31) as f64;
min + pseudo_random * (max - min)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
fn create_test_estimate(x: f64, y: f64, z: f64) -> PositionEstimate {
PositionEstimate {
position: Coordinates3D::with_default_uncertainty(x, y, z),
timestamp: Utc::now(),
source: EstimateSource::RssiTriangulation,
weight: 1.0,
}
}
#[test]
fn test_single_estimate_fusion() {
let fuser = PositionFuser::new();
let estimate = create_test_estimate(5.0, 10.0, -2.0);
let result = fuser.fuse(&[estimate]);
assert!(result.is_some());
let pos = result.unwrap();
assert!((pos.x - 5.0).abs() < 0.001);
}
#[test]
fn test_multiple_estimate_fusion() {
let fuser = PositionFuser::new();
let estimates = vec![
create_test_estimate(4.0, 9.0, -1.5),
create_test_estimate(6.0, 11.0, -2.5),
];
let result = fuser.fuse(&estimates);
assert!(result.is_some());
let pos = result.unwrap();
// Should be roughly in between
assert!(pos.x > 4.0 && pos.x < 6.0);
assert!(pos.y > 9.0 && pos.y < 11.0);
}
#[test]
fn test_fused_uncertainty_reduction() {
let fuser = PositionFuser::new();
let estimates = vec![
create_test_estimate(5.0, 10.0, -2.0),
create_test_estimate(5.1, 10.1, -2.1),
create_test_estimate(4.9, 9.9, -1.9),
];
let single_uncertainty = estimates[0].position.uncertainty.horizontal_error;
let fused_uncertainty = fuser.calculate_fused_uncertainty(&estimates);
// Fused should have lower uncertainty
assert!(fused_uncertainty.horizontal_error < single_uncertainty);
}
#[test]
fn test_localization_service_creation() {
let service = LocalizationService::new();
// Just verify it creates without panic
assert!(true);
drop(service);
}
}

View File

@@ -0,0 +1,14 @@
//! Localization module for survivor position estimation.
//!
//! This module provides:
//! - Triangulation from multiple access points
//! - Depth estimation through debris
//! - Position fusion combining multiple techniques
mod triangulation;
mod depth;
mod fusion;
pub use triangulation::{Triangulator, TriangulationConfig};
pub use depth::{DepthEstimator, DepthEstimatorConfig};
pub use fusion::{PositionFuser, LocalizationService};

View File

@@ -0,0 +1,377 @@
//! Triangulation for 2D/3D position estimation from multiple sensors.
use crate::domain::{Coordinates3D, LocationUncertainty, SensorPosition};
/// Configuration for triangulation
#[derive(Debug, Clone)]
pub struct TriangulationConfig {
/// Minimum number of sensors required
pub min_sensors: usize,
/// Maximum position uncertainty to accept (meters)
pub max_uncertainty: f64,
/// Path loss exponent for distance estimation
pub path_loss_exponent: f64,
/// Reference distance for path loss model (meters)
pub reference_distance: f64,
/// Reference RSSI at reference distance (dBm)
pub reference_rssi: f64,
/// Use weighted least squares
pub weighted: bool,
}
impl Default for TriangulationConfig {
fn default() -> Self {
Self {
min_sensors: 3,
max_uncertainty: 5.0,
path_loss_exponent: 3.0, // Indoor with obstacles
reference_distance: 1.0,
reference_rssi: -30.0,
weighted: true,
}
}
}
/// Result of a distance estimation
#[derive(Debug, Clone)]
pub struct DistanceEstimate {
/// Sensor ID
pub sensor_id: String,
/// Estimated distance in meters
pub distance: f64,
/// Estimation confidence
pub confidence: f64,
}
/// Triangulator for position estimation
pub struct Triangulator {
config: TriangulationConfig,
}
impl Triangulator {
/// Create a new triangulator
pub fn new(config: TriangulationConfig) -> Self {
Self { config }
}
/// Create with default configuration
pub fn with_defaults() -> Self {
Self::new(TriangulationConfig::default())
}
/// Estimate position from RSSI measurements
pub fn estimate_position(
&self,
sensors: &[SensorPosition],
rssi_values: &[(String, f64)], // (sensor_id, rssi)
) -> Option<Coordinates3D> {
// Get distance estimates from RSSI
let distances: Vec<(SensorPosition, f64)> = rssi_values
.iter()
.filter_map(|(id, rssi)| {
let sensor = sensors.iter().find(|s| &s.id == id)?;
if !sensor.is_operational {
return None;
}
let distance = self.rssi_to_distance(*rssi);
Some((sensor.clone(), distance))
})
.collect();
if distances.len() < self.config.min_sensors {
return None;
}
// Perform trilateration
self.trilaterate(&distances)
}
/// Estimate position from Time of Arrival measurements
pub fn estimate_from_toa(
&self,
sensors: &[SensorPosition],
toa_values: &[(String, f64)], // (sensor_id, time_of_arrival_ns)
) -> Option<Coordinates3D> {
const SPEED_OF_LIGHT: f64 = 299_792_458.0; // m/s
let distances: Vec<(SensorPosition, f64)> = toa_values
.iter()
.filter_map(|(id, toa)| {
let sensor = sensors.iter().find(|s| &s.id == id)?;
if !sensor.is_operational {
return None;
}
// Convert nanoseconds to distance
let distance = (*toa * 1e-9) * SPEED_OF_LIGHT / 2.0; // Round trip
Some((sensor.clone(), distance))
})
.collect();
if distances.len() < self.config.min_sensors {
return None;
}
self.trilaterate(&distances)
}
/// Convert RSSI to distance using path loss model
fn rssi_to_distance(&self, rssi: f64) -> f64 {
// Log-distance path loss model:
// RSSI = RSSI_0 - 10 * n * log10(d / d_0)
// Solving for d:
// d = d_0 * 10^((RSSI_0 - RSSI) / (10 * n))
let exponent = (self.config.reference_rssi - rssi)
/ (10.0 * self.config.path_loss_exponent);
self.config.reference_distance * 10.0_f64.powf(exponent)
}
/// Perform trilateration using least squares
fn trilaterate(&self, distances: &[(SensorPosition, f64)]) -> Option<Coordinates3D> {
if distances.len() < 3 {
return None;
}
// Use linearized least squares approach
// Reference: https://en.wikipedia.org/wiki/Trilateration
// Use first sensor as reference
let (ref_sensor, ref_dist) = &distances[0];
let x1 = ref_sensor.x;
let y1 = ref_sensor.y;
let r1 = *ref_dist;
// Build system of linear equations: A * [x, y]^T = b
let n = distances.len() - 1;
let mut a_matrix = vec![vec![0.0; 2]; n];
let mut b_vector = vec![0.0; n];
for (i, (sensor, dist)) in distances.iter().skip(1).enumerate() {
let xi = sensor.x;
let yi = sensor.y;
let ri = *dist;
// Linearized equation from difference of squared distances
a_matrix[i][0] = 2.0 * (xi - x1);
a_matrix[i][1] = 2.0 * (yi - y1);
b_vector[i] = r1 * r1 - ri * ri - x1 * x1 + xi * xi - y1 * y1 + yi * yi;
}
// Solve using least squares: (A^T * A)^-1 * A^T * b
let solution = self.solve_least_squares(&a_matrix, &b_vector)?;
// Calculate uncertainty from residuals
let uncertainty = self.calculate_uncertainty(&solution, distances);
if uncertainty.horizontal_error > self.config.max_uncertainty {
return None;
}
Some(Coordinates3D::new(
solution[0],
solution[1],
0.0, // Z estimated separately
uncertainty,
))
}
/// Solve linear system using least squares
fn solve_least_squares(&self, a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
let n = a.len();
if n < 2 || a[0].len() != 2 {
return None;
}
// Calculate A^T * A
let mut ata = vec![vec![0.0; 2]; 2];
for i in 0..2 {
for j in 0..2 {
for k in 0..n {
ata[i][j] += a[k][i] * a[k][j];
}
}
}
// Calculate A^T * b
let mut atb = vec![0.0; 2];
for i in 0..2 {
for k in 0..n {
atb[i] += a[k][i] * b[k];
}
}
// Solve 2x2 system using Cramer's rule
let det = ata[0][0] * ata[1][1] - ata[0][1] * ata[1][0];
if det.abs() < 1e-10 {
return None;
}
let x = (atb[0] * ata[1][1] - atb[1] * ata[0][1]) / det;
let y = (ata[0][0] * atb[1] - ata[1][0] * atb[0]) / det;
Some(vec![x, y])
}
/// Calculate position uncertainty from residuals
fn calculate_uncertainty(
&self,
position: &[f64],
distances: &[(SensorPosition, f64)],
) -> LocationUncertainty {
// Calculate root mean square error
let mut sum_sq_error = 0.0;
for (sensor, measured_dist) in distances {
let dx = position[0] - sensor.x;
let dy = position[1] - sensor.y;
let estimated_dist = (dx * dx + dy * dy).sqrt();
let error = measured_dist - estimated_dist;
sum_sq_error += error * error;
}
let rmse = (sum_sq_error / distances.len() as f64).sqrt();
// GDOP (Geometric Dilution of Precision) approximation
let gdop = self.estimate_gdop(position, distances);
LocationUncertainty {
horizontal_error: rmse * gdop,
vertical_error: rmse * gdop * 1.5, // Vertical typically less accurate
confidence: 0.95,
}
}
/// Estimate Geometric Dilution of Precision
fn estimate_gdop(&self, position: &[f64], distances: &[(SensorPosition, f64)]) -> f64 {
// Simplified GDOP based on sensor geometry
let mut sum_angle = 0.0;
let n = distances.len();
for i in 0..n {
for j in (i + 1)..n {
let dx1 = distances[i].0.x - position[0];
let dy1 = distances[i].0.y - position[1];
let dx2 = distances[j].0.x - position[0];
let dy2 = distances[j].0.y - position[1];
let dot = dx1 * dx2 + dy1 * dy2;
let mag1 = (dx1 * dx1 + dy1 * dy1).sqrt();
let mag2 = (dx2 * dx2 + dy2 * dy2).sqrt();
if mag1 > 0.0 && mag2 > 0.0 {
let cos_angle = (dot / (mag1 * mag2)).clamp(-1.0, 1.0);
let angle = cos_angle.acos();
sum_angle += angle;
}
}
}
// Average angle between sensor pairs
let num_pairs = (n * (n - 1)) as f64 / 2.0;
let avg_angle = if num_pairs > 0.0 {
sum_angle / num_pairs
} else {
std::f64::consts::PI / 4.0
};
// GDOP is better when sensors are spread out (angle closer to 90 degrees)
// GDOP gets worse as sensors are collinear
let optimal_angle = std::f64::consts::PI / 2.0;
let angle_factor = (avg_angle / optimal_angle - 1.0).abs() + 1.0;
angle_factor.max(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::SensorType;
fn create_test_sensors() -> Vec<SensorPosition> {
vec![
SensorPosition {
id: "s1".to_string(),
x: 0.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
},
SensorPosition {
id: "s2".to_string(),
x: 10.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
},
SensorPosition {
id: "s3".to_string(),
x: 5.0,
y: 10.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
},
]
}
#[test]
fn test_rssi_to_distance() {
let triangulator = Triangulator::with_defaults();
// At reference distance, RSSI should equal reference RSSI
let distance = triangulator.rssi_to_distance(-30.0);
assert!((distance - 1.0).abs() < 0.1);
// Weaker signal = further distance
let distance2 = triangulator.rssi_to_distance(-60.0);
assert!(distance2 > distance);
}
#[test]
fn test_trilateration() {
let triangulator = Triangulator::with_defaults();
let sensors = create_test_sensors();
// Target at (5, 4) - calculate distances
let target = (5.0, 4.0);
let distances: Vec<(&str, f64)> = vec![
("s1", ((target.0 - 0.0).powi(2) + (target.1 - 0.0).powi(2)).sqrt()),
("s2", ((target.0 - 10.0).powi(2) + (target.1 - 0.0).powi(2)).sqrt()),
("s3", ((target.0 - 5.0).powi(2) + (target.1 - 10.0).powi(2)).sqrt()),
];
let dist_vec: Vec<(SensorPosition, f64)> = distances
.iter()
.filter_map(|(id, d)| {
let sensor = sensors.iter().find(|s| s.id == *id)?;
Some((sensor.clone(), *d))
})
.collect();
let result = triangulator.trilaterate(&dist_vec);
assert!(result.is_some());
let pos = result.unwrap();
assert!((pos.x - target.0).abs() < 0.5);
assert!((pos.y - target.1).abs() < 0.5);
}
#[test]
fn test_insufficient_sensors() {
let triangulator = Triangulator::with_defaults();
let sensors = create_test_sensors();
// Only 2 distance measurements
let rssi_values = vec![
("s1".to_string(), -40.0),
("s2".to_string(), -45.0),
];
let result = triangulator.estimate_position(&sensors, &rssi_values);
assert!(result.is_none());
}
}