iints-sdk-python35 1.3.2__py3-none-any.whl → 1.5.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/assistant.py +3 -0
- iints/ai/backends/ollama.py +32 -2
- iints/ai/cli.py +51 -0
- iints/ai/mdmp_guard.py +2 -3
- iints/ai/prepare.py +28 -1
- iints/ai/prompts.py +19 -1
- iints/analysis/booth_demo.py +3 -0
- iints/analysis/carelink_workbench.py +17 -0
- 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/templates/demos/live_stage_demo.py +2 -0
- {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/METADATA +26 -18
- {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/RECORD +73 -32
- {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/entry_points.txt +1 -0
- iints_sdk_python35-1.5.0.dist-info/licenses/LICENSE +173 -0
- iints_sdk_python35-1.3.2.dist-info/licenses/LICENSE → iints_sdk_python35-1.5.0.dist-info/licenses/LICENSE-MIT-IINTS-LEGACY +0 -7
- iints_sdk_python35-1.5.0.dist-info/licenses/NOTICE +17 -0
- iints_sdk_python35-1.5.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 +1352 -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.5.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.5.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/assistant.py
CHANGED
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/cli.py
CHANGED
|
@@ -43,6 +43,7 @@ def _default_prepared_payload(task: str, ai_dir: Path) -> Path:
|
|
|
43
43
|
"trends": ["trends_payload.json"],
|
|
44
44
|
"anomalies": ["anomalies_payload.json"],
|
|
45
45
|
"report": ["report_payload.json"],
|
|
46
|
+
"review": ["review_payload.json", "report_payload.json"],
|
|
46
47
|
}.get(task, [])
|
|
47
48
|
for filename in candidates:
|
|
48
49
|
candidate = ai_dir / filename
|
|
@@ -95,6 +96,14 @@ def _write_output(path: Path | None, response: AIResponse) -> None:
|
|
|
95
96
|
path.write_text(response.text + "\n", encoding="utf-8")
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def _default_output_for_review(input_path: Path, output: Path | None) -> Path | None:
|
|
100
|
+
if output is not None:
|
|
101
|
+
return output
|
|
102
|
+
if input_path.is_dir():
|
|
103
|
+
return input_path / "ai" / "realism_review.md"
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
98
107
|
def _render_response(console: Console, title: str, response: AIResponse, output: Path | None) -> None:
|
|
99
108
|
console.print(Panel(response.text, title=title, border_style="cyan"))
|
|
100
109
|
console.print(
|
|
@@ -438,3 +447,45 @@ def report(
|
|
|
438
447
|
except Exception as exc:
|
|
439
448
|
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
440
449
|
raise typer.Exit(code=1)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@app.command("review")
|
|
453
|
+
def review(
|
|
454
|
+
input_json: Annotated[Path, typer.Argument(help="Prepared run directory or JSON file with run-level simulation outputs.")],
|
|
455
|
+
mdmp_cert: Annotated[Optional[Path], typer.Option(help="Signed MDMP artifact required before AI analysis can run.")] = None,
|
|
456
|
+
mode: Annotated[str, typer.Option(help="AI backend mode. Use 'local' for Ollama/Ministral.")] = "auto",
|
|
457
|
+
model: Annotated[str, typer.Option(help="Ollama model name to use.")] = DEFAULT_MINISTRAL_MODEL,
|
|
458
|
+
minimum_grade: Annotated[str, typer.Option(help="Minimum MDMP grade required to allow analysis.")] = "research_grade",
|
|
459
|
+
public_key: Annotated[Optional[Path], typer.Option(help="Explicit MDMP public key for verification.")] = None,
|
|
460
|
+
trust_store: Annotated[Optional[Path], typer.Option(help="MDMP trust store for verification.")] = None,
|
|
461
|
+
ollama_host: Annotated[Optional[str], typer.Option(help="Override the Ollama base URL.")] = None,
|
|
462
|
+
timeout_seconds: Annotated[float, typer.Option(help="HTTP timeout for Ollama generation requests.")] = 120.0,
|
|
463
|
+
output: Annotated[Optional[Path], typer.Option(help="Optional file path to save the realism review.")] = None,
|
|
464
|
+
) -> None:
|
|
465
|
+
console = Console()
|
|
466
|
+
try:
|
|
467
|
+
resolved_input, resolved_cert, resolved_public_key = _resolve_cli_inputs(
|
|
468
|
+
task="review",
|
|
469
|
+
input_path=input_json,
|
|
470
|
+
mdmp_cert=mdmp_cert,
|
|
471
|
+
public_key=public_key,
|
|
472
|
+
trust_store=trust_store,
|
|
473
|
+
)
|
|
474
|
+
payload = _load_json_payload(resolved_input, "Input JSON")
|
|
475
|
+
assistant = _build_assistant(
|
|
476
|
+
mdmp_cert=resolved_cert,
|
|
477
|
+
mode=mode,
|
|
478
|
+
model=model,
|
|
479
|
+
minimum_grade=minimum_grade,
|
|
480
|
+
public_key=resolved_public_key,
|
|
481
|
+
trust_store=trust_store,
|
|
482
|
+
ollama_host=ollama_host,
|
|
483
|
+
timeout_seconds=timeout_seconds,
|
|
484
|
+
)
|
|
485
|
+
resolved_output = _default_output_for_review(input_json, output)
|
|
486
|
+
response = assistant.review_realism(payload)
|
|
487
|
+
_write_output(resolved_output, response)
|
|
488
|
+
_render_response(console, "IINTS AI Realism Review", response, resolved_output)
|
|
489
|
+
except Exception as exc:
|
|
490
|
+
console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
491
|
+
raise typer.Exit(code=1)
|
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
|
@@ -97,6 +97,13 @@ def _sample_trace(df: pd.DataFrame, *, max_rows: int = 48) -> list[dict[str, Any
|
|
|
97
97
|
return [_normalize_series_record(record) for record in sampled.to_dict(orient="records")]
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _max_glucose_delta_per_step(df: pd.DataFrame, glucose_column: str) -> float:
|
|
101
|
+
clean = pd.to_numeric(df[glucose_column], errors="coerce").dropna()
|
|
102
|
+
if clean.empty or len(clean) < 2:
|
|
103
|
+
return 0.0
|
|
104
|
+
return float(clean.diff().abs().dropna().max())
|
|
105
|
+
|
|
106
|
+
|
|
100
107
|
def _position_for_label(df: pd.DataFrame, label: Any) -> int:
|
|
101
108
|
location = df.index.get_loc(label)
|
|
102
109
|
if isinstance(location, int):
|
|
@@ -166,9 +173,12 @@ def _build_summary(df: pd.DataFrame, run_metadata: dict[str, Any], audit_summary
|
|
|
166
173
|
"mean_glucose_mgdl": _normalize_value(float(glucose.mean())),
|
|
167
174
|
"min_glucose_mgdl": _normalize_value(float(glucose.min())),
|
|
168
175
|
"max_glucose_mgdl": _normalize_value(float(glucose.max())),
|
|
176
|
+
"max_glucose_delta_per_step_mgdl": _normalize_value(_max_glucose_delta_per_step(df, glucose_column)),
|
|
169
177
|
"time_in_range_70_180_pct": _normalize_value(_time_in_band_pct(glucose, 70.0, 180.0)),
|
|
170
178
|
"time_below_70_pct": _normalize_value(float((glucose < 70.0).mean() * 100.0)),
|
|
179
|
+
"time_below_54_pct": _normalize_value(float((glucose < 54.0).mean() * 100.0)),
|
|
171
180
|
"time_above_180_pct": _normalize_value(float((glucose > 180.0).mean() * 100.0)),
|
|
181
|
+
"time_above_250_pct": _normalize_value(float((glucose > 250.0).mean() * 100.0)),
|
|
172
182
|
"delivered_insulin_total_units": _normalize_value(_safe_sum(df, "delivered_insulin_units")),
|
|
173
183
|
"recommended_insulin_total_units": _normalize_value(_safe_sum(df, "algo_recommended_insulin_units")),
|
|
174
184
|
"safety_trigger_count": _bool_sum(df, "safety_triggered"),
|
|
@@ -228,6 +238,23 @@ def _build_payloads(
|
|
|
228
238
|
"trace_sample": trace_sample,
|
|
229
239
|
"baseline_comparison": baseline_comparison,
|
|
230
240
|
},
|
|
241
|
+
"review_payload.json": {
|
|
242
|
+
**common,
|
|
243
|
+
"audit_summary": audit_summary,
|
|
244
|
+
"baseline_comparison": baseline_comparison,
|
|
245
|
+
"trace_sample": trace_sample,
|
|
246
|
+
"review_focus": {
|
|
247
|
+
"goal": "Judge whether the simulation or imported dataset looks physiologically plausible and internally coherent.",
|
|
248
|
+
"checks": [
|
|
249
|
+
"glucose range plausibility",
|
|
250
|
+
"excursion size and recovery behavior",
|
|
251
|
+
"time in range and severe hypo/hyper exposure",
|
|
252
|
+
"insulin delivery realism relative to observed glucose behavior",
|
|
253
|
+
"safety trigger and override consistency",
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
"run_manifest": run_manifest,
|
|
257
|
+
},
|
|
231
258
|
"step_riskiest.json": {
|
|
232
259
|
**common,
|
|
233
260
|
**risk_payload,
|
|
@@ -245,7 +272,7 @@ def _load_mdmp_signer_tools() -> tuple[type[Any], Any]:
|
|
|
245
272
|
module = importlib.import_module("mdmp_core")
|
|
246
273
|
except Exception as exc:
|
|
247
274
|
raise ImportError(
|
|
248
|
-
"Local AI certification requires the
|
|
275
|
+
"Local AI certification requires the bundled MDMP crypto support.\n"
|
|
249
276
|
"Install with: pip install 'iints-sdk-python35[mdmp]'"
|
|
250
277
|
) from exc
|
|
251
278
|
|
iints/ai/prompts.py
CHANGED
|
@@ -4,7 +4,13 @@ import json
|
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
TaskName = Literal[
|
|
7
|
+
TaskName = Literal[
|
|
8
|
+
"explain_decision",
|
|
9
|
+
"analyze_trends",
|
|
10
|
+
"detect_anomalies",
|
|
11
|
+
"generate_report",
|
|
12
|
+
"review_realism",
|
|
13
|
+
]
|
|
8
14
|
MAX_PROMPT_PAYLOAD_CHARS = 12000
|
|
9
15
|
|
|
10
16
|
SYSTEM_PROMPT = (
|
|
@@ -52,6 +58,18 @@ TASK_TEMPLATES: dict[TaskName, str] = {
|
|
|
52
58
|
"5. Research-only conclusion\n\n"
|
|
53
59
|
"Input JSON:\n{data}"
|
|
54
60
|
),
|
|
61
|
+
"review_realism": (
|
|
62
|
+
"Review this simulation or imported-data payload and judge whether the results look physiologically plausible for research use.\n"
|
|
63
|
+
"Be conservative and do not overclaim. If the payload is incomplete, say so clearly.\n"
|
|
64
|
+
"Respond in markdown with these sections:\n"
|
|
65
|
+
"1. Overall realism verdict (Likely realistic / Needs review / Likely unrealistic)\n"
|
|
66
|
+
"2. Strong realism signals\n"
|
|
67
|
+
"3. Questionable or unrealistic patterns\n"
|
|
68
|
+
"4. Concrete feedback points the SDK developer can improve\n"
|
|
69
|
+
"5. Suggested follow-up validation checks\n\n"
|
|
70
|
+
"Focus on glycemic ranges, excursion patterns, insulin behavior, safety overrides, and whether the data looks internally coherent.\n\n"
|
|
71
|
+
"Input JSON:\n{data}"
|
|
72
|
+
),
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
|
iints/analysis/booth_demo.py
CHANGED
|
@@ -196,6 +196,7 @@ def _build_jury_brief(
|
|
|
196
196
|
"```bash",
|
|
197
197
|
"iints ai local-check --model ministral-3:3b",
|
|
198
198
|
f"iints ai report {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
|
|
199
|
+
f"iints ai review {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
|
|
199
200
|
f"iints ai explain {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
|
|
200
201
|
"```",
|
|
201
202
|
"",
|
|
@@ -247,6 +248,7 @@ def _build_commands_markdown(
|
|
|
247
248
|
"```bash\n"
|
|
248
249
|
"iints ai local-check --model ministral-3:3b\n"
|
|
249
250
|
f"iints ai report {supervisor_dir} --model ministral-3:3b\n"
|
|
251
|
+
f"iints ai review {supervisor_dir} --model ministral-3:3b\n"
|
|
250
252
|
f"iints ai explain {supervisor_dir} --model ministral-3:3b\n"
|
|
251
253
|
"```\n"
|
|
252
254
|
)
|
|
@@ -296,6 +298,7 @@ def _build_live_demo_script_text(
|
|
|
296
298
|
"- If Ollama is ready, run:\n"
|
|
297
299
|
" iints ai local-check --model ministral-3:3b\n"
|
|
298
300
|
f" iints ai report {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
|
|
301
|
+
f" iints ai review {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
|
|
299
302
|
f" iints ai explain {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
|
|
300
303
|
"- Say: the local model explains the result, but only after the SDK has prepared the run artifacts.\n\n"
|
|
301
304
|
"7. IF THE JURY ASKS WHY THIS MATTERS\n"
|
|
@@ -202,6 +202,23 @@ def _prepare_ai_payloads(
|
|
|
202
202
|
"profile_24h": profile_records,
|
|
203
203
|
"trace_sample": trace_sample,
|
|
204
204
|
},
|
|
205
|
+
"review_payload.json": {
|
|
206
|
+
**common,
|
|
207
|
+
"daily_summary": daily_records,
|
|
208
|
+
"profile_24h": profile_records,
|
|
209
|
+
"trace_sample": trace_sample,
|
|
210
|
+
"top_alerts": alert_counts,
|
|
211
|
+
"top_sensor_exceptions": sensor_exception_counts,
|
|
212
|
+
"review_focus": {
|
|
213
|
+
"goal": "Judge whether the imported glucose history looks internally coherent and physiologically plausible.",
|
|
214
|
+
"checks": [
|
|
215
|
+
"time in range versus extreme exposure",
|
|
216
|
+
"daily variability and day-to-day stability",
|
|
217
|
+
"consistency between glucose, carbs, and insulin logs",
|
|
218
|
+
"frequency of alerts and sensor exceptions",
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
205
222
|
"anomalies_payload.json": {
|
|
206
223
|
**common,
|
|
207
224
|
"lowest_readings": [
|
|
@@ -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),
|