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
iints/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core module initialization
|
|
File without changes
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from iints.core.simulator import Simulator, StressEvent
|
|
5
|
+
from iints.analysis.clinical_metrics import ClinicalMetricsCalculator
|
|
6
|
+
from iints.core.patient.models import PatientModel
|
|
7
|
+
|
|
8
|
+
class BattleRunner:
|
|
9
|
+
"""Runs a battle between different insulin algorithms."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, algorithms: Dict[str, Any], patient_data: pd.DataFrame, stress_events: Optional[List[StressEvent]] = None, scenario_name: str = "standard"):
|
|
12
|
+
"""
|
|
13
|
+
Initializes the BattleRunner.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
algorithms: A dictionary of algorithm names to their instances.
|
|
17
|
+
patient_data: A DataFrame with 'time', 'glucose', and 'carbs' columns.
|
|
18
|
+
stress_events: An optional list of StressEvent objects to apply during simulation.
|
|
19
|
+
scenario_name: The name of the scenario being run.
|
|
20
|
+
"""
|
|
21
|
+
self.algorithms = algorithms
|
|
22
|
+
self.patient_data = patient_data
|
|
23
|
+
self.metrics_calculator = ClinicalMetricsCalculator()
|
|
24
|
+
self.stress_events = stress_events if stress_events is not None else []
|
|
25
|
+
self.scenario_name = scenario_name
|
|
26
|
+
|
|
27
|
+
def run_battle(self,
|
|
28
|
+
isf_override: Optional[float] = None,
|
|
29
|
+
icr_override: Optional[float] = None) -> Tuple[Dict[str, Any], Dict[str, pd.DataFrame]]: # Modify return type
|
|
30
|
+
"""
|
|
31
|
+
Runs the simulation for each algorithm and returns a battle report and detailed simulation data.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
isf_override (Optional[float]): Override value for Insulin Sensitivity Factor.
|
|
35
|
+
icr_override (Optional[float]): Override value for Insulin-to-Carb Ratio.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple[Dict[str, Any], Dict[str, pd.DataFrame]]: A tuple containing:
|
|
39
|
+
- A dictionary containing the battle report.
|
|
40
|
+
- A dictionary mapping algorithm names to their full simulation results DataFrame.
|
|
41
|
+
"""
|
|
42
|
+
battle_results = {}
|
|
43
|
+
detailed_simulation_data = {} # New dictionary to store full dfs
|
|
44
|
+
|
|
45
|
+
for algo_name, algo_instance in self.algorithms.items():
|
|
46
|
+
print(f"Running simulation for {algo_name}...")
|
|
47
|
+
|
|
48
|
+
# Apply overrides if provided
|
|
49
|
+
if isf_override is not None and hasattr(algo_instance, 'set_isf'):
|
|
50
|
+
algo_instance.set_isf(isf_override)
|
|
51
|
+
if icr_override is not None and hasattr(algo_instance, 'set_icr'):
|
|
52
|
+
algo_instance.set_icr(icr_override)
|
|
53
|
+
|
|
54
|
+
initial_glucose = self.patient_data['glucose'].iloc[0]
|
|
55
|
+
patient_model = PatientModel(initial_glucose=initial_glucose)
|
|
56
|
+
|
|
57
|
+
simulator = Simulator(patient_model=patient_model, algorithm=algo_instance, time_step=5)
|
|
58
|
+
|
|
59
|
+
# Add predefined stress events to the simulator
|
|
60
|
+
for event in self.stress_events:
|
|
61
|
+
simulator.add_stress_event(event)
|
|
62
|
+
|
|
63
|
+
duration = self.patient_data['time'].max()
|
|
64
|
+
|
|
65
|
+
# Existing carb events from patient_data are also added as stress events
|
|
66
|
+
# This needs careful consideration if scenario also adds carb events
|
|
67
|
+
for index, row in self.patient_data.iterrows():
|
|
68
|
+
if row['carbs'] > 0:
|
|
69
|
+
simulator.add_stress_event(StressEvent(start_time=int(row['time']), event_type='meal', value=row['carbs']))
|
|
70
|
+
|
|
71
|
+
# Unpack the tuple returned by simulator.run()
|
|
72
|
+
simulation_results_df, algorithm_safety_report = simulator.run(duration_minutes=duration)
|
|
73
|
+
|
|
74
|
+
detailed_simulation_data[algo_name] = simulation_results_df # Store the full df
|
|
75
|
+
|
|
76
|
+
glucose_series = simulation_results_df['glucose_actual_mgdl']
|
|
77
|
+
metrics = self.metrics_calculator.calculate(glucose=glucose_series, duration_hours=duration/60)
|
|
78
|
+
|
|
79
|
+
# Calculate uncertainty score
|
|
80
|
+
uncertainty_score = simulation_results_df['uncertainty'].mean() if 'uncertainty' in simulation_results_df.columns else 0.0
|
|
81
|
+
|
|
82
|
+
metrics_dict = metrics.to_dict()
|
|
83
|
+
metrics_dict['uncertainty_score'] = uncertainty_score
|
|
84
|
+
|
|
85
|
+
# Add safety report metrics
|
|
86
|
+
bolus_interventions_count = algorithm_safety_report.get('bolus_interventions_count', 0)
|
|
87
|
+
metrics_dict['bolus_interventions_count'] = bolus_interventions_count
|
|
88
|
+
|
|
89
|
+
# The existing 'safety_events_count' will now be based on the bolus_interventions_count
|
|
90
|
+
metrics_dict['safety_events_count'] = bolus_interventions_count
|
|
91
|
+
|
|
92
|
+
battle_results[algo_name] = metrics_dict
|
|
93
|
+
|
|
94
|
+
winner = max(battle_results, key=lambda algo: battle_results[algo]['tir_70_180'])
|
|
95
|
+
|
|
96
|
+
report = {
|
|
97
|
+
"battle_name": "Algorithm Battle",
|
|
98
|
+
"winner": winner,
|
|
99
|
+
"scenario_name": self.scenario_name, # Add scenario name
|
|
100
|
+
"rankings": sorted(
|
|
101
|
+
[
|
|
102
|
+
{
|
|
103
|
+
"participant": algo_name,
|
|
104
|
+
"overall_score": result['tir_70_180'],
|
|
105
|
+
"tir": result['tir_70_180'],
|
|
106
|
+
"tir_tight": result.get('tir_70_140', 0),
|
|
107
|
+
"cv": result['cv'],
|
|
108
|
+
"time_below_70": result['tir_below_70'],
|
|
109
|
+
"time_above_180": result['tir_above_180'],
|
|
110
|
+
"gmi": result['gmi'],
|
|
111
|
+
"lbgi": result['lbgi'],
|
|
112
|
+
"uncertainty_score": result['uncertainty_score'],
|
|
113
|
+
"safety_events_count": result['safety_events_count'],
|
|
114
|
+
"bolus_interventions_count": result['bolus_interventions_count'],
|
|
115
|
+
}
|
|
116
|
+
for algo_name, result in battle_results.items()
|
|
117
|
+
],
|
|
118
|
+
key=lambda x: x["overall_score"],
|
|
119
|
+
reverse=True,
|
|
120
|
+
),
|
|
121
|
+
"detailed_simulation_data": {algo_name: df.to_json() for algo_name, df in detailed_simulation_data.items()} # Convert DataFrames to JSON strings
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return report, detailed_simulation_data
|
|
125
|
+
|
|
126
|
+
def print_battle_report(self, battle_report: Dict[str, Any]):
|
|
127
|
+
"""
|
|
128
|
+
Prints the battle report to the console.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
battle_report: The battle report dictionary.
|
|
132
|
+
"""
|
|
133
|
+
print("\nBATTLE REPORT")
|
|
134
|
+
print("="*70)
|
|
135
|
+
print(f"Winner: {battle_report['winner']}")
|
|
136
|
+
print("\nRankings:")
|
|
137
|
+
for rank in battle_report['rankings']:
|
|
138
|
+
print(f"- {rank['participant']}: TIR = {rank['tir']:.1f}%, CV = {rank['cv']:.1f}%, Safety Events = {rank['safety_events_count']}")
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput
|
|
3
|
+
|
|
4
|
+
class CorrectionBolus(InsulinAlgorithm):
|
|
5
|
+
"""
|
|
6
|
+
An insulin algorithm that calculates a meal bolus based on carbohydrates
|
|
7
|
+
and adds a correction bolus if current glucose is above a target.
|
|
8
|
+
|
|
9
|
+
This algorithm introduces sensitivity to current glucose levels.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
12
|
+
super().__init__(settings)
|
|
13
|
+
# Default settings, can be overridden by 'settings' dict
|
|
14
|
+
self.default_settings = {
|
|
15
|
+
"fixed_basal_rate": 0.8, # Units per hour
|
|
16
|
+
"carb_ratio": 10.0, # Grams of carbs per unit of insulin
|
|
17
|
+
"insulin_sensitivity_factor": 50.0, # mg/dL per unit of insulin
|
|
18
|
+
"target_glucose": 100.0, # mg/dL
|
|
19
|
+
"max_bolus": 10.0 # Maximum single bolus in Units
|
|
20
|
+
}
|
|
21
|
+
# Merge default settings with provided settings
|
|
22
|
+
self.settings = {**self.default_settings, **self.settings}
|
|
23
|
+
|
|
24
|
+
# Validate essential settings
|
|
25
|
+
if not all(k in self.settings for k in ["fixed_basal_rate", "carb_ratio", "insulin_sensitivity_factor", "target_glucose"]):
|
|
26
|
+
raise ValueError("CorrectionBolus algorithm requires 'fixed_basal_rate', 'carb_ratio', 'insulin_sensitivity_factor', and 'target_glucose' in settings.")
|
|
27
|
+
|
|
28
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
29
|
+
self.why_log = [] # Clear the log for this prediction cycle
|
|
30
|
+
|
|
31
|
+
basal_rate = self.settings["fixed_basal_rate"]
|
|
32
|
+
if data.basal_rate_u_per_hr is not None:
|
|
33
|
+
basal_rate = float(data.basal_rate_u_per_hr)
|
|
34
|
+
basal_rate_units_per_minute = basal_rate / 60.0
|
|
35
|
+
basal_insulin = basal_rate_units_per_minute * data.time_step
|
|
36
|
+
self._log_reason(f"Basal insulin calculated ({basal_rate} U/hr)", "basal_delivery", basal_insulin)
|
|
37
|
+
|
|
38
|
+
carb_intake = data.carb_intake
|
|
39
|
+
meal_bolus = 0.0
|
|
40
|
+
if carb_intake > 0:
|
|
41
|
+
carb_ratio = self.settings["carb_ratio"]
|
|
42
|
+
if data.icr is not None:
|
|
43
|
+
carb_ratio = float(data.icr)
|
|
44
|
+
meal_bolus = carb_intake / carb_ratio
|
|
45
|
+
self._log_reason(f"Meal bolus calculated for {carb_intake:.0f}g carbs (CR: {carb_ratio})", "meal_response", meal_bolus)
|
|
46
|
+
else:
|
|
47
|
+
self._log_reason("No meal bolus needed (no carb intake)", "meal_response", 0.0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
correction_bolus = 0.0
|
|
51
|
+
if data.current_glucose > self.settings["target_glucose"]:
|
|
52
|
+
glucose_deviation = data.current_glucose - self.settings["target_glucose"]
|
|
53
|
+
isf = self.settings["insulin_sensitivity_factor"]
|
|
54
|
+
if data.isf is not None:
|
|
55
|
+
isf = float(data.isf)
|
|
56
|
+
correction_bolus = glucose_deviation / isf
|
|
57
|
+
self._log_reason(f"Correction bolus calculated for high glucose (Current: {data.current_glucose:.0f}mg/dL, Target: {self.settings['target_glucose']:.0f}mg/dL, ISF: {isf})", "glucose_correction", correction_bolus)
|
|
58
|
+
else:
|
|
59
|
+
self._log_reason(f"No correction bolus needed (glucose at or below target: {self.settings['target_glucose']:.0f}mg/dL)", "glucose_correction", 0.0)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Ensure total bolus does not exceed max_bolus
|
|
63
|
+
total_bolus_before_cap = meal_bolus + correction_bolus
|
|
64
|
+
if total_bolus_before_cap > self.settings["max_bolus"]:
|
|
65
|
+
original_meal_bolus = meal_bolus
|
|
66
|
+
original_correction_bolus = correction_bolus
|
|
67
|
+
|
|
68
|
+
# Prioritize meal bolus, then cap correction bolus
|
|
69
|
+
if meal_bolus < self.settings["max_bolus"]:
|
|
70
|
+
remaining_capacity = self.settings["max_bolus"] - meal_bolus
|
|
71
|
+
correction_bolus = min(correction_bolus, remaining_capacity)
|
|
72
|
+
else:
|
|
73
|
+
meal_bolus = self.settings["max_bolus"] # Cap meal bolus too
|
|
74
|
+
correction_bolus = 0.0 # No correction if meal bolus maxed out
|
|
75
|
+
|
|
76
|
+
self._log_reason(f"Total bolus capped at {self.settings['max_bolus']}. Original: Meal={original_meal_bolus:.2f}, Corr={original_correction_bolus:.2f}. Adjusted: Meal={meal_bolus:.2f}, Corr={correction_bolus:.2f}.", "safety_constraint", self.settings['max_bolus'])
|
|
77
|
+
|
|
78
|
+
total_bolus = meal_bolus + correction_bolus # Recalculate after capping
|
|
79
|
+
total_insulin_delivered = basal_insulin + total_bolus
|
|
80
|
+
|
|
81
|
+
self._log_reason(f"Final total insulin delivered: {total_insulin_delivered:.2f} U (Basal: {basal_insulin:.2f}, Meal Bolus: {meal_bolus:.2f}, Correction Bolus: {correction_bolus:.2f})", "final_decision", total_insulin_delivered)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"basal_insulin": basal_insulin,
|
|
85
|
+
"meal_bolus": meal_bolus,
|
|
86
|
+
"correction_bolus": correction_bolus,
|
|
87
|
+
"total_insulin_delivered": total_insulin_delivered
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return (f"CorrectionBolus Algorithm:\n"
|
|
92
|
+
f" Fixed Basal Rate: {self.settings['fixed_basal_rate']} U/hr\n"
|
|
93
|
+
f" Carb Ratio (CR): {self.settings['carb_ratio']} g/U\n"
|
|
94
|
+
f" Insulin Sensitivity Factor (ISF): {self.settings['insulin_sensitivity_factor']} mg/dL/U\n"
|
|
95
|
+
f" Target Glucose: {self.settings['target_glucose']} mg/dL")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# src/algorithm/discovery.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import importlib
|
|
5
|
+
import inspect
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Type
|
|
8
|
+
|
|
9
|
+
from iints.api.base_algorithm import InsulinAlgorithm
|
|
10
|
+
|
|
11
|
+
def discover_algorithms() -> Dict[str, Type[InsulinAlgorithm]]:
|
|
12
|
+
"""
|
|
13
|
+
Dynamically discovers and loads all algorithm classes that inherit from InsulinAlgorithm.
|
|
14
|
+
|
|
15
|
+
It scans the `src/algorithm` directory and its subdirectories (like `user`).
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A dictionary mapping the algorithm's display name to its class type.
|
|
19
|
+
e.g., {"PID Controller": PIDController, "My Custom Algo": MyCustomAlgo}
|
|
20
|
+
"""
|
|
21
|
+
algorithms: Dict[str, Type[InsulinAlgorithm]] = {}
|
|
22
|
+
|
|
23
|
+
# The root directory for algorithm discovery
|
|
24
|
+
# We start from 'src' to make the imports work correctly (e.g., src.algorithm.pid_controller)
|
|
25
|
+
root_path = Path(__file__).parent.parent.parent # Corrected to point to project root
|
|
26
|
+
algorithm_dir = Path(__file__).parent
|
|
27
|
+
|
|
28
|
+
for root, _, files in os.walk(algorithm_dir):
|
|
29
|
+
for filename in files:
|
|
30
|
+
# Consider only Python files, excluding __init__ and base files
|
|
31
|
+
if filename.endswith(".py") and not filename.startswith(("_", "base_")):
|
|
32
|
+
|
|
33
|
+
# Construct the module path for importlib
|
|
34
|
+
# e.g., /path/to/project/src/algorithm/user/my_algo.py
|
|
35
|
+
# becomes -> src.algorithm.user.my_algo
|
|
36
|
+
|
|
37
|
+
module_path = Path(root) / filename
|
|
38
|
+
# Get relative path from the 'src' directory
|
|
39
|
+
relative_path = module_path.relative_to(root_path)
|
|
40
|
+
# Convert path to module name (e.g., algorithm/user/my_algo.py -> algorithm.user.my_algo)
|
|
41
|
+
module_name = str(relative_path).replace(os.sep, '.')[:-3]
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
module = importlib.import_module(module_name)
|
|
45
|
+
|
|
46
|
+
# Find all classes in the module that are subclasses of InsulinAlgorithm
|
|
47
|
+
for _, member_class in inspect.getmembers(module, inspect.isclass):
|
|
48
|
+
# Ensure the class is defined in this module (not imported)
|
|
49
|
+
# and is a subclass of InsulinAlgorithm (but not the base class itself)
|
|
50
|
+
if (
|
|
51
|
+
issubclass(member_class, InsulinAlgorithm)
|
|
52
|
+
and member_class is not InsulinAlgorithm
|
|
53
|
+
and member_class.__module__ == module_name
|
|
54
|
+
):
|
|
55
|
+
# Instantiate the class to get its metadata
|
|
56
|
+
try:
|
|
57
|
+
instance = member_class()
|
|
58
|
+
metadata = instance.get_algorithm_metadata()
|
|
59
|
+
display_name = metadata.name
|
|
60
|
+
|
|
61
|
+
# Avoid overwriting algorithms with the same display name
|
|
62
|
+
if display_name in algorithms:
|
|
63
|
+
print(f"Warning: Duplicate algorithm name '{display_name}' found. Skipping {member_class.__name__}.")
|
|
64
|
+
else:
|
|
65
|
+
algorithms[display_name] = member_class
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"Warning: Could not instantiate or get metadata for {member_class.__name__}. Error: {e}")
|
|
68
|
+
|
|
69
|
+
except ImportError as e:
|
|
70
|
+
print(f"Warning: Could not import module {module_name}. Error: {e}")
|
|
71
|
+
|
|
72
|
+
return algorithms
|
|
73
|
+
|
|
74
|
+
if __name__ == '__main__':
|
|
75
|
+
# A simple test to demonstrate the discovery mechanism
|
|
76
|
+
print("Discovering all available IINTS-AF algorithms...")
|
|
77
|
+
discovered_algos = discover_algorithms()
|
|
78
|
+
|
|
79
|
+
if not discovered_algos:
|
|
80
|
+
print("No algorithms found.")
|
|
81
|
+
else:
|
|
82
|
+
print("\nFound the following algorithms:")
|
|
83
|
+
for name, algo_class in discovered_algos.items():
|
|
84
|
+
print(f"- '{name}' (Class: {algo_class.__name__})")
|
|
85
|
+
|
|
86
|
+
# Example of instantiating and using a discovered algorithm
|
|
87
|
+
if "Template Algorithm" in discovered_algos:
|
|
88
|
+
print("\n--- Testing 'Template Algorithm' ---")
|
|
89
|
+
TemplateAlgoClass = discovered_algos["Template Algorithm"]
|
|
90
|
+
template_instance = TemplateAlgoClass()
|
|
91
|
+
metadata = template_instance.get_algorithm_metadata()
|
|
92
|
+
print(f"Successfully instantiated. Author: {metadata.author}")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Dict, Any, Optional
|
|
2
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput
|
|
3
|
+
|
|
4
|
+
class FixedBasalBolus(InsulinAlgorithm):
|
|
5
|
+
"""
|
|
6
|
+
A simple insulin algorithm that delivers a fixed basal rate and
|
|
7
|
+
a meal bolus based on carbohydrate intake.
|
|
8
|
+
|
|
9
|
+
This algorithm is purely rule-based and stateless (or nearly so) for transparency.
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
12
|
+
super().__init__(settings)
|
|
13
|
+
# Default settings, can be overridden by 'settings' dict
|
|
14
|
+
self.default_settings = {
|
|
15
|
+
"fixed_basal_rate": 0.8, # Units per hour
|
|
16
|
+
"carb_ratio": 10.0, # Grams of carbs per unit of insulin
|
|
17
|
+
}
|
|
18
|
+
# Merge default settings with provided settings
|
|
19
|
+
self.settings = {**self.default_settings, **self.settings}
|
|
20
|
+
|
|
21
|
+
# Validate essential settings
|
|
22
|
+
if not all(k in self.settings for k in ["fixed_basal_rate", "carb_ratio"]):
|
|
23
|
+
raise ValueError("FixedBasalBolus algorithm requires 'fixed_basal_rate' and 'carb_ratio' in settings.")
|
|
24
|
+
|
|
25
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Calculates insulin dose based on fixed basal rate and carb intake.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data (AlgorithmInput): Dataclass containing all input data.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dict[str, Any]: Contains 'basal_insulin' and 'bolus_insulin' for the time step.
|
|
34
|
+
"""
|
|
35
|
+
basal_rate = self.settings["fixed_basal_rate"]
|
|
36
|
+
if data.basal_rate_u_per_hr is not None:
|
|
37
|
+
basal_rate = float(data.basal_rate_u_per_hr)
|
|
38
|
+
basal_rate_units_per_minute = basal_rate / 60.0
|
|
39
|
+
basal_insulin = basal_rate_units_per_minute * data.time_step
|
|
40
|
+
|
|
41
|
+
carb_intake = data.carb_intake
|
|
42
|
+
bolus_insulin = 0.0
|
|
43
|
+
if carb_intake > 0:
|
|
44
|
+
carb_ratio = self.settings["carb_ratio"]
|
|
45
|
+
if data.icr is not None:
|
|
46
|
+
carb_ratio = float(data.icr)
|
|
47
|
+
bolus_insulin = carb_intake / carb_ratio
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"basal_insulin": basal_insulin,
|
|
51
|
+
"bolus_insulin": bolus_insulin,
|
|
52
|
+
"total_insulin_delivered": basal_insulin + bolus_insulin
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def __str__(self):
|
|
56
|
+
return (f"FixedBasalBolus Algorithm:\n"
|
|
57
|
+
f" Fixed Basal Rate: {self.settings['fixed_basal_rate']} U/hr\n"
|
|
58
|
+
f" Carb Ratio (CR): {self.settings['carb_ratio']} g/U")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import torch # type: ignore
|
|
3
|
+
_TORCH_AVAILABLE = True
|
|
4
|
+
except Exception: # pragma: no cover - optional dependency
|
|
5
|
+
torch = None # type: ignore
|
|
6
|
+
_TORCH_AVAILABLE = False
|
|
7
|
+
import numpy as np
|
|
8
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput
|
|
9
|
+
from .lstm_algorithm import LSTMInsulinAlgorithm
|
|
10
|
+
from .correction_bolus import CorrectionBolus
|
|
11
|
+
|
|
12
|
+
if _TORCH_AVAILABLE:
|
|
13
|
+
class HybridInsulinAlgorithm(InsulinAlgorithm):
|
|
14
|
+
"""Hybrid algorithm that switches between LSTM and rule-based based on uncertainty."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, uncertainty_threshold=0.15, mc_samples=30):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.lstm_algo = LSTMInsulinAlgorithm()
|
|
19
|
+
self.rule_algo = CorrectionBolus()
|
|
20
|
+
self.uncertainty_threshold = uncertainty_threshold
|
|
21
|
+
self.mc_samples = mc_samples
|
|
22
|
+
self.switch_count = 0
|
|
23
|
+
self.lstm_count = 0
|
|
24
|
+
|
|
25
|
+
def calculate_uncertainty(self, data: AlgorithmInput):
|
|
26
|
+
"""Calculate uncertainty using MC Dropout."""
|
|
27
|
+
if not hasattr(self.lstm_algo, 'model') or self.lstm_algo.model is None:
|
|
28
|
+
return 1.0 # High uncertainty if model not loaded
|
|
29
|
+
|
|
30
|
+
# Use the same input format as LSTM algorithm
|
|
31
|
+
placeholder_input = [3, data.current_glucose, 72, 29, 32, 0.47, 33]
|
|
32
|
+
input_tensor = torch.tensor(placeholder_input, dtype=torch.float32).reshape(1, 1, 7)
|
|
33
|
+
|
|
34
|
+
self.lstm_algo.model.train() # Enable dropout
|
|
35
|
+
predictions = []
|
|
36
|
+
|
|
37
|
+
with torch.no_grad():
|
|
38
|
+
for _ in range(self.mc_samples):
|
|
39
|
+
pred = self.lstm_algo.model(input_tensor).item()
|
|
40
|
+
predictions.append(pred)
|
|
41
|
+
|
|
42
|
+
self.lstm_algo.model.eval() # Disable dropout
|
|
43
|
+
return np.std(predictions) / (np.mean(predictions) + 1e-8) # Coefficient of variation
|
|
44
|
+
|
|
45
|
+
def predict_insulin(self, data: AlgorithmInput):
|
|
46
|
+
self.why_log = [] # Clear the log for this prediction cycle
|
|
47
|
+
|
|
48
|
+
uncertainty = self.calculate_uncertainty(data)
|
|
49
|
+
self._log_reason(f"Calculated uncertainty: {uncertainty:.4f}", "uncertainty_quantification", uncertainty)
|
|
50
|
+
|
|
51
|
+
if uncertainty > self.uncertainty_threshold:
|
|
52
|
+
self.switch_count += 1
|
|
53
|
+
self._log_reason(f"Uncertainty ({uncertainty:.4f}) exceeds threshold ({self.uncertainty_threshold:.4f}). Switching to Rule-Based Algorithm.", "decision_switch", "Rule-Based")
|
|
54
|
+
insulin_output = self.rule_algo.predict_insulin(data)
|
|
55
|
+
insulin_output["uncertainty"] = uncertainty
|
|
56
|
+
# Append child algorithm's why_log to hybrid's why_log
|
|
57
|
+
self.why_log.extend(self.rule_algo.get_why_log())
|
|
58
|
+
return insulin_output
|
|
59
|
+
else:
|
|
60
|
+
self.lstm_count += 1
|
|
61
|
+
self._log_reason(f"Uncertainty ({uncertainty:.4f}) is within threshold ({self.uncertainty_threshold:.4f}). Using LSTM Algorithm.", "decision_switch", "LSTM")
|
|
62
|
+
insulin_output = self.lstm_algo.predict_insulin(data)
|
|
63
|
+
insulin_output["uncertainty"] = uncertainty
|
|
64
|
+
# Append child algorithm's why_log to hybrid's why_log
|
|
65
|
+
self.why_log.extend(self.lstm_algo.get_why_log())
|
|
66
|
+
return insulin_output
|
|
67
|
+
|
|
68
|
+
def reset(self):
|
|
69
|
+
self.lstm_algo.reset()
|
|
70
|
+
self.rule_algo.reset()
|
|
71
|
+
self.switch_count = 0
|
|
72
|
+
self.lstm_count = 0
|
|
73
|
+
|
|
74
|
+
def get_state(self):
|
|
75
|
+
"""Get current algorithm state."""
|
|
76
|
+
return {
|
|
77
|
+
"switch_count": self.switch_count,
|
|
78
|
+
"lstm_count": self.lstm_count,
|
|
79
|
+
"lstm_usage": self.lstm_count / (self.switch_count + self.lstm_count) if (self.switch_count + self.lstm_count) > 0 else 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def get_stats(self):
|
|
83
|
+
total = self.switch_count + self.lstm_count
|
|
84
|
+
return {
|
|
85
|
+
"lstm_usage": self.lstm_count / total if total > 0 else 0,
|
|
86
|
+
"rule_usage": self.switch_count / total if total > 0 else 0,
|
|
87
|
+
"total_decisions": total
|
|
88
|
+
}
|
|
89
|
+
else:
|
|
90
|
+
class HybridInsulinAlgorithm(InsulinAlgorithm): # type: ignore
|
|
91
|
+
def __init__(self, *args, **kwargs):
|
|
92
|
+
raise ImportError("Torch is required for HybridInsulinAlgorithm. Install with `pip install iints[torch]`.")
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import torch # type: ignore
|
|
3
|
+
import torch.nn as nn # type: ignore
|
|
4
|
+
_TORCH_AVAILABLE = True
|
|
5
|
+
except Exception: # pragma: no cover - optional dependency
|
|
6
|
+
torch = None # type: ignore
|
|
7
|
+
nn = None # type: ignore
|
|
8
|
+
_TORCH_AVAILABLE = False
|
|
9
|
+
import os
|
|
10
|
+
import numpy as np
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
from collections import deque
|
|
13
|
+
from iints.api.base_algorithm import InsulinAlgorithm, AlgorithmInput
|
|
14
|
+
from .correction_bolus import CorrectionBolus
|
|
15
|
+
|
|
16
|
+
if _TORCH_AVAILABLE:
|
|
17
|
+
# Define the LSTM model with Dropout
|
|
18
|
+
class LSTMModel(nn.Module):
|
|
19
|
+
def __init__(self, input_size, hidden_size, output_size, dropout_prob=0.5):
|
|
20
|
+
super(LSTMModel, self).__init__()
|
|
21
|
+
self.hidden_size = hidden_size
|
|
22
|
+
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
|
|
23
|
+
self.dropout = nn.Dropout(p=dropout_prob)
|
|
24
|
+
self.fc = nn.Linear(hidden_size, output_size)
|
|
25
|
+
|
|
26
|
+
def forward(self, x):
|
|
27
|
+
# x shape: (batch_size, sequence_length, input_size)
|
|
28
|
+
lstm_out, _ = self.lstm(x)
|
|
29
|
+
# Use the output from the last time step and apply dropout
|
|
30
|
+
out = self.dropout(lstm_out[:, -1, :])
|
|
31
|
+
output = self.fc(out)
|
|
32
|
+
return output
|
|
33
|
+
else:
|
|
34
|
+
class LSTMModel: # type: ignore
|
|
35
|
+
def __init__(self, *args, **kwargs):
|
|
36
|
+
raise ImportError("Torch is required for LSTMModel. Install with `pip install iints[torch]`.")
|
|
37
|
+
|
|
38
|
+
if _TORCH_AVAILABLE:
|
|
39
|
+
class LSTMInsulinAlgorithm(InsulinAlgorithm):
|
|
40
|
+
"""
|
|
41
|
+
An insulin algorithm that uses a simple LSTM model with Monte Carlo Dropout
|
|
42
|
+
for uncertainty estimation. If uncertainty is high, it falls back to a rule-based
|
|
43
|
+
algorithm.
|
|
44
|
+
"""
|
|
45
|
+
def __init__(self, settings: Optional[Dict[str, Any]] = None):
|
|
46
|
+
super().__init__(settings)
|
|
47
|
+
self.default_settings = {
|
|
48
|
+
"input_features": 7,
|
|
49
|
+
"hidden_size": 50,
|
|
50
|
+
"output_size": 1,
|
|
51
|
+
"dropout_prob": 0.5,
|
|
52
|
+
"model_path": os.path.join(os.path.dirname(__file__), 'trained_lstm_model.pth'),
|
|
53
|
+
"mc_samples": 50,
|
|
54
|
+
"uncertainty_threshold": 0.5, # This threshold may need tuning
|
|
55
|
+
}
|
|
56
|
+
self.settings = {**self.default_settings, **(settings or {})}
|
|
57
|
+
|
|
58
|
+
self.model = LSTMModel(
|
|
59
|
+
self.settings["input_features"],
|
|
60
|
+
self.settings["hidden_size"],
|
|
61
|
+
self.settings["output_size"],
|
|
62
|
+
self.settings["dropout_prob"]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Load the trained model if it exists
|
|
66
|
+
if os.path.exists(self.settings['model_path']):
|
|
67
|
+
print(f"Loading trained model from {self.settings['model_path']}")
|
|
68
|
+
self.model.load_state_dict(torch.load(self.settings['model_path'], weights_only=True))
|
|
69
|
+
else:
|
|
70
|
+
print(f"Warning: Trained model not found at {self.settings['model_path']}. LSTM will make random predictions.")
|
|
71
|
+
|
|
72
|
+
# Instantiate fallback algorithm
|
|
73
|
+
self.fallback_algo = CorrectionBolus()
|
|
74
|
+
self.reset()
|
|
75
|
+
|
|
76
|
+
def reset(self):
|
|
77
|
+
"""Resets the algorithm's internal state."""
|
|
78
|
+
super().reset()
|
|
79
|
+
|
|
80
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
81
|
+
self.why_log = [] # Clear the log for this prediction cycle
|
|
82
|
+
|
|
83
|
+
placeholder_input = [3, data.current_glucose, 72, 29, 32, 0.47, 33]
|
|
84
|
+
input_tensor = torch.tensor(placeholder_input, dtype=torch.float32).reshape(1, 1, self.settings['input_features'])
|
|
85
|
+
|
|
86
|
+
self._log_reason("LSTM input tensor created", "data_preparation", input_tensor.tolist())
|
|
87
|
+
|
|
88
|
+
# --- Monte Carlo Dropout ---
|
|
89
|
+
self.model.train() # Enable dropout
|
|
90
|
+
predictions = []
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
for _ in range(self.settings['mc_samples']):
|
|
93
|
+
pred = self.model(input_tensor).item()
|
|
94
|
+
predictions.append(pred)
|
|
95
|
+
self.model.eval() # Disable dropout for future use if any
|
|
96
|
+
|
|
97
|
+
mc_predictions_array = np.array(predictions)
|
|
98
|
+
mean_prediction = np.mean(mc_predictions_array)
|
|
99
|
+
std_dev = np.std(mc_predictions_array)
|
|
100
|
+
self._log_reason(f"Monte Carlo Dropout predictions generated (mean: {mean_prediction:.4f}, std dev: {std_dev:.4f})", "uncertainty_quantification", {'mean': mean_prediction, 'std_dev': std_dev})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- Hybrid Safety Controller ---
|
|
104
|
+
if std_dev > self.settings['uncertainty_threshold']:
|
|
105
|
+
self._log_reason(f"High uncertainty detected ({std_dev:.3f} > {self.settings['uncertainty_threshold']}). Falling back to rule-based algorithm.", "safety_fallback", std_dev)
|
|
106
|
+
fallback_result = self.fallback_algo.predict_insulin(data)
|
|
107
|
+
fallback_result['uncertainty'] = std_dev
|
|
108
|
+
fallback_result['fallback_triggered'] = True
|
|
109
|
+
# Extend fallback algo's log
|
|
110
|
+
self.why_log.extend(self.fallback_algo.get_why_log())
|
|
111
|
+
return fallback_result
|
|
112
|
+
|
|
113
|
+
total_insulin_delivered = max(0.0, mean_prediction * 10) # Arbitrary scaling for demo
|
|
114
|
+
self._log_reason(f"LSTM prediction accepted (uncertainty: {std_dev:.4f}). Delivered insulin scaled from raw prediction.", "lstm_prediction", total_insulin_delivered)
|
|
115
|
+
|
|
116
|
+
self.state['last_prediction'] = total_insulin_delivered
|
|
117
|
+
self.state['raw_prediction'] = mean_prediction
|
|
118
|
+
self.state['uncertainty'] = std_dev
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"total_insulin_delivered": total_insulin_delivered,
|
|
122
|
+
"predicted_insulin_raw": mean_prediction,
|
|
123
|
+
"uncertainty": std_dev,
|
|
124
|
+
"fallback_triggered": False
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def __str__(self):
|
|
128
|
+
return (f"Hybrid LSTM/Rule-Based Algorithm:\n"
|
|
129
|
+
f" Model Path: {self.settings['model_path']}\n"
|
|
130
|
+
f" MC Samples: {self.settings['mc_samples']}\n"
|
|
131
|
+
f" Uncertainty Threshold: {self.settings['uncertainty_threshold']}")
|
|
132
|
+
else:
|
|
133
|
+
class LSTMInsulinAlgorithm(InsulinAlgorithm): # type: ignore
|
|
134
|
+
def __init__(self, *args, **kwargs):
|
|
135
|
+
raise ImportError("Torch is required for LSTMInsulinAlgorithm. Install with `pip install iints[torch]`.")
|
|
136
|
+
|
|
137
|
+
def predict_insulin(self, data: AlgorithmInput) -> Dict[str, Any]:
|
|
138
|
+
raise ImportError("Torch is required for LSTMInsulinAlgorithm. Install with `pip install iints[torch]`.")
|