This commit is contained in:
rUv
2025-06-07 11:44:19 +00:00
parent 43e92c5494
commit c378b705ca
95 changed files with 43677 additions and 0 deletions

View File

@@ -0,0 +1,736 @@
"""
End-to-end tests for healthcare fall detection scenario.
Tests complete workflow from CSI data collection to fall alert generation.
"""
import pytest
import asyncio
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch
import json
from dataclasses import dataclass
from enum import Enum
class AlertSeverity(Enum):
"""Alert severity levels."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class HealthcareAlert:
"""Healthcare alert data structure."""
alert_id: str
timestamp: datetime
alert_type: str
severity: AlertSeverity
patient_id: str
location: str
confidence: float
description: str
metadata: Dict[str, Any]
class MockPatientMonitor:
"""Mock patient monitoring system."""
def __init__(self, patient_id: str, room_id: str):
self.patient_id = patient_id
self.room_id = room_id
self.is_monitoring = False
self.baseline_activity = None
self.activity_history = []
self.alerts_generated = []
self.fall_detection_enabled = True
self.sensitivity_level = "medium"
async def start_monitoring(self) -> bool:
"""Start patient monitoring."""
if self.is_monitoring:
return False
self.is_monitoring = True
return True
async def stop_monitoring(self) -> bool:
"""Stop patient monitoring."""
if not self.is_monitoring:
return False
self.is_monitoring = False
return True
async def process_pose_data(self, pose_data: Dict[str, Any]) -> Optional[HealthcareAlert]:
"""Process pose data and detect potential issues."""
if not self.is_monitoring:
return None
# Extract activity metrics
activity_metrics = self._extract_activity_metrics(pose_data)
self.activity_history.append(activity_metrics)
# Keep only recent history
if len(self.activity_history) > 100:
self.activity_history = self.activity_history[-100:]
# Detect anomalies
alert = await self._detect_anomalies(activity_metrics, pose_data)
if alert:
self.alerts_generated.append(alert)
return alert
def _extract_activity_metrics(self, pose_data: Dict[str, Any]) -> Dict[str, Any]:
"""Extract activity metrics from pose data."""
persons = pose_data.get("persons", [])
if not persons:
return {
"person_count": 0,
"activity_level": 0.0,
"posture": "unknown",
"movement_speed": 0.0,
"stability_score": 1.0
}
# Analyze first person (primary patient)
person = persons[0]
# Extract posture from activity field or bounding box analysis
posture = person.get("activity", "standing")
# If no activity specified, analyze bounding box for fall detection
if posture == "standing" and "bounding_box" in person:
bbox = person["bounding_box"]
width = bbox.get("width", 80)
height = bbox.get("height", 180)
# Fall detection: if width > height, likely fallen
if width > height * 1.5:
posture = "fallen"
# Calculate activity metrics based on posture
if posture == "fallen":
activity_level = 0.1
movement_speed = 0.0
stability_score = 0.2
elif posture == "walking":
activity_level = 0.8
movement_speed = 1.5
stability_score = 0.7
elif posture == "sitting":
activity_level = 0.3
movement_speed = 0.1
stability_score = 0.9
else: # standing or other
activity_level = 0.5
movement_speed = 0.2
stability_score = 0.8
return {
"person_count": len(persons),
"activity_level": activity_level,
"posture": posture,
"movement_speed": movement_speed,
"stability_score": stability_score,
"confidence": person.get("confidence", 0.0)
}
async def _detect_anomalies(self, current_metrics: Dict[str, Any], pose_data: Dict[str, Any]) -> Optional[HealthcareAlert]:
"""Detect health-related anomalies."""
# Fall detection
if current_metrics["posture"] == "fallen":
return await self._generate_fall_alert(current_metrics, pose_data)
# Prolonged inactivity detection
if len(self.activity_history) >= 10:
recent_activity = [m["activity_level"] for m in self.activity_history[-10:]]
avg_activity = np.mean(recent_activity)
if avg_activity < 0.1: # Very low activity
return await self._generate_inactivity_alert(current_metrics, pose_data)
# Unusual movement patterns
if current_metrics["stability_score"] < 0.4:
return await self._generate_instability_alert(current_metrics, pose_data)
return None
async def _generate_fall_alert(self, metrics: Dict[str, Any], pose_data: Dict[str, Any]) -> HealthcareAlert:
"""Generate fall detection alert."""
return HealthcareAlert(
alert_id=f"fall_{self.patient_id}_{int(datetime.utcnow().timestamp())}",
timestamp=datetime.utcnow(),
alert_type="fall_detected",
severity=AlertSeverity.CRITICAL,
patient_id=self.patient_id,
location=self.room_id,
confidence=metrics["confidence"],
description=f"Fall detected for patient {self.patient_id} in {self.room_id}",
metadata={
"posture": metrics["posture"],
"stability_score": metrics["stability_score"],
"pose_data": pose_data
}
)
async def _generate_inactivity_alert(self, metrics: Dict[str, Any], pose_data: Dict[str, Any]) -> HealthcareAlert:
"""Generate prolonged inactivity alert."""
return HealthcareAlert(
alert_id=f"inactivity_{self.patient_id}_{int(datetime.utcnow().timestamp())}",
timestamp=datetime.utcnow(),
alert_type="prolonged_inactivity",
severity=AlertSeverity.MEDIUM,
patient_id=self.patient_id,
location=self.room_id,
confidence=metrics["confidence"],
description=f"Prolonged inactivity detected for patient {self.patient_id}",
metadata={
"activity_level": metrics["activity_level"],
"duration_minutes": 10,
"pose_data": pose_data
}
)
async def _generate_instability_alert(self, metrics: Dict[str, Any], pose_data: Dict[str, Any]) -> HealthcareAlert:
"""Generate movement instability alert."""
return HealthcareAlert(
alert_id=f"instability_{self.patient_id}_{int(datetime.utcnow().timestamp())}",
timestamp=datetime.utcnow(),
alert_type="movement_instability",
severity=AlertSeverity.HIGH,
patient_id=self.patient_id,
location=self.room_id,
confidence=metrics["confidence"],
description=f"Movement instability detected for patient {self.patient_id}",
metadata={
"stability_score": metrics["stability_score"],
"movement_speed": metrics["movement_speed"],
"pose_data": pose_data
}
)
def get_monitoring_stats(self) -> Dict[str, Any]:
"""Get monitoring statistics."""
return {
"patient_id": self.patient_id,
"room_id": self.room_id,
"is_monitoring": self.is_monitoring,
"total_alerts": len(self.alerts_generated),
"alert_types": {
alert.alert_type: len([a for a in self.alerts_generated if a.alert_type == alert.alert_type])
for alert in self.alerts_generated
},
"activity_samples": len(self.activity_history),
"fall_detection_enabled": self.fall_detection_enabled
}
class MockHealthcareNotificationSystem:
"""Mock healthcare notification system."""
def __init__(self):
self.notifications_sent = []
self.notification_channels = {
"nurse_station": True,
"mobile_app": True,
"email": True,
"sms": False
}
self.escalation_rules = {
AlertSeverity.CRITICAL: ["nurse_station", "mobile_app", "sms"],
AlertSeverity.HIGH: ["nurse_station", "mobile_app"],
AlertSeverity.MEDIUM: ["nurse_station"],
AlertSeverity.LOW: ["mobile_app"]
}
async def send_alert_notification(self, alert: HealthcareAlert) -> Dict[str, bool]:
"""Send alert notification through appropriate channels."""
channels_to_notify = self.escalation_rules.get(alert.severity, ["nurse_station"])
results = {}
for channel in channels_to_notify:
if self.notification_channels.get(channel, False):
success = await self._send_to_channel(channel, alert)
results[channel] = success
if success:
self.notifications_sent.append({
"alert_id": alert.alert_id,
"channel": channel,
"timestamp": datetime.utcnow(),
"severity": alert.severity.value
})
return results
async def _send_to_channel(self, channel: str, alert: HealthcareAlert) -> bool:
"""Send notification to specific channel."""
# Simulate network delay
await asyncio.sleep(0.01)
# Simulate occasional failures
if np.random.random() < 0.05: # 5% failure rate
return False
return True
def get_notification_stats(self) -> Dict[str, Any]:
"""Get notification statistics."""
return {
"total_notifications": len(self.notifications_sent),
"notifications_by_channel": {
channel: len([n for n in self.notifications_sent if n["channel"] == channel])
for channel in self.notification_channels.keys()
},
"notifications_by_severity": {
severity.value: len([n for n in self.notifications_sent if n["severity"] == severity.value])
for severity in AlertSeverity
}
}
class TestHealthcareFallDetection:
"""Test healthcare fall detection workflow."""
@pytest.fixture
def patient_monitor(self):
"""Create patient monitor."""
return MockPatientMonitor("patient_001", "room_101")
@pytest.fixture
def notification_system(self):
"""Create notification system."""
return MockHealthcareNotificationSystem()
@pytest.fixture
def fall_pose_data(self):
"""Create pose data indicating a fall."""
return {
"persons": [
{
"person_id": "patient_001",
"confidence": 0.92,
"bounding_box": {"x": 200, "y": 400, "width": 150, "height": 80}, # Horizontal position
"activity": "fallen",
"keypoints": [[x, y, 0.8] for x, y in zip(range(17), range(17))]
}
],
"zone_summary": {"room_101": 1},
"timestamp": datetime.utcnow().isoformat()
}
@pytest.fixture
def normal_pose_data(self):
"""Create normal pose data."""
return {
"persons": [
{
"person_id": "patient_001",
"confidence": 0.88,
"bounding_box": {"x": 200, "y": 150, "width": 80, "height": 180},
"activity": "standing",
"keypoints": [[x, y, 0.9] for x, y in zip(range(17), range(17))]
}
],
"zone_summary": {"room_101": 1},
"timestamp": datetime.utcnow().isoformat()
}
@pytest.mark.asyncio
async def test_fall_detection_workflow_should_fail_initially(self, patient_monitor, notification_system, fall_pose_data):
"""Test fall detection workflow - should fail initially."""
# Start monitoring
result = await patient_monitor.start_monitoring()
# This will fail initially
assert result is True
assert patient_monitor.is_monitoring is True
# Process fall pose data
alert = await patient_monitor.process_pose_data(fall_pose_data)
# Should generate fall alert
assert alert is not None
assert alert.alert_type == "fall_detected"
assert alert.severity == AlertSeverity.CRITICAL
assert alert.patient_id == "patient_001"
# Send notification
notification_results = await notification_system.send_alert_notification(alert)
# Should notify appropriate channels
assert len(notification_results) > 0
assert any(notification_results.values()) # At least one channel should succeed
# Check statistics
monitor_stats = patient_monitor.get_monitoring_stats()
assert monitor_stats["total_alerts"] == 1
notification_stats = notification_system.get_notification_stats()
assert notification_stats["total_notifications"] > 0
@pytest.mark.asyncio
async def test_normal_activity_monitoring_should_fail_initially(self, patient_monitor, normal_pose_data):
"""Test normal activity monitoring - should fail initially."""
await patient_monitor.start_monitoring()
# Process multiple normal pose data samples
alerts_generated = []
for i in range(10):
alert = await patient_monitor.process_pose_data(normal_pose_data)
if alert:
alerts_generated.append(alert)
# This will fail initially
# Should not generate alerts for normal activity
assert len(alerts_generated) == 0
# Should have activity history
stats = patient_monitor.get_monitoring_stats()
assert stats["activity_samples"] == 10
assert stats["is_monitoring"] is True
@pytest.mark.asyncio
async def test_prolonged_inactivity_detection_should_fail_initially(self, patient_monitor):
"""Test prolonged inactivity detection - should fail initially."""
await patient_monitor.start_monitoring()
# Simulate prolonged inactivity
inactive_pose_data = {
"persons": [], # No person detected
"zone_summary": {"room_101": 0},
"timestamp": datetime.utcnow().isoformat()
}
alerts_generated = []
# Process multiple inactive samples
for i in range(15):
alert = await patient_monitor.process_pose_data(inactive_pose_data)
if alert:
alerts_generated.append(alert)
# This will fail initially
# Should generate inactivity alert after sufficient samples
inactivity_alerts = [a for a in alerts_generated if a.alert_type == "prolonged_inactivity"]
assert len(inactivity_alerts) > 0
# Check alert properties
alert = inactivity_alerts[0]
assert alert.severity == AlertSeverity.MEDIUM
assert alert.patient_id == "patient_001"
@pytest.mark.asyncio
async def test_movement_instability_detection_should_fail_initially(self, patient_monitor):
"""Test movement instability detection - should fail initially."""
await patient_monitor.start_monitoring()
# Simulate unstable movement
unstable_pose_data = {
"persons": [
{
"person_id": "patient_001",
"confidence": 0.65, # Lower confidence indicates instability
"bounding_box": {"x": 200, "y": 150, "width": 80, "height": 180},
"activity": "walking",
"keypoints": [[x, y, 0.5] for x, y in zip(range(17), range(17))] # Low keypoint confidence
}
],
"zone_summary": {"room_101": 1},
"timestamp": datetime.utcnow().isoformat()
}
# Process unstable pose data
alert = await patient_monitor.process_pose_data(unstable_pose_data)
# This will fail initially
# May generate instability alert based on stability score
if alert and alert.alert_type == "movement_instability":
assert alert.severity == AlertSeverity.HIGH
assert alert.patient_id == "patient_001"
assert "stability_score" in alert.metadata
class TestHealthcareMultiPatientMonitoring:
"""Test multi-patient monitoring scenarios."""
@pytest.fixture
def multi_patient_setup(self):
"""Create multi-patient monitoring setup."""
patients = {
"patient_001": MockPatientMonitor("patient_001", "room_101"),
"patient_002": MockPatientMonitor("patient_002", "room_102"),
"patient_003": MockPatientMonitor("patient_003", "room_103")
}
notification_system = MockHealthcareNotificationSystem()
return patients, notification_system
@pytest.mark.asyncio
async def test_concurrent_patient_monitoring_should_fail_initially(self, multi_patient_setup):
"""Test concurrent patient monitoring - should fail initially."""
patients, notification_system = multi_patient_setup
# Start monitoring for all patients
start_results = []
for patient_id, monitor in patients.items():
result = await monitor.start_monitoring()
start_results.append(result)
# This will fail initially
assert all(start_results)
assert all(monitor.is_monitoring for monitor in patients.values())
# Simulate concurrent pose data processing
pose_data_samples = [
{
"persons": [
{
"person_id": patient_id,
"confidence": 0.85,
"bounding_box": {"x": 200, "y": 150, "width": 80, "height": 180},
"activity": "standing"
}
],
"zone_summary": {f"room_{101 + i}": 1},
"timestamp": datetime.utcnow().isoformat()
}
for i, patient_id in enumerate(patients.keys())
]
# Process data for all patients concurrently
tasks = []
for (patient_id, monitor), pose_data in zip(patients.items(), pose_data_samples):
task = asyncio.create_task(monitor.process_pose_data(pose_data))
tasks.append(task)
alerts = await asyncio.gather(*tasks)
# Check results
assert len(alerts) == len(patients)
# Get statistics for all patients
all_stats = {}
for patient_id, monitor in patients.items():
all_stats[patient_id] = monitor.get_monitoring_stats()
assert len(all_stats) == 3
assert all(stats["is_monitoring"] for stats in all_stats.values())
@pytest.mark.asyncio
async def test_alert_prioritization_should_fail_initially(self, multi_patient_setup):
"""Test alert prioritization across patients - should fail initially."""
patients, notification_system = multi_patient_setup
# Start monitoring
for monitor in patients.values():
await monitor.start_monitoring()
# Generate different severity alerts
alert_scenarios = [
("patient_001", "fall_detected", AlertSeverity.CRITICAL),
("patient_002", "prolonged_inactivity", AlertSeverity.MEDIUM),
("patient_003", "movement_instability", AlertSeverity.HIGH)
]
generated_alerts = []
for patient_id, alert_type, expected_severity in alert_scenarios:
# Create appropriate pose data for each scenario
if alert_type == "fall_detected":
pose_data = {
"persons": [{"person_id": patient_id, "confidence": 0.9, "activity": "fallen"}],
"zone_summary": {f"room_{patients[patient_id].room_id}": 1}
}
else:
pose_data = {
"persons": [{"person_id": patient_id, "confidence": 0.7, "activity": "standing"}],
"zone_summary": {f"room_{patients[patient_id].room_id}": 1}
}
alert = await patients[patient_id].process_pose_data(pose_data)
if alert:
generated_alerts.append(alert)
# This will fail initially
# Should have generated alerts
assert len(generated_alerts) > 0
# Send notifications for all alerts
notification_tasks = [
notification_system.send_alert_notification(alert)
for alert in generated_alerts
]
notification_results = await asyncio.gather(*notification_tasks)
# Check notification prioritization
notification_stats = notification_system.get_notification_stats()
assert notification_stats["total_notifications"] > 0
# Critical alerts should use more channels
critical_notifications = [
n for n in notification_system.notifications_sent
if n["severity"] == "critical"
]
if critical_notifications:
# Critical alerts should be sent to multiple channels
critical_channels = set(n["channel"] for n in critical_notifications)
assert len(critical_channels) >= 1
class TestHealthcareSystemIntegration:
"""Test healthcare system integration scenarios."""
@pytest.mark.asyncio
async def test_end_to_end_healthcare_workflow_should_fail_initially(self):
"""Test complete end-to-end healthcare workflow - should fail initially."""
# Setup complete healthcare monitoring system
class HealthcareMonitoringSystem:
def __init__(self):
self.patient_monitors = {}
self.notification_system = MockHealthcareNotificationSystem()
self.alert_history = []
self.system_status = "operational"
async def add_patient(self, patient_id: str, room_id: str) -> bool:
"""Add patient to monitoring system."""
if patient_id in self.patient_monitors:
return False
monitor = MockPatientMonitor(patient_id, room_id)
self.patient_monitors[patient_id] = monitor
return await monitor.start_monitoring()
async def process_pose_update(self, room_id: str, pose_data: Dict[str, Any]) -> List[HealthcareAlert]:
"""Process pose update for room."""
alerts = []
# Find patients in this room
room_patients = [
(patient_id, monitor) for patient_id, monitor in self.patient_monitors.items()
if monitor.room_id == room_id
]
for patient_id, monitor in room_patients:
alert = await monitor.process_pose_data(pose_data)
if alert:
alerts.append(alert)
self.alert_history.append(alert)
# Send notification
await self.notification_system.send_alert_notification(alert)
return alerts
def get_system_status(self) -> Dict[str, Any]:
"""Get overall system status."""
return {
"system_status": self.system_status,
"total_patients": len(self.patient_monitors),
"active_monitors": sum(1 for m in self.patient_monitors.values() if m.is_monitoring),
"total_alerts": len(self.alert_history),
"notification_stats": self.notification_system.get_notification_stats()
}
healthcare_system = HealthcareMonitoringSystem()
# Add patients to system
patients = [
("patient_001", "room_101"),
("patient_002", "room_102"),
("patient_003", "room_103")
]
for patient_id, room_id in patients:
result = await healthcare_system.add_patient(patient_id, room_id)
assert result is True
# Simulate pose data updates for different rooms
pose_updates = [
("room_101", {
"persons": [{"person_id": "patient_001", "confidence": 0.9, "activity": "fallen"}],
"zone_summary": {"room_101": 1}
}),
("room_102", {
"persons": [{"person_id": "patient_002", "confidence": 0.8, "activity": "standing"}],
"zone_summary": {"room_102": 1}
}),
("room_103", {
"persons": [], # No person detected
"zone_summary": {"room_103": 0}
})
]
all_alerts = []
for room_id, pose_data in pose_updates:
alerts = await healthcare_system.process_pose_update(room_id, pose_data)
all_alerts.extend(alerts)
# This will fail initially
# Should have processed all updates
assert len(pose_updates) == 3
# Check system status
system_status = healthcare_system.get_system_status()
assert system_status["total_patients"] == 3
assert system_status["active_monitors"] == 3
assert system_status["system_status"] == "operational"
# Should have generated some alerts
if all_alerts:
assert len(all_alerts) > 0
assert system_status["total_alerts"] > 0
@pytest.mark.asyncio
async def test_healthcare_system_resilience_should_fail_initially(self):
"""Test healthcare system resilience - should fail initially."""
patient_monitor = MockPatientMonitor("patient_001", "room_101")
notification_system = MockHealthcareNotificationSystem()
await patient_monitor.start_monitoring()
# Simulate system stress with rapid pose updates
rapid_updates = 50
alerts_generated = []
for i in range(rapid_updates):
# Alternate between normal and concerning pose data
if i % 10 == 0: # Every 10th update is concerning
pose_data = {
"persons": [{"person_id": "patient_001", "confidence": 0.9, "activity": "fallen"}],
"zone_summary": {"room_101": 1}
}
else:
pose_data = {
"persons": [{"person_id": "patient_001", "confidence": 0.85, "activity": "standing"}],
"zone_summary": {"room_101": 1}
}
alert = await patient_monitor.process_pose_data(pose_data)
if alert:
alerts_generated.append(alert)
await notification_system.send_alert_notification(alert)
# This will fail initially
# System should handle rapid updates gracefully
stats = patient_monitor.get_monitoring_stats()
assert stats["activity_samples"] == rapid_updates
assert stats["is_monitoring"] is True
# Should have generated some alerts but not excessive
assert len(alerts_generated) <= rapid_updates / 5 # At most 20% alert rate
notification_stats = notification_system.get_notification_stats()
assert notification_stats["total_notifications"] >= len(alerts_generated)