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,307 @@
|
|
|
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
|
+
carbs_on_board: float = 0.0
|
|
16
|
+
isf: Optional[float] = None
|
|
17
|
+
icr: Optional[float] = None
|
|
18
|
+
dia_minutes: Optional[float] = None
|
|
19
|
+
basal_rate_u_per_hr: Optional[float] = None
|
|
20
|
+
glucose_trend_mgdl_min: Optional[float] = None
|
|
21
|
+
predicted_glucose_30min: Optional[float] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AlgorithmMetadata:
|
|
26
|
+
"""Metadata for algorithm registration and identification"""
|
|
27
|
+
name: str
|
|
28
|
+
version: str = "1.0.0"
|
|
29
|
+
author: str = "IINTS-AF Team"
|
|
30
|
+
paper_reference: Optional[str] = None
|
|
31
|
+
description: str = ""
|
|
32
|
+
algorithm_type: str = "rule_based" # 'rule_based', 'ml', 'hybrid'
|
|
33
|
+
requires_training: bool = False
|
|
34
|
+
supported_scenarios: List[str] = field(default_factory=lambda: [
|
|
35
|
+
'standard_meal', 'unannounced_meal', 'exercise',
|
|
36
|
+
'stress', 'hypoglycemia', 'hyperglycemia'
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> Dict:
|
|
40
|
+
return {
|
|
41
|
+
'name': self.name,
|
|
42
|
+
'version': self.version,
|
|
43
|
+
'author': self.author,
|
|
44
|
+
'paper_reference': self.paper_reference,
|
|
45
|
+
'description': self.description,
|
|
46
|
+
'algorithm_type': self.algorithm_type,
|
|
47
|
+
'requires_training': self.requires_training,
|
|
48
|
+
'supported_scenarios': self.supported_scenarios
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AlgorithmResult:
|
|
54
|
+
"""Result of an insulin prediction with uncertainty"""
|
|
55
|
+
total_insulin_delivered: float
|
|
56
|
+
bolus_insulin: float = 0.0
|
|
57
|
+
basal_insulin: float = 0.0
|
|
58
|
+
correction_bolus: float = 0.0
|
|
59
|
+
meal_bolus: float = 0.0
|
|
60
|
+
|
|
61
|
+
# Uncertainty quantification
|
|
62
|
+
uncertainty: float = 0.0 # 0.0 = certain, 1.0 = very uncertain
|
|
63
|
+
confidence_interval: tuple = (0.0, 0.0) # (lower, upper)
|
|
64
|
+
|
|
65
|
+
# Clinical reasoning
|
|
66
|
+
primary_reason: str = ""
|
|
67
|
+
secondary_reasons: List[str] = field(default_factory=list)
|
|
68
|
+
safety_constraints: List[str] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
# Metadata
|
|
71
|
+
algorithm_name: str = ""
|
|
72
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
73
|
+
|
|
74
|
+
def to_dict(self) -> Dict:
|
|
75
|
+
return {
|
|
76
|
+
'total_insulin_delivered': self.total_insulin_delivered,
|
|
77
|
+
'bolus_insulin': self.bolus_insulin,
|
|
78
|
+
'basal_insulin': self.basal_insulin,
|
|
79
|
+
'correction_bolus': self.correction_bolus,
|
|
80
|
+
'meal_bolus': self.meal_bolus,
|
|
81
|
+
'uncertainty': self.uncertainty,
|
|
82
|
+
'confidence_interval': self.confidence_interval,
|
|
83
|
+
'primary_reason': self.primary_reason,
|
|
84
|
+
'secondary_reasons': self.secondary_reasons,
|
|
85
|
+
'safety_constraints': self.safety_constraints,
|
|
86
|
+
'algorithm_name': self.algorithm_name,
|
|
87
|
+
'timestamp': self.timestamp.isoformat()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class WhyLogEntry:
|
|
93
|
+
"""Single entry in the Why Log explaining a decision reason"""
|
|
94
|
+
reason: str
|
|
95
|
+
category: str # 'glucose_level', 'velocity', 'insulin_on_board', 'safety', 'context'
|
|
96
|
+
value: Any = None
|
|
97
|
+
clinical_impact: str = ""
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> Dict:
|
|
100
|
+
return {
|
|
101
|
+
'reason': self.reason,
|
|
102
|
+
'category': self.category,
|
|
103
|
+
'value': self.value,
|
|
104
|
+
'clinical_impact': self.clinical_impact
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class InsulinAlgorithm(ABC):
|
|
108
|
+
"""
|
|
109
|
+
Abstract base class for insulin delivery algorithms.
|
|
110
|
+
|
|
111
|
+
All specific insulin algorithms used in the simulation framework should
|
|
112
|
+
inherit from this class and implement its abstract methods.
|
|
113
|
+
|
|
114
|
+
This class supports the Plug-and-Play architecture, allowing any algorithm
|
|
115
|
+
to be registered and compared in Battle Mode.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
119
|
+
"""
|
|
120
|
+
Initializes the algorithm with its specific settings.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
settings (Dict[str, Any]): A dictionary of algorithm-specific parameters.
|
|
124
|
+
"""
|
|
125
|
+
self.settings = settings if settings is not None else {}
|
|
126
|
+
self.state: Dict[str, Any] = {} # To store internal algorithm state across calls
|
|
127
|
+
self.why_log: List[WhyLogEntry] = [] # Decision reasoning log
|
|
128
|
+
self._metadata: Optional[AlgorithmMetadata] = None # Lazy-loaded metadata
|
|
129
|
+
self.isf = self.settings.get('isf', 50.0) # Default ISF
|
|
130
|
+
self.icr = self.settings.get('icr', 10.0) # Default ICR
|
|
131
|
+
|
|
132
|
+
def set_isf(self, isf: float):
|
|
133
|
+
"""Set the Insulin Sensitivity Factor (mg/dL per unit)."""
|
|
134
|
+
if isf <= 0:
|
|
135
|
+
raise ValueError("ISF must be a positive value.")
|
|
136
|
+
self.isf = isf
|
|
137
|
+
|
|
138
|
+
def set_icr(self, icr: float):
|
|
139
|
+
"""Set the Insulin-to-Carb Ratio (grams per unit)."""
|
|
140
|
+
if icr <= 0:
|
|
141
|
+
raise ValueError("ICR must be a positive value.")
|
|
142
|
+
self.icr = icr
|
|
143
|
+
|
|
144
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
145
|
+
"""
|
|
146
|
+
Get algorithm metadata. Override in subclasses for custom metadata.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
AlgorithmMetadata: Information about the algorithm for registration
|
|
150
|
+
"""
|
|
151
|
+
if self._metadata is None:
|
|
152
|
+
# Default metadata - override in subclasses
|
|
153
|
+
self._metadata = AlgorithmMetadata(
|
|
154
|
+
name=self.__class__.__name__,
|
|
155
|
+
version="1.0.0",
|
|
156
|
+
author="IINTS-AF Team",
|
|
157
|
+
description=f"Insulin algorithm: {self.__class__.__name__}",
|
|
158
|
+
algorithm_type="rule_based"
|
|
159
|
+
)
|
|
160
|
+
return self._metadata
|
|
161
|
+
|
|
162
|
+
def set_algorithm_metadata(self, metadata: AlgorithmMetadata):
|
|
163
|
+
"""Set custom algorithm metadata"""
|
|
164
|
+
self._metadata = metadata
|
|
165
|
+
|
|
166
|
+
def calculate_uncertainty(self, data: AlgorithmInput) -> float:
|
|
167
|
+
"""
|
|
168
|
+
Calculate uncertainty score for the current prediction.
|
|
169
|
+
|
|
170
|
+
Override in subclasses to implement custom uncertainty quantification.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
data: Current algorithm input
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
float: Uncertainty score between 0.0 (certain) and 1.0 (very uncertain)
|
|
177
|
+
"""
|
|
178
|
+
# Default: low uncertainty for rule-based algorithms
|
|
179
|
+
return 0.1
|
|
180
|
+
|
|
181
|
+
def calculate_confidence_interval(self,
|
|
182
|
+
data: AlgorithmInput,
|
|
183
|
+
prediction: float,
|
|
184
|
+
uncertainty: float) -> tuple:
|
|
185
|
+
"""
|
|
186
|
+
Calculate confidence interval for the prediction.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data: Current algorithm input
|
|
190
|
+
prediction: Predicted insulin dose
|
|
191
|
+
uncertainty: Uncertainty score
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
tuple: (lower_bound, upper_bound) for the prediction
|
|
195
|
+
"""
|
|
196
|
+
# Default: ±20% of prediction based on uncertainty
|
|
197
|
+
margin = prediction * 0.2 * (1 + uncertainty)
|
|
198
|
+
return (max(0, prediction - margin), prediction + margin)
|
|
199
|
+
|
|
200
|
+
def explain_prediction(self,
|
|
201
|
+
data: AlgorithmInput,
|
|
202
|
+
prediction: Dict[str, Any]) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Generate human-readable explanation for the prediction.
|
|
205
|
+
|
|
206
|
+
This is used for the Reasoning Log in the Clinical Control Center.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data: Algorithm input
|
|
210
|
+
prediction: Prediction dictionary
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: Explanation like "2 units delivered because glucose rising >2 mg/dL/min"
|
|
214
|
+
"""
|
|
215
|
+
reasons = []
|
|
216
|
+
|
|
217
|
+
# Glucose level reasoning
|
|
218
|
+
if data.current_glucose < 70:
|
|
219
|
+
reasons.append(f"glucose critically low at {data.current_glucose:.0f} mg/dL")
|
|
220
|
+
elif data.current_glucose < 100:
|
|
221
|
+
reasons.append(f"glucose approaching low at {data.current_glucose:.0f} mg/dL")
|
|
222
|
+
elif data.current_glucose > 180:
|
|
223
|
+
reasons.append(f"glucose elevated at {data.current_glucose:.0f} mg/dL")
|
|
224
|
+
else:
|
|
225
|
+
reasons.append(f"glucose in target range at {data.current_glucose:.0f} mg/dL")
|
|
226
|
+
|
|
227
|
+
# Insulin on board
|
|
228
|
+
if data.insulin_on_board > 2.0:
|
|
229
|
+
reasons.append(f"high insulin on board ({data.insulin_on_board:.1f} U)")
|
|
230
|
+
elif data.insulin_on_board > 0.5:
|
|
231
|
+
reasons.append(f"moderate insulin on board ({data.insulin_on_board:.1f} U)")
|
|
232
|
+
|
|
233
|
+
# Carbs
|
|
234
|
+
if data.carb_intake > 0:
|
|
235
|
+
reasons.append(f"meal detected ({data.carb_intake:.0f}g carbs)")
|
|
236
|
+
|
|
237
|
+
if prediction.get('total_insulin_delivered', 0) > 0:
|
|
238
|
+
return f"{prediction['total_insulin_delivered']:.2f} units delivered because " + ", ".join(reasons)
|
|
239
|
+
else:
|
|
240
|
+
return f"No insulin delivered: " + ", ".join(reasons)
|
|
241
|
+
|
|
242
|
+
@abstractmethod
|
|
243
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Calculates the insulin dose based on current physiological data.
|
|
246
|
+
This is the primary method to be implemented by custom algorithms.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
data (AlgorithmInput): A dataclass containing all relevant data for the decision.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dict[str, Any]: A dictionary containing the calculated insulin doses.
|
|
253
|
+
A key 'total_insulin_delivered' is expected by the simulator.
|
|
254
|
+
(e.g., {'total_insulin_delivered': 1.5, 'bolus_insulin': 1.0, 'basal_insulin': 0.5})
|
|
255
|
+
"""
|
|
256
|
+
# Clear why_log at start of each calculation
|
|
257
|
+
self.why_log = []
|
|
258
|
+
raise NotImplementedError("Subclasses must implement predict_insulin method")
|
|
259
|
+
|
|
260
|
+
def _log_reason(self, reason: str, category: str, value: Any = None, clinical_impact: str = ""):
|
|
261
|
+
"""Helper method to add reasoning to why_log"""
|
|
262
|
+
entry = WhyLogEntry(
|
|
263
|
+
reason=reason,
|
|
264
|
+
category=category,
|
|
265
|
+
value=value,
|
|
266
|
+
clinical_impact=clinical_impact
|
|
267
|
+
)
|
|
268
|
+
self.why_log.append(entry)
|
|
269
|
+
|
|
270
|
+
def get_why_log(self) -> List[WhyLogEntry]:
|
|
271
|
+
"""Get the decision reasoning log for the last calculation"""
|
|
272
|
+
return self.why_log
|
|
273
|
+
|
|
274
|
+
def get_why_log_text(self) -> str:
|
|
275
|
+
"""Get human-readable why log"""
|
|
276
|
+
if not self.why_log:
|
|
277
|
+
return "No decision reasoning available"
|
|
278
|
+
|
|
279
|
+
text = "WHY_LOG:\n"
|
|
280
|
+
for entry in self.why_log:
|
|
281
|
+
text += f"- {entry.reason}"
|
|
282
|
+
if entry.value is not None:
|
|
283
|
+
text += f" (value: {entry.value})"
|
|
284
|
+
if entry.clinical_impact:
|
|
285
|
+
text += f" → {entry.clinical_impact}"
|
|
286
|
+
text += "\n"
|
|
287
|
+
return text
|
|
288
|
+
|
|
289
|
+
def reset(self):
|
|
290
|
+
"""
|
|
291
|
+
Resets the algorithm's internal state.
|
|
292
|
+
This should be called at the start of each new simulation run.
|
|
293
|
+
"""
|
|
294
|
+
self.state = {}
|
|
295
|
+
self.why_log = []
|
|
296
|
+
|
|
297
|
+
def get_state(self) -> Dict[str, Any]:
|
|
298
|
+
"""
|
|
299
|
+
Returns the current internal state of the algorithm.
|
|
300
|
+
"""
|
|
301
|
+
return self.state
|
|
302
|
+
|
|
303
|
+
def set_state(self, state: Dict[str, Any]):
|
|
304
|
+
"""
|
|
305
|
+
Sets the internal state of the algorithm.
|
|
306
|
+
"""
|
|
307
|
+
self.state = state
|
iints/api/registry.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Optional, Mapping, Sequence, Iterable, Any, cast
|
|
5
|
+
import importlib
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from importlib import metadata as importlib_metadata
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
import importlib_metadata # type: ignore
|
|
11
|
+
|
|
12
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmMetadata
|
|
13
|
+
from iints.core.algorithms.discovery import discover_algorithms
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class AlgorithmListing:
|
|
18
|
+
name: str
|
|
19
|
+
class_path: str
|
|
20
|
+
source: str
|
|
21
|
+
metadata: Optional[AlgorithmMetadata]
|
|
22
|
+
status: str = "available"
|
|
23
|
+
error: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_entry_point(ep) -> AlgorithmListing:
|
|
27
|
+
try:
|
|
28
|
+
obj = ep.load()
|
|
29
|
+
if isinstance(obj, type) and issubclass(obj, InsulinAlgorithm):
|
|
30
|
+
instance = obj()
|
|
31
|
+
meta = instance.get_algorithm_metadata()
|
|
32
|
+
return AlgorithmListing(
|
|
33
|
+
name=meta.name,
|
|
34
|
+
class_path=f"{obj.__module__}.{obj.__name__}",
|
|
35
|
+
source=f"entry_point:{ep.name}",
|
|
36
|
+
metadata=meta,
|
|
37
|
+
)
|
|
38
|
+
return AlgorithmListing(
|
|
39
|
+
name=ep.name,
|
|
40
|
+
class_path=f"{ep.module}:{ep.attr}",
|
|
41
|
+
source=f"entry_point:{ep.name}",
|
|
42
|
+
metadata=None,
|
|
43
|
+
status="invalid",
|
|
44
|
+
error="Entry point does not resolve to an InsulinAlgorithm",
|
|
45
|
+
)
|
|
46
|
+
except Exception as exc:
|
|
47
|
+
return AlgorithmListing(
|
|
48
|
+
name=ep.name,
|
|
49
|
+
class_path=f"{ep.module}:{ep.attr}",
|
|
50
|
+
source=f"entry_point:{ep.name}",
|
|
51
|
+
metadata=None,
|
|
52
|
+
status="unavailable",
|
|
53
|
+
error=str(exc),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def list_algorithm_plugins() -> List[AlgorithmListing]:
|
|
58
|
+
listings: List[AlgorithmListing] = []
|
|
59
|
+
|
|
60
|
+
# Built-in discovery
|
|
61
|
+
try:
|
|
62
|
+
discovered = discover_algorithms()
|
|
63
|
+
for name, cls in discovered.items():
|
|
64
|
+
try:
|
|
65
|
+
instance = cls()
|
|
66
|
+
meta = instance.get_algorithm_metadata()
|
|
67
|
+
listings.append(
|
|
68
|
+
AlgorithmListing(
|
|
69
|
+
name=meta.name,
|
|
70
|
+
class_path=f"{cls.__module__}.{cls.__name__}",
|
|
71
|
+
source="builtin",
|
|
72
|
+
metadata=meta,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
except Exception as exc:
|
|
76
|
+
listings.append(
|
|
77
|
+
AlgorithmListing(
|
|
78
|
+
name=name,
|
|
79
|
+
class_path=f"{cls.__module__}.{cls.__name__}",
|
|
80
|
+
source="builtin",
|
|
81
|
+
metadata=None,
|
|
82
|
+
status="unavailable",
|
|
83
|
+
error=str(exc),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
# Entry points
|
|
90
|
+
try:
|
|
91
|
+
eps = importlib_metadata.entry_points()
|
|
92
|
+
group: Iterable[Any]
|
|
93
|
+
if hasattr(eps, "select"):
|
|
94
|
+
group = list(eps.select(group="iints.algorithms"))
|
|
95
|
+
else:
|
|
96
|
+
eps_mapping = cast(Mapping[str, Sequence[object]], eps)
|
|
97
|
+
group = eps_mapping.get("iints.algorithms", ())
|
|
98
|
+
for ep in group:
|
|
99
|
+
listings.append(_load_entry_point(ep))
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return listings
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# IINTS-AF RESEARCH SDK: ALGORITHM TEMPLATE
|
|
2
|
+
#
|
|
3
|
+
# Welcome to the IINTS-AF Research SDK! This file is your starting point
|
|
4
|
+
# for creating your own insulin delivery algorithm.
|
|
5
|
+
#
|
|
6
|
+
# HOW IT WORKS:
|
|
7
|
+
# 1. COPY THIS FILE: Copy this template into a new file within this same
|
|
8
|
+
# `user` directory (e.g., `MyAwesomeAlgorithm.py`).
|
|
9
|
+
# 2. RENAME THE CLASS: Change the class name from `TemplateAlgorithm` to something
|
|
10
|
+
# unique (e.g., `MyAwesomeAlgorithm`).
|
|
11
|
+
# 3. WRITE YOUR LOGIC: Implement your insulin calculation logic in the
|
|
12
|
+
# `predict_insulin` method.
|
|
13
|
+
# 4. RUN THE BATTLE: Your new algorithm will be automatically detected
|
|
14
|
+
# and will appear as an option in the IINTS-AF terminal.
|
|
15
|
+
#
|
|
16
|
+
# Happy coding!
|
|
17
|
+
#
|
|
18
|
+
# -----------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
from typing import Dict, Any, List
|
|
21
|
+
|
|
22
|
+
# All algorithms must inherit from the InsulinAlgorithm base class.
|
|
23
|
+
# It provides the necessary structure and methods that the IINTS-AF simulator expects.
|
|
24
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput, AlgorithmMetadata, WhyLogEntry
|
|
25
|
+
|
|
26
|
+
# A "Dose" is a dictionary containing the insulin breakdown. The simulator
|
|
27
|
+
# currently only requires 'total_insulin_delivered'. The other keys are for
|
|
28
|
+
# more detailed logging and analysis.
|
|
29
|
+
Dose = Dict[str, float]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TemplateAlgorithm(InsulinAlgorithm):
|
|
33
|
+
"""
|
|
34
|
+
This is a template for a user-defined insulin delivery algorithm.
|
|
35
|
+
It demonstrates the required structure and provides a basic, safe implementation
|
|
36
|
+
of a correction and meal bolus logic.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
40
|
+
"""
|
|
41
|
+
REQUIRED: This method provides metadata about your algorithm.
|
|
42
|
+
The simulator uses this to identify and display your algorithm in the UI.
|
|
43
|
+
"""
|
|
44
|
+
return AlgorithmMetadata(
|
|
45
|
+
name="Template Algorithm", # The name that will appear in the UI
|
|
46
|
+
version="1.0.0",
|
|
47
|
+
author="Your Name Here",
|
|
48
|
+
description="A basic example algorithm demonstrating the IINTS-AF SDK.",
|
|
49
|
+
algorithm_type="rule_based" # 'rule_based', 'ml', or 'hybrid'
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dose:
|
|
53
|
+
"""
|
|
54
|
+
REQUIRED: This is the core method of your algorithm.
|
|
55
|
+
The simulator calls this method at each time step (e.g., every 5 minutes).
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data (AlgorithmInput): An object containing the current patient data.
|
|
59
|
+
- data.current_glucose (float): The latest glucose reading in mg/dL.
|
|
60
|
+
- data.insulin_on_board (float): Estimated insulin still active from previous doses.
|
|
61
|
+
- data.carb_intake (float): Carbohydrates from a meal announced at this time step (in grams).
|
|
62
|
+
- data.time_step (float): The duration since the last call, in minutes.
|
|
63
|
+
- data.current_time (float): The current simulation time in minutes from the start.
|
|
64
|
+
- data.patient_state (Dict): A dictionary for you to store custom state between function calls (e.g., tracking glucose trends).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Dose (Dict[str, float]): A dictionary specifying the insulin dose to be delivered.
|
|
68
|
+
Must contain at least the 'total_insulin_delivered' key.
|
|
69
|
+
"""
|
|
70
|
+
# This function is called at every time step.
|
|
71
|
+
# First, we clear the reasoning log from the previous step.
|
|
72
|
+
self.why_log = []
|
|
73
|
+
|
|
74
|
+
# --- Example: Accessing Patient Data ---
|
|
75
|
+
current_glucose = data.current_glucose
|
|
76
|
+
iob = data.insulin_on_board
|
|
77
|
+
carbs = data.carb_intake
|
|
78
|
+
|
|
79
|
+
# --- Your Custom State Management (Optional) ---
|
|
80
|
+
# The `self.state` dictionary persists across calls for this simulation run.
|
|
81
|
+
# You can use it to track trends, previous states, etc.
|
|
82
|
+
previous_glucose = self.state.get('previous_glucose', current_glucose)
|
|
83
|
+
glucose_trend = (current_glucose - previous_glucose) / data.time_step # mg/dL/min
|
|
84
|
+
self.state['previous_glucose'] = current_glucose # Update state for the next call
|
|
85
|
+
|
|
86
|
+
# ----------------------------------------------------------------------
|
|
87
|
+
# --- YOUR CUSTOM LOGIC START ---
|
|
88
|
+
# ----------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
# Initialize insulin components
|
|
91
|
+
correction_bolus = 0.0
|
|
92
|
+
meal_bolus = 0.0
|
|
93
|
+
basal_rate = 0.0 # This simple example does not use a dynamic basal rate
|
|
94
|
+
|
|
95
|
+
# Define algorithm parameters
|
|
96
|
+
# In a real algorithm, these would be managed more robustly.
|
|
97
|
+
target_glucose = 110 # mg/dL
|
|
98
|
+
insulin_sensitivity_factor = self.isf # mg/dL per Unit of insulin (set in base class)
|
|
99
|
+
carb_to_insulin_ratio = self.icr # grams of carb per Unit of insulin (set in base class)
|
|
100
|
+
|
|
101
|
+
# --- Log Key Data for Traceability ---
|
|
102
|
+
# This is how you make your algorithm's reasoning transparent!
|
|
103
|
+
self._log_reason(f"Current glucose is {current_glucose:.0f} mg/dL.", "glucose_level", current_glucose)
|
|
104
|
+
self._log_reason(f"Glucose trend is {glucose_trend:+.1f} mg/dL/min.", "velocity", glucose_trend)
|
|
105
|
+
self._log_reason(f"Active insulin (IOB) is {iob:.2f} U.", "insulin_on_board", iob)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --- Example Correction Bolus Logic ---
|
|
109
|
+
# Only correct if glucose is above the target
|
|
110
|
+
if current_glucose > target_glucose:
|
|
111
|
+
glucose_diff = current_glucose - target_glucose
|
|
112
|
+
# Standard correction formula: (Current - Target) / ISF
|
|
113
|
+
correction_bolus = glucose_diff / insulin_sensitivity_factor
|
|
114
|
+
self._log_reason(
|
|
115
|
+
f"Calculated correction bolus of {correction_bolus:.2f} U for high glucose.",
|
|
116
|
+
"calculation",
|
|
117
|
+
correction_bolus,
|
|
118
|
+
clinical_impact="Aims to bring glucose back to target range."
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
self._log_reason("No correction bolus needed (glucose is at or below target).", "calculation")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- Example Meal Bolus Logic ---
|
|
125
|
+
# Deliver a bolus if carbs are announced
|
|
126
|
+
if carbs > 0:
|
|
127
|
+
# Standard meal bolus formula: Carbs / ICR
|
|
128
|
+
meal_bolus = carbs / carb_to_insulin_ratio
|
|
129
|
+
self._log_reason(
|
|
130
|
+
f"Calculated meal bolus of {meal_bolus:.2f} U for {carbs}g carbs.",
|
|
131
|
+
"calculation",
|
|
132
|
+
meal_bolus,
|
|
133
|
+
clinical_impact="Aims to cover carbohydrate intake from a meal."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# --- Example Safety Guard: Insulin Stacking ---
|
|
137
|
+
# Reduce the calculated dose by the amount of insulin already on board (IOB).
|
|
138
|
+
# This is a crucial safety feature to prevent hypoglycemia from "stacking" insulin.
|
|
139
|
+
total_calculated_dose = meal_bolus + correction_bolus
|
|
140
|
+
|
|
141
|
+
if total_calculated_dose > iob:
|
|
142
|
+
final_bolus = total_calculated_dose - iob
|
|
143
|
+
self._log_reason(
|
|
144
|
+
f"Reducing dose by IOB ({iob:.2f} U). Final bolus: {final_bolus:.2f} U.",
|
|
145
|
+
"safety",
|
|
146
|
+
final_bolus,
|
|
147
|
+
clinical_impact="Prevents insulin stacking and reduces hypoglycemia risk."
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
final_bolus = 0.0 # Don't give a negative dose
|
|
151
|
+
self._log_reason(
|
|
152
|
+
f"Calculated dose ({total_calculated_dose:.2f} U) is less than IOB ({iob:.2f} U). No bolus advised.",
|
|
153
|
+
"safety",
|
|
154
|
+
final_bolus,
|
|
155
|
+
clinical_impact="High IOB indicates sufficient active insulin; delivering more could be dangerous."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# --- Example Safety Guard: Hypoglycemia Prevention ---
|
|
159
|
+
# If glucose is low or dropping fast, suspend all insulin delivery.
|
|
160
|
+
if current_glucose < 80 or glucose_trend < -3.0:
|
|
161
|
+
final_bolus = 0.0
|
|
162
|
+
basal_rate = 0.0
|
|
163
|
+
self._log_reason(
|
|
164
|
+
"SUSPENDING INSULIN: Glucose is low or dropping rapidly.",
|
|
165
|
+
"safety",
|
|
166
|
+
current_glucose,
|
|
167
|
+
clinical_impact="Critical safety action to prevent severe hypoglycemia."
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# ----------------------------------------------------------------------
|
|
171
|
+
# --- YOUR CUSTOM LOGIC END ---
|
|
172
|
+
# ----------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
# The final dose must be assembled into a dictionary.
|
|
175
|
+
# The simulator only strictly requires 'total_insulin_delivered'.
|
|
176
|
+
# The other keys are for detailed analysis and logging.
|
|
177
|
+
dose: Dose = {
|
|
178
|
+
'basal_insulin': basal_rate,
|
|
179
|
+
'meal_bolus': meal_bolus,
|
|
180
|
+
'correction_bolus': correction_bolus,
|
|
181
|
+
'total_insulin_delivered': max(0, final_bolus + basal_rate) # Ensure no negative insulin
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return dose
|
|
185
|
+
|
|
186
|
+
def reset(self):
|
|
187
|
+
"""
|
|
188
|
+
This method is called by the simulator at the start of a new simulation.
|
|
189
|
+
Use it to reset any internal state of your algorithm.
|
|
190
|
+
"""
|
|
191
|
+
# Reset the state dictionary for the new simulation run.
|
|
192
|
+
self.state = {}
|
|
193
|
+
# Also clear the reasoning log.
|
|
194
|
+
self.why_log = []
|
|
195
|
+
print(f"[{self.get_algorithm_metadata().name}] has been reset.")
|
|
Binary file
|
iints/cli/__init__.py
ADDED
|
File without changes
|