iints-sdk-python35 0.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iints/__init__.py +183 -0
- iints/analysis/__init__.py +12 -0
- iints/analysis/algorithm_xray.py +387 -0
- iints/analysis/baseline.py +92 -0
- iints/analysis/clinical_benchmark.py +198 -0
- iints/analysis/clinical_metrics.py +551 -0
- iints/analysis/clinical_tir_analyzer.py +136 -0
- iints/analysis/diabetes_metrics.py +43 -0
- iints/analysis/edge_efficiency.py +33 -0
- iints/analysis/edge_performance_monitor.py +315 -0
- iints/analysis/explainability.py +94 -0
- iints/analysis/explainable_ai.py +232 -0
- iints/analysis/hardware_benchmark.py +221 -0
- iints/analysis/metrics.py +117 -0
- iints/analysis/population_report.py +188 -0
- iints/analysis/reporting.py +345 -0
- iints/analysis/safety_index.py +311 -0
- iints/analysis/sensor_filtering.py +54 -0
- iints/analysis/validator.py +273 -0
- iints/api/__init__.py +0 -0
- iints/api/base_algorithm.py +307 -0
- iints/api/registry.py +103 -0
- iints/api/template_algorithm.py +195 -0
- iints/assets/iints_logo.png +0 -0
- iints/cli/__init__.py +0 -0
- iints/cli/cli.py +2598 -0
- iints/core/__init__.py +1 -0
- iints/core/algorithms/__init__.py +0 -0
- iints/core/algorithms/battle_runner.py +138 -0
- iints/core/algorithms/correction_bolus.py +95 -0
- iints/core/algorithms/discovery.py +92 -0
- iints/core/algorithms/fixed_basal_bolus.py +58 -0
- iints/core/algorithms/hybrid_algorithm.py +92 -0
- iints/core/algorithms/lstm_algorithm.py +138 -0
- iints/core/algorithms/mock_algorithms.py +162 -0
- iints/core/algorithms/pid_controller.py +88 -0
- iints/core/algorithms/standard_pump_algo.py +64 -0
- iints/core/device.py +0 -0
- iints/core/device_manager.py +64 -0
- iints/core/devices/__init__.py +3 -0
- iints/core/devices/models.py +160 -0
- iints/core/patient/__init__.py +9 -0
- iints/core/patient/bergman_model.py +341 -0
- iints/core/patient/models.py +285 -0
- iints/core/patient/patient_factory.py +117 -0
- iints/core/patient/profile.py +41 -0
- iints/core/safety/__init__.py +12 -0
- iints/core/safety/config.py +37 -0
- iints/core/safety/input_validator.py +95 -0
- iints/core/safety/supervisor.py +39 -0
- iints/core/simulation/__init__.py +0 -0
- iints/core/simulation/scenario_parser.py +61 -0
- iints/core/simulator.py +874 -0
- iints/core/supervisor.py +367 -0
- iints/data/__init__.py +53 -0
- iints/data/adapter.py +142 -0
- iints/data/column_mapper.py +398 -0
- iints/data/datasets.json +132 -0
- iints/data/demo/__init__.py +1 -0
- iints/data/demo/demo_cgm.csv +289 -0
- iints/data/importer.py +275 -0
- iints/data/ingestor.py +162 -0
- iints/data/nightscout.py +128 -0
- iints/data/quality_checker.py +550 -0
- iints/data/registry.py +166 -0
- iints/data/tidepool.py +38 -0
- iints/data/universal_parser.py +813 -0
- iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
- iints/data/virtual_patients/default_patient.yaml +11 -0
- iints/data/virtual_patients/patient_559_config.yaml +11 -0
- iints/emulation/__init__.py +80 -0
- iints/emulation/legacy_base.py +414 -0
- iints/emulation/medtronic_780g.py +337 -0
- iints/emulation/omnipod_5.py +367 -0
- iints/emulation/tandem_controliq.py +393 -0
- iints/highlevel.py +451 -0
- iints/learning/__init__.py +3 -0
- iints/learning/autonomous_optimizer.py +194 -0
- iints/learning/learning_system.py +122 -0
- iints/metrics.py +34 -0
- iints/population/__init__.py +11 -0
- iints/population/generator.py +131 -0
- iints/population/runner.py +327 -0
- iints/presets/__init__.py +28 -0
- iints/presets/presets.json +114 -0
- iints/research/__init__.py +30 -0
- iints/research/config.py +68 -0
- iints/research/dataset.py +319 -0
- iints/research/losses.py +73 -0
- iints/research/predictor.py +329 -0
- iints/scenarios/__init__.py +3 -0
- iints/scenarios/generator.py +92 -0
- iints/templates/__init__.py +0 -0
- iints/templates/default_algorithm.py +91 -0
- iints/templates/scenarios/__init__.py +0 -0
- iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
- iints/templates/scenarios/chaos_runaway_ai.json +25 -0
- iints/templates/scenarios/example_scenario.json +35 -0
- iints/templates/scenarios/exercise_stress.json +30 -0
- iints/utils/__init__.py +3 -0
- iints/utils/plotting.py +50 -0
- iints/utils/run_io.py +152 -0
- iints/validation/__init__.py +133 -0
- iints/validation/schemas.py +94 -0
- iints/visualization/__init__.py +34 -0
- iints/visualization/cockpit.py +691 -0
- iints/visualization/uncertainty_cloud.py +612 -0
- iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
- iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
- iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
- iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
- iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
- iints_sdk_python35-0.0.18.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Clinical Control Center - IINTS-AF
|
|
4
|
+
Professional medical dashboard for diabetes algorithm research.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- Real-time glucose visualization with uncertainty cloud
|
|
8
|
+
- Algorithm reasoning log ("Why" panel)
|
|
9
|
+
- Multi-algorithm comparison (Battle Mode)
|
|
10
|
+
- Legacy pump comparison
|
|
11
|
+
- Clinical metrics dashboard
|
|
12
|
+
- Alert and safety monitoring
|
|
13
|
+
|
|
14
|
+
This is not a hobby app - it's a professional medical cockpit.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import matplotlib.pyplot as plt
|
|
20
|
+
from matplotlib.gridspec import GridSpec
|
|
21
|
+
from typing import Dict, List, Optional, Any, Tuple
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
import json
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CockpitConfig:
|
|
29
|
+
"""Configuration for the clinical cockpit"""
|
|
30
|
+
# Layout
|
|
31
|
+
figure_size: Tuple[int, int] = (20, 12)
|
|
32
|
+
dpi: int = 150
|
|
33
|
+
|
|
34
|
+
# Colors
|
|
35
|
+
primary_color: str = '#2196F3'
|
|
36
|
+
secondary_color: str = '#4CAF50'
|
|
37
|
+
alert_color: str = '#F44336'
|
|
38
|
+
warning_color: str = '#FF9800'
|
|
39
|
+
success_color: str = '#4CAF50'
|
|
40
|
+
|
|
41
|
+
# Target zones
|
|
42
|
+
target_low: float = 70.0
|
|
43
|
+
target_high: float = 180.0
|
|
44
|
+
critical_low: float = 54.0
|
|
45
|
+
critical_high: float = 250.0
|
|
46
|
+
|
|
47
|
+
# Update interval (for real-time mode)
|
|
48
|
+
update_interval: float = 0.5 # seconds
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DashboardState:
|
|
53
|
+
"""Current state of the dashboard"""
|
|
54
|
+
current_time: int = 0
|
|
55
|
+
current_glucose: float = 0.0
|
|
56
|
+
glucose_velocity: float = 0.0
|
|
57
|
+
insulin_delivered: float = 0.0
|
|
58
|
+
iob: float = 0.0
|
|
59
|
+
cob: float = 0.0
|
|
60
|
+
|
|
61
|
+
# Algorithm state
|
|
62
|
+
algorithm_name: str = ""
|
|
63
|
+
algorithm_confidence: float = 0.0
|
|
64
|
+
prediction: float = 0.0
|
|
65
|
+
uncertainty: float = 0.0
|
|
66
|
+
|
|
67
|
+
# Safety state
|
|
68
|
+
safety_alerts: List[str] = field(default_factory=list)
|
|
69
|
+
hypo_risk: str = "normal"
|
|
70
|
+
hyper_risk: str = "normal"
|
|
71
|
+
|
|
72
|
+
# Metrics
|
|
73
|
+
tir: float = 0.0
|
|
74
|
+
cv: float = 0.0
|
|
75
|
+
gmi: float = 0.0
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict:
|
|
78
|
+
return {
|
|
79
|
+
'current_time': self.current_time,
|
|
80
|
+
'current_glucose': self.current_glucose,
|
|
81
|
+
'glucose_velocity': self.glucose_velocity,
|
|
82
|
+
'insulin_delivered': self.insulin_delivered,
|
|
83
|
+
'iob': self.iob,
|
|
84
|
+
'cob': self.cob,
|
|
85
|
+
'algorithm_name': self.algorithm_name,
|
|
86
|
+
'algorithm_confidence': self.algorithm_confidence,
|
|
87
|
+
'prediction': self.prediction,
|
|
88
|
+
'uncertainty': self.uncertainty,
|
|
89
|
+
'safety_alerts': self.safety_alerts,
|
|
90
|
+
'tir': self.tir,
|
|
91
|
+
'cv': self.cv,
|
|
92
|
+
'gmi': self.gmi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ClinicalCockpit:
|
|
97
|
+
"""
|
|
98
|
+
Professional clinical dashboard for diabetes algorithm research.
|
|
99
|
+
|
|
100
|
+
This dashboard provides:
|
|
101
|
+
1. Main glucose display with target zones
|
|
102
|
+
2. Uncertainty cloud around predictions
|
|
103
|
+
3. Reasoning log ("Why" panel)
|
|
104
|
+
4. Algorithm personality metrics
|
|
105
|
+
5. Safety alerts
|
|
106
|
+
6. Battle mode comparison
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
cockpit = ClinicalCockpit()
|
|
110
|
+
|
|
111
|
+
# For real-time updates
|
|
112
|
+
cockpit.update(state)
|
|
113
|
+
|
|
114
|
+
# For end-to-end visualization
|
|
115
|
+
cockpit.visualize_results(simulation_data)
|
|
116
|
+
|
|
117
|
+
# For battle mode comparison
|
|
118
|
+
cockpit.compare_battle(battle_report)
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(self, config: Optional[CockpitConfig] = None):
|
|
122
|
+
"""
|
|
123
|
+
Initialize clinical cockpit.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
config: Dashboard configuration (default used if None)
|
|
127
|
+
"""
|
|
128
|
+
self.config = config or CockpitConfig()
|
|
129
|
+
self.state = DashboardState()
|
|
130
|
+
self.history: List[Dict[str, Any]] = []
|
|
131
|
+
|
|
132
|
+
def _create_glucose_panel(self,
|
|
133
|
+
gs: GridSpec,
|
|
134
|
+
simulation_data: pd.DataFrame,
|
|
135
|
+
predictions: Optional[np.ndarray] = None,
|
|
136
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
137
|
+
"""Create main glucose visualization panel"""
|
|
138
|
+
if ax is None:
|
|
139
|
+
ax = plt.subplot(gs[0, :])
|
|
140
|
+
|
|
141
|
+
timestamps = simulation_data['time_minutes']
|
|
142
|
+
glucose = simulation_data['glucose_actual_mgdl']
|
|
143
|
+
|
|
144
|
+
# Target zones
|
|
145
|
+
ax.axhspan(self.config.target_low, self.config.target_high,
|
|
146
|
+
alpha=0.15, color='green', label='Target (70-180)')
|
|
147
|
+
ax.axhspan(self.config.critical_low, self.config.target_low,
|
|
148
|
+
alpha=0.2, color='red', label='Low Zone')
|
|
149
|
+
ax.axhspan(self.config.target_high, self.config.critical_high,
|
|
150
|
+
alpha=0.2, color='orange', label='High Zone')
|
|
151
|
+
|
|
152
|
+
# Glucose line
|
|
153
|
+
ax.plot(timestamps, glucose,
|
|
154
|
+
color=self.config.primary_color,
|
|
155
|
+
linewidth=2,
|
|
156
|
+
label='Glucose')
|
|
157
|
+
|
|
158
|
+
# Safety overrides timeline markers
|
|
159
|
+
if 'safety_triggered' in simulation_data.columns:
|
|
160
|
+
safety_mask = simulation_data['safety_triggered'].astype(bool)
|
|
161
|
+
if safety_mask.any():
|
|
162
|
+
ax.scatter(
|
|
163
|
+
timestamps[safety_mask],
|
|
164
|
+
glucose[safety_mask],
|
|
165
|
+
color='red',
|
|
166
|
+
s=20,
|
|
167
|
+
label='Safety Override',
|
|
168
|
+
zorder=3
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Add predictions if provided
|
|
172
|
+
if predictions is not None:
|
|
173
|
+
ax.plot(timestamps, predictions,
|
|
174
|
+
color=self.config.secondary_color,
|
|
175
|
+
linewidth=1.5,
|
|
176
|
+
linestyle='--',
|
|
177
|
+
label='Prediction',
|
|
178
|
+
alpha=0.7)
|
|
179
|
+
|
|
180
|
+
# Uncertainty band
|
|
181
|
+
if 'uncertainty' in simulation_data.columns:
|
|
182
|
+
uncertainty = np.asarray(simulation_data['uncertainty'], dtype=float)
|
|
183
|
+
lower = predictions - 30 * (1.0 + uncertainty)
|
|
184
|
+
upper = predictions + 30 * (1.0 + uncertainty)
|
|
185
|
+
ax.fill_between(timestamps, lower, upper,
|
|
186
|
+
alpha=0.2, color=self.config.secondary_color,
|
|
187
|
+
label='Uncertainty')
|
|
188
|
+
|
|
189
|
+
# Reference lines
|
|
190
|
+
ax.axhline(y=120, color='gray', linestyle=':', alpha=0.5)
|
|
191
|
+
ax.axhline(y=self.config.target_low, color='red', linestyle='--', alpha=0.5)
|
|
192
|
+
ax.axhline(y=self.config.target_high, color='orange', linestyle='--', alpha=0.5)
|
|
193
|
+
|
|
194
|
+
# Formatting
|
|
195
|
+
ax.set_xlim(timestamps.min(), timestamps.max())
|
|
196
|
+
ax.set_ylim(40, 350)
|
|
197
|
+
ax.set_xlabel('Time (minutes)', fontsize=10)
|
|
198
|
+
ax.set_ylabel('Glucose (mg/dL)', fontsize=10)
|
|
199
|
+
ax.set_title('Glucose Monitor', fontsize=12, fontweight='bold')
|
|
200
|
+
ax.legend(loc='upper right', fontsize=8)
|
|
201
|
+
ax.grid(True, alpha=0.3)
|
|
202
|
+
|
|
203
|
+
return ax
|
|
204
|
+
|
|
205
|
+
def _create_reasoning_panel(self,
|
|
206
|
+
gs: GridSpec,
|
|
207
|
+
reasoning_logs: List[Dict],
|
|
208
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
209
|
+
"""Create reasoning log ("Why") panel"""
|
|
210
|
+
if ax is None:
|
|
211
|
+
ax = plt.subplot(gs[1, :2])
|
|
212
|
+
|
|
213
|
+
ax.set_xlim(0, 10)
|
|
214
|
+
ax.set_ylim(0, 10)
|
|
215
|
+
ax.axis('off')
|
|
216
|
+
|
|
217
|
+
ax.set_title('Reasoning Log', fontsize=12, fontweight='bold', pad=10)
|
|
218
|
+
|
|
219
|
+
# Display most recent reasoning
|
|
220
|
+
if reasoning_logs:
|
|
221
|
+
latest = reasoning_logs[-1]
|
|
222
|
+
algorithm = latest.get('algorithm', 'Unknown')
|
|
223
|
+
decision = latest.get('decision', 0)
|
|
224
|
+
reasons = latest.get('reasons', ['No reasoning available'])
|
|
225
|
+
|
|
226
|
+
# Algorithm name
|
|
227
|
+
ax.text(0.5, 9.5, f"Algorithm: {algorithm}", fontsize=11, fontweight='bold')
|
|
228
|
+
|
|
229
|
+
# Decision
|
|
230
|
+
ax.text(0.5, 8.5, f"Decision: {decision:.2f} units", fontsize=10)
|
|
231
|
+
|
|
232
|
+
# Primary reason
|
|
233
|
+
ax.text(0.5, 7.5, "Why?", fontsize=10, fontweight='bold', color='blue')
|
|
234
|
+
|
|
235
|
+
for i, reason in enumerate(reasons[:5]):
|
|
236
|
+
y_pos = 6.5 - i * 0.8
|
|
237
|
+
ax.text(0.7, y_pos, f"• {reason}", fontsize=9)
|
|
238
|
+
|
|
239
|
+
# Confidence indicator
|
|
240
|
+
confidence = latest.get('confidence', 0.5)
|
|
241
|
+
conf_color = 'green' if confidence > 0.7 else 'orange' if confidence > 0.4 else 'red'
|
|
242
|
+
ax.text(0.5, 2.5, f"AI Confidence: {confidence:.0%}",
|
|
243
|
+
fontsize=10, color=conf_color, fontweight='bold')
|
|
244
|
+
else:
|
|
245
|
+
ax.text(0.5, 5, "No decisions recorded", fontsize=10, ha='center')
|
|
246
|
+
|
|
247
|
+
return ax
|
|
248
|
+
|
|
249
|
+
def _create_metrics_panel(self,
|
|
250
|
+
gs: GridSpec,
|
|
251
|
+
metrics: Dict,
|
|
252
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
253
|
+
"""Create clinical metrics panel"""
|
|
254
|
+
if ax is None:
|
|
255
|
+
ax = plt.subplot(gs[1, 2:4])
|
|
256
|
+
|
|
257
|
+
ax.set_xlim(0, 10)
|
|
258
|
+
ax.set_ylim(0, 10)
|
|
259
|
+
ax.axis('off')
|
|
260
|
+
|
|
261
|
+
ax.set_title('Clinical Metrics', fontsize=12, fontweight='bold', pad=10)
|
|
262
|
+
|
|
263
|
+
# Key metrics
|
|
264
|
+
metrics_to_show = [
|
|
265
|
+
('TIR (70-180)', f"{metrics.get('tir_70_180', 0):.1f}%"),
|
|
266
|
+
('TIR (70-140)', f"{metrics.get('tir_70_140', 0):.1f}%"),
|
|
267
|
+
('Time <70', f"{metrics.get('tir_below_70', 0):.1f}%"),
|
|
268
|
+
('Time >180', f"{metrics.get('tir_above_180', 0):.1f}%"),
|
|
269
|
+
('CV', f"{metrics.get('cv', 0):.1f}%"),
|
|
270
|
+
('GMI', f"{metrics.get('gmi', 0):.1f}%"),
|
|
271
|
+
('LBGI', f"{metrics.get('lbgi', 0):.2f}"),
|
|
272
|
+
('Total Insulin', f"{metrics.get('total_insulin', 0):.1f} U"),
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
for i, (name, value) in enumerate(metrics_to_show):
|
|
276
|
+
ax.text(0.5, 8.5 - i * 0.9, f"{name}:", fontsize=9, fontweight='bold')
|
|
277
|
+
|
|
278
|
+
# Color code TIR
|
|
279
|
+
if 'TIR' in name and '70-180' in name:
|
|
280
|
+
val = float(value.replace('%', ''))
|
|
281
|
+
color = 'green' if val > 70 else 'orange' if val > 50 else 'red'
|
|
282
|
+
elif 'CV' in name:
|
|
283
|
+
val = float(value.replace('%', ''))
|
|
284
|
+
color = 'green' if val < 36 else 'orange' if val < 50 else 'red'
|
|
285
|
+
else:
|
|
286
|
+
color = 'black'
|
|
287
|
+
|
|
288
|
+
ax.text(4.5, 8.5 - i * 0.9, value, fontsize=9, color=color)
|
|
289
|
+
|
|
290
|
+
return ax
|
|
291
|
+
|
|
292
|
+
def _create_safety_panel(self,
|
|
293
|
+
gs: GridSpec,
|
|
294
|
+
safety_timeline: List[Dict[str, Any]],
|
|
295
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
296
|
+
"""Create safety alerts panel with timeline"""
|
|
297
|
+
if ax is None:
|
|
298
|
+
ax = plt.subplot(gs[1, 4:6])
|
|
299
|
+
|
|
300
|
+
ax.set_xlim(0, 10)
|
|
301
|
+
ax.set_ylim(0, 10)
|
|
302
|
+
ax.axis('off')
|
|
303
|
+
|
|
304
|
+
ax.set_title('Safety Monitor', fontsize=12, fontweight='bold', pad=10)
|
|
305
|
+
|
|
306
|
+
if safety_timeline:
|
|
307
|
+
recent = safety_timeline[-6:]
|
|
308
|
+
for i, entry in enumerate(recent):
|
|
309
|
+
y_pos = 8.5 - i * 1.2
|
|
310
|
+
reason = entry.get('reason', 'UNKNOWN')
|
|
311
|
+
time_min = entry.get('time_min', 0)
|
|
312
|
+
marker = '🔴' if 'HYPO' in reason or 'EMERGENCY' in reason else '🟡'
|
|
313
|
+
ax.text(0.5, y_pos, f"{marker} t={time_min:.0f}m {reason}", fontsize=8)
|
|
314
|
+
ax.text(0.5, 1.0, f"Overrides: {len(safety_timeline)}", fontsize=9, fontweight='bold')
|
|
315
|
+
else:
|
|
316
|
+
ax.text(0.5, 5, "✅ No active alerts", fontsize=10, ha='center', color='green')
|
|
317
|
+
|
|
318
|
+
return ax
|
|
319
|
+
|
|
320
|
+
def _create_algorithm_personality_panel(self,
|
|
321
|
+
gs: GridSpec,
|
|
322
|
+
personality: Dict,
|
|
323
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
324
|
+
"""Create algorithm personality panel"""
|
|
325
|
+
if ax is None:
|
|
326
|
+
ax = plt.subplot(gs[2, :2])
|
|
327
|
+
|
|
328
|
+
ax.set_xlim(0, 10)
|
|
329
|
+
ax.set_ylim(0, 10)
|
|
330
|
+
ax.axis('off')
|
|
331
|
+
|
|
332
|
+
ax.set_title('Algorithm Personality', fontsize=12, fontweight='bold', pad=10)
|
|
333
|
+
|
|
334
|
+
name = personality.get('name', 'Unknown')
|
|
335
|
+
p = personality.get('personality', {})
|
|
336
|
+
|
|
337
|
+
ax.text(0.5, 9.0, name, fontsize=11, fontweight='bold')
|
|
338
|
+
|
|
339
|
+
traits = [
|
|
340
|
+
('Aggressiveness', p.get('aggressiveness', 'Unknown')),
|
|
341
|
+
('Hypo Aversion', p.get('hypo_aversion', 'Unknown')),
|
|
342
|
+
('Response Speed', p.get('response_speed', 'Unknown')),
|
|
343
|
+
('Correction Style', p.get('correction_aggressiveness', 'Unknown')),
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
for i, (trait, value) in enumerate(traits):
|
|
347
|
+
ax.text(0.5, 7.5 - i * 1.0, f"{trait}:", fontsize=9, fontweight='bold')
|
|
348
|
+
ax.text(4.0, 7.5 - i * 1.0, value, fontsize=9)
|
|
349
|
+
|
|
350
|
+
return ax
|
|
351
|
+
|
|
352
|
+
def _create_insulin_panel(self,
|
|
353
|
+
gs: GridSpec,
|
|
354
|
+
simulation_data: pd.DataFrame,
|
|
355
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
356
|
+
"""Create insulin delivery panel"""
|
|
357
|
+
if ax is None:
|
|
358
|
+
ax = plt.subplot(gs[2, 2:4])
|
|
359
|
+
|
|
360
|
+
timestamps = simulation_data['time_minutes']
|
|
361
|
+
insulin = simulation_data['delivered_insulin_units']
|
|
362
|
+
if 'patient_iob_units' in simulation_data.columns:
|
|
363
|
+
iob = simulation_data['patient_iob_units']
|
|
364
|
+
else:
|
|
365
|
+
iob = pd.Series(np.zeros(len(timestamps)))
|
|
366
|
+
|
|
367
|
+
ax.bar(timestamps, insulin, width=4, alpha=0.7,
|
|
368
|
+
color=self.config.secondary_color, label='Insulin Delivered')
|
|
369
|
+
ax.plot(timestamps, iob, color='purple', linewidth=2,
|
|
370
|
+
label='Insulin on Board')
|
|
371
|
+
|
|
372
|
+
ax.set_xlabel('Time (minutes)', fontsize=10)
|
|
373
|
+
ax.set_ylabel('Units', fontsize=10)
|
|
374
|
+
ax.set_title('Insulin Delivery', fontsize=12, fontweight='bold')
|
|
375
|
+
ax.legend(loc='upper right', fontsize=8)
|
|
376
|
+
ax.grid(True, alpha=0.3)
|
|
377
|
+
|
|
378
|
+
return ax
|
|
379
|
+
|
|
380
|
+
def _create_summary_panel(self,
|
|
381
|
+
gs: GridSpec,
|
|
382
|
+
summary: Dict,
|
|
383
|
+
ax: Optional[plt.Axes] = None) -> plt.Axes:
|
|
384
|
+
"""Create summary statistics panel"""
|
|
385
|
+
if ax is None:
|
|
386
|
+
ax = plt.subplot(gs[2, 4:6])
|
|
387
|
+
|
|
388
|
+
ax.set_xlim(0, 10)
|
|
389
|
+
ax.set_ylim(0, 10)
|
|
390
|
+
ax.axis('off')
|
|
391
|
+
|
|
392
|
+
ax.set_title('Summary', fontsize=12, fontweight='bold', pad=10)
|
|
393
|
+
|
|
394
|
+
summary_text = [
|
|
395
|
+
f"Duration: {summary.get('duration_hours', 0):.1f} hours",
|
|
396
|
+
f"Data Points: {summary.get('data_points', 0)}",
|
|
397
|
+
f"Decisions: {summary.get('decisions', 0)}",
|
|
398
|
+
f"Hypo Events: {summary.get('hypo_events', 0)}",
|
|
399
|
+
f"Hyper Events: {summary.get('hyper_events', 0)}",
|
|
400
|
+
f"Avg Glucose: {summary.get('mean_glucose', 0):.0f} mg/dL",
|
|
401
|
+
f"Glucose Range: {summary.get('min_glucose', 0):.0f}-{summary.get('max_glucose', 0):.0f}",
|
|
402
|
+
]
|
|
403
|
+
|
|
404
|
+
for i, line in enumerate(summary_text):
|
|
405
|
+
ax.text(0.5, 8.5 - i * 1.0, line, fontsize=9)
|
|
406
|
+
|
|
407
|
+
return ax
|
|
408
|
+
|
|
409
|
+
def visualize_results(self,
|
|
410
|
+
simulation_data: pd.DataFrame,
|
|
411
|
+
predictions: Optional[np.ndarray] = None,
|
|
412
|
+
reasoning_logs: Optional[List[Dict]] = None,
|
|
413
|
+
metrics: Optional[Dict] = None,
|
|
414
|
+
personality: Optional[Dict] = None,
|
|
415
|
+
save_path: Optional[str] = None) -> plt.Figure:
|
|
416
|
+
"""
|
|
417
|
+
Create complete clinical dashboard visualization.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
simulation_data: DataFrame with simulation results
|
|
421
|
+
predictions: Optional array of glucose predictions
|
|
422
|
+
reasoning_logs: Optional list of reasoning logs
|
|
423
|
+
metrics: Optional clinical metrics dictionary
|
|
424
|
+
personality: Optional algorithm personality
|
|
425
|
+
save_path: Optional path to save figure
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
matplotlib Figure
|
|
429
|
+
"""
|
|
430
|
+
# Create figure
|
|
431
|
+
fig = plt.figure(
|
|
432
|
+
figsize=self.config.figure_size,
|
|
433
|
+
dpi=self.config.dpi
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Create grid layout
|
|
437
|
+
gs = GridSpec(3, 6, figure=fig, hspace=0.35, wspace=0.3)
|
|
438
|
+
|
|
439
|
+
# Create panels
|
|
440
|
+
self._create_glucose_panel(gs, simulation_data, predictions)
|
|
441
|
+
|
|
442
|
+
reasoning_logs = reasoning_logs or []
|
|
443
|
+
self._create_reasoning_panel(gs, reasoning_logs)
|
|
444
|
+
|
|
445
|
+
metrics = metrics or {}
|
|
446
|
+
self._create_metrics_panel(gs, metrics)
|
|
447
|
+
|
|
448
|
+
safety_timeline: List[Dict[str, Any]] = []
|
|
449
|
+
if 'safety_triggered' in simulation_data.columns and 'safety_reason' in simulation_data.columns:
|
|
450
|
+
for _, row in simulation_data.iterrows():
|
|
451
|
+
if bool(row.get('safety_triggered', False)):
|
|
452
|
+
safety_timeline.append({
|
|
453
|
+
'time_min': row.get('time_minutes', 0),
|
|
454
|
+
'reason': row.get('safety_reason', 'UNKNOWN')
|
|
455
|
+
})
|
|
456
|
+
self._create_safety_panel(gs, safety_timeline)
|
|
457
|
+
|
|
458
|
+
personality = personality or {}
|
|
459
|
+
self._create_algorithm_personality_panel(gs, personality)
|
|
460
|
+
|
|
461
|
+
self._create_insulin_panel(gs, simulation_data)
|
|
462
|
+
|
|
463
|
+
# Calculate summary
|
|
464
|
+
glucose_col = pd.to_numeric(simulation_data['glucose_actual_mgdl'])
|
|
465
|
+
summary = {
|
|
466
|
+
'duration_hours': (simulation_data['time_minutes'].max() -
|
|
467
|
+
simulation_data['time_minutes'].min()) / 60,
|
|
468
|
+
'data_points': len(simulation_data),
|
|
469
|
+
'decisions': (simulation_data['delivered_insulin_units'].gt(0)).sum(),
|
|
470
|
+
'hypo_events': ((glucose_col.lt(70.0)) & (glucose_col.diff().lt(0))).sum(),
|
|
471
|
+
'hyper_events': ((glucose_col.gt(250.0)) & (glucose_col.diff().gt(0))).sum(),
|
|
472
|
+
'mean_glucose': glucose_col.mean(),
|
|
473
|
+
'min_glucose': glucose_col.min(),
|
|
474
|
+
'max_glucose': glucose_col.max(),
|
|
475
|
+
}
|
|
476
|
+
self._create_summary_panel(gs, summary)
|
|
477
|
+
|
|
478
|
+
# Add title
|
|
479
|
+
fig.suptitle('IINTS-AF Clinical Control Center',
|
|
480
|
+
fontsize=16, fontweight='bold', y=0.98)
|
|
481
|
+
|
|
482
|
+
plt.tight_layout(rect=(0, 0, 1, 0.96))
|
|
483
|
+
|
|
484
|
+
if save_path:
|
|
485
|
+
plt.savefig(save_path, dpi=self.config.dpi, bbox_inches='tight')
|
|
486
|
+
print(f" Dashboard saved to: {save_path}")
|
|
487
|
+
|
|
488
|
+
return fig
|
|
489
|
+
|
|
490
|
+
def compare_battle(self,
|
|
491
|
+
battle_report,
|
|
492
|
+
save_path: Optional[str] = None) -> plt.Figure:
|
|
493
|
+
"""
|
|
494
|
+
Create battle mode comparison dashboard.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
battle_report: BattleReport object
|
|
498
|
+
save_path: Optional path to save
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
matplotlib Figure
|
|
502
|
+
"""
|
|
503
|
+
fig = plt.figure(
|
|
504
|
+
figsize=(20, 14),
|
|
505
|
+
dpi=self.config.dpi
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
gs = GridSpec(3, 2, figure=fig, hspace=0.35, wspace=0.3)
|
|
509
|
+
|
|
510
|
+
# Title
|
|
511
|
+
fig.suptitle(f' Battle Mode: {battle_report.battle_name}\n Winner: {battle_report.winner}',
|
|
512
|
+
fontsize=14, fontweight='bold')
|
|
513
|
+
|
|
514
|
+
# Rankings summary
|
|
515
|
+
ax_rankings = plt.subplot(gs[0, :])
|
|
516
|
+
ax_rankings.axis('off')
|
|
517
|
+
|
|
518
|
+
ranking_text = "RANKINGS\n" + "="*50 + "\n"
|
|
519
|
+
for i, rank in enumerate(battle_report.rankings, 1):
|
|
520
|
+
medal = "1st" if i == 1 else "2nd" if i == 2 else "3rd"
|
|
521
|
+
ranking_text += (
|
|
522
|
+
f"{medal} {i}. {rank['participant']}: "
|
|
523
|
+
f"Score={rank['overall_score']:.3f}, "
|
|
524
|
+
f"TIR={rank['tir']:.1f}%, "
|
|
525
|
+
f"CV={rank['cv']:.1f}%\n"
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
ax_rankings.text(0.5, 0.5, ranking_text, fontsize=11,
|
|
529
|
+
ha='center', va='center', transform=ax_rankings.transAxes,
|
|
530
|
+
family='monospace')
|
|
531
|
+
|
|
532
|
+
# Comparison bars
|
|
533
|
+
ax_comparison = plt.subplot(gs[1, :])
|
|
534
|
+
|
|
535
|
+
algorithms = [r['participant'] for r in battle_report.rankings]
|
|
536
|
+
tir_scores = [r['tir'] for r in battle_report.rankings]
|
|
537
|
+
cv_scores = [r['cv'] for r in battle_report.rankings]
|
|
538
|
+
|
|
539
|
+
x = np.arange(len(algorithms))
|
|
540
|
+
width = 0.35
|
|
541
|
+
|
|
542
|
+
bars1 = ax_comparison.bar(x - width/2, tir_scores, width,
|
|
543
|
+
label='TIR (%)', color='#2196F3')
|
|
544
|
+
bars2 = ax_comparison.bar(x + width/2, cv_scores, width,
|
|
545
|
+
label='CV (%)', color='#4CAF50')
|
|
546
|
+
|
|
547
|
+
ax_comparison.set_ylabel('Percentage')
|
|
548
|
+
ax_comparison.set_title('Algorithm Comparison: TIR vs CV')
|
|
549
|
+
ax_comparison.set_xticks(x)
|
|
550
|
+
ax_comparison.set_xticklabels(algorithms)
|
|
551
|
+
ax_comparison.legend()
|
|
552
|
+
ax_comparison.grid(True, alpha=0.3, axis='y')
|
|
553
|
+
|
|
554
|
+
# Add value labels
|
|
555
|
+
for bar in bars1:
|
|
556
|
+
height = bar.get_height()
|
|
557
|
+
ax_comparison.text(bar.get_x() + bar.get_width()/2., height + 1,
|
|
558
|
+
f'{height:.1f}%', ha='center', fontsize=9)
|
|
559
|
+
|
|
560
|
+
# Detailed metrics table
|
|
561
|
+
ax_table = plt.subplot(gs[2, :])
|
|
562
|
+
ax_table.axis('off')
|
|
563
|
+
|
|
564
|
+
table_data = []
|
|
565
|
+
headers = ['Algorithm', 'TIR', 'TIR Tight', '<70', '>180', 'CV', 'GMI', 'LBGI']
|
|
566
|
+
|
|
567
|
+
for rank in battle_report.rankings:
|
|
568
|
+
table_data.append([
|
|
569
|
+
rank['participant'],
|
|
570
|
+
f"{rank['tir']:.1f}%",
|
|
571
|
+
f"{rank['tir_tight']:.1f}%",
|
|
572
|
+
f"{rank['time_below_70']:.1f}%",
|
|
573
|
+
f"{rank['time_above_180']:.1f}%",
|
|
574
|
+
f"{rank['cv']:.1f}%",
|
|
575
|
+
f"{rank['gmi']:.1f}%",
|
|
576
|
+
f"{rank['lbgi']:.2f}"
|
|
577
|
+
])
|
|
578
|
+
|
|
579
|
+
table = ax_table.table(
|
|
580
|
+
cellText=table_data,
|
|
581
|
+
colLabels=headers,
|
|
582
|
+
loc='center',
|
|
583
|
+
cellLoc='center'
|
|
584
|
+
)
|
|
585
|
+
table.auto_set_font_size(False)
|
|
586
|
+
table.set_fontsize(10)
|
|
587
|
+
table.scale(1.2, 1.5)
|
|
588
|
+
|
|
589
|
+
plt.tight_layout(rect=(0, 0, 1, 0.95))
|
|
590
|
+
|
|
591
|
+
if save_path:
|
|
592
|
+
plt.savefig(save_path, dpi=self.config.dpi, bbox_inches='tight')
|
|
593
|
+
|
|
594
|
+
return fig
|
|
595
|
+
|
|
596
|
+
def update(self, state: DashboardState):
|
|
597
|
+
"""Update dashboard with new state (for real-time mode)"""
|
|
598
|
+
self.state = state
|
|
599
|
+
self.history.append(state.to_dict())
|
|
600
|
+
|
|
601
|
+
def export_state(self) -> Dict:
|
|
602
|
+
"""Export current dashboard state"""
|
|
603
|
+
return {
|
|
604
|
+
'state': self.state.to_dict(),
|
|
605
|
+
'history': self.history,
|
|
606
|
+
'timestamp': datetime.now().isoformat()
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def demo_clinical_cockpit():
|
|
611
|
+
"""Demonstrate clinical cockpit functionality"""
|
|
612
|
+
print("=" * 70)
|
|
613
|
+
print("CLINICAL CONTROL CENTER DEMONSTRATION")
|
|
614
|
+
print("=" * 70)
|
|
615
|
+
|
|
616
|
+
# Generate sample data
|
|
617
|
+
np.random.seed(42)
|
|
618
|
+
n_points = 97
|
|
619
|
+
|
|
620
|
+
timestamps = np.arange(0, n_points * 5, 5)
|
|
621
|
+
glucose = 120 + 30 * np.sin(timestamps / (24 * 12 / (2 * np.pi))) + np.random.normal(0, 15, n_points)
|
|
622
|
+
glucose = np.clip(glucose, 40, 350)
|
|
623
|
+
|
|
624
|
+
simulation_data = pd.DataFrame({
|
|
625
|
+
'time_minutes': timestamps,
|
|
626
|
+
'glucose_actual_mgdl': glucose,
|
|
627
|
+
'delivered_insulin_units': np.random.uniform(0, 1, n_points),
|
|
628
|
+
'patient_iob_units': np.cumsum(np.random.uniform(0, 0.1, n_points)),
|
|
629
|
+
'uncertainty': np.random.uniform(0.1, 0.4, n_points)
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
# Create cockpit
|
|
633
|
+
cockpit = ClinicalCockpit()
|
|
634
|
+
|
|
635
|
+
# Create visualization
|
|
636
|
+
print("\n Generating clinical dashboard...")
|
|
637
|
+
predictions = np.roll(glucose, 1)
|
|
638
|
+
predictions[0] = glucose[0]
|
|
639
|
+
|
|
640
|
+
reasoning_logs = [
|
|
641
|
+
{
|
|
642
|
+
'algorithm': 'PID Controller',
|
|
643
|
+
'decision': 0.5,
|
|
644
|
+
'reasons': [
|
|
645
|
+
'Glucose elevated at 145 mg/dL',
|
|
646
|
+
'Rising at 1.5 mg/dL/min',
|
|
647
|
+
'No significant IOB'
|
|
648
|
+
],
|
|
649
|
+
'confidence': 0.85
|
|
650
|
+
}
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
metrics = {
|
|
654
|
+
'tir_70_180': 72.5,
|
|
655
|
+
'tir_70_140': 45.2,
|
|
656
|
+
'tir_below_70': 5.1,
|
|
657
|
+
'tir_above_180': 22.4,
|
|
658
|
+
'cv': 32.5,
|
|
659
|
+
'gmi': 6.8,
|
|
660
|
+
'lbgi': 2.1,
|
|
661
|
+
'total_insulin': 45.2
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
personality = {
|
|
665
|
+
'name': 'PID Controller',
|
|
666
|
+
'personality': {
|
|
667
|
+
'aggressiveness': 'Moderate',
|
|
668
|
+
'hypo_aversion': 'Moderate',
|
|
669
|
+
'response_speed': 'Fast',
|
|
670
|
+
'correction_aggressiveness': 'Moderate'
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
fig = cockpit.visualize_results(
|
|
675
|
+
simulation_data=simulation_data,
|
|
676
|
+
predictions=predictions,
|
|
677
|
+
reasoning_logs=reasoning_logs,
|
|
678
|
+
metrics=metrics,
|
|
679
|
+
personality=personality,
|
|
680
|
+
save_path="results/visualization/clinical_cockpit.png"
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
print(" Dashboard saved to: results/visualization/clinical_cockpit.png")
|
|
684
|
+
|
|
685
|
+
print("\n" + "=" * 70)
|
|
686
|
+
print("CLINICAL CONTROL CENTER DEMONSTRATION COMPLETE")
|
|
687
|
+
print("=" * 70)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
if __name__ == "__main__":
|
|
691
|
+
demo_clinical_cockpit()
|