iints-sdk-python35 0.1.7__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 +134 -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_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/reporting.py +261 -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 +300 -0
- iints/api/template_algorithm.py +195 -0
- iints/cli/__init__.py +0 -0
- iints/cli/cli.py +1286 -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 +86 -0
- iints/core/algorithms/discovery.py +92 -0
- iints/core/algorithms/fixed_basal_bolus.py +52 -0
- iints/core/algorithms/hybrid_algorithm.py +92 -0
- iints/core/algorithms/lstm_algorithm.py +138 -0
- iints/core/algorithms/mock_algorithms.py +69 -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 +155 -0
- iints/core/patient/__init__.py +3 -0
- iints/core/patient/models.py +246 -0
- iints/core/patient/patient_factory.py +117 -0
- iints/core/patient/profile.py +41 -0
- iints/core/safety/__init__.py +4 -0
- iints/core/safety/input_validator.py +87 -0
- iints/core/safety/supervisor.py +29 -0
- iints/core/simulation/__init__.py +0 -0
- iints/core/simulation/scenario_parser.py +61 -0
- iints/core/simulator.py +519 -0
- iints/core/supervisor.py +275 -0
- iints/data/__init__.py +42 -0
- iints/data/adapter.py +142 -0
- iints/data/column_mapper.py +398 -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/quality_checker.py +550 -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 +192 -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/presets/__init__.py +28 -0
- iints/presets/presets.json +114 -0
- iints/templates/__init__.py +0 -0
- iints/templates/default_algorithm.py +56 -0
- iints/templates/scenarios/__init__.py +0 -0
- iints/templates/scenarios/example_scenario.json +34 -0
- iints/utils/__init__.py +3 -0
- iints/utils/plotting.py +50 -0
- iints/validation/__init__.py +117 -0
- iints/validation/schemas.py +72 -0
- iints/visualization/__init__.py +34 -0
- iints/visualization/cockpit.py +691 -0
- iints/visualization/uncertainty_cloud.py +612 -0
- iints_sdk_python35-0.1.7.dist-info/METADATA +122 -0
- iints_sdk_python35-0.1.7.dist-info/RECORD +93 -0
- iints_sdk_python35-0.1.7.dist-info/WHEEL +5 -0
- iints_sdk_python35-0.1.7.dist-info/entry_points.txt +2 -0
- iints_sdk_python35-0.1.7.dist-info/licenses/LICENSE +28 -0
- iints_sdk_python35-0.1.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Dict, List, Tuple, Optional, Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
class ReliabilityLevel(Enum):
|
|
8
|
+
HIGH = "high"
|
|
9
|
+
MEDIUM = "medium"
|
|
10
|
+
LOW = "low"
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ValidationResult:
|
|
15
|
+
passed: bool
|
|
16
|
+
reliability_score: float # 0-100%
|
|
17
|
+
level: ReliabilityLevel
|
|
18
|
+
issues: List[str]
|
|
19
|
+
warnings: List[str]
|
|
20
|
+
|
|
21
|
+
class DataIntegrityValidator:
|
|
22
|
+
"""Validates data integrity for reverse engineering analysis."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
# Physiological limits
|
|
26
|
+
self.max_glucose_rate = 10 # mg/dL per minute
|
|
27
|
+
self.glucose_range = (20, 600) # Physiologically possible range
|
|
28
|
+
self.max_insulin_per_step = 5.0 # Units per 5-min step
|
|
29
|
+
|
|
30
|
+
def validate_glucose_data(self, glucose_values: List[float], timestamps: List[float]) -> ValidationResult:
|
|
31
|
+
"""Validate glucose data for physiological plausibility."""
|
|
32
|
+
issues = []
|
|
33
|
+
warnings: List[str] = []
|
|
34
|
+
score = 100.0
|
|
35
|
+
|
|
36
|
+
# Check range
|
|
37
|
+
for i, glucose in enumerate(glucose_values):
|
|
38
|
+
if not (self.glucose_range[0] <= glucose <= self.glucose_range[1]):
|
|
39
|
+
issues.append(f"Glucose {glucose} at step {i} outside physiological range")
|
|
40
|
+
score -= 10
|
|
41
|
+
|
|
42
|
+
# Check rate of change
|
|
43
|
+
for i in range(1, len(glucose_values)):
|
|
44
|
+
if len(timestamps) > i:
|
|
45
|
+
time_diff = timestamps[i] - timestamps[i-1]
|
|
46
|
+
glucose_diff = abs(glucose_values[i] - glucose_values[i-1])
|
|
47
|
+
rate = glucose_diff / time_diff if time_diff > 0 else float('inf')
|
|
48
|
+
|
|
49
|
+
if rate > self.max_glucose_rate:
|
|
50
|
+
issues.append(f"Impossible glucose rate: {rate:.1f} mg/dL/min at step {i}")
|
|
51
|
+
score -= 15
|
|
52
|
+
|
|
53
|
+
# Check for missing values (NaN)
|
|
54
|
+
nan_count = sum(1 for g in glucose_values if pd.isna(g))
|
|
55
|
+
if nan_count > 0:
|
|
56
|
+
warnings.append(f"{nan_count} missing glucose values detected")
|
|
57
|
+
score -= nan_count * 5
|
|
58
|
+
|
|
59
|
+
# Determine reliability level
|
|
60
|
+
if score >= 90:
|
|
61
|
+
level = ReliabilityLevel.HIGH
|
|
62
|
+
elif score >= 70:
|
|
63
|
+
level = ReliabilityLevel.MEDIUM
|
|
64
|
+
elif score >= 50:
|
|
65
|
+
level = ReliabilityLevel.LOW
|
|
66
|
+
else:
|
|
67
|
+
level = ReliabilityLevel.CRITICAL
|
|
68
|
+
|
|
69
|
+
return ValidationResult(
|
|
70
|
+
passed=len(issues) == 0,
|
|
71
|
+
reliability_score=max(0, score),
|
|
72
|
+
level=level,
|
|
73
|
+
issues=issues,
|
|
74
|
+
warnings=warnings
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def validate_insulin_data(self, insulin_values: List[float]) -> ValidationResult:
|
|
78
|
+
"""Validate insulin delivery data."""
|
|
79
|
+
issues = []
|
|
80
|
+
warnings: List[str] = []
|
|
81
|
+
score = 100.0
|
|
82
|
+
|
|
83
|
+
for i, insulin in enumerate(insulin_values):
|
|
84
|
+
if insulin < 0:
|
|
85
|
+
issues.append(f"Negative insulin {insulin} at step {i}")
|
|
86
|
+
score -= 20
|
|
87
|
+
elif insulin > self.max_insulin_per_step:
|
|
88
|
+
warnings.append(f"High insulin dose {insulin} at step {i}")
|
|
89
|
+
score -= 5
|
|
90
|
+
|
|
91
|
+
level = ReliabilityLevel.HIGH if score >= 90 else ReliabilityLevel.MEDIUM if score >= 70 else ReliabilityLevel.LOW
|
|
92
|
+
|
|
93
|
+
return ValidationResult(
|
|
94
|
+
passed=len(issues) == 0,
|
|
95
|
+
reliability_score=max(0, score),
|
|
96
|
+
level=level,
|
|
97
|
+
issues=issues,
|
|
98
|
+
warnings=warnings
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
class AlgorithmicDriftDetector:
|
|
102
|
+
"""Detects when AI algorithms drift from safe baseline behavior."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, drift_threshold=0.5): # 50% difference threshold
|
|
105
|
+
self.drift_threshold = drift_threshold
|
|
106
|
+
|
|
107
|
+
def detect_drift(self, ai_outputs: List[float], baseline_outputs: List[float]) -> ValidationResult:
|
|
108
|
+
"""Compare AI outputs against rule-based baseline."""
|
|
109
|
+
issues = []
|
|
110
|
+
warnings: List[str] = []
|
|
111
|
+
score = 100.0
|
|
112
|
+
|
|
113
|
+
if len(ai_outputs) != len(baseline_outputs):
|
|
114
|
+
issues.append("AI and baseline output lengths don't match")
|
|
115
|
+
return ValidationResult(False, 0, ReliabilityLevel.CRITICAL, issues, warnings)
|
|
116
|
+
|
|
117
|
+
drift_count = 0
|
|
118
|
+
extreme_drift_count = 0
|
|
119
|
+
|
|
120
|
+
for i, (ai_val, baseline_val) in enumerate(zip(ai_outputs, baseline_outputs)):
|
|
121
|
+
if baseline_val == 0:
|
|
122
|
+
if ai_val > 0.1: # AI gives insulin when baseline gives none
|
|
123
|
+
drift_count += 1
|
|
124
|
+
warnings.append(f"AI delivers insulin ({ai_val:.2f}) when baseline gives none at step {i}")
|
|
125
|
+
else:
|
|
126
|
+
relative_diff = abs(ai_val - baseline_val) / baseline_val
|
|
127
|
+
if relative_diff > self.drift_threshold:
|
|
128
|
+
drift_count += 1
|
|
129
|
+
if relative_diff > 1.0: # 100% difference
|
|
130
|
+
extreme_drift_count += 1
|
|
131
|
+
issues.append(f"Extreme drift: AI={ai_val:.2f}, Baseline={baseline_val:.2f} at step {i}")
|
|
132
|
+
else:
|
|
133
|
+
warnings.append(f"Drift detected: {relative_diff:.1%} difference at step {i}")
|
|
134
|
+
|
|
135
|
+
# Calculate score based on drift frequency
|
|
136
|
+
drift_rate = drift_count / len(ai_outputs)
|
|
137
|
+
extreme_drift_rate = extreme_drift_count / len(ai_outputs)
|
|
138
|
+
|
|
139
|
+
score -= drift_rate * 50 # Penalize drift
|
|
140
|
+
score -= extreme_drift_rate * 30 # Extra penalty for extreme drift
|
|
141
|
+
|
|
142
|
+
if extreme_drift_count > 0:
|
|
143
|
+
level = ReliabilityLevel.CRITICAL
|
|
144
|
+
elif drift_rate > 0.3:
|
|
145
|
+
level = ReliabilityLevel.LOW
|
|
146
|
+
elif drift_rate > 0.1:
|
|
147
|
+
level = ReliabilityLevel.MEDIUM
|
|
148
|
+
else:
|
|
149
|
+
level = ReliabilityLevel.HIGH
|
|
150
|
+
|
|
151
|
+
return ValidationResult(
|
|
152
|
+
passed=extreme_drift_count == 0,
|
|
153
|
+
reliability_score=max(0, score),
|
|
154
|
+
level=level,
|
|
155
|
+
issues=issues,
|
|
156
|
+
warnings=warnings
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
class StatisticalReliabilityChecker:
|
|
160
|
+
"""Checks statistical reliability of Monte Carlo results."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, min_runs=10, max_cv=0.3): # Max 30% coefficient of variation
|
|
163
|
+
self.min_runs = min_runs
|
|
164
|
+
self.max_cv = max_cv
|
|
165
|
+
|
|
166
|
+
def check_monte_carlo_reliability(self, results: List[List[float]]) -> ValidationResult:
|
|
167
|
+
"""Check if Monte Carlo results are statistically reliable."""
|
|
168
|
+
issues = []
|
|
169
|
+
warnings: List[str] = []
|
|
170
|
+
score = 100.0
|
|
171
|
+
|
|
172
|
+
if len(results) < self.min_runs:
|
|
173
|
+
issues.append(f"Insufficient runs: {len(results)} < {self.min_runs}")
|
|
174
|
+
score -= 30
|
|
175
|
+
|
|
176
|
+
# Calculate coefficient of variation for each time step
|
|
177
|
+
if len(results) > 1:
|
|
178
|
+
results_array = np.array(results)
|
|
179
|
+
means = np.mean(results_array, axis=0)
|
|
180
|
+
stds = np.std(results_array, axis=0)
|
|
181
|
+
|
|
182
|
+
# Avoid division by zero
|
|
183
|
+
cvs = np.divide(stds, means, out=np.zeros_like(stds), where=means!=0)
|
|
184
|
+
|
|
185
|
+
high_variance_steps = np.sum(cvs > self.max_cv)
|
|
186
|
+
if high_variance_steps > 0:
|
|
187
|
+
variance_rate = high_variance_steps / len(cvs)
|
|
188
|
+
if variance_rate > 0.5:
|
|
189
|
+
issues.append(f"High variance in {variance_rate:.1%} of time steps")
|
|
190
|
+
score -= 40
|
|
191
|
+
else:
|
|
192
|
+
warnings.append(f"Moderate variance in {variance_rate:.1%} of time steps")
|
|
193
|
+
score -= 20
|
|
194
|
+
|
|
195
|
+
level = ReliabilityLevel.HIGH if score >= 90 else ReliabilityLevel.MEDIUM if score >= 70 else ReliabilityLevel.LOW
|
|
196
|
+
|
|
197
|
+
return ValidationResult(
|
|
198
|
+
passed=len(issues) == 0,
|
|
199
|
+
reliability_score=max(0, score),
|
|
200
|
+
level=level,
|
|
201
|
+
issues=issues,
|
|
202
|
+
warnings=warnings
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
class ReverseEngineeringValidator:
|
|
206
|
+
"""Main validator for reverse engineering analysis."""
|
|
207
|
+
|
|
208
|
+
def __init__(self):
|
|
209
|
+
self.data_validator = DataIntegrityValidator()
|
|
210
|
+
self.drift_detector = AlgorithmicDriftDetector()
|
|
211
|
+
self.reliability_checker = StatisticalReliabilityChecker()
|
|
212
|
+
|
|
213
|
+
def validate_simulation_results(self, simulation_df: pd.DataFrame,
|
|
214
|
+
baseline_results: Optional[List[float]] = None,
|
|
215
|
+
monte_carlo_results: Optional[List[List[float]]] = None) -> Dict[str, ValidationResult]:
|
|
216
|
+
"""Comprehensive validation of simulation results."""
|
|
217
|
+
|
|
218
|
+
results = {}
|
|
219
|
+
|
|
220
|
+
# 1. Data integrity validation
|
|
221
|
+
glucose_values = simulation_df['glucose_actual_mgdl'].tolist()
|
|
222
|
+
timestamps = simulation_df['time_minutes'].tolist()
|
|
223
|
+
insulin_values = simulation_df['delivered_insulin_units'].tolist()
|
|
224
|
+
|
|
225
|
+
results['glucose_integrity'] = self.data_validator.validate_glucose_data(glucose_values, timestamps)
|
|
226
|
+
results['insulin_integrity'] = self.data_validator.validate_insulin_data(insulin_values)
|
|
227
|
+
|
|
228
|
+
# 2. Algorithmic drift detection
|
|
229
|
+
if baseline_results:
|
|
230
|
+
results['algorithmic_drift'] = self.drift_detector.detect_drift(insulin_values, baseline_results)
|
|
231
|
+
|
|
232
|
+
# 3. Statistical reliability
|
|
233
|
+
if monte_carlo_results:
|
|
234
|
+
results['statistical_reliability'] = self.reliability_checker.check_monte_carlo_reliability(monte_carlo_results)
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
def generate_reliability_report(self, validation_results: Dict[str, ValidationResult]) -> Dict[str, Any]:
|
|
239
|
+
"""Generate comprehensive reliability report."""
|
|
240
|
+
|
|
241
|
+
overall_score = np.mean([result.reliability_score for result in validation_results.values()])
|
|
242
|
+
|
|
243
|
+
all_issues = []
|
|
244
|
+
all_warnings = []
|
|
245
|
+
|
|
246
|
+
for category, result in validation_results.items():
|
|
247
|
+
all_issues.extend([f"{category}: {issue}" for issue in result.issues])
|
|
248
|
+
all_warnings.extend([f"{category}: {warning}" for warning in result.warnings])
|
|
249
|
+
|
|
250
|
+
# Determine overall reliability
|
|
251
|
+
if overall_score >= 90:
|
|
252
|
+
overall_level = ReliabilityLevel.HIGH
|
|
253
|
+
recommendation = "Results are highly reliable for reverse engineering analysis"
|
|
254
|
+
elif overall_score >= 70:
|
|
255
|
+
overall_level = ReliabilityLevel.MEDIUM
|
|
256
|
+
recommendation = "Results are moderately reliable - consider additional validation"
|
|
257
|
+
elif overall_score >= 50:
|
|
258
|
+
overall_level = ReliabilityLevel.LOW
|
|
259
|
+
recommendation = "Results have low reliability - use with caution"
|
|
260
|
+
else:
|
|
261
|
+
overall_level = ReliabilityLevel.CRITICAL
|
|
262
|
+
recommendation = "Results are unreliable - do not use for analysis"
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"overall_reliability_score": overall_score,
|
|
266
|
+
"overall_level": overall_level.value,
|
|
267
|
+
"recommendation": recommendation,
|
|
268
|
+
"total_issues": len(all_issues),
|
|
269
|
+
"total_warnings": len(all_warnings),
|
|
270
|
+
"issues": all_issues,
|
|
271
|
+
"warnings": all_warnings,
|
|
272
|
+
"category_scores": {category: result.reliability_score for category, result in validation_results.items()}
|
|
273
|
+
}
|
iints/api/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AlgorithmInput:
|
|
8
|
+
"""Dataclass for inputs to the insulin prediction algorithm."""
|
|
9
|
+
current_glucose: float
|
|
10
|
+
time_step: float
|
|
11
|
+
insulin_on_board: float = 0.0
|
|
12
|
+
carb_intake: float = 0.0
|
|
13
|
+
patient_state: Dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
current_time: float = 0.0 # Added current_time
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AlgorithmMetadata:
|
|
19
|
+
"""Metadata for algorithm registration and identification"""
|
|
20
|
+
name: str
|
|
21
|
+
version: str = "1.0.0"
|
|
22
|
+
author: str = "IINTS-AF Team"
|
|
23
|
+
paper_reference: Optional[str] = None
|
|
24
|
+
description: str = ""
|
|
25
|
+
algorithm_type: str = "rule_based" # 'rule_based', 'ml', 'hybrid'
|
|
26
|
+
requires_training: bool = False
|
|
27
|
+
supported_scenarios: List[str] = field(default_factory=lambda: [
|
|
28
|
+
'standard_meal', 'unannounced_meal', 'exercise',
|
|
29
|
+
'stress', 'hypoglycemia', 'hyperglycemia'
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> Dict:
|
|
33
|
+
return {
|
|
34
|
+
'name': self.name,
|
|
35
|
+
'version': self.version,
|
|
36
|
+
'author': self.author,
|
|
37
|
+
'paper_reference': self.paper_reference,
|
|
38
|
+
'description': self.description,
|
|
39
|
+
'algorithm_type': self.algorithm_type,
|
|
40
|
+
'requires_training': self.requires_training,
|
|
41
|
+
'supported_scenarios': self.supported_scenarios
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class AlgorithmResult:
|
|
47
|
+
"""Result of an insulin prediction with uncertainty"""
|
|
48
|
+
total_insulin_delivered: float
|
|
49
|
+
bolus_insulin: float = 0.0
|
|
50
|
+
basal_insulin: float = 0.0
|
|
51
|
+
correction_bolus: float = 0.0
|
|
52
|
+
meal_bolus: float = 0.0
|
|
53
|
+
|
|
54
|
+
# Uncertainty quantification
|
|
55
|
+
uncertainty: float = 0.0 # 0.0 = certain, 1.0 = very uncertain
|
|
56
|
+
confidence_interval: tuple = (0.0, 0.0) # (lower, upper)
|
|
57
|
+
|
|
58
|
+
# Clinical reasoning
|
|
59
|
+
primary_reason: str = ""
|
|
60
|
+
secondary_reasons: List[str] = field(default_factory=list)
|
|
61
|
+
safety_constraints: List[str] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
# Metadata
|
|
64
|
+
algorithm_name: str = ""
|
|
65
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict:
|
|
68
|
+
return {
|
|
69
|
+
'total_insulin_delivered': self.total_insulin_delivered,
|
|
70
|
+
'bolus_insulin': self.bolus_insulin,
|
|
71
|
+
'basal_insulin': self.basal_insulin,
|
|
72
|
+
'correction_bolus': self.correction_bolus,
|
|
73
|
+
'meal_bolus': self.meal_bolus,
|
|
74
|
+
'uncertainty': self.uncertainty,
|
|
75
|
+
'confidence_interval': self.confidence_interval,
|
|
76
|
+
'primary_reason': self.primary_reason,
|
|
77
|
+
'secondary_reasons': self.secondary_reasons,
|
|
78
|
+
'safety_constraints': self.safety_constraints,
|
|
79
|
+
'algorithm_name': self.algorithm_name,
|
|
80
|
+
'timestamp': self.timestamp.isoformat()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class WhyLogEntry:
|
|
86
|
+
"""Single entry in the Why Log explaining a decision reason"""
|
|
87
|
+
reason: str
|
|
88
|
+
category: str # 'glucose_level', 'velocity', 'insulin_on_board', 'safety', 'context'
|
|
89
|
+
value: Any = None
|
|
90
|
+
clinical_impact: str = ""
|
|
91
|
+
|
|
92
|
+
def to_dict(self) -> Dict:
|
|
93
|
+
return {
|
|
94
|
+
'reason': self.reason,
|
|
95
|
+
'category': self.category,
|
|
96
|
+
'value': self.value,
|
|
97
|
+
'clinical_impact': self.clinical_impact
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class InsulinAlgorithm(ABC):
|
|
101
|
+
"""
|
|
102
|
+
Abstract base class for insulin delivery algorithms.
|
|
103
|
+
|
|
104
|
+
All specific insulin algorithms used in the simulation framework should
|
|
105
|
+
inherit from this class and implement its abstract methods.
|
|
106
|
+
|
|
107
|
+
This class supports the Plug-and-Play architecture, allowing any algorithm
|
|
108
|
+
to be registered and compared in Battle Mode.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
112
|
+
"""
|
|
113
|
+
Initializes the algorithm with its specific settings.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
settings (Dict[str, Any]): A dictionary of algorithm-specific parameters.
|
|
117
|
+
"""
|
|
118
|
+
self.settings = settings if settings is not None else {}
|
|
119
|
+
self.state: Dict[str, Any] = {} # To store internal algorithm state across calls
|
|
120
|
+
self.why_log: List[WhyLogEntry] = [] # Decision reasoning log
|
|
121
|
+
self._metadata: Optional[AlgorithmMetadata] = None # Lazy-loaded metadata
|
|
122
|
+
self.isf = self.settings.get('isf', 50.0) # Default ISF
|
|
123
|
+
self.icr = self.settings.get('icr', 10.0) # Default ICR
|
|
124
|
+
|
|
125
|
+
def set_isf(self, isf: float):
|
|
126
|
+
"""Set the Insulin Sensitivity Factor (mg/dL per unit)."""
|
|
127
|
+
if isf <= 0:
|
|
128
|
+
raise ValueError("ISF must be a positive value.")
|
|
129
|
+
self.isf = isf
|
|
130
|
+
|
|
131
|
+
def set_icr(self, icr: float):
|
|
132
|
+
"""Set the Insulin-to-Carb Ratio (grams per unit)."""
|
|
133
|
+
if icr <= 0:
|
|
134
|
+
raise ValueError("ICR must be a positive value.")
|
|
135
|
+
self.icr = icr
|
|
136
|
+
|
|
137
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
138
|
+
"""
|
|
139
|
+
Get algorithm metadata. Override in subclasses for custom metadata.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
AlgorithmMetadata: Information about the algorithm for registration
|
|
143
|
+
"""
|
|
144
|
+
if self._metadata is None:
|
|
145
|
+
# Default metadata - override in subclasses
|
|
146
|
+
self._metadata = AlgorithmMetadata(
|
|
147
|
+
name=self.__class__.__name__,
|
|
148
|
+
version="1.0.0",
|
|
149
|
+
author="IINTS-AF Team",
|
|
150
|
+
description=f"Insulin algorithm: {self.__class__.__name__}",
|
|
151
|
+
algorithm_type="rule_based"
|
|
152
|
+
)
|
|
153
|
+
return self._metadata
|
|
154
|
+
|
|
155
|
+
def set_algorithm_metadata(self, metadata: AlgorithmMetadata):
|
|
156
|
+
"""Set custom algorithm metadata"""
|
|
157
|
+
self._metadata = metadata
|
|
158
|
+
|
|
159
|
+
def calculate_uncertainty(self, data: AlgorithmInput) -> float:
|
|
160
|
+
"""
|
|
161
|
+
Calculate uncertainty score for the current prediction.
|
|
162
|
+
|
|
163
|
+
Override in subclasses to implement custom uncertainty quantification.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
data: Current algorithm input
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
float: Uncertainty score between 0.0 (certain) and 1.0 (very uncertain)
|
|
170
|
+
"""
|
|
171
|
+
# Default: low uncertainty for rule-based algorithms
|
|
172
|
+
return 0.1
|
|
173
|
+
|
|
174
|
+
def calculate_confidence_interval(self,
|
|
175
|
+
data: AlgorithmInput,
|
|
176
|
+
prediction: float,
|
|
177
|
+
uncertainty: float) -> tuple:
|
|
178
|
+
"""
|
|
179
|
+
Calculate confidence interval for the prediction.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
data: Current algorithm input
|
|
183
|
+
prediction: Predicted insulin dose
|
|
184
|
+
uncertainty: Uncertainty score
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
tuple: (lower_bound, upper_bound) for the prediction
|
|
188
|
+
"""
|
|
189
|
+
# Default: ±20% of prediction based on uncertainty
|
|
190
|
+
margin = prediction * 0.2 * (1 + uncertainty)
|
|
191
|
+
return (max(0, prediction - margin), prediction + margin)
|
|
192
|
+
|
|
193
|
+
def explain_prediction(self,
|
|
194
|
+
data: AlgorithmInput,
|
|
195
|
+
prediction: Dict[str, Any]) -> str:
|
|
196
|
+
"""
|
|
197
|
+
Generate human-readable explanation for the prediction.
|
|
198
|
+
|
|
199
|
+
This is used for the Reasoning Log in the Clinical Control Center.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
data: Algorithm input
|
|
203
|
+
prediction: Prediction dictionary
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
str: Explanation like "2 units delivered because glucose rising >2 mg/dL/min"
|
|
207
|
+
"""
|
|
208
|
+
reasons = []
|
|
209
|
+
|
|
210
|
+
# Glucose level reasoning
|
|
211
|
+
if data.current_glucose < 70:
|
|
212
|
+
reasons.append(f"glucose critically low at {data.current_glucose:.0f} mg/dL")
|
|
213
|
+
elif data.current_glucose < 100:
|
|
214
|
+
reasons.append(f"glucose approaching low at {data.current_glucose:.0f} mg/dL")
|
|
215
|
+
elif data.current_glucose > 180:
|
|
216
|
+
reasons.append(f"glucose elevated at {data.current_glucose:.0f} mg/dL")
|
|
217
|
+
else:
|
|
218
|
+
reasons.append(f"glucose in target range at {data.current_glucose:.0f} mg/dL")
|
|
219
|
+
|
|
220
|
+
# Insulin on board
|
|
221
|
+
if data.insulin_on_board > 2.0:
|
|
222
|
+
reasons.append(f"high insulin on board ({data.insulin_on_board:.1f} U)")
|
|
223
|
+
elif data.insulin_on_board > 0.5:
|
|
224
|
+
reasons.append(f"moderate insulin on board ({data.insulin_on_board:.1f} U)")
|
|
225
|
+
|
|
226
|
+
# Carbs
|
|
227
|
+
if data.carb_intake > 0:
|
|
228
|
+
reasons.append(f"meal detected ({data.carb_intake:.0f}g carbs)")
|
|
229
|
+
|
|
230
|
+
if prediction.get('total_insulin_delivered', 0) > 0:
|
|
231
|
+
return f"{prediction['total_insulin_delivered']:.2f} units delivered because " + ", ".join(reasons)
|
|
232
|
+
else:
|
|
233
|
+
return f"No insulin delivered: " + ", ".join(reasons)
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Calculates the insulin dose based on current physiological data.
|
|
239
|
+
This is the primary method to be implemented by custom algorithms.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
data (AlgorithmInput): A dataclass containing all relevant data for the decision.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Dict[str, Any]: A dictionary containing the calculated insulin doses.
|
|
246
|
+
A key 'total_insulin_delivered' is expected by the simulator.
|
|
247
|
+
(e.g., {'total_insulin_delivered': 1.5, 'bolus_insulin': 1.0, 'basal_insulin': 0.5})
|
|
248
|
+
"""
|
|
249
|
+
# Clear why_log at start of each calculation
|
|
250
|
+
self.why_log = []
|
|
251
|
+
raise NotImplementedError("Subclasses must implement predict_insulin method")
|
|
252
|
+
|
|
253
|
+
def _log_reason(self, reason: str, category: str, value: Any = None, clinical_impact: str = ""):
|
|
254
|
+
"""Helper method to add reasoning to why_log"""
|
|
255
|
+
entry = WhyLogEntry(
|
|
256
|
+
reason=reason,
|
|
257
|
+
category=category,
|
|
258
|
+
value=value,
|
|
259
|
+
clinical_impact=clinical_impact
|
|
260
|
+
)
|
|
261
|
+
self.why_log.append(entry)
|
|
262
|
+
|
|
263
|
+
def get_why_log(self) -> List[WhyLogEntry]:
|
|
264
|
+
"""Get the decision reasoning log for the last calculation"""
|
|
265
|
+
return self.why_log
|
|
266
|
+
|
|
267
|
+
def get_why_log_text(self) -> str:
|
|
268
|
+
"""Get human-readable why log"""
|
|
269
|
+
if not self.why_log:
|
|
270
|
+
return "No decision reasoning available"
|
|
271
|
+
|
|
272
|
+
text = "WHY_LOG:\n"
|
|
273
|
+
for entry in self.why_log:
|
|
274
|
+
text += f"- {entry.reason}"
|
|
275
|
+
if entry.value is not None:
|
|
276
|
+
text += f" (value: {entry.value})"
|
|
277
|
+
if entry.clinical_impact:
|
|
278
|
+
text += f" → {entry.clinical_impact}"
|
|
279
|
+
text += "\n"
|
|
280
|
+
return text
|
|
281
|
+
|
|
282
|
+
def reset(self):
|
|
283
|
+
"""
|
|
284
|
+
Resets the algorithm's internal state.
|
|
285
|
+
This should be called at the start of each new simulation run.
|
|
286
|
+
"""
|
|
287
|
+
self.state = {}
|
|
288
|
+
self.why_log = []
|
|
289
|
+
|
|
290
|
+
def get_state(self) -> Dict[str, Any]:
|
|
291
|
+
"""
|
|
292
|
+
Returns the current internal state of the algorithm.
|
|
293
|
+
"""
|
|
294
|
+
return self.state
|
|
295
|
+
|
|
296
|
+
def set_state(self, state: Dict[str, Any]):
|
|
297
|
+
"""
|
|
298
|
+
Sets the internal state of the algorithm.
|
|
299
|
+
"""
|
|
300
|
+
self.state = state
|