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.
Files changed (118) hide show
  1. iints/__init__.py +183 -0
  2. iints/analysis/__init__.py +12 -0
  3. iints/analysis/algorithm_xray.py +387 -0
  4. iints/analysis/baseline.py +92 -0
  5. iints/analysis/clinical_benchmark.py +198 -0
  6. iints/analysis/clinical_metrics.py +551 -0
  7. iints/analysis/clinical_tir_analyzer.py +136 -0
  8. iints/analysis/diabetes_metrics.py +43 -0
  9. iints/analysis/edge_efficiency.py +33 -0
  10. iints/analysis/edge_performance_monitor.py +315 -0
  11. iints/analysis/explainability.py +94 -0
  12. iints/analysis/explainable_ai.py +232 -0
  13. iints/analysis/hardware_benchmark.py +221 -0
  14. iints/analysis/metrics.py +117 -0
  15. iints/analysis/population_report.py +188 -0
  16. iints/analysis/reporting.py +345 -0
  17. iints/analysis/safety_index.py +311 -0
  18. iints/analysis/sensor_filtering.py +54 -0
  19. iints/analysis/validator.py +273 -0
  20. iints/api/__init__.py +0 -0
  21. iints/api/base_algorithm.py +307 -0
  22. iints/api/registry.py +103 -0
  23. iints/api/template_algorithm.py +195 -0
  24. iints/assets/iints_logo.png +0 -0
  25. iints/cli/__init__.py +0 -0
  26. iints/cli/cli.py +2598 -0
  27. iints/core/__init__.py +1 -0
  28. iints/core/algorithms/__init__.py +0 -0
  29. iints/core/algorithms/battle_runner.py +138 -0
  30. iints/core/algorithms/correction_bolus.py +95 -0
  31. iints/core/algorithms/discovery.py +92 -0
  32. iints/core/algorithms/fixed_basal_bolus.py +58 -0
  33. iints/core/algorithms/hybrid_algorithm.py +92 -0
  34. iints/core/algorithms/lstm_algorithm.py +138 -0
  35. iints/core/algorithms/mock_algorithms.py +162 -0
  36. iints/core/algorithms/pid_controller.py +88 -0
  37. iints/core/algorithms/standard_pump_algo.py +64 -0
  38. iints/core/device.py +0 -0
  39. iints/core/device_manager.py +64 -0
  40. iints/core/devices/__init__.py +3 -0
  41. iints/core/devices/models.py +160 -0
  42. iints/core/patient/__init__.py +9 -0
  43. iints/core/patient/bergman_model.py +341 -0
  44. iints/core/patient/models.py +285 -0
  45. iints/core/patient/patient_factory.py +117 -0
  46. iints/core/patient/profile.py +41 -0
  47. iints/core/safety/__init__.py +12 -0
  48. iints/core/safety/config.py +37 -0
  49. iints/core/safety/input_validator.py +95 -0
  50. iints/core/safety/supervisor.py +39 -0
  51. iints/core/simulation/__init__.py +0 -0
  52. iints/core/simulation/scenario_parser.py +61 -0
  53. iints/core/simulator.py +874 -0
  54. iints/core/supervisor.py +367 -0
  55. iints/data/__init__.py +53 -0
  56. iints/data/adapter.py +142 -0
  57. iints/data/column_mapper.py +398 -0
  58. iints/data/datasets.json +132 -0
  59. iints/data/demo/__init__.py +1 -0
  60. iints/data/demo/demo_cgm.csv +289 -0
  61. iints/data/importer.py +275 -0
  62. iints/data/ingestor.py +162 -0
  63. iints/data/nightscout.py +128 -0
  64. iints/data/quality_checker.py +550 -0
  65. iints/data/registry.py +166 -0
  66. iints/data/tidepool.py +38 -0
  67. iints/data/universal_parser.py +813 -0
  68. iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
  69. iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
  70. iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
  71. iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
  72. iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
  73. iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
  74. iints/data/virtual_patients/default_patient.yaml +11 -0
  75. iints/data/virtual_patients/patient_559_config.yaml +11 -0
  76. iints/emulation/__init__.py +80 -0
  77. iints/emulation/legacy_base.py +414 -0
  78. iints/emulation/medtronic_780g.py +337 -0
  79. iints/emulation/omnipod_5.py +367 -0
  80. iints/emulation/tandem_controliq.py +393 -0
  81. iints/highlevel.py +451 -0
  82. iints/learning/__init__.py +3 -0
  83. iints/learning/autonomous_optimizer.py +194 -0
  84. iints/learning/learning_system.py +122 -0
  85. iints/metrics.py +34 -0
  86. iints/population/__init__.py +11 -0
  87. iints/population/generator.py +131 -0
  88. iints/population/runner.py +327 -0
  89. iints/presets/__init__.py +28 -0
  90. iints/presets/presets.json +114 -0
  91. iints/research/__init__.py +30 -0
  92. iints/research/config.py +68 -0
  93. iints/research/dataset.py +319 -0
  94. iints/research/losses.py +73 -0
  95. iints/research/predictor.py +329 -0
  96. iints/scenarios/__init__.py +3 -0
  97. iints/scenarios/generator.py +92 -0
  98. iints/templates/__init__.py +0 -0
  99. iints/templates/default_algorithm.py +91 -0
  100. iints/templates/scenarios/__init__.py +0 -0
  101. iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
  102. iints/templates/scenarios/chaos_runaway_ai.json +25 -0
  103. iints/templates/scenarios/example_scenario.json +35 -0
  104. iints/templates/scenarios/exercise_stress.json +30 -0
  105. iints/utils/__init__.py +3 -0
  106. iints/utils/plotting.py +50 -0
  107. iints/utils/run_io.py +152 -0
  108. iints/validation/__init__.py +133 -0
  109. iints/validation/schemas.py +94 -0
  110. iints/visualization/__init__.py +34 -0
  111. iints/visualization/cockpit.py +691 -0
  112. iints/visualization/uncertainty_cloud.py +612 -0
  113. iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
  114. iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
  115. iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
  116. iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
  117. iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
  118. 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]`.")