iints-sdk-python35 1.3.1__py3-none-any.whl → 1.4.0__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 +1 -1
- iints/ai/backends/ollama.py +32 -2
- iints/ai/mdmp_guard.py +2 -3
- iints/ai/prepare.py +1 -1
- iints/analysis/booth_demo.py +3 -1
- iints/analysis/clinical_metrics.py +13 -7
- iints/analysis/validator.py +10 -4
- iints/cli/cli.py +34 -0
- iints/core/patient/bergman_model.py +3 -2
- iints/core/patient/models.py +1 -1
- iints/core/patient/patient_factory.py +8 -8
- iints/core/patient/profile.py +1 -1
- iints/core/safety/config.py +13 -4
- iints/core/safety/input_validator.py +12 -7
- iints/data/adapter.py +7 -2
- iints/data/ingestor.py +7 -2
- iints/data/quality_checker.py +12 -4
- iints/data/registry.py +23 -1
- iints/demo_assets.py +50 -0
- iints/highlevel.py +1 -1
- iints/learning/autonomous_optimizer.py +13 -1
- iints/mdmp/backend.py +2 -3
- iints/research/predictor.py +9 -1
- iints/templates/demos/__init__.py +1 -0
- iints/templates/demos/live_stage_demo.py +410 -0
- {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/METADATA +88 -24
- {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/RECORD +72 -28
- {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/entry_points.txt +1 -0
- iints_sdk_python35-1.4.0.dist-info/licenses/LICENSE +173 -0
- iints_sdk_python35-1.3.1.dist-info/licenses/LICENSE → iints_sdk_python35-1.4.0.dist-info/licenses/LICENSE-MIT-IINTS-LEGACY +0 -7
- iints_sdk_python35-1.4.0.dist-info/licenses/NOTICE +17 -0
- iints_sdk_python35-1.4.0.dist-info/top_level.txt +5 -0
- mdmp_ai/__init__.py +3 -0
- mdmp_ai/lineage.py +198 -0
- mdmp_core/__init__.py +133 -0
- mdmp_core/audit.py +166 -0
- mdmp_core/bias_hooks.py +36 -0
- mdmp_core/bundle.py +142 -0
- mdmp_core/certification.py +38 -0
- mdmp_core/cli.py +1351 -0
- mdmp_core/compare.py +56 -0
- mdmp_core/conformance.py +429 -0
- mdmp_core/contracts.py +164 -0
- mdmp_core/crypto.py +372 -0
- mdmp_core/data/conformance/vectors/delegation.json +56 -0
- mdmp_core/data/conformance/vectors/fingerprint.json +24 -0
- mdmp_core/data/conformance/vectors/grading.json +83 -0
- mdmp_core/data/conformance/vectors/signing.json +37 -0
- mdmp_core/delegate.py +476 -0
- mdmp_core/diffing.py +128 -0
- mdmp_core/drift.py +203 -0
- mdmp_core/exceptions.py +33 -0
- mdmp_core/fingerprint.py +92 -0
- mdmp_core/fingerprint_store.py +42 -0
- mdmp_core/hf.py +60 -0
- mdmp_core/keys/mdmp_pub_v1.pem +3 -0
- mdmp_core/llm_provenance.py +34 -0
- mdmp_core/migrate.py +182 -0
- mdmp_core/policy.py +185 -0
- mdmp_core/prov.py +31 -0
- mdmp_core/registry.py +350 -0
- mdmp_core/runner.py +292 -0
- mdmp_core/schema_export.py +92 -0
- mdmp_core/synthetic.py +30 -0
- mdmp_core/trust.py +216 -0
- mdmp_core/visualizer.py +61 -0
- mdmp_flavors/__init__.py +146 -0
- mdmp_integrations/__init__.py +9 -0
- mdmp_integrations/dvc.py +30 -0
- mdmp_integrations/mlflow.py +14 -0
- mdmp_integrations/wandb.py +19 -0
- iints_sdk_python35-1.3.1.dist-info/top_level.txt +0 -1
- {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/WHEEL +0 -0
iints/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - Python < 3.8 fallback
|
|
|
11
11
|
try:
|
|
12
12
|
__version__ = version("iints-sdk-python35")
|
|
13
13
|
except PackageNotFoundError: # pragma: no cover - source tree fallback
|
|
14
|
-
__version__ = "1.
|
|
14
|
+
__version__ = "1.4.0"
|
|
15
15
|
|
|
16
16
|
# Note to developers: this SDK is currently maintained by a single author.
|
|
17
17
|
# Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
|
iints/ai/backends/ollama.py
CHANGED
|
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
from ipaddress import ip_address
|
|
5
6
|
from http.client import IncompleteRead, RemoteDisconnected
|
|
6
7
|
from time import sleep
|
|
7
8
|
from urllib import error, request
|
|
9
|
+
from urllib.parse import urlparse
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"
|
|
@@ -35,10 +37,38 @@ class OllamaBackend:
|
|
|
35
37
|
timeout_seconds: float = 120.0,
|
|
36
38
|
) -> None:
|
|
37
39
|
self.model_name = model_name
|
|
38
|
-
|
|
40
|
+
raw_base_url = base_url or os.getenv("OLLAMA_HOST") or DEFAULT_OLLAMA_HOST
|
|
41
|
+
self.base_url = self._validate_base_url(raw_base_url)
|
|
39
42
|
self.timeout_seconds = timeout_seconds
|
|
40
43
|
self.resolved_model_name: str | None = None
|
|
41
44
|
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _is_loopback_host(hostname: str) -> bool:
|
|
47
|
+
if hostname == "localhost":
|
|
48
|
+
return True
|
|
49
|
+
try:
|
|
50
|
+
return ip_address(hostname).is_loopback
|
|
51
|
+
except ValueError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def _validate_base_url(cls, raw_base_url: str) -> str:
|
|
56
|
+
parsed = urlparse(raw_base_url)
|
|
57
|
+
if parsed.scheme not in {"http", "https"}:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"OLLAMA_HOST/base_url must use http or https. Other URL schemes are blocked."
|
|
60
|
+
)
|
|
61
|
+
if not parsed.hostname:
|
|
62
|
+
raise ValueError("OLLAMA_HOST/base_url must include a hostname.")
|
|
63
|
+
if parsed.path not in {"", "/"}:
|
|
64
|
+
raise ValueError("OLLAMA_HOST/base_url must not include a path component.")
|
|
65
|
+
if not cls._is_loopback_host(parsed.hostname) and os.getenv("IINTS_ALLOW_REMOTE_OLLAMA") != "1":
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Remote Ollama endpoints are disabled by default. "
|
|
68
|
+
"Use localhost/127.0.0.1 or set IINTS_ALLOW_REMOTE_OLLAMA=1 explicitly."
|
|
69
|
+
)
|
|
70
|
+
return raw_base_url.rstrip("/")
|
|
71
|
+
|
|
42
72
|
def _pull_hint(self) -> str:
|
|
43
73
|
return f"ollama pull {self.model_name}"
|
|
44
74
|
|
|
@@ -102,7 +132,7 @@ class OllamaBackend:
|
|
|
102
132
|
headers["Content-Type"] = "application/json"
|
|
103
133
|
req = request.Request(url, data=body, headers=headers, method=method)
|
|
104
134
|
try:
|
|
105
|
-
with request.urlopen(req, timeout=self.timeout_seconds) as response:
|
|
135
|
+
with request.urlopen(req, timeout=self.timeout_seconds) as response: # nosec B310 - base_url is scheme/host validated
|
|
106
136
|
text = response.read().decode("utf-8")
|
|
107
137
|
except error.HTTPError as exc:
|
|
108
138
|
detail = exc.read().decode("utf-8", errors="replace").strip()
|
iints/ai/mdmp_guard.py
CHANGED
|
@@ -14,9 +14,8 @@ def _load_mdmp_verifier() -> type[Any]:
|
|
|
14
14
|
module = importlib.import_module("mdmp_core")
|
|
15
15
|
except Exception as exc:
|
|
16
16
|
raise ImportError(
|
|
17
|
-
"MDMP verification requires the
|
|
18
|
-
"Install with: pip install iints-sdk-python35[mdmp]
|
|
19
|
-
"or: pip install 'mdmp-protocol>=0.3.0'"
|
|
17
|
+
"MDMP verification requires the bundled MDMP crypto support.\n"
|
|
18
|
+
"Install with: pip install iints-sdk-python35[mdmp]"
|
|
20
19
|
) from exc
|
|
21
20
|
verifier_cls = getattr(module, "MDMPVerifier", None)
|
|
22
21
|
if verifier_cls is None:
|
iints/ai/prepare.py
CHANGED
|
@@ -245,7 +245,7 @@ def _load_mdmp_signer_tools() -> tuple[type[Any], Any]:
|
|
|
245
245
|
module = importlib.import_module("mdmp_core")
|
|
246
246
|
except Exception as exc:
|
|
247
247
|
raise ImportError(
|
|
248
|
-
"Local AI certification requires the
|
|
248
|
+
"Local AI certification requires the bundled MDMP crypto support.\n"
|
|
249
249
|
"Install with: pip install 'iints-sdk-python35[mdmp]'"
|
|
250
250
|
) from exc
|
|
251
251
|
|
iints/analysis/booth_demo.py
CHANGED
|
@@ -233,7 +233,7 @@ def _build_commands_markdown(
|
|
|
233
233
|
"# Booth Demo Commands\n\n"
|
|
234
234
|
"## Showable live demo script\n\n"
|
|
235
235
|
"```bash\n"
|
|
236
|
-
f"
|
|
236
|
+
f"python3 {example_script} --output-dir {output_dir}\n"
|
|
237
237
|
"```\n\n"
|
|
238
238
|
"## Run from source tree\n\n"
|
|
239
239
|
"```bash\n"
|
|
@@ -264,6 +264,8 @@ def _build_live_demo_script_text(
|
|
|
264
264
|
"1. WHAT CODE TO SHOW FIRST\n"
|
|
265
265
|
"- Show examples/demos/07_live_stage_demo.py first.\n"
|
|
266
266
|
" Reason: the top of that file exposes the patient profile, output folder, duration, and seed on one screen.\n"
|
|
267
|
+
"- Point out the visible SDK feature calls in that script:\n"
|
|
268
|
+
" run_full(...), generate_results_poster(...), and prepare_ai_ready_artifacts(...).\n"
|
|
267
269
|
"- Point out that you can swap PATIENT_CONFIG to another packaged profile such as patient_559_config or clinic_safe_hypo_prone.\n"
|
|
268
270
|
"- If someone asks how the full bundle is generated, open examples/demos/06_booth_demo.py and then src/iints/analysis/booth_demo.py.\n"
|
|
269
271
|
" Reason: those files define the three scenarios and write the poster, talk track, and run bundle outputs.\n\n"
|
|
@@ -144,6 +144,12 @@ class ClinicalMetricsCalculator:
|
|
|
144
144
|
"""
|
|
145
145
|
self.target_range = target_range
|
|
146
146
|
self.tight_range = tight_range
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _percentage(mask: pd.Series) -> float:
|
|
150
|
+
if len(mask) == 0:
|
|
151
|
+
return 0.0
|
|
152
|
+
return float(mask.sum()) / float(len(mask)) * 100.0
|
|
147
153
|
|
|
148
154
|
def calculate_tir(self,
|
|
149
155
|
glucose: pd.Series,
|
|
@@ -169,13 +175,13 @@ class ClinicalMetricsCalculator:
|
|
|
169
175
|
def calculate_all_tir_metrics(self, glucose: pd.Series) -> Dict[str, float]:
|
|
170
176
|
"""Calculate all TIR-related metrics"""
|
|
171
177
|
return {
|
|
172
|
-
'tir_70_180': self.
|
|
173
|
-
'tir_70_140': self.
|
|
174
|
-
'tir_70_110': self.
|
|
175
|
-
'tir_below_70': self.
|
|
176
|
-
'tir_below_54': self.
|
|
177
|
-
'tir_above_180': self.
|
|
178
|
-
'tir_above_250': self.
|
|
178
|
+
'tir_70_180': self._percentage((glucose >= 70) & (glucose <= 180)),
|
|
179
|
+
'tir_70_140': self._percentage((glucose >= 70) & (glucose <= 140)),
|
|
180
|
+
'tir_70_110': self._percentage((glucose >= 70) & (glucose <= 110)),
|
|
181
|
+
'tir_below_70': self._percentage(glucose < 70),
|
|
182
|
+
'tir_below_54': self._percentage(glucose < 54),
|
|
183
|
+
'tir_above_180': self._percentage(glucose > 180),
|
|
184
|
+
'tir_above_250': self._percentage(glucose > 250)
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
def calculate_gmi(self, glucose: pd.Series) -> float:
|
iints/analysis/validator.py
CHANGED
|
@@ -4,6 +4,12 @@ from typing import Dict, List, Tuple, Optional, Any
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import Enum
|
|
6
6
|
|
|
7
|
+
from iints.core.safety.config import (
|
|
8
|
+
SENSOR_GLUCOSE_MAX_MGDL,
|
|
9
|
+
SENSOR_GLUCOSE_MIN_MGDL,
|
|
10
|
+
SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL,
|
|
11
|
+
)
|
|
12
|
+
|
|
7
13
|
class ReliabilityLevel(Enum):
|
|
8
14
|
HIGH = "high"
|
|
9
15
|
MEDIUM = "medium"
|
|
@@ -22,9 +28,9 @@ class DataIntegrityValidator:
|
|
|
22
28
|
"""Validates data integrity for reverse engineering analysis."""
|
|
23
29
|
|
|
24
30
|
def __init__(self):
|
|
25
|
-
#
|
|
26
|
-
self.max_glucose_rate =
|
|
27
|
-
self.glucose_range = (
|
|
31
|
+
# Broad CGM/sensor fail-soft limits
|
|
32
|
+
self.max_glucose_rate = SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL
|
|
33
|
+
self.glucose_range = (SENSOR_GLUCOSE_MIN_MGDL, SENSOR_GLUCOSE_MAX_MGDL)
|
|
28
34
|
self.max_insulin_per_step = 5.0 # Units per 5-min step
|
|
29
35
|
|
|
30
36
|
def validate_glucose_data(self, glucose_values: List[float], timestamps: List[float]) -> ValidationResult:
|
|
@@ -270,4 +276,4 @@ class ReverseEngineeringValidator:
|
|
|
270
276
|
"issues": all_issues,
|
|
271
277
|
"warnings": all_warnings,
|
|
272
278
|
"category_scores": {category: result.reliability_score for category, result in validation_results.items()}
|
|
273
|
-
}
|
|
279
|
+
}
|
iints/cli/cli.py
CHANGED
|
@@ -53,6 +53,7 @@ from iints.data.registry import (
|
|
|
53
53
|
)
|
|
54
54
|
from iints.data.contracts import load_contract_yaml
|
|
55
55
|
from iints.data.synthetic_mirror import generate_synthetic_mirror
|
|
56
|
+
from iints.demo_assets import export_live_stage_demo
|
|
56
57
|
from iints.mdmp.backend import (
|
|
57
58
|
MDMP_GRADE_ORDER,
|
|
58
59
|
active_mdmp_backend,
|
|
@@ -2740,6 +2741,39 @@ def demo_booth(
|
|
|
2740
2741
|
)
|
|
2741
2742
|
|
|
2742
2743
|
|
|
2744
|
+
@app.command("demo-export")
|
|
2745
|
+
def demo_export(
|
|
2746
|
+
output_dir: Annotated[
|
|
2747
|
+
Path,
|
|
2748
|
+
typer.Option(help="Directory where the bundled live stage demo files should be written."),
|
|
2749
|
+
] = Path("./iints_demo"),
|
|
2750
|
+
overwrite: Annotated[
|
|
2751
|
+
bool,
|
|
2752
|
+
typer.Option("--overwrite/--no-overwrite", help="Allow overwriting exported demo files."),
|
|
2753
|
+
] = False,
|
|
2754
|
+
) -> None:
|
|
2755
|
+
"""Export the showable live demo code from the installed SDK."""
|
|
2756
|
+
console = Console()
|
|
2757
|
+
try:
|
|
2758
|
+
outputs = export_live_stage_demo(output_dir=output_dir, overwrite=overwrite)
|
|
2759
|
+
except FileExistsError as exc:
|
|
2760
|
+
console.print(f"[bold red]Demo export stopped:[/bold red] {exc}")
|
|
2761
|
+
raise typer.Exit(code=1)
|
|
2762
|
+
except Exception as exc:
|
|
2763
|
+
console.print(f"[bold red]Demo export failed:[/bold red] {exc}")
|
|
2764
|
+
raise typer.Exit(code=1)
|
|
2765
|
+
|
|
2766
|
+
table = Table(title="IINTS Demo Export")
|
|
2767
|
+
table.add_column("Artifact", style="cyan")
|
|
2768
|
+
table.add_column("Path", overflow="fold")
|
|
2769
|
+
table.add_row("script", outputs["script_path"])
|
|
2770
|
+
table.add_row("notes", outputs["notes_path"])
|
|
2771
|
+
console.print(table)
|
|
2772
|
+
console.print(
|
|
2773
|
+
"[green]Next:[/green] open `07_live_stage_demo.py`, explain the visible SDK calls, then run `python 07_live_stage_demo.py`."
|
|
2774
|
+
)
|
|
2775
|
+
|
|
2776
|
+
|
|
2743
2777
|
@app.command()
|
|
2744
2778
|
def report(
|
|
2745
2779
|
results_csv: Annotated[Path, typer.Option(help="Path to a simulation results CSV")],
|
|
@@ -46,7 +46,7 @@ class BergmanParameters:
|
|
|
46
46
|
n: float = 0.23 # 1/min — fractional insulin degradation
|
|
47
47
|
Ib: float = 7.0 # mU/L — basal plasma insulin
|
|
48
48
|
Vi: float = 0.05 # L/kg — insulin distribution volume
|
|
49
|
-
gamma: float = 0.
|
|
49
|
+
gamma: float = 0.0 # (mU/L)/(mg/dL)/min — endogenous secretion gain (0 for T1D default)
|
|
50
50
|
h: float = 80.0 # mg/dL — secretion glucose threshold
|
|
51
51
|
|
|
52
52
|
# --- Gut absorption ---
|
|
@@ -330,7 +330,8 @@ class BergmanPatientModel:
|
|
|
330
330
|
dXdt = -p.p2 * X + p.p3 * max(I - p.Ib, 0.0)
|
|
331
331
|
|
|
332
332
|
# --- dI/dt ---
|
|
333
|
-
#
|
|
333
|
+
# T1D defaults keep endogenous secretion disabled (gamma=0). Override
|
|
334
|
+
# gamma explicitly if you intentionally want residual beta-cell function.
|
|
334
335
|
secretion = p.gamma * max(G - p.h, 0.0)
|
|
335
336
|
dIdt = -p.n * (I - p.Ib) + secretion + u_insulin_mu_per_min / Vi_abs
|
|
336
337
|
|
iints/core/patient/models.py
CHANGED
|
@@ -14,7 +14,7 @@ class CustomPatientModel:
|
|
|
14
14
|
This model is intended for educational and stress-testing purposes, not for clinical accuracy.
|
|
15
15
|
"""
|
|
16
16
|
def __init__(self, basal_insulin_rate: float = 0.8, insulin_sensitivity: float = 50.0,
|
|
17
|
-
carb_factor: float = 10.0, glucose_decay_rate: float = 0.
|
|
17
|
+
carb_factor: float = 10.0, glucose_decay_rate: float = 0.05,
|
|
18
18
|
initial_glucose: float = 120.0, glucose_absorption_rate: float = 0.03,
|
|
19
19
|
basal_glucose_target: Optional[float] = None,
|
|
20
20
|
insulin_action_duration: float = 300.0, # minutes, e.g., 5 hours
|
|
@@ -63,14 +63,14 @@ class PatientFactory:
|
|
|
63
63
|
"""Get a diverse set of patients for population studies."""
|
|
64
64
|
if not SIMGLUCOSE_AVAILABLE:
|
|
65
65
|
# Create diverse custom patients with different parameters
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
return [
|
|
67
|
+
CustomPatientModel(initial_glucose=120, insulin_sensitivity=40), # High sensitivity
|
|
68
|
+
CustomPatientModel(initial_glucose=120, insulin_sensitivity=60), # Low sensitivity
|
|
69
|
+
CustomPatientModel(initial_glucose=120, carb_factor=8), # Fast carb absorption
|
|
70
|
+
CustomPatientModel(initial_glucose=120, carb_factor=12), # Slow carb absorption
|
|
71
|
+
CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.03), # Slower homeostatic drift
|
|
72
|
+
CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.07), # Faster homeostatic drift
|
|
73
|
+
]
|
|
74
74
|
else:
|
|
75
75
|
# Use FDA-approved virtual patients
|
|
76
76
|
selected_patients = [
|
iints/core/patient/profile.py
CHANGED
|
@@ -18,7 +18,7 @@ class PatientProfile:
|
|
|
18
18
|
dawn_end_hour: float = 8.0
|
|
19
19
|
|
|
20
20
|
# Advanced knobs (optional)
|
|
21
|
-
glucose_decay_rate: float = 0.
|
|
21
|
+
glucose_decay_rate: float = 0.05
|
|
22
22
|
glucose_absorption_rate: float = 0.03
|
|
23
23
|
insulin_action_duration: float = 300.0
|
|
24
24
|
insulin_peak_time: float = 75.0
|
iints/core/safety/config.py
CHANGED
|
@@ -2,16 +2,25 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
+
SENSOR_GLUCOSE_MIN_MGDL = 40.0
|
|
6
|
+
SENSOR_GLUCOSE_MAX_MGDL = 500.0
|
|
7
|
+
SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL = 20.0
|
|
8
|
+
SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL = 4.0
|
|
9
|
+
SIMULATION_GLUCOSE_FLOOR_MGDL = 20.0
|
|
10
|
+
SIMULATION_GLUCOSE_CEILING_MGDL = 600.0
|
|
11
|
+
|
|
5
12
|
|
|
6
13
|
@dataclass
|
|
7
14
|
class SafetyConfig:
|
|
8
15
|
"""
|
|
9
16
|
Central safety configuration for simulator, input validation, and supervisor.
|
|
10
17
|
"""
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
# Broad CGM/sensor plausibility limits.
|
|
19
|
+
# These are intentionally device-aware fail-soft bounds, not a claim about
|
|
20
|
+
# the full physiologic envelope of blood glucose inside the simulator.
|
|
21
|
+
min_glucose: float = SENSOR_GLUCOSE_MIN_MGDL
|
|
22
|
+
max_glucose: float = SENSOR_GLUCOSE_MAX_MGDL
|
|
23
|
+
max_glucose_delta_per_5_min: float = SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL
|
|
15
24
|
|
|
16
25
|
# Supervisor thresholds
|
|
17
26
|
hypoglycemia_threshold: float = 70.0
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
from typing import Any, Dict, Optional
|
|
2
2
|
|
|
3
|
-
from iints.core.safety.config import
|
|
3
|
+
from iints.core.safety.config import (
|
|
4
|
+
SENSOR_GLUCOSE_MAX_MGDL,
|
|
5
|
+
SENSOR_GLUCOSE_MIN_MGDL,
|
|
6
|
+
SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL,
|
|
7
|
+
SafetyConfig,
|
|
8
|
+
)
|
|
4
9
|
|
|
5
10
|
class InputValidator:
|
|
6
11
|
"""
|
|
@@ -9,16 +14,16 @@ class InputValidator:
|
|
|
9
14
|
This component makes the system robust against common sensor errors.
|
|
10
15
|
"""
|
|
11
16
|
def __init__(self,
|
|
12
|
-
min_glucose: float =
|
|
13
|
-
max_glucose: float =
|
|
14
|
-
max_glucose_delta_per_5_min: float =
|
|
17
|
+
min_glucose: float = SENSOR_GLUCOSE_MIN_MGDL,
|
|
18
|
+
max_glucose: float = SENSOR_GLUCOSE_MAX_MGDL,
|
|
19
|
+
max_glucose_delta_per_5_min: float = SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL,
|
|
15
20
|
safety_config: Optional[SafetyConfig] = None):
|
|
16
21
|
"""
|
|
17
22
|
Initializes the validator with plausible biological limits.
|
|
18
23
|
|
|
19
24
|
Args:
|
|
20
|
-
min_glucose (float):
|
|
21
|
-
max_glucose (float):
|
|
25
|
+
min_glucose (float): Broad fail-soft lower bound for incoming CGM/sensor values (mg/dL).
|
|
26
|
+
max_glucose (float): Broad fail-soft upper bound for incoming CGM/sensor values (mg/dL).
|
|
22
27
|
max_glucose_delta_per_5_min (float): The maximum plausible change in glucose
|
|
23
28
|
over a 5-minute period (mg/dL).
|
|
24
29
|
"""
|
|
@@ -62,7 +67,7 @@ class InputValidator:
|
|
|
62
67
|
Raises:
|
|
63
68
|
ValueError: If the value is outside biological plausibility limits.
|
|
64
69
|
"""
|
|
65
|
-
# 1.
|
|
70
|
+
# 1. Broad CGM/sensor plausibility check
|
|
66
71
|
if not (self.min_glucose <= glucose_value <= self.max_glucose):
|
|
67
72
|
raise ValueError(
|
|
68
73
|
f"BIOLOGICAL_PLAUSIBILITY_ERROR: Glucose {glucose_value} mg/dL is outside the "
|
iints/data/adapter.py
CHANGED
|
@@ -10,6 +10,8 @@ import numpy as np
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Dict, List, Optional, Union, Any
|
|
12
12
|
|
|
13
|
+
from iints.core.safety.config import SENSOR_GLUCOSE_MAX_MGDL, SENSOR_GLUCOSE_MIN_MGDL
|
|
14
|
+
|
|
13
15
|
class DataAdapter:
|
|
14
16
|
"""Universal data adapter for IINTS-AF framework"""
|
|
15
17
|
|
|
@@ -51,8 +53,11 @@ class DataAdapter:
|
|
|
51
53
|
raise ValueError(f"Missing required columns: {missing_cols}")
|
|
52
54
|
|
|
53
55
|
# Validate glucose range
|
|
54
|
-
if df['glucose'].min() <
|
|
55
|
-
raise ValueError(
|
|
56
|
+
if df['glucose'].min() < SENSOR_GLUCOSE_MIN_MGDL or df['glucose'].max() > SENSOR_GLUCOSE_MAX_MGDL:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Glucose values outside broad CGM/sensor-valid range "
|
|
59
|
+
f"({int(SENSOR_GLUCOSE_MIN_MGDL)}-{int(SENSOR_GLUCOSE_MAX_MGDL)} mg/dL)"
|
|
60
|
+
)
|
|
56
61
|
|
|
57
62
|
def load_ohio_dataset(self, patient_id: str) -> pd.DataFrame:
|
|
58
63
|
"""Load Ohio T1DM dataset with clinical benchmarks"""
|
iints/data/ingestor.py
CHANGED
|
@@ -3,6 +3,8 @@ from pathlib import Path
|
|
|
3
3
|
from typing import Dict, Any, Union, Optional
|
|
4
4
|
import yaml
|
|
5
5
|
|
|
6
|
+
from iints.core.safety.config import SENSOR_GLUCOSE_MAX_MGDL, SENSOR_GLUCOSE_MIN_MGDL
|
|
7
|
+
|
|
6
8
|
class DataIngestor:
|
|
7
9
|
"""
|
|
8
10
|
Standardized Data Bridge for ingesting various diabetes datasets into a
|
|
@@ -64,8 +66,11 @@ class DataIngestor:
|
|
|
64
66
|
|
|
65
67
|
# Basic quality checks (from DATA_SCHEMA.md)
|
|
66
68
|
if 'glucose' in df.columns:
|
|
67
|
-
if not ((df['glucose'] >=
|
|
68
|
-
raise ValueError(
|
|
69
|
+
if not ((df['glucose'] >= SENSOR_GLUCOSE_MIN_MGDL) & (df['glucose'] <= SENSOR_GLUCOSE_MAX_MGDL)).all():
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Glucose values outside acceptable CGM/sensor range "
|
|
72
|
+
f"({int(SENSOR_GLUCOSE_MIN_MGDL)}-{int(SENSOR_GLUCOSE_MAX_MGDL)} mg/dL)"
|
|
73
|
+
)
|
|
69
74
|
if 'insulin' in df.columns and not df['insulin'].isna().all():
|
|
70
75
|
if not ((df['insulin'] >= 0) & (df['insulin'] <= 50)).all():
|
|
71
76
|
raise ValueError("Insulin values outside acceptable range (0-50 units)")
|
iints/data/quality_checker.py
CHANGED
|
@@ -10,6 +10,12 @@ from datetime import datetime, timedelta
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import numpy as np
|
|
12
12
|
|
|
13
|
+
from iints.core.safety.config import (
|
|
14
|
+
SENSOR_GLUCOSE_MAX_MGDL,
|
|
15
|
+
SENSOR_GLUCOSE_MIN_MGDL,
|
|
16
|
+
SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL,
|
|
17
|
+
)
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
@dataclass
|
|
15
21
|
class QualityReport:
|
|
@@ -100,14 +106,16 @@ class DataQualityChecker:
|
|
|
100
106
|
|
|
101
107
|
# Physiological limits for glucose values
|
|
102
108
|
GLUCOSE_LIMITS = {
|
|
103
|
-
'minimum':
|
|
104
|
-
'maximum':
|
|
109
|
+
'minimum': SENSOR_GLUCOSE_MIN_MGDL, # mg/dL - broad CGM/sensor-valid minimum
|
|
110
|
+
'maximum': SENSOR_GLUCOSE_MAX_MGDL, # mg/dL - broad CGM/sensor-valid maximum
|
|
105
111
|
'critical_low': 54, # mg/dL - clinically significant low
|
|
106
112
|
'critical_high': 350 # mg/dL - clinically significant high
|
|
107
113
|
}
|
|
108
|
-
|
|
114
|
+
|
|
109
115
|
PHYSIOLOGICAL_RATES = {
|
|
110
|
-
|
|
116
|
+
# Broad fail-soft CGM plausibility cap; common CGM trend-arrow systems
|
|
117
|
+
# already treat >2 mg/dL/min as "rapid", so this keeps a conservative margin.
|
|
118
|
+
'max_glucose_change_per_min': SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
# Expected sampling intervals (in minutes)
|
iints/data/registry.py
CHANGED
|
@@ -5,8 +5,10 @@ import urllib.request
|
|
|
5
5
|
import zipfile
|
|
6
6
|
import hashlib
|
|
7
7
|
import shutil
|
|
8
|
+
from ipaddress import ip_address
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Any, Dict, List, Optional, IO, cast
|
|
11
|
+
from urllib.parse import urlparse
|
|
10
12
|
|
|
11
13
|
try: # Python 3.9+
|
|
12
14
|
from importlib.resources import files
|
|
@@ -54,10 +56,30 @@ def list_dataset_ids() -> List[str]:
|
|
|
54
56
|
return ids
|
|
55
57
|
|
|
56
58
|
|
|
59
|
+
def _is_loopback_host(hostname: str) -> bool:
|
|
60
|
+
if hostname == "localhost":
|
|
61
|
+
return True
|
|
62
|
+
try:
|
|
63
|
+
return ip_address(hostname).is_loopback
|
|
64
|
+
except ValueError:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_download_url(url: str) -> str:
|
|
69
|
+
parsed = urlparse(url)
|
|
70
|
+
if parsed.scheme == "https":
|
|
71
|
+
return url
|
|
72
|
+
if parsed.scheme == "http" and parsed.hostname and _is_loopback_host(parsed.hostname):
|
|
73
|
+
return url
|
|
74
|
+
raise DatasetFetchError(
|
|
75
|
+
"Dataset download URL must use https, or http only for localhost/loopback development mirrors."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
57
79
|
def _download_file(url: str, output_path: Path) -> Path:
|
|
58
80
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
81
|
try:
|
|
60
|
-
urllib.request.urlretrieve(url, output_path)
|
|
82
|
+
urllib.request.urlretrieve(_validate_download_url(url), output_path) # nosec B310 - URL is scheme validated before download
|
|
61
83
|
except Exception as exc:
|
|
62
84
|
raise DatasetFetchError(f"Failed to download {url}: {exc}") from exc
|
|
63
85
|
return output_path
|
iints/demo_assets.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def export_live_stage_demo(output_dir: str | Path = '.', *, overwrite: bool = False) -> dict[str, str]:
|
|
8
|
+
"""Export the bundled live stage demo script for users running the installed SDK."""
|
|
9
|
+
resolved_output = Path(output_dir).expanduser().resolve()
|
|
10
|
+
resolved_output.mkdir(parents=True, exist_ok=True)
|
|
11
|
+
|
|
12
|
+
script_target = resolved_output / '07_live_stage_demo.py'
|
|
13
|
+
notes_target = resolved_output / 'RUN_ME_FIRST.txt'
|
|
14
|
+
|
|
15
|
+
if not overwrite and script_target.exists():
|
|
16
|
+
raise FileExistsError(f'Demo script already exists: {script_target}')
|
|
17
|
+
if not overwrite and notes_target.exists():
|
|
18
|
+
raise FileExistsError(f'Instruction file already exists: {notes_target}')
|
|
19
|
+
|
|
20
|
+
script_content = files('iints.templates.demos').joinpath('live_stage_demo.py').read_text(encoding='utf-8')
|
|
21
|
+
notes_content = (
|
|
22
|
+
'IINTS LIVE DEMO EXPORT\n'
|
|
23
|
+
'======================\n\n'
|
|
24
|
+
'This folder was exported from the installed IINTS SDK.\n\n'
|
|
25
|
+
'1. Activate the virtual environment that contains IINTS.\n'
|
|
26
|
+
'2. Open 07_live_stage_demo.py and point to:\n'
|
|
27
|
+
' - PATIENT_CONFIG\n'
|
|
28
|
+
' - OUTPUT_DIR\n'
|
|
29
|
+
' - DURATION_MINUTES\n'
|
|
30
|
+
' - TIME_STEP_MINUTES\n'
|
|
31
|
+
' - SEED\n'
|
|
32
|
+
'3. Explain that the script visibly calls:\n'
|
|
33
|
+
' - run_full(...)\n'
|
|
34
|
+
' - generate_results_poster(...)\n'
|
|
35
|
+
' - prepare_ai_ready_artifacts(...)\n'
|
|
36
|
+
'4. Run:\n'
|
|
37
|
+
' python 07_live_stage_demo.py\n'
|
|
38
|
+
'5. Open the generated files under results/booth_demo_live/.\n\n'
|
|
39
|
+
'Tip: if you also cloned the SDK repo, you can run the repo wrapper instead:\n'
|
|
40
|
+
' ./scripts/run_live_stage_demo.sh\n'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
script_target.write_text(script_content, encoding='utf-8')
|
|
44
|
+
notes_target.write_text(notes_content, encoding='utf-8')
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
'output_dir': str(resolved_output),
|
|
48
|
+
'script_path': str(script_target),
|
|
49
|
+
'notes_path': str(notes_target),
|
|
50
|
+
}
|
iints/highlevel.py
CHANGED
|
@@ -442,7 +442,7 @@ def run_population(
|
|
|
442
442
|
dawn_phenomenon_strength=patient_params.get("dawn_phenomenon_strength", 0.0),
|
|
443
443
|
dawn_start_hour=patient_params.get("dawn_start_hour", 4.0),
|
|
444
444
|
dawn_end_hour=patient_params.get("dawn_end_hour", 8.0),
|
|
445
|
-
glucose_decay_rate=patient_params.get("glucose_decay_rate", 0.
|
|
445
|
+
glucose_decay_rate=patient_params.get("glucose_decay_rate", 0.05),
|
|
446
446
|
glucose_absorption_rate=patient_params.get("glucose_absorption_rate", 0.03),
|
|
447
447
|
insulin_peak_time=patient_params.get("insulin_peak_time", 75.0),
|
|
448
448
|
meal_mismatch_epsilon=patient_params.get("meal_mismatch_epsilon", 1.0),
|
|
@@ -15,6 +15,18 @@ except Exception: # pragma: no cover
|
|
|
15
15
|
_TORCH_AVAILABLE = False
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def _safe_torch_load_weights(path: str):
|
|
19
|
+
if torch is None: # pragma: no cover
|
|
20
|
+
raise ImportError("Torch required for model loading.")
|
|
21
|
+
try:
|
|
22
|
+
return torch.load(path, map_location="cpu", weights_only=True)
|
|
23
|
+
except TypeError as exc:
|
|
24
|
+
raise RuntimeError(
|
|
25
|
+
"This PyTorch build does not support secure weights-only loading. "
|
|
26
|
+
"Upgrade torch to a version that supports `weights_only=True`."
|
|
27
|
+
) from exc
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
@dataclass
|
|
19
31
|
class ClinicalConstraints:
|
|
20
32
|
"""Physiological constraints based on medical literature."""
|
|
@@ -98,7 +110,7 @@ if _TORCH_AVAILABLE:
|
|
|
98
110
|
model = LSTMModel(input_size=7, hidden_size=50, output_size=1)
|
|
99
111
|
|
|
100
112
|
if os.path.exists(self.model_path):
|
|
101
|
-
model.load_state_dict(
|
|
113
|
+
model.load_state_dict(_safe_torch_load_weights(self.model_path))
|
|
102
114
|
|
|
103
115
|
improved_model = self._clinical_fine_tuning(model, clinical_X, clinical_y)
|
|
104
116
|
safety_score = self._validate_clinical_safety(improved_model, clinical_X, clinical_y)
|
iints/mdmp/backend.py
CHANGED
|
@@ -114,9 +114,8 @@ def get_backend() -> str:
|
|
|
114
114
|
if requested in {BACKEND_MDMP, "mdmp", "external"}:
|
|
115
115
|
if not is_mdmp_available():
|
|
116
116
|
raise ImportError(
|
|
117
|
-
"
|
|
118
|
-
"Install with: pip install iints-sdk-python35[mdmp]
|
|
119
|
-
"or: pip install 'mdmp-protocol>=0.3.0'"
|
|
117
|
+
"Bundled MDMP support is not available in this environment.\n"
|
|
118
|
+
"Install with: pip install iints-sdk-python35[mdmp]"
|
|
120
119
|
)
|
|
121
120
|
return BACKEND_MDMP
|
|
122
121
|
if requested == "auto" and is_mdmp_available():
|
iints/research/predictor.py
CHANGED
|
@@ -293,7 +293,15 @@ def load_predictor(model_path: Path) -> Tuple["LSTMPredictor", dict]:
|
|
|
293
293
|
raise ImportError(
|
|
294
294
|
"Torch is required for predictor loading. Install with `pip install iints-sdk-python35[research]`."
|
|
295
295
|
) from _IMPORT_ERROR
|
|
296
|
-
|
|
296
|
+
try:
|
|
297
|
+
payload = torch.load(model_path, map_location="cpu", weights_only=True)
|
|
298
|
+
except TypeError as exc:
|
|
299
|
+
raise RuntimeError(
|
|
300
|
+
"This PyTorch build does not support secure checkpoint loading. "
|
|
301
|
+
"Upgrade torch to a version with `weights_only=True` support."
|
|
302
|
+
) from exc
|
|
303
|
+
if not isinstance(payload, dict):
|
|
304
|
+
raise RuntimeError("Predictor checkpoint must decode to a dictionary payload.")
|
|
297
305
|
config = payload["config"]
|
|
298
306
|
model = LSTMPredictor(
|
|
299
307
|
input_size=config["input_size"],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bundled demo script templates for installed IINTS users."""
|