iints-sdk-python35 1.3.2__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/clinical_metrics.py +13 -7
- iints/analysis/validator.py +10 -4
- 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/highlevel.py +1 -1
- iints/learning/autonomous_optimizer.py +13 -1
- iints/mdmp/backend.py +2 -3
- iints/research/predictor.py +9 -1
- {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.4.0.dist-info}/METADATA +23 -17
- {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.4.0.dist-info}/RECORD +67 -26
- {iints_sdk_python35-1.3.2.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.2.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.2.dist-info/top_level.txt +0 -1
- {iints_sdk_python35-1.3.2.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
|
|
|
@@ -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
|
+
}
|
|
@@ -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/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"],
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iints-sdk-python35
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: A pre-clinical Edge-AI SDK for diabetes management validation.
|
|
5
5
|
Author-email: Rune Bobbaers <rune.bobbaers@gmail.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
6
7
|
Project-URL: Homepage, https://github.com/python35/IINTS-SDK
|
|
7
8
|
Classifier: Programming Language :: Python :: 3
|
|
8
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
9
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
+
License-File: NOTICE
|
|
19
|
+
License-File: LICENSE-MIT-IINTS-LEGACY
|
|
18
20
|
Requires-Dist: fpdf2>=2.8.0
|
|
19
21
|
Requires-Dist: matplotlib>=3.5.0
|
|
20
22
|
Requires-Dist: numpy>=1.24.0
|
|
@@ -46,7 +48,7 @@ Requires-Dist: h5py>=3.10.0; extra == "research"
|
|
|
46
48
|
Requires-Dist: onnx>=1.16.0; extra == "research"
|
|
47
49
|
Requires-Dist: onnxscript>=0.1.0; extra == "research"
|
|
48
50
|
Provides-Extra: mdmp
|
|
49
|
-
Requires-Dist:
|
|
51
|
+
Requires-Dist: cryptography>=42.0.0; extra == "mdmp"
|
|
50
52
|
Dynamic: license-file
|
|
51
53
|
|
|
52
54
|
# IINTS-AF SDK
|
|
@@ -83,6 +85,8 @@ iints presets run --name baseline_t1d --algo algorithms/example_algorithm.py
|
|
|
83
85
|
|
|
84
86
|
If you want the clearest install/path rules first, read:
|
|
85
87
|
- `docs/INSTALLATION.md`
|
|
88
|
+
- `docs/AI_ASSISTANT.md` for the small Ollama setup and SDK linking flow
|
|
89
|
+
- `docs/EVIDENCE_BASE.md` for the full medical + technical source legend
|
|
86
90
|
|
|
87
91
|
Short rule:
|
|
88
92
|
- installed `iints ...` commands can run from any folder
|
|
@@ -160,6 +164,12 @@ ollama pull ministral-3:8b
|
|
|
160
164
|
iints ai models
|
|
161
165
|
```
|
|
162
166
|
|
|
167
|
+
If you want the short "install Ollama + link it to the SDK" guide, read:
|
|
168
|
+
- `docs/AI_ASSISTANT.md`
|
|
169
|
+
|
|
170
|
+
If you want the full legend of medical, dataset, runtime, and emulator references used across the project docs:
|
|
171
|
+
- `docs/EVIDENCE_BASE.md`
|
|
172
|
+
|
|
163
173
|
Recommended first-time setup:
|
|
164
174
|
|
|
165
175
|
```bash
|
|
@@ -345,13 +355,13 @@ iints mdmp validate mdmp_contract.yaml data/my_cgm.csv --output-json results/mdm
|
|
|
345
355
|
iints mdmp visualizer results/mdmp_report.json --output-html results/mdmp_dashboard.html
|
|
346
356
|
```
|
|
347
357
|
|
|
348
|
-
Use
|
|
358
|
+
Use bundled `mdmp_core` backend (optional):
|
|
349
359
|
|
|
350
360
|
```bash
|
|
351
361
|
export IINTS_MDMP_BACKEND=mdmp_core
|
|
352
362
|
```
|
|
353
363
|
|
|
354
|
-
Staleness / lineage checks
|
|
364
|
+
Staleness / lineage checks via the bundled `mdmp` CLI:
|
|
355
365
|
|
|
356
366
|
```bash
|
|
357
367
|
mdmp fingerprint-record data/my_cgm.csv --output-json results/fingerprint.json --expires-days 365
|
|
@@ -361,21 +371,17 @@ mdmp registry init --registry registry/mdmp_registry.json
|
|
|
361
371
|
mdmp registry push --registry registry/mdmp_registry.json --report results/mdmp_report.json
|
|
362
372
|
```
|
|
363
373
|
|
|
364
|
-
##
|
|
365
|
-
- SDK repo: `python35/IINTS-SDK`
|
|
366
|
-
- MDMP repo: `python35/MDMP`
|
|
374
|
+
## Bundled MDMP
|
|
367
375
|
|
|
368
|
-
|
|
369
|
-
- `tools/dev/dual_repo_status.sh`
|
|
370
|
-
- `tools/dev/dual_repo_commit_push.sh`
|
|
376
|
+
MDMP now ships inside the SDK, so the SDK no longer depends on a separate public MDMP repository checkout.
|
|
371
377
|
|
|
372
|
-
|
|
378
|
+
That means:
|
|
379
|
+
- `iints mdmp ...` stays available
|
|
380
|
+
- the bundled `mdmp` CLI can still be used
|
|
381
|
+
- local AI signing and verification can still use `mdmp_core`
|
|
373
382
|
|
|
374
|
-
|
|
375
|
-
-
|
|
376
|
-
- Uses private-repo checkout when `MDMP_REPO_TOKEN` is configured.
|
|
377
|
-
- Falls back to `mdmp-protocol` from PyPI when checkout is unavailable.
|
|
378
|
-
- Auto dependency updates for MDMP are handled via Dependabot (`.github/dependabot.yml`).
|
|
383
|
+
Reference:
|
|
384
|
+
- `docs/DUAL_REPO_WORKFLOW.md`
|
|
379
385
|
|
|
380
386
|
## Tools Layout
|
|
381
387
|
|