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
@@ -0,0 +1,311 @@
1
+ """
2
+ IINTS-AF Safety Index
3
+ =====================
4
+ A single composite metric (0–100) that summarises the clinical safety of an
5
+ insulin dosing algorithm's output. Higher is safer.
6
+
7
+ The index is designed to be:
8
+ * **Clinically meaningful** — components map directly to FDA SaMD guidance
9
+ and ATTD/ADA consensus recommendations.
10
+ * **Configurable** — researchers can adjust component weights via CLI flags
11
+ or direct API arguments to match the emphasis of their study protocol.
12
+ * **Transparent** — every component and its contribution is reported
13
+ separately so the index can be audited and reproduced.
14
+
15
+ Grades
16
+ ------
17
+ A ≥ 90 Excellent — suitable for well-controlled clinical setting
18
+ B ≥ 75 Good — minor improvement recommended
19
+ C ≥ 60 Acceptable — targeted optimisation required
20
+ D ≥ 40 Poor — significant safety concerns
21
+ F < 40 Fail — unsafe; do not proceed to clinical evaluation
22
+
23
+ References
24
+ ----------
25
+ * Battelino et al. (2019) ATTD International Consensus on CGM Metrics.
26
+ * Kovatchev et al. (2006) Symmetrisation of the Blood Glucose Measurement Scale.
27
+ * FDA Guidance: Software as a Medical Device (SaMD).
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from dataclasses import dataclass, field
32
+ from typing import Dict, Optional, Tuple
33
+
34
+ import numpy as np
35
+ import pandas as pd
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Default component weights
40
+ # ---------------------------------------------------------------------------
41
+
42
+ DEFAULT_WEIGHTS: Dict[str, float] = {
43
+ "w_below54": 0.40, # time-below-range critical (<54 mg/dL) — most dangerous
44
+ "w_below70": 0.25, # time-below-range mild (<70 mg/dL)
45
+ "w_supervisor": 0.20, # safety supervisor trigger rate (interventions/hr)
46
+ "w_recovery": 0.10, # mean hypoglycaemia episode duration
47
+ "w_tail": 0.05, # tail-risk binary (any glucose ever < 54 mg/dL)
48
+ }
49
+
50
+ # Normalisation scales — the raw value at which the component contributes
51
+ # its FULL 100-point penalty. Values above the scale are clamped to 100.
52
+ NORM_SCALES: Dict[str, float] = {
53
+ "w_below54": 5.0, # 5 % TBR critical → full penalty
54
+ "w_below70": 20.0, # 20 % TBR low → full penalty
55
+ "w_supervisor": 12.0, # 12 supervisor triggers/hr → full penalty
56
+ "w_recovery": 1.0, # 1 hour mean episode duration → full penalty
57
+ "w_tail": 1.0, # binary 0 or 1
58
+ }
59
+
60
+
61
+ def _validate_weights(weights: Dict[str, float]) -> None:
62
+ for key in DEFAULT_WEIGHTS:
63
+ if key not in weights:
64
+ raise ValueError(f"Missing weight key: {key!r}")
65
+ if weights[key] < 0:
66
+ raise ValueError(f"Weight {key!r} must be >= 0, got {weights[key]}")
67
+ total = sum(weights.values())
68
+ if total <= 0:
69
+ raise ValueError("Sum of all weights must be > 0")
70
+
71
+
72
+ def _norm(value: float, scale: float) -> float:
73
+ """Normalise raw value to [0, 100] given the full-penalty scale."""
74
+ if scale <= 0:
75
+ return 0.0
76
+ return min(value / scale, 1.0) * 100.0
77
+
78
+
79
+ def _grade(score: float) -> str:
80
+ if score >= 90:
81
+ return "A"
82
+ if score >= 75:
83
+ return "B"
84
+ if score >= 60:
85
+ return "C"
86
+ if score >= 40:
87
+ return "D"
88
+ return "F"
89
+
90
+
91
+ def _interpret(score: float, penalties: Dict[str, float], weights: Dict[str, float]) -> str:
92
+ """Generate a one-sentence human-readable interpretation."""
93
+ grade = _grade(score)
94
+ grade_labels = {"A": "Excellent", "B": "Good", "C": "Acceptable", "D": "Poor", "F": "Fail"}
95
+ label = grade_labels[grade]
96
+
97
+ # Identify the top penalty contributor
98
+ weighted = {k: weights[k] * _norm(v, NORM_SCALES[k]) for k, v in penalties.items()}
99
+ top_key = max(weighted, key=lambda k: weighted[k])
100
+ key_labels = {
101
+ "w_below54": "time critically below 54 mg/dL",
102
+ "w_below70": "time below 70 mg/dL",
103
+ "w_supervisor": "safety supervisor trigger rate",
104
+ "w_recovery": "hypoglycaemia episode duration",
105
+ "w_tail": "occurrence of critical hypoglycaemia",
106
+ }
107
+
108
+ if score >= 90:
109
+ return f"{label}: Minimal safety concerns across all components."
110
+ dominant = key_labels[top_key]
111
+ return f"{label}: Primary concern is {dominant} (score={score:.1f}/100)."
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Main result dataclass
116
+ # ---------------------------------------------------------------------------
117
+
118
+ @dataclass
119
+ class SafetyIndexResult:
120
+ """Result of the IINTS Safety Index computation."""
121
+
122
+ score: float
123
+ """Composite safety score in [0, 100]. Higher is safer."""
124
+
125
+ grade: str
126
+ """Letter grade: A, B, C, D, or F."""
127
+
128
+ components: Dict[str, float]
129
+ """Normalised (0–100) per-component penalty before weighting."""
130
+
131
+ penalties: Dict[str, float]
132
+ """Raw penalty values (before normalisation and weighting)."""
133
+
134
+ weights: Dict[str, float]
135
+ """Component weights used in this computation."""
136
+
137
+ interpretation: str
138
+ """Human-readable one-sentence summary."""
139
+
140
+ def to_dict(self) -> dict:
141
+ return {
142
+ "safety_index": self.score,
143
+ "grade": self.grade,
144
+ "interpretation": self.interpretation,
145
+ "components": self.components,
146
+ "penalties": self.penalties,
147
+ "weights": self.weights,
148
+ }
149
+
150
+ def __str__(self) -> str:
151
+ return (
152
+ f"Safety Index: {self.score:.1f}/100 (Grade: {self.grade})\n"
153
+ f" {self.interpretation}\n"
154
+ f" Components: "
155
+ + ", ".join(f"{k.replace('w_', '')}={v:.1f}" for k, v in self.components.items())
156
+ )
157
+
158
+
159
+ # ---------------------------------------------------------------------------
160
+ # Core computation
161
+ # ---------------------------------------------------------------------------
162
+
163
+ def _compute_hypo_episode_duration(glucose: pd.Series, time_step_minutes: float) -> float:
164
+ """
165
+ Compute mean hypoglycaemia episode duration in minutes.
166
+
167
+ An episode starts when glucose drops below 70 mg/dL and ends when it
168
+ rises back above 70 mg/dL. Returns 0.0 if no episodes.
169
+ """
170
+ below = (glucose < 70.0).to_numpy(dtype=bool)
171
+ if not np.any(below):
172
+ return 0.0
173
+
174
+ episode_lengths: list[int] = []
175
+ in_episode = False
176
+ length = 0
177
+ for b in below:
178
+ if b:
179
+ in_episode = True
180
+ length += 1
181
+ else:
182
+ if in_episode:
183
+ episode_lengths.append(length)
184
+ in_episode = False
185
+ length = 0
186
+ if in_episode and length > 0:
187
+ episode_lengths.append(length)
188
+
189
+ if not episode_lengths:
190
+ return 0.0
191
+ return float(np.mean(episode_lengths)) * time_step_minutes
192
+
193
+
194
+ def compute_safety_index(
195
+ results_df: pd.DataFrame,
196
+ safety_report: Dict,
197
+ duration_minutes: int,
198
+ weights: Optional[Dict[str, float]] = None,
199
+ time_step_minutes: float = 5.0,
200
+ ) -> SafetyIndexResult:
201
+ """
202
+ Compute the IINTS Safety Index from a simulation run.
203
+
204
+ Parameters
205
+ ----------
206
+ results_df : pd.DataFrame
207
+ Output of ``Simulator.run_batch()``. Must contain
208
+ ``glucose_actual_mgdl`` and ``safety_triggered`` columns.
209
+ safety_report : dict
210
+ Safety report dict returned by ``Simulator.run_batch()``.
211
+ Must contain ``bolus_interventions_count`` (int).
212
+ duration_minutes : int
213
+ Total simulation duration in minutes.
214
+ weights : dict, optional
215
+ Component weights. If None, ``DEFAULT_WEIGHTS`` are used.
216
+ Must contain keys: ``w_below54``, ``w_below70``, ``w_supervisor``,
217
+ ``w_recovery``, ``w_tail``.
218
+ time_step_minutes : float
219
+ Simulator time step in minutes (default 5).
220
+
221
+ Returns
222
+ -------
223
+ SafetyIndexResult
224
+ """
225
+ if weights is None:
226
+ weights = dict(DEFAULT_WEIGHTS)
227
+ else:
228
+ weights = dict(weights) # defensive copy
229
+ _validate_weights(weights)
230
+
231
+ glucose_series = results_df["glucose_actual_mgdl"].astype(float)
232
+ glucose = glucose_series.to_numpy(dtype=float)
233
+ n = max(len(glucose), 1)
234
+
235
+ # --- Component 1: TBR critical < 54 mg/dL (%) ---
236
+ tbr_critical = float((glucose < 54.0).sum()) / n * 100.0
237
+
238
+ # --- Component 2: TBR low < 70 mg/dL (%) ---
239
+ tbr_low = float((glucose < 70.0).sum()) / n * 100.0
240
+
241
+ # --- Component 3: Supervisor trigger rate (triggers / hour) ---
242
+ duration_hours = max(duration_minutes / 60.0, 1e-9)
243
+ interventions = int(safety_report.get("bolus_interventions_count", 0))
244
+ supervisor_rate = interventions / duration_hours
245
+
246
+ # --- Component 4: Mean hypo episode duration (hours) ---
247
+ mean_episode_hr = _compute_hypo_episode_duration(glucose_series, time_step_minutes) / 60.0
248
+
249
+ # --- Component 5: Tail-risk binary (1 if any glucose < 54, else 0) ---
250
+ tail_risk = 1.0 if np.any(glucose < 54.0) else 0.0
251
+
252
+ penalties: Dict[str, float] = {
253
+ "w_below54": tbr_critical,
254
+ "w_below70": tbr_low,
255
+ "w_supervisor": supervisor_rate,
256
+ "w_recovery": mean_episode_hr,
257
+ "w_tail": tail_risk,
258
+ }
259
+
260
+ components: Dict[str, float] = {
261
+ k: _norm(penalties[k], NORM_SCALES[k]) for k in penalties
262
+ }
263
+
264
+ # --- Weighted sum of penalties ---
265
+ total_weight = sum(weights.values())
266
+ weighted_penalty = sum(
267
+ weights[k] * components[k] for k in weights
268
+ ) / total_weight
269
+
270
+ score = float(np.clip(100.0 - weighted_penalty, 0.0, 100.0))
271
+ grade = _grade(score)
272
+ interpretation = _interpret(score, penalties, weights)
273
+
274
+ return SafetyIndexResult(
275
+ score=score,
276
+ grade=grade,
277
+ components=components,
278
+ penalties=penalties,
279
+ weights=dict(weights),
280
+ interpretation=interpretation,
281
+ )
282
+
283
+
284
+ def safety_weights_from_cli(
285
+ w_below54: Optional[float] = None,
286
+ w_below70: Optional[float] = None,
287
+ w_supervisor: Optional[float] = None,
288
+ w_recovery: Optional[float] = None,
289
+ w_tail: Optional[float] = None,
290
+ ) -> Dict[str, float]:
291
+ """
292
+ Build a weights dict from CLI arguments. Any ``None`` values fall back
293
+ to the corresponding default weight.
294
+
295
+ Intended usage in CLI commands::
296
+
297
+ weights = safety_weights_from_cli(
298
+ w_below54=args.safety_w_below54,
299
+ w_below70=args.safety_w_below70,
300
+ ...
301
+ )
302
+ result = compute_safety_index(df, report, duration, weights=weights)
303
+ """
304
+ defaults = dict(DEFAULT_WEIGHTS)
305
+ return {
306
+ "w_below54": w_below54 if w_below54 is not None else defaults["w_below54"],
307
+ "w_below70": w_below70 if w_below70 is not None else defaults["w_below70"],
308
+ "w_supervisor": w_supervisor if w_supervisor is not None else defaults["w_supervisor"],
309
+ "w_recovery": w_recovery if w_recovery is not None else defaults["w_recovery"],
310
+ "w_tail": w_tail if w_tail is not None else defaults["w_tail"],
311
+ }
@@ -0,0 +1,54 @@
1
+ import numpy as np
2
+ from collections import deque
3
+
4
+ class SensorNoiseModel:
5
+ """Realistic sensor noise with drift and filtering."""
6
+
7
+ def __init__(self, white_noise_std=15, drift_rate=0.1, drift_amplitude=10):
8
+ self.white_noise_std = white_noise_std
9
+ self.drift_rate = drift_rate
10
+ self.drift_amplitude = drift_amplitude
11
+ self.drift_phase = 0
12
+
13
+ def add_noise(self, true_glucose, time_step):
14
+ """Add realistic sensor noise with drift."""
15
+ white_noise = np.random.normal(0, self.white_noise_std)
16
+ drift = self.drift_amplitude * np.sin(self.drift_phase)
17
+ self.drift_phase += self.drift_rate * time_step / 60 # Convert to hours
18
+
19
+ return true_glucose + white_noise + drift
20
+
21
+ class KalmanFilter:
22
+ """Simple Kalman filter for glucose smoothing."""
23
+
24
+ def __init__(self, process_variance=1, measurement_variance=225):
25
+ self.process_variance = process_variance
26
+ self.measurement_variance = measurement_variance
27
+ self.estimate = None
28
+ self.error_estimate = 1000
29
+
30
+ def update(self, measurement):
31
+ if self.estimate is None:
32
+ self.estimate = measurement
33
+ return measurement
34
+
35
+ # Prediction
36
+ prediction = self.estimate
37
+ prediction_error = self.error_estimate + self.process_variance
38
+
39
+ # Update
40
+ kalman_gain = prediction_error / (prediction_error + self.measurement_variance)
41
+ self.estimate = prediction + kalman_gain * (measurement - prediction)
42
+ self.error_estimate = (1 - kalman_gain) * prediction_error
43
+
44
+ return self.estimate
45
+
46
+ class MovingAverageFilter:
47
+ """Simple moving average filter."""
48
+
49
+ def __init__(self, window_size=3):
50
+ self.window = deque(maxlen=window_size)
51
+
52
+ def update(self, value):
53
+ self.window.append(value)
54
+ return sum(self.window) / len(self.window)
@@ -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