iints-sdk-python35 0.1.7__py3-none-any.whl

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