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.
- iints/__init__.py +134 -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_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/reporting.py +261 -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 +300 -0
- iints/api/template_algorithm.py +195 -0
- iints/cli/__init__.py +0 -0
- iints/cli/cli.py +1286 -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 +86 -0
- iints/core/algorithms/discovery.py +92 -0
- iints/core/algorithms/fixed_basal_bolus.py +52 -0
- iints/core/algorithms/hybrid_algorithm.py +92 -0
- iints/core/algorithms/lstm_algorithm.py +138 -0
- iints/core/algorithms/mock_algorithms.py +69 -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 +155 -0
- iints/core/patient/__init__.py +3 -0
- iints/core/patient/models.py +246 -0
- iints/core/patient/patient_factory.py +117 -0
- iints/core/patient/profile.py +41 -0
- iints/core/safety/__init__.py +4 -0
- iints/core/safety/input_validator.py +87 -0
- iints/core/safety/supervisor.py +29 -0
- iints/core/simulation/__init__.py +0 -0
- iints/core/simulation/scenario_parser.py +61 -0
- iints/core/simulator.py +519 -0
- iints/core/supervisor.py +275 -0
- iints/data/__init__.py +42 -0
- iints/data/adapter.py +142 -0
- iints/data/column_mapper.py +398 -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/quality_checker.py +550 -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 +192 -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/presets/__init__.py +28 -0
- iints/presets/presets.json +114 -0
- iints/templates/__init__.py +0 -0
- iints/templates/default_algorithm.py +56 -0
- iints/templates/scenarios/__init__.py +0 -0
- iints/templates/scenarios/example_scenario.json +34 -0
- iints/utils/__init__.py +3 -0
- iints/utils/plotting.py +50 -0
- iints/validation/__init__.py +117 -0
- iints/validation/schemas.py +72 -0
- iints/visualization/__init__.py +34 -0
- iints/visualization/cockpit.py +691 -0
- iints/visualization/uncertainty_cloud.py +612 -0
- iints_sdk_python35-0.1.7.dist-info/METADATA +122 -0
- iints_sdk_python35-0.1.7.dist-info/RECORD +93 -0
- iints_sdk_python35-0.1.7.dist-info/WHEEL +5 -0
- iints_sdk_python35-0.1.7.dist-info/entry_points.txt +2 -0
- iints_sdk_python35-0.1.7.dist-info/licenses/LICENSE +28 -0
- iints_sdk_python35-0.1.7.dist-info/top_level.txt +1 -0
iints/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# src/iints/__init__.py
|
|
2
|
+
|
|
3
|
+
import pandas as pd # Required for type hints like pd.DataFrame
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.5"
|
|
7
|
+
|
|
8
|
+
# API Components for Algorithm Development
|
|
9
|
+
from .api.base_algorithm import (
|
|
10
|
+
InsulinAlgorithm,
|
|
11
|
+
AlgorithmInput,
|
|
12
|
+
AlgorithmResult,
|
|
13
|
+
AlgorithmMetadata,
|
|
14
|
+
WhyLogEntry,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Core Simulation Components
|
|
18
|
+
from .core.simulator import Simulator, StressEvent, SimulationLimitError
|
|
19
|
+
from .core.patient.models import PatientModel
|
|
20
|
+
from .core.patient.profile import PatientProfile
|
|
21
|
+
try:
|
|
22
|
+
from .core.device_manager import DeviceManager
|
|
23
|
+
except Exception: # pragma: no cover - fallback if torch/device manager import fails
|
|
24
|
+
class DeviceManager: # type: ignore
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._device = "cpu"
|
|
27
|
+
|
|
28
|
+
def get_device(self):
|
|
29
|
+
return self._device
|
|
30
|
+
from .core.safety import SafetySupervisor
|
|
31
|
+
from .core.devices.models import SensorModel, PumpModel
|
|
32
|
+
from .core.algorithms.standard_pump_algo import StandardPumpAlgorithm
|
|
33
|
+
from .core.algorithms.mock_algorithms import ConstantDoseAlgorithm, RandomDoseAlgorithm
|
|
34
|
+
|
|
35
|
+
# Data Handling
|
|
36
|
+
from .data.ingestor import DataIngestor
|
|
37
|
+
from .data.importer import (
|
|
38
|
+
ImportResult,
|
|
39
|
+
export_demo_csv,
|
|
40
|
+
export_standard_csv,
|
|
41
|
+
guess_column_mapping,
|
|
42
|
+
import_cgm_csv,
|
|
43
|
+
import_cgm_dataframe,
|
|
44
|
+
load_demo_dataframe,
|
|
45
|
+
scenario_from_csv,
|
|
46
|
+
scenario_from_dataframe,
|
|
47
|
+
)
|
|
48
|
+
from .analysis.metrics import generate_benchmark_metrics # Added for benchmark
|
|
49
|
+
from .analysis.reporting import ClinicalReportGenerator
|
|
50
|
+
from .highlevel import run_simulation, run_full
|
|
51
|
+
|
|
52
|
+
# Placeholder for Reporting/Analysis
|
|
53
|
+
# This will be further developed in a dedicated module (e.g., iints.analysis.reporting)
|
|
54
|
+
def generate_report(simulation_results: 'pd.DataFrame', output_path: Optional[str] = None, safety_report: Optional[dict] = None) -> Optional[str]:
|
|
55
|
+
"""
|
|
56
|
+
Generate a clinical PDF report from simulation results.
|
|
57
|
+
"""
|
|
58
|
+
if output_path is None:
|
|
59
|
+
return None
|
|
60
|
+
generator = ClinicalReportGenerator()
|
|
61
|
+
return generator.generate_pdf(simulation_results, safety_report or {}, output_path)
|
|
62
|
+
|
|
63
|
+
def generate_quickstart_report(
|
|
64
|
+
simulation_results: 'pd.DataFrame',
|
|
65
|
+
output_path: Optional[str] = None,
|
|
66
|
+
safety_report: Optional[dict] = None,
|
|
67
|
+
) -> Optional[str]:
|
|
68
|
+
"""
|
|
69
|
+
Generate a concise Quickstart PDF report from simulation results.
|
|
70
|
+
"""
|
|
71
|
+
if output_path is None:
|
|
72
|
+
return None
|
|
73
|
+
generator = ClinicalReportGenerator()
|
|
74
|
+
return generator.generate_pdf(
|
|
75
|
+
simulation_results,
|
|
76
|
+
safety_report or {},
|
|
77
|
+
output_path,
|
|
78
|
+
title="IINTS-AF Quickstart Report",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def generate_demo_report(
|
|
82
|
+
simulation_results: 'pd.DataFrame',
|
|
83
|
+
output_path: Optional[str] = None,
|
|
84
|
+
safety_report: Optional[dict] = None,
|
|
85
|
+
) -> Optional[str]:
|
|
86
|
+
"""
|
|
87
|
+
Generate a demo-friendly PDF with big visuals (Maker Faire style).
|
|
88
|
+
"""
|
|
89
|
+
if output_path is None:
|
|
90
|
+
return None
|
|
91
|
+
generator = ClinicalReportGenerator()
|
|
92
|
+
return generator.generate_demo_pdf(
|
|
93
|
+
simulation_results,
|
|
94
|
+
safety_report or {},
|
|
95
|
+
output_path,
|
|
96
|
+
title="IINTS-AF Demo Report",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# You can also define __all__ to explicitly control what gets imported with `from iints import *`
|
|
100
|
+
__all__ = [
|
|
101
|
+
# API
|
|
102
|
+
"InsulinAlgorithm", "AlgorithmInput", "AlgorithmResult", "AlgorithmMetadata", "WhyLogEntry",
|
|
103
|
+
# Core
|
|
104
|
+
"Simulator", "StressEvent", "PatientModel", "DeviceManager",
|
|
105
|
+
"PatientProfile",
|
|
106
|
+
"SimulationLimitError",
|
|
107
|
+
"SafetySupervisor",
|
|
108
|
+
"SensorModel",
|
|
109
|
+
"PumpModel",
|
|
110
|
+
"StandardPumpAlgorithm",
|
|
111
|
+
"ConstantDoseAlgorithm",
|
|
112
|
+
"RandomDoseAlgorithm",
|
|
113
|
+
# Data
|
|
114
|
+
"DataIngestor",
|
|
115
|
+
"ImportResult",
|
|
116
|
+
"export_demo_csv",
|
|
117
|
+
"export_standard_csv",
|
|
118
|
+
"guess_column_mapping",
|
|
119
|
+
"import_cgm_csv",
|
|
120
|
+
"import_cgm_dataframe",
|
|
121
|
+
"load_demo_dataframe",
|
|
122
|
+
"scenario_from_csv",
|
|
123
|
+
"scenario_from_dataframe",
|
|
124
|
+
# Analysis Metrics
|
|
125
|
+
"generate_benchmark_metrics",
|
|
126
|
+
"ClinicalReportGenerator",
|
|
127
|
+
# Reporting
|
|
128
|
+
"generate_report",
|
|
129
|
+
"generate_quickstart_report",
|
|
130
|
+
"generate_demo_report",
|
|
131
|
+
# High-level API
|
|
132
|
+
"run_simulation",
|
|
133
|
+
"run_full",
|
|
134
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .clinical_metrics import ClinicalMetricsCalculator, ClinicalMetricsResult
|
|
2
|
+
from .baseline import compute_metrics, run_baseline_comparison, write_baseline_comparison
|
|
3
|
+
from .reporting import ClinicalReportGenerator
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"ClinicalMetricsCalculator",
|
|
7
|
+
"ClinicalMetricsResult",
|
|
8
|
+
"ClinicalReportGenerator",
|
|
9
|
+
"compute_metrics",
|
|
10
|
+
"run_baseline_comparison",
|
|
11
|
+
"write_baseline_comparison",
|
|
12
|
+
]
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Algorithm X-Ray - IINTS-AF
|
|
4
|
+
Make invisible medical decisions visible through decision replay and what-if analysis
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
import json
|
|
12
|
+
from iints.data.quality_checker import QualityReport
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DecisionPoint:
|
|
16
|
+
"""Single decision point in algorithm timeline"""
|
|
17
|
+
timestamp: datetime
|
|
18
|
+
glucose_mgdl: float
|
|
19
|
+
glucose_velocity: float
|
|
20
|
+
insulin_on_board: float
|
|
21
|
+
carbs_on_board: float
|
|
22
|
+
|
|
23
|
+
# Decision components
|
|
24
|
+
decision: float
|
|
25
|
+
confidence: float
|
|
26
|
+
reasoning: List[str]
|
|
27
|
+
safety_constraints: List[str]
|
|
28
|
+
risk_level: str
|
|
29
|
+
|
|
30
|
+
# What-if scenarios
|
|
31
|
+
alternative_decisions: Dict[str, float]
|
|
32
|
+
|
|
33
|
+
def to_dict(self):
|
|
34
|
+
return {
|
|
35
|
+
'timestamp': self.timestamp.isoformat(),
|
|
36
|
+
'glucose_mgdl': self.glucose_mgdl,
|
|
37
|
+
'glucose_velocity': self.glucose_velocity,
|
|
38
|
+
'insulin_on_board': self.insulin_on_board,
|
|
39
|
+
'carbs_on_board': self.carbs_on_board,
|
|
40
|
+
'decision': self.decision,
|
|
41
|
+
'confidence': self.confidence,
|
|
42
|
+
'reasoning': self.reasoning,
|
|
43
|
+
'safety_constraints': self.safety_constraints,
|
|
44
|
+
'risk_level': self.risk_level,
|
|
45
|
+
'alternative_decisions': self.alternative_decisions
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class AlgorithmXRay:
|
|
49
|
+
"""X-ray vision into algorithm decision-making process"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, quality_report: Optional[QualityReport] = None):
|
|
52
|
+
self.decision_timeline: List[DecisionPoint] = []
|
|
53
|
+
self.personality_profile: Dict[str, Any] = {}
|
|
54
|
+
self.quality_report = quality_report
|
|
55
|
+
|
|
56
|
+
def get_quality_report_summary(self) -> Dict[str, Any]:
|
|
57
|
+
"""
|
|
58
|
+
Returns a summary of the data quality report if available.
|
|
59
|
+
"""
|
|
60
|
+
if self.quality_report:
|
|
61
|
+
return {
|
|
62
|
+
"overall_score": self.quality_report.overall_score,
|
|
63
|
+
"summary": self.quality_report.summary,
|
|
64
|
+
"gaps_detected": len(self.quality_report.gaps),
|
|
65
|
+
"anomalies_detected": len(self.quality_report.anomalies),
|
|
66
|
+
"warnings": self.quality_report.warnings
|
|
67
|
+
}
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
def analyze_decision(self,
|
|
71
|
+
glucose_mgdl: float,
|
|
72
|
+
glucose_history: List[float],
|
|
73
|
+
insulin_history: List[float],
|
|
74
|
+
time_minutes: int) -> DecisionPoint:
|
|
75
|
+
"""
|
|
76
|
+
Deep analysis of single decision point with full reasoning chain
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
# Calculate glucose velocity
|
|
80
|
+
if len(glucose_history) >= 2:
|
|
81
|
+
glucose_velocity = (glucose_history[-1] - glucose_history[-2]) / 5.0 # mg/dL per minute
|
|
82
|
+
else:
|
|
83
|
+
glucose_velocity = 0.0
|
|
84
|
+
|
|
85
|
+
# Estimate insulin on board (simplified)
|
|
86
|
+
insulin_on_board = sum(insulin_history[-6:]) * 0.5 if insulin_history else 0.0
|
|
87
|
+
|
|
88
|
+
# Estimate carbs on board (simplified - would need meal data)
|
|
89
|
+
carbs_on_board = 0.0
|
|
90
|
+
|
|
91
|
+
# Build reasoning chain
|
|
92
|
+
reasoning = []
|
|
93
|
+
safety_constraints = []
|
|
94
|
+
risk_level = "NORMAL"
|
|
95
|
+
|
|
96
|
+
# Glucose level reasoning
|
|
97
|
+
if glucose_mgdl < 70:
|
|
98
|
+
reasoning.append(f"Glucose critically low at {glucose_mgdl:.1f} mg/dL")
|
|
99
|
+
risk_level = "HIGH"
|
|
100
|
+
elif glucose_mgdl < 80:
|
|
101
|
+
reasoning.append(f"Glucose approaching low threshold at {glucose_mgdl:.1f} mg/dL")
|
|
102
|
+
risk_level = "MODERATE"
|
|
103
|
+
elif glucose_mgdl > 180:
|
|
104
|
+
reasoning.append(f"Glucose elevated at {glucose_mgdl:.1f} mg/dL")
|
|
105
|
+
risk_level = "MODERATE"
|
|
106
|
+
elif glucose_mgdl > 250:
|
|
107
|
+
reasoning.append(f"Glucose critically high at {glucose_mgdl:.1f} mg/dL")
|
|
108
|
+
risk_level = "HIGH"
|
|
109
|
+
else:
|
|
110
|
+
reasoning.append(f"Glucose in target range at {glucose_mgdl:.1f} mg/dL")
|
|
111
|
+
|
|
112
|
+
# Velocity reasoning
|
|
113
|
+
if abs(glucose_velocity) > 2.0:
|
|
114
|
+
direction = "rising" if glucose_velocity > 0 else "falling"
|
|
115
|
+
reasoning.append(f"Glucose {direction} rapidly at {abs(glucose_velocity):.2f} mg/dL/min")
|
|
116
|
+
if glucose_velocity < -2.0 and glucose_mgdl < 100:
|
|
117
|
+
safety_constraints.append("Rapid fall near hypoglycemia - insulin blocked")
|
|
118
|
+
elif abs(glucose_velocity) > 1.0:
|
|
119
|
+
direction = "rising" if glucose_velocity > 0 else "falling"
|
|
120
|
+
reasoning.append(f"Glucose {direction} moderately at {abs(glucose_velocity):.2f} mg/dL/min")
|
|
121
|
+
else:
|
|
122
|
+
reasoning.append("Glucose stable")
|
|
123
|
+
|
|
124
|
+
# Insulin on board reasoning
|
|
125
|
+
if insulin_on_board > 2.0:
|
|
126
|
+
reasoning.append(f"High insulin on board: {insulin_on_board:.2f} units")
|
|
127
|
+
safety_constraints.append("Insulin stacking prevention active")
|
|
128
|
+
elif insulin_on_board > 0.5:
|
|
129
|
+
reasoning.append(f"Moderate insulin on board: {insulin_on_board:.2f} units")
|
|
130
|
+
|
|
131
|
+
# Calculate base decision
|
|
132
|
+
error = glucose_mgdl - 120 # Target 120 mg/dL
|
|
133
|
+
base_decision = max(0, error * 0.02)
|
|
134
|
+
|
|
135
|
+
# Apply safety constraints
|
|
136
|
+
final_decision = base_decision
|
|
137
|
+
confidence = 0.8
|
|
138
|
+
|
|
139
|
+
if glucose_mgdl < 70 or (glucose_velocity < -2.0 and glucose_mgdl < 100):
|
|
140
|
+
final_decision = 0.0
|
|
141
|
+
confidence = 0.95
|
|
142
|
+
safety_constraints.append("Safety supervisor override: insulin delivery blocked")
|
|
143
|
+
elif insulin_on_board > 2.0:
|
|
144
|
+
final_decision = base_decision * 0.5
|
|
145
|
+
confidence = 0.7
|
|
146
|
+
safety_constraints.append("Insulin dose reduced due to stacking risk")
|
|
147
|
+
|
|
148
|
+
# Generate what-if scenarios
|
|
149
|
+
alternative_decisions = {
|
|
150
|
+
'if_exercised_30min_ago': final_decision * 0.6,
|
|
151
|
+
'if_meal_detected': final_decision * 1.3,
|
|
152
|
+
'if_stress_detected': final_decision * 1.2,
|
|
153
|
+
'if_sensor_noise_high': final_decision * 0.8,
|
|
154
|
+
'aggressive_mode': base_decision * 1.5,
|
|
155
|
+
'conservative_mode': base_decision * 0.5
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
decision_point = DecisionPoint(
|
|
159
|
+
timestamp=datetime.now(),
|
|
160
|
+
glucose_mgdl=glucose_mgdl,
|
|
161
|
+
glucose_velocity=glucose_velocity,
|
|
162
|
+
insulin_on_board=insulin_on_board,
|
|
163
|
+
carbs_on_board=carbs_on_board,
|
|
164
|
+
decision=final_decision,
|
|
165
|
+
confidence=confidence,
|
|
166
|
+
reasoning=reasoning,
|
|
167
|
+
safety_constraints=safety_constraints,
|
|
168
|
+
risk_level=risk_level,
|
|
169
|
+
alternative_decisions=alternative_decisions
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
self.decision_timeline.append(decision_point)
|
|
173
|
+
return decision_point
|
|
174
|
+
|
|
175
|
+
def calculate_personality_profile(self, decision_history: List[DecisionPoint]) -> Dict:
|
|
176
|
+
"""
|
|
177
|
+
Calculate algorithm personality traits from decision history
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
if not decision_history:
|
|
181
|
+
return {}
|
|
182
|
+
|
|
183
|
+
# Hypo-aversion: how much does it avoid low glucose
|
|
184
|
+
hypo_decisions = [d for d in decision_history if d.glucose_mgdl < 80]
|
|
185
|
+
hypo_aversion = np.mean([1.0 - d.decision for d in hypo_decisions]) if hypo_decisions else 0.5
|
|
186
|
+
|
|
187
|
+
# Reaction speed: how quickly does it respond to changes
|
|
188
|
+
velocity_responses = [abs(d.decision - 0.5) for d in decision_history if abs(d.glucose_velocity) > 1.0]
|
|
189
|
+
reaction_speed = np.mean(velocity_responses) if velocity_responses else 0.5
|
|
190
|
+
|
|
191
|
+
# Correction intensity: how aggressive are corrections
|
|
192
|
+
high_glucose = [d for d in decision_history if d.glucose_mgdl > 180]
|
|
193
|
+
correction_intensity = np.mean([d.decision for d in high_glucose]) if high_glucose else 0.5
|
|
194
|
+
|
|
195
|
+
# Consistency: how stable are decisions in similar conditions
|
|
196
|
+
decision_values = [d.decision for d in decision_history]
|
|
197
|
+
consistency = 1.0 - (np.std(decision_values) if len(decision_values) > 1 else 0.5)
|
|
198
|
+
|
|
199
|
+
# Safety-first: how often safety constraints are triggered
|
|
200
|
+
safety_triggers = sum(1 for d in decision_history if d.safety_constraints)
|
|
201
|
+
safety_first = safety_triggers / len(decision_history) if decision_history else 0.0
|
|
202
|
+
|
|
203
|
+
personality = {
|
|
204
|
+
'hypo_aversion': float(hypo_aversion),
|
|
205
|
+
'reaction_speed': float(reaction_speed),
|
|
206
|
+
'correction_intensity': float(correction_intensity),
|
|
207
|
+
'consistency': float(consistency),
|
|
208
|
+
'safety_first': float(safety_first),
|
|
209
|
+
'decision_count': len(decision_history),
|
|
210
|
+
'risk_distribution': {
|
|
211
|
+
'high': sum(1 for d in decision_history if d.risk_level == 'HIGH'),
|
|
212
|
+
'moderate': sum(1 for d in decision_history if d.risk_level == 'MODERATE'),
|
|
213
|
+
'normal': sum(1 for d in decision_history if d.risk_level == 'NORMAL')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
self.personality_profile = personality
|
|
218
|
+
return personality
|
|
219
|
+
|
|
220
|
+
def generate_decision_replay(self,
|
|
221
|
+
start_index: int = 0,
|
|
222
|
+
end_index: Optional[int] = None) -> Dict:
|
|
223
|
+
"""
|
|
224
|
+
Generate interactive decision replay with full reasoning
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
if end_index is None:
|
|
228
|
+
end_index = len(self.decision_timeline)
|
|
229
|
+
|
|
230
|
+
replay_segment = self.decision_timeline[start_index:end_index]
|
|
231
|
+
|
|
232
|
+
replay_data = {
|
|
233
|
+
'timeline': [d.to_dict() for d in replay_segment],
|
|
234
|
+
'personality_profile': self.personality_profile,
|
|
235
|
+
'summary': {
|
|
236
|
+
'total_decisions': len(replay_segment),
|
|
237
|
+
'safety_overrides': sum(1 for d in replay_segment if d.safety_constraints),
|
|
238
|
+
'high_risk_moments': sum(1 for d in replay_segment if d.risk_level == 'HIGH'),
|
|
239
|
+
'average_confidence': np.mean([d.confidence for d in replay_segment]),
|
|
240
|
+
'glucose_range': {
|
|
241
|
+
'min': min(d.glucose_mgdl for d in replay_segment),
|
|
242
|
+
'max': max(d.glucose_mgdl for d in replay_segment),
|
|
243
|
+
'mean': np.mean([d.glucose_mgdl for d in replay_segment])
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return replay_data
|
|
249
|
+
|
|
250
|
+
def compare_what_if_scenarios(self, decision_point: DecisionPoint) -> Dict:
|
|
251
|
+
"""
|
|
252
|
+
Compare actual decision with what-if alternatives
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
actual = decision_point.decision
|
|
256
|
+
alternatives = decision_point.alternative_decisions
|
|
257
|
+
|
|
258
|
+
comparison = {
|
|
259
|
+
'actual_decision': actual,
|
|
260
|
+
'alternatives': alternatives,
|
|
261
|
+
'differences': {
|
|
262
|
+
scenario: {
|
|
263
|
+
'absolute_diff': alt - actual,
|
|
264
|
+
'percent_diff': ((alt - actual) / actual * 100) if actual > 0 else 0,
|
|
265
|
+
'clinical_impact': self._estimate_clinical_impact(actual, alt, decision_point.glucose_mgdl)
|
|
266
|
+
}
|
|
267
|
+
for scenario, alt in alternatives.items()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return comparison
|
|
272
|
+
|
|
273
|
+
def _estimate_clinical_impact(self, actual: float, alternative: float, glucose: float) -> str:
|
|
274
|
+
"""Estimate clinical impact of alternative decision"""
|
|
275
|
+
|
|
276
|
+
diff = alternative - actual
|
|
277
|
+
|
|
278
|
+
if abs(diff) < 0.1:
|
|
279
|
+
return "Minimal impact"
|
|
280
|
+
elif glucose < 70:
|
|
281
|
+
if diff < 0:
|
|
282
|
+
return "Safer - reduces hypo risk"
|
|
283
|
+
else:
|
|
284
|
+
return "Riskier - increases hypo risk"
|
|
285
|
+
elif glucose > 180:
|
|
286
|
+
if diff > 0:
|
|
287
|
+
return "More aggressive correction"
|
|
288
|
+
else:
|
|
289
|
+
return "More conservative approach"
|
|
290
|
+
else:
|
|
291
|
+
return "Moderate impact on glucose trajectory"
|
|
292
|
+
|
|
293
|
+
def export_xray_report(self, filepath: str):
|
|
294
|
+
"""Export complete X-ray analysis to JSON"""
|
|
295
|
+
|
|
296
|
+
report = {
|
|
297
|
+
'generated_at': datetime.now().isoformat(),
|
|
298
|
+
'total_decisions': len(self.decision_timeline),
|
|
299
|
+
'personality_profile': self.personality_profile,
|
|
300
|
+
'decision_timeline': [d.to_dict() for d in self.decision_timeline],
|
|
301
|
+
'summary_statistics': self._calculate_summary_stats()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
with open(filepath, 'w') as f:
|
|
305
|
+
json.dump(report, f, indent=2)
|
|
306
|
+
|
|
307
|
+
return filepath
|
|
308
|
+
|
|
309
|
+
def _calculate_summary_stats(self) -> Dict:
|
|
310
|
+
"""Calculate summary statistics across all decisions"""
|
|
311
|
+
|
|
312
|
+
if not self.decision_timeline:
|
|
313
|
+
return {}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
'average_decision': np.mean([d.decision for d in self.decision_timeline]),
|
|
317
|
+
'decision_std': np.std([d.decision for d in self.decision_timeline]),
|
|
318
|
+
'average_confidence': np.mean([d.confidence for d in self.decision_timeline]),
|
|
319
|
+
'safety_override_rate': sum(1 for d in self.decision_timeline if d.safety_constraints) / len(self.decision_timeline),
|
|
320
|
+
'high_risk_rate': sum(1 for d in self.decision_timeline if d.risk_level == 'HIGH') / len(self.decision_timeline),
|
|
321
|
+
'glucose_stats': {
|
|
322
|
+
'mean': np.mean([d.glucose_mgdl for d in self.decision_timeline]),
|
|
323
|
+
'std': np.std([d.glucose_mgdl for d in self.decision_timeline]),
|
|
324
|
+
'min': min(d.glucose_mgdl for d in self.decision_timeline),
|
|
325
|
+
'max': max(d.glucose_mgdl for d in self.decision_timeline)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
def main():
|
|
330
|
+
"""Demonstration of Algorithm X-Ray system"""
|
|
331
|
+
|
|
332
|
+
print(" ALGORITHM X-RAY DEMONSTRATION")
|
|
333
|
+
print("=" * 50)
|
|
334
|
+
print("Making invisible medical decisions visible\n")
|
|
335
|
+
|
|
336
|
+
xray = AlgorithmXRay()
|
|
337
|
+
|
|
338
|
+
# Simulate decision sequence
|
|
339
|
+
glucose_history = [120, 125, 135, 150, 165, 175, 180, 185, 190, 185]
|
|
340
|
+
insulin_history = [0.5, 0.6, 0.8, 1.0, 1.2, 1.5, 1.8, 2.0, 1.8, 1.5]
|
|
341
|
+
|
|
342
|
+
print("Analyzing decision sequence...\n")
|
|
343
|
+
|
|
344
|
+
for i, glucose in enumerate(glucose_history):
|
|
345
|
+
decision = xray.analyze_decision(
|
|
346
|
+
glucose_mgdl=glucose,
|
|
347
|
+
glucose_history=glucose_history[:i+1],
|
|
348
|
+
insulin_history=insulin_history[:i+1],
|
|
349
|
+
time_minutes=i * 5
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
print(f"Decision Point {i+1}:")
|
|
353
|
+
print(f" Glucose: {decision.glucose_mgdl:.1f} mg/dL")
|
|
354
|
+
print(f" Velocity: {decision.glucose_velocity:.2f} mg/dL/min")
|
|
355
|
+
print(f" Decision: {decision.decision:.2f} units")
|
|
356
|
+
print(f" Confidence: {decision.confidence:.1%}")
|
|
357
|
+
print(f" Risk Level: {decision.risk_level}")
|
|
358
|
+
print(f" Reasoning:")
|
|
359
|
+
for reason in decision.reasoning:
|
|
360
|
+
print(f" - {reason}")
|
|
361
|
+
if decision.safety_constraints:
|
|
362
|
+
print(f" Safety Constraints:")
|
|
363
|
+
for constraint in decision.safety_constraints:
|
|
364
|
+
print(f" {constraint}")
|
|
365
|
+
print()
|
|
366
|
+
|
|
367
|
+
# Calculate personality
|
|
368
|
+
print("\n ALGORITHM PERSONALITY PROFILE")
|
|
369
|
+
print("=" * 50)
|
|
370
|
+
personality = xray.calculate_personality_profile(xray.decision_timeline)
|
|
371
|
+
|
|
372
|
+
print(f"Hypo-Aversion: {personality['hypo_aversion']:.2f} (0=aggressive, 1=cautious)")
|
|
373
|
+
print(f"Reaction Speed: {personality['reaction_speed']:.2f} (0=slow, 1=fast)")
|
|
374
|
+
print(f"Correction Intensity: {personality['correction_intensity']:.2f} (0=gentle, 1=aggressive)")
|
|
375
|
+
print(f"Consistency: {personality['consistency']:.2f} (0=variable, 1=stable)")
|
|
376
|
+
print(f"Safety-First: {personality['safety_first']:.2f} (0=permissive, 1=strict)")
|
|
377
|
+
|
|
378
|
+
# Export report
|
|
379
|
+
from pathlib import Path
|
|
380
|
+
results_dir = Path("results/algorithm_xray")
|
|
381
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
|
|
383
|
+
report_path = xray.export_xray_report(str(results_dir / "xray_report.json"))
|
|
384
|
+
print(f"\n X-Ray report exported to: {report_path}")
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from iints.analysis.clinical_metrics import ClinicalMetricsCalculator
|
|
10
|
+
from iints.api.base_algorithm import InsulinAlgorithm
|
|
11
|
+
from iints.core.algorithms.pid_controller import PIDController
|
|
12
|
+
from iints.core.algorithms.standard_pump_algo import StandardPumpAlgorithm
|
|
13
|
+
from iints.core.patient.models import PatientModel
|
|
14
|
+
from iints.core.simulator import Simulator
|
|
15
|
+
from iints.validation import build_stress_events
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def compute_metrics(results_df: pd.DataFrame) -> Dict[str, float]:
|
|
19
|
+
calculator = ClinicalMetricsCalculator()
|
|
20
|
+
duration_hours = results_df["time_minutes"].max() / 60.0 if len(results_df) else 0.0
|
|
21
|
+
metrics = calculator.calculate(
|
|
22
|
+
glucose=results_df["glucose_actual_mgdl"],
|
|
23
|
+
duration_hours=duration_hours,
|
|
24
|
+
)
|
|
25
|
+
return metrics.to_dict()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_baseline_comparison(
|
|
29
|
+
patient_params: Dict[str, Any],
|
|
30
|
+
stress_event_payloads: List[Dict[str, Any]],
|
|
31
|
+
duration: int,
|
|
32
|
+
time_step: int,
|
|
33
|
+
primary_label: str,
|
|
34
|
+
primary_results: pd.DataFrame,
|
|
35
|
+
primary_safety: Dict[str, Any],
|
|
36
|
+
compare_standard_pump: bool = True,
|
|
37
|
+
seed: Optional[int] = None,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
rows: List[Dict[str, Any]] = []
|
|
40
|
+
|
|
41
|
+
primary_metrics = compute_metrics(primary_results)
|
|
42
|
+
rows.append(
|
|
43
|
+
{
|
|
44
|
+
"algorithm": primary_label,
|
|
45
|
+
"tir_70_180": primary_metrics.get("tir_70_180", 0.0),
|
|
46
|
+
"tir_below_70": primary_metrics.get("tir_below_70", 0.0),
|
|
47
|
+
"tir_above_180": primary_metrics.get("tir_above_180", 0.0),
|
|
48
|
+
"bolus_interventions": primary_safety.get("bolus_interventions_count", 0),
|
|
49
|
+
"total_violations": primary_safety.get("total_violations", 0),
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
baselines: List[Tuple[str, InsulinAlgorithm]] = [("Standard PID", PIDController())]
|
|
54
|
+
if compare_standard_pump:
|
|
55
|
+
baselines.append(("Standard Pump", StandardPumpAlgorithm()))
|
|
56
|
+
|
|
57
|
+
for label, algo in baselines:
|
|
58
|
+
patient_model = PatientModel(**patient_params)
|
|
59
|
+
simulator = Simulator(
|
|
60
|
+
patient_model=patient_model,
|
|
61
|
+
algorithm=algo,
|
|
62
|
+
time_step=time_step,
|
|
63
|
+
seed=seed,
|
|
64
|
+
)
|
|
65
|
+
for event in build_stress_events(stress_event_payloads):
|
|
66
|
+
simulator.add_stress_event(event)
|
|
67
|
+
results_df, safety_report = simulator.run_batch(duration)
|
|
68
|
+
metrics = compute_metrics(results_df)
|
|
69
|
+
rows.append(
|
|
70
|
+
{
|
|
71
|
+
"algorithm": label,
|
|
72
|
+
"tir_70_180": metrics.get("tir_70_180", 0.0),
|
|
73
|
+
"tir_below_70": metrics.get("tir_below_70", 0.0),
|
|
74
|
+
"tir_above_180": metrics.get("tir_above_180", 0.0),
|
|
75
|
+
"bolus_interventions": safety_report.get("bolus_interventions_count", 0),
|
|
76
|
+
"total_violations": safety_report.get("total_violations", 0),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"reference": "Standard PID",
|
|
82
|
+
"rows": rows,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_baseline_comparison(comparison: Dict[str, Any], output_dir: Path) -> Dict[str, str]:
|
|
87
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
json_path = output_dir / "baseline_comparison.json"
|
|
89
|
+
csv_path = output_dir / "baseline_comparison.csv"
|
|
90
|
+
json_path.write_text(json.dumps(comparison, indent=2))
|
|
91
|
+
pd.DataFrame(comparison.get("rows", [])).to_csv(csv_path, index=False)
|
|
92
|
+
return {"json": str(json_path), "csv": str(csv_path)}
|