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,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput, AlgorithmMetadata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConstantDoseAlgorithm(InsulinAlgorithm):
|
|
11
|
+
"""Always returns a fixed insulin dose for CI and smoke testing."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, dose: float = 0.5, settings: Optional[Dict[str, Any]] = None):
|
|
14
|
+
super().__init__(settings)
|
|
15
|
+
self.dose = max(0.0, float(dose))
|
|
16
|
+
|
|
17
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
18
|
+
return AlgorithmMetadata(
|
|
19
|
+
name="ConstantDoseAlgorithm",
|
|
20
|
+
version="1.0",
|
|
21
|
+
author="IINTS-AF SDK",
|
|
22
|
+
algorithm_type="Mock",
|
|
23
|
+
description="Always delivers a fixed insulin dose for CI testing.",
|
|
24
|
+
requires_training=False,
|
|
25
|
+
supported_scenarios=["baseline", "testing"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
29
|
+
return {
|
|
30
|
+
"total_insulin_delivered": self.dose,
|
|
31
|
+
"basal_insulin": self.dose,
|
|
32
|
+
"bolus_insulin": 0.0,
|
|
33
|
+
"meal_bolus": 0.0,
|
|
34
|
+
"correction_bolus": 0.0,
|
|
35
|
+
"uncertainty": 0.0,
|
|
36
|
+
"fallback_triggered": False,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RandomDoseAlgorithm(InsulinAlgorithm):
|
|
41
|
+
"""Returns a random dose within a safe range for stochastic testing."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, max_dose: float = 1.0, seed: Optional[int] = None, settings: Optional[Dict[str, Any]] = None):
|
|
44
|
+
super().__init__(settings)
|
|
45
|
+
self.max_dose = max(0.0, float(max_dose))
|
|
46
|
+
self.rng = np.random.default_rng(seed)
|
|
47
|
+
|
|
48
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
49
|
+
return AlgorithmMetadata(
|
|
50
|
+
name="RandomDoseAlgorithm",
|
|
51
|
+
version="1.0",
|
|
52
|
+
author="IINTS-AF SDK",
|
|
53
|
+
algorithm_type="Mock",
|
|
54
|
+
description="Produces randomized doses for stress-testing and CI.",
|
|
55
|
+
requires_training=False,
|
|
56
|
+
supported_scenarios=["baseline", "testing"],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
60
|
+
dose = float(self.rng.uniform(0.0, self.max_dose))
|
|
61
|
+
return {
|
|
62
|
+
"total_insulin_delivered": dose,
|
|
63
|
+
"basal_insulin": dose,
|
|
64
|
+
"bolus_insulin": 0.0,
|
|
65
|
+
"meal_bolus": 0.0,
|
|
66
|
+
"correction_bolus": 0.0,
|
|
67
|
+
"uncertainty": 0.0,
|
|
68
|
+
"fallback_triggered": False,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RunawayAIAlgorithm(InsulinAlgorithm):
|
|
73
|
+
"""Delivers a maximal bolus during glucose decline to stress-test supervisor."""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
max_bolus: float = 5.0,
|
|
78
|
+
trigger_glucose: float = 140.0,
|
|
79
|
+
settings: Optional[Dict[str, Any]] = None,
|
|
80
|
+
):
|
|
81
|
+
super().__init__(settings)
|
|
82
|
+
self.max_bolus = max(0.0, float(max_bolus))
|
|
83
|
+
self.trigger_glucose = float(trigger_glucose)
|
|
84
|
+
|
|
85
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
86
|
+
return AlgorithmMetadata(
|
|
87
|
+
name="RunawayAIAlgorithm",
|
|
88
|
+
version="1.0",
|
|
89
|
+
author="IINTS-AF SDK",
|
|
90
|
+
algorithm_type="Chaos",
|
|
91
|
+
description="Forces max bolus during falling glucose to test safety supervisor.",
|
|
92
|
+
requires_training=False,
|
|
93
|
+
supported_scenarios=["chaos", "runaway_ai"],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
97
|
+
trend = data.glucose_trend_mgdl_min
|
|
98
|
+
if data.current_glucose <= self.trigger_glucose or (trend is not None and trend < 0):
|
|
99
|
+
dose = self.max_bolus
|
|
100
|
+
else:
|
|
101
|
+
dose = 0.0
|
|
102
|
+
return {
|
|
103
|
+
"total_insulin_delivered": dose,
|
|
104
|
+
"basal_insulin": 0.0,
|
|
105
|
+
"bolus_insulin": dose,
|
|
106
|
+
"meal_bolus": 0.0,
|
|
107
|
+
"correction_bolus": dose,
|
|
108
|
+
"uncertainty": 0.0,
|
|
109
|
+
"fallback_triggered": False,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class StackingAIAlgorithm(InsulinAlgorithm):
|
|
114
|
+
"""Delivers repeated boluses over consecutive steps to simulate stacking."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
bolus_units: float = 4.0,
|
|
119
|
+
stack_steps: int = 3,
|
|
120
|
+
trigger_glucose: float = 180.0,
|
|
121
|
+
settings: Optional[Dict[str, Any]] = None,
|
|
122
|
+
):
|
|
123
|
+
super().__init__(settings)
|
|
124
|
+
self.bolus_units = max(0.0, float(bolus_units))
|
|
125
|
+
self.stack_steps = max(1, int(stack_steps))
|
|
126
|
+
self.trigger_glucose = float(trigger_glucose)
|
|
127
|
+
self._remaining = 0
|
|
128
|
+
|
|
129
|
+
def get_algorithm_metadata(self) -> AlgorithmMetadata:
|
|
130
|
+
return AlgorithmMetadata(
|
|
131
|
+
name="StackingAIAlgorithm",
|
|
132
|
+
version="1.0",
|
|
133
|
+
author="IINTS-AF SDK",
|
|
134
|
+
algorithm_type="Chaos",
|
|
135
|
+
description="Stacks multiple boluses across consecutive steps.",
|
|
136
|
+
requires_training=False,
|
|
137
|
+
supported_scenarios=["chaos", "stacking"],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def reset(self) -> None:
|
|
141
|
+
super().reset()
|
|
142
|
+
self._remaining = 0
|
|
143
|
+
|
|
144
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
145
|
+
if self._remaining <= 0 and data.current_glucose >= self.trigger_glucose:
|
|
146
|
+
self._remaining = self.stack_steps
|
|
147
|
+
|
|
148
|
+
if self._remaining > 0:
|
|
149
|
+
dose = self.bolus_units
|
|
150
|
+
self._remaining -= 1
|
|
151
|
+
else:
|
|
152
|
+
dose = 0.0
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"total_insulin_delivered": dose,
|
|
156
|
+
"basal_insulin": 0.0,
|
|
157
|
+
"bolus_insulin": dose,
|
|
158
|
+
"meal_bolus": 0.0,
|
|
159
|
+
"correction_bolus": dose,
|
|
160
|
+
"uncertainty": 0.0,
|
|
161
|
+
"fallback_triggered": False,
|
|
162
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Industry-Standard PID Controller - IINTS-AF
|
|
4
|
+
Simple PID implementation for algorithm comparison
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput
|
|
9
|
+
|
|
10
|
+
class PIDController(InsulinAlgorithm):
|
|
11
|
+
"""Industry-standard PID controller for glucose management"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
|
|
16
|
+
# PID parameters (tuned for glucose control)
|
|
17
|
+
self.kp = 0.1 # Proportional gain
|
|
18
|
+
self.ki = 0.01 # Integral gain
|
|
19
|
+
self.kd = 0.05 # Derivative gain
|
|
20
|
+
|
|
21
|
+
# Controller state
|
|
22
|
+
self.integral = 0
|
|
23
|
+
self.previous_error = 0
|
|
24
|
+
self.target_glucose = 120 # mg/dL target
|
|
25
|
+
|
|
26
|
+
# Safety limits
|
|
27
|
+
self.max_insulin = 5.0 # Maximum insulin dose
|
|
28
|
+
self.min_insulin = 0.0 # Minimum insulin dose
|
|
29
|
+
|
|
30
|
+
def predict_insulin(self, data: AlgorithmInput):
|
|
31
|
+
self.why_log = [] # Clear the log for this prediction cycle
|
|
32
|
+
|
|
33
|
+
# Calculate error from target
|
|
34
|
+
error = data.current_glucose - self.target_glucose
|
|
35
|
+
self._log_reason(f"Glucose error from target ({self.target_glucose} mg/dL)", "glucose_level", error, f"Current glucose: {data.current_glucose:.0f} mg/dL")
|
|
36
|
+
|
|
37
|
+
# Integral term (accumulated error)
|
|
38
|
+
self.integral += error
|
|
39
|
+
self._log_reason("Integral term updated", "control_parameter", self.integral)
|
|
40
|
+
|
|
41
|
+
# Derivative term (rate of change)
|
|
42
|
+
derivative = error - self.previous_error
|
|
43
|
+
self._log_reason("Derivative term calculated", "control_parameter", derivative)
|
|
44
|
+
|
|
45
|
+
# PID formula
|
|
46
|
+
insulin_dose = (self.kp * error +
|
|
47
|
+
self.ki * self.integral +
|
|
48
|
+
self.kd * derivative)
|
|
49
|
+
self._log_reason("Initial insulin dose calculated by PID", "insulin_calculation", insulin_dose)
|
|
50
|
+
|
|
51
|
+
# Update previous error for next iteration
|
|
52
|
+
self.previous_error = error
|
|
53
|
+
|
|
54
|
+
# Apply safety constraints
|
|
55
|
+
original_insulin_dose = insulin_dose
|
|
56
|
+
insulin_dose = max(self.min_insulin, min(insulin_dose, self.max_insulin))
|
|
57
|
+
if insulin_dose != original_insulin_dose:
|
|
58
|
+
self._log_reason(f"Insulin dose adjusted due to safety limits (min: {self.min_insulin}, max: {self.max_insulin})",
|
|
59
|
+
"safety_constraint",
|
|
60
|
+
f"Original: {original_insulin_dose:.2f}, Adjusted: {insulin_dose:.2f}")
|
|
61
|
+
else:
|
|
62
|
+
self._log_reason("Insulin dose within safety limits", "safety_constraint", insulin_dose)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
'total_insulin_delivered': insulin_dose,
|
|
66
|
+
'bolus_insulin': insulin_dose,
|
|
67
|
+
'basal_insulin': 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def reset(self):
|
|
71
|
+
"""Reset controller state for new patient"""
|
|
72
|
+
super().reset()
|
|
73
|
+
self.integral = 0
|
|
74
|
+
self.previous_error = 0
|
|
75
|
+
|
|
76
|
+
def get_algorithm_info(self):
|
|
77
|
+
"""Return algorithm information"""
|
|
78
|
+
return {
|
|
79
|
+
'name': 'Industry PID Controller',
|
|
80
|
+
'type': 'Classical Control',
|
|
81
|
+
'parameters': {
|
|
82
|
+
'kp': self.kp,
|
|
83
|
+
'ki': self.ki,
|
|
84
|
+
'kd': self.kd,
|
|
85
|
+
'target_glucose': self.target_glucose
|
|
86
|
+
},
|
|
87
|
+
'description': 'Industry-standard PID controller for glucose regulation'
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from iints import InsulinAlgorithm, AlgorithmInput, AlgorithmMetadata
|
|
2
|
+
from typing import Dict, Any, Optional
|
|
3
|
+
|
|
4
|
+
class StandardPumpAlgorithm(InsulinAlgorithm):
|
|
5
|
+
"""
|
|
6
|
+
A simplified algorithm representing a standard insulin pump.
|
|
7
|
+
It delivers a fixed basal rate and a simple bolus based on carbs,
|
|
8
|
+
with minimal correction for high glucose.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
11
|
+
super().__init__(settings)
|
|
12
|
+
self.set_algorithm_metadata(AlgorithmMetadata(
|
|
13
|
+
name="Standard Pump",
|
|
14
|
+
author="IINTS-AF Team",
|
|
15
|
+
description="A basic insulin pump algorithm with fixed basal and simple carb/correction bolus.",
|
|
16
|
+
algorithm_type="rule_based"
|
|
17
|
+
))
|
|
18
|
+
# Default settings for a standard pump
|
|
19
|
+
self.isf = self.settings.get('isf', 50.0) # Insulin Sensitivity Factor (mg/dL per Unit)
|
|
20
|
+
self.icr = self.settings.get('icr', 10.0) # Insulin to Carb Ratio (grams per Unit)
|
|
21
|
+
self.basal_rate_per_hour = self.settings.get('basal_rate_per_hour', 0.8) # U/hr
|
|
22
|
+
self.target_glucose = self.settings.get('target_glucose', 100.0) # mg/dL
|
|
23
|
+
|
|
24
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
25
|
+
self.why_log = [] # Clear log for each prediction
|
|
26
|
+
|
|
27
|
+
total_insulin = 0.0
|
|
28
|
+
bolus_insulin = 0.0
|
|
29
|
+
basal_insulin = 0.0
|
|
30
|
+
correction_bolus = 0.0
|
|
31
|
+
meal_bolus = 0.0
|
|
32
|
+
|
|
33
|
+
# 1. Basal Insulin
|
|
34
|
+
# Convert hourly basal rate to dose for the current time step
|
|
35
|
+
basal_insulin = (self.basal_rate_per_hour / 60.0) * data.time_step
|
|
36
|
+
total_insulin += basal_insulin
|
|
37
|
+
self._log_reason("Fixed basal insulin", "basal", basal_insulin, f"Delivered {basal_insulin:.2f} units for {data.time_step} min.")
|
|
38
|
+
|
|
39
|
+
# 2. Meal Bolus
|
|
40
|
+
if data.carb_intake > 0:
|
|
41
|
+
meal_bolus = data.carb_intake / self.icr
|
|
42
|
+
total_insulin += meal_bolus
|
|
43
|
+
self._log_reason("Meal bolus for carb intake", "carb_intake", data.carb_intake, f"Delivered {meal_bolus:.2f} units for {data.carb_intake}g carbs.")
|
|
44
|
+
|
|
45
|
+
# 3. Correction Bolus (simple)
|
|
46
|
+
# Only correct if glucose is significantly above target and not too much IOB
|
|
47
|
+
if data.current_glucose > self.target_glucose + 20 and data.insulin_on_board < 1.0:
|
|
48
|
+
correction_bolus = (data.current_glucose - self.target_glucose) / self.isf / 2 # Correct half the difference
|
|
49
|
+
if correction_bolus > 0:
|
|
50
|
+
total_insulin += correction_bolus
|
|
51
|
+
self._log_reason("Correction bolus for high glucose", "glucose_level", data.current_glucose, f"Delivered {correction_bolus:.2f} units to correct {data.current_glucose} mg/dL.")
|
|
52
|
+
|
|
53
|
+
# Ensure no negative insulin delivery
|
|
54
|
+
total_insulin = max(0.0, total_insulin)
|
|
55
|
+
|
|
56
|
+
self._log_reason(f"Final insulin decision: {total_insulin:.2f} units", "decision", total_insulin)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"total_insulin_delivered": total_insulin,
|
|
60
|
+
"bolus_insulin": bolus_insulin,
|
|
61
|
+
"basal_insulin": basal_insulin,
|
|
62
|
+
"correction_bolus": correction_bolus,
|
|
63
|
+
"meal_bolus": meal_bolus,
|
|
64
|
+
}
|
iints/core/device.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import torch # type: ignore
|
|
6
|
+
_TORCH_AVAILABLE = True
|
|
7
|
+
except Exception: # pragma: no cover - handles missing torch in CI
|
|
8
|
+
torch = None # type: ignore
|
|
9
|
+
_TORCH_AVAILABLE = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _FallbackDevice:
|
|
13
|
+
def __init__(self, name: str = "cpu") -> None:
|
|
14
|
+
self.type = name
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return self.type
|
|
18
|
+
|
|
19
|
+
def __repr__(self) -> str:
|
|
20
|
+
return f"Device({self.type})"
|
|
21
|
+
|
|
22
|
+
class DeviceManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages hardware device detection for cross-platform compatibility.
|
|
25
|
+
Detects MPS (Apple Silicon), CUDA (NVIDIA GPUs), or falls back to CPU.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self._device = self._detect_device()
|
|
29
|
+
|
|
30
|
+
def _detect_device(self):
|
|
31
|
+
if not _TORCH_AVAILABLE:
|
|
32
|
+
print("Torch not available. Falling back to CPU.")
|
|
33
|
+
return _FallbackDevice("cpu")
|
|
34
|
+
|
|
35
|
+
if sys.platform == "darwin" and torch.backends.mps.is_available():
|
|
36
|
+
print("Detected Apple Silicon (MPS) for accelerated computing.")
|
|
37
|
+
return torch.device("mps")
|
|
38
|
+
elif torch.cuda.is_available():
|
|
39
|
+
print(f"Detected NVIDIA GPU (CUDA) with {torch.cuda.device_count()} device(s) for accelerated computing.")
|
|
40
|
+
return torch.device("cuda")
|
|
41
|
+
else:
|
|
42
|
+
print("No GPU detected. Falling back to CPU for computing.")
|
|
43
|
+
return torch.device("cpu")
|
|
44
|
+
|
|
45
|
+
def get_device(self):
|
|
46
|
+
"""
|
|
47
|
+
Returns the detected torch.device object.
|
|
48
|
+
"""
|
|
49
|
+
return self._device
|
|
50
|
+
|
|
51
|
+
# Example usage (for testing purposes, remove in final SDK if not needed)
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
device_manager = DeviceManager()
|
|
54
|
+
device = device_manager.get_device()
|
|
55
|
+
print(f"Using device: {device}")
|
|
56
|
+
|
|
57
|
+
# Small test to ensure device is working
|
|
58
|
+
try:
|
|
59
|
+
x = torch.randn(10, 10, device=device)
|
|
60
|
+
y = torch.randn(10, 10, device=device)
|
|
61
|
+
z = x @ y
|
|
62
|
+
print(f"Successfully performed a matrix multiplication on {device}.")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"Error performing test on {device}: {e}")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Dict, Any, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SensorReading:
|
|
11
|
+
value: float
|
|
12
|
+
status: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SensorModel:
|
|
16
|
+
"""
|
|
17
|
+
Sensor error model for CGM readings.
|
|
18
|
+
|
|
19
|
+
Supports noise, bias, lag (minutes), and dropout (hold last value).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
noise_std: float = 0.0,
|
|
25
|
+
bias: float = 0.0,
|
|
26
|
+
lag_minutes: int = 0,
|
|
27
|
+
dropout_prob: float = 0.0,
|
|
28
|
+
seed: Optional[int] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.noise_std = noise_std
|
|
31
|
+
self.bias = bias
|
|
32
|
+
self.lag_minutes = lag_minutes
|
|
33
|
+
self.dropout_prob = dropout_prob
|
|
34
|
+
self._rng = np.random.default_rng(seed)
|
|
35
|
+
self._history: list[tuple[float, float]] = []
|
|
36
|
+
self._last_reading: Optional[float] = None
|
|
37
|
+
|
|
38
|
+
def reset(self) -> None:
|
|
39
|
+
self._history = []
|
|
40
|
+
self._last_reading = None
|
|
41
|
+
|
|
42
|
+
def read(self, true_glucose: float, current_time: float) -> SensorReading:
|
|
43
|
+
self._history.append((current_time, true_glucose))
|
|
44
|
+
# Keep history window bounded
|
|
45
|
+
if self.lag_minutes > 0:
|
|
46
|
+
cutoff = current_time - (self.lag_minutes * 2)
|
|
47
|
+
self._history = [(t, v) for (t, v) in self._history if t >= cutoff]
|
|
48
|
+
|
|
49
|
+
if self.lag_minutes > 0:
|
|
50
|
+
target_time = current_time - self.lag_minutes
|
|
51
|
+
candidates = [v for (t, v) in self._history if t <= target_time]
|
|
52
|
+
base = candidates[-1] if candidates else true_glucose
|
|
53
|
+
else:
|
|
54
|
+
base = true_glucose
|
|
55
|
+
|
|
56
|
+
reading = base + self.bias
|
|
57
|
+
if self.noise_std > 0:
|
|
58
|
+
reading += float(self._rng.normal(0, self.noise_std))
|
|
59
|
+
|
|
60
|
+
status = "ok"
|
|
61
|
+
if self.dropout_prob > 0 and float(self._rng.random()) < self.dropout_prob:
|
|
62
|
+
status = "dropout_hold"
|
|
63
|
+
if self._last_reading is not None:
|
|
64
|
+
reading = self._last_reading
|
|
65
|
+
|
|
66
|
+
self._last_reading = reading
|
|
67
|
+
return SensorReading(value=reading, status=status)
|
|
68
|
+
|
|
69
|
+
def get_state(self) -> Dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"noise_std": self.noise_std,
|
|
72
|
+
"bias": self.bias,
|
|
73
|
+
"lag_minutes": self.lag_minutes,
|
|
74
|
+
"dropout_prob": self.dropout_prob,
|
|
75
|
+
"last_reading": self._last_reading,
|
|
76
|
+
"history": self._history,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
def set_state(self, state: Dict[str, Any]) -> None:
|
|
80
|
+
self.noise_std = state.get("noise_std", self.noise_std)
|
|
81
|
+
self.bias = state.get("bias", self.bias)
|
|
82
|
+
self.lag_minutes = state.get("lag_minutes", self.lag_minutes)
|
|
83
|
+
self.dropout_prob = state.get("dropout_prob", self.dropout_prob)
|
|
84
|
+
self._last_reading = state.get("last_reading")
|
|
85
|
+
self._history = state.get("history", [])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class PumpDelivery:
|
|
90
|
+
delivered_units: float
|
|
91
|
+
status: str
|
|
92
|
+
reason: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PumpModel:
|
|
96
|
+
"""
|
|
97
|
+
Pump error model for insulin delivery.
|
|
98
|
+
|
|
99
|
+
Supports max delivery per step, quantization, and occlusion/dropout.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
max_units_per_step: Optional[float] = None,
|
|
105
|
+
quantization_units: Optional[float] = None,
|
|
106
|
+
dropout_prob: float = 0.0,
|
|
107
|
+
delivery_noise_std: float = 0.0,
|
|
108
|
+
seed: Optional[int] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
self.max_units_per_step = max_units_per_step
|
|
111
|
+
self.quantization_units = quantization_units
|
|
112
|
+
self.dropout_prob = dropout_prob
|
|
113
|
+
self.delivery_noise_std = delivery_noise_std
|
|
114
|
+
self._rng = np.random.default_rng(seed)
|
|
115
|
+
|
|
116
|
+
def reset(self) -> None:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def deliver(self, requested_units: float, time_step_minutes: float) -> PumpDelivery:
|
|
120
|
+
delivered = requested_units
|
|
121
|
+
status = "ok"
|
|
122
|
+
reason = "approved"
|
|
123
|
+
|
|
124
|
+
if delivered < 0.0:
|
|
125
|
+
delivered = 0.0
|
|
126
|
+
status = "clamped"
|
|
127
|
+
reason = "negative_request"
|
|
128
|
+
|
|
129
|
+
if self.max_units_per_step is not None and delivered > self.max_units_per_step:
|
|
130
|
+
delivered = self.max_units_per_step
|
|
131
|
+
status = "capped"
|
|
132
|
+
reason = f"max_units_per_step {self.max_units_per_step:.2f}"
|
|
133
|
+
|
|
134
|
+
if self.quantization_units:
|
|
135
|
+
delivered = round(delivered / self.quantization_units) * self.quantization_units
|
|
136
|
+
|
|
137
|
+
if self.delivery_noise_std > 0:
|
|
138
|
+
delivered += float(self._rng.normal(0, self.delivery_noise_std))
|
|
139
|
+
delivered = max(0.0, delivered)
|
|
140
|
+
|
|
141
|
+
if self.dropout_prob > 0 and float(self._rng.random()) < self.dropout_prob:
|
|
142
|
+
delivered = 0.0
|
|
143
|
+
status = "occlusion"
|
|
144
|
+
reason = "pump_dropout"
|
|
145
|
+
|
|
146
|
+
return PumpDelivery(delivered_units=delivered, status=status, reason=reason)
|
|
147
|
+
|
|
148
|
+
def get_state(self) -> Dict[str, Any]:
|
|
149
|
+
return {
|
|
150
|
+
"max_units_per_step": self.max_units_per_step,
|
|
151
|
+
"quantization_units": self.quantization_units,
|
|
152
|
+
"dropout_prob": self.dropout_prob,
|
|
153
|
+
"delivery_noise_std": self.delivery_noise_std,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
def set_state(self, state: Dict[str, Any]) -> None:
|
|
157
|
+
self.max_units_per_step = state.get("max_units_per_step", self.max_units_per_step)
|
|
158
|
+
self.quantization_units = state.get("quantization_units", self.quantization_units)
|
|
159
|
+
self.dropout_prob = state.get("dropout_prob", self.dropout_prob)
|
|
160
|
+
self.delivery_noise_std = state.get("delivery_noise_std", self.delivery_noise_std)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .profile import PatientProfile
|
|
2
|
+
from .models import PatientModel
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from .bergman_model import BergmanPatientModel
|
|
6
|
+
except ImportError: # pragma: no cover - scipy may not be installed
|
|
7
|
+
BergmanPatientModel = None # type: ignore[assignment,misc]
|
|
8
|
+
|
|
9
|
+
__all__ = ["PatientProfile", "PatientModel", "BergmanPatientModel"]
|