iints-sdk-python35 1.5.7__py3-none-any.whl → 1.5.9__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 +43 -1
- iints/analysis/__init__.py +3 -0
- iints/analysis/evidence_bundle.py +328 -0
- iints/analysis/reporting.py +351 -0
- iints/analysis/run_quality.py +91 -0
- iints/analysis/safety_visualizer.py +363 -0
- iints/cli/cli.py +1365 -73
- iints/core/patient/bergman_model.py +5 -1
- iints/data/__init__.py +36 -0
- iints/data/datasets.json +239 -0
- iints/data/evidence.py +62 -0
- iints/data/medtronic_live.py +484 -0
- iints/data/realism_governance.py +208 -0
- iints/data/realism_validator.py +1 -0
- iints/data/research_catalog.py +248 -0
- iints/data/virtual_patients/reference_azt1d_t1d.yaml +1 -1
- iints/data/virtual_patients/reference_free_living_t1d.yaml +1 -1
- iints/data/virtual_patients/reference_hupa_ucm_t1d.yaml +1 -1
- iints/highlevel.py +39 -0
- iints/live_patient/__init__.py +18 -0
- iints/live_patient/medtronic_direct.py +365 -0
- iints/live_patient/pico_pump.py +65 -0
- iints/mdmp/__init__.py +12 -0
- iints/mdmp/eu_ai_pact.py +168 -0
- iints/presets/evidence_sources.yaml +8 -0
- iints/research/__init__.py +12 -0
- iints/research/control_eval.py +21 -2
- iints/research/local_ai.py +12 -0
- iints/research/local_ai_gate.py +247 -0
- iints/validation/run_doctor.py +304 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/METADATA +2 -2
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/RECORD +40 -29
- mdmp_core/certification.py +3 -0
- mdmp_core/drift.py +4 -3
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/WHEEL +0 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/entry_points.txt +0 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/licenses/LICENSE +0 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/licenses/LICENSE-MIT-IINTS-LEGACY +0 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/licenses/NOTICE +0 -0
- {iints_sdk_python35-1.5.7.dist-info → iints_sdk_python35-1.5.9.dist-info}/top_level.txt +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.5.
|
|
14
|
+
__version__ = "1.5.9"
|
|
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.
|
|
@@ -211,6 +211,46 @@ def generate_demo_report(
|
|
|
211
211
|
title="IINTS-AF Demo Report",
|
|
212
212
|
)
|
|
213
213
|
|
|
214
|
+
def generate_agp_report(
|
|
215
|
+
simulation_results: 'pd.DataFrame',
|
|
216
|
+
output_path: Optional[str] = None,
|
|
217
|
+
safety_report: Optional[dict] = None,
|
|
218
|
+
subject_name: str = "Research simulation",
|
|
219
|
+
summary_json_path: Optional[str] = None,
|
|
220
|
+
) -> Optional[str]:
|
|
221
|
+
"""
|
|
222
|
+
Generate an AGP-style research PDF report from dense CGM/simulation data.
|
|
223
|
+
"""
|
|
224
|
+
if output_path is None:
|
|
225
|
+
return None
|
|
226
|
+
generator = ClinicalReportGenerator()
|
|
227
|
+
return generator.generate_agp_pdf(
|
|
228
|
+
simulation_results,
|
|
229
|
+
output_path,
|
|
230
|
+
subject_name=subject_name,
|
|
231
|
+
safety_report=safety_report or {},
|
|
232
|
+
summary_json_path=summary_json_path,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def generate_agp_assets(
|
|
236
|
+
simulation_results: 'pd.DataFrame',
|
|
237
|
+
output_dir: Optional[str] = None,
|
|
238
|
+
subject_name: str = "Research simulation",
|
|
239
|
+
summary_json_path: Optional[str] = None,
|
|
240
|
+
) -> Optional[dict]:
|
|
241
|
+
"""
|
|
242
|
+
Export AGP-style PNG assets and summary JSON from dense CGM/simulation data.
|
|
243
|
+
"""
|
|
244
|
+
if output_dir is None:
|
|
245
|
+
return None
|
|
246
|
+
generator = ClinicalReportGenerator()
|
|
247
|
+
return generator.export_agp_assets(
|
|
248
|
+
simulation_results,
|
|
249
|
+
output_dir,
|
|
250
|
+
subject_name=subject_name,
|
|
251
|
+
summary_json_path=summary_json_path,
|
|
252
|
+
)
|
|
253
|
+
|
|
214
254
|
# You can also define __all__ to explicitly control what gets imported with `from iints import *`
|
|
215
255
|
__all__ = [
|
|
216
256
|
# API
|
|
@@ -287,6 +327,8 @@ __all__ = [
|
|
|
287
327
|
"generate_report",
|
|
288
328
|
"generate_quickstart_report",
|
|
289
329
|
"generate_demo_report",
|
|
330
|
+
"generate_agp_report",
|
|
331
|
+
"generate_agp_assets",
|
|
290
332
|
"generate_results_poster",
|
|
291
333
|
# High-level API
|
|
292
334
|
"run_simulation",
|
iints/analysis/__init__.py
CHANGED
|
@@ -40,6 +40,7 @@ from .eucys_results import (
|
|
|
40
40
|
generate_eucys_main_figure,
|
|
41
41
|
generate_eucys_results_bundle,
|
|
42
42
|
)
|
|
43
|
+
from .evidence_bundle import EVIDENCE_SCOPE, build_evidence_bundle
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def _missing_reports_dependency(feature: str, exc: Exception) -> None:
|
|
@@ -100,9 +101,11 @@ __all__ = [
|
|
|
100
101
|
"analyze_study_directory",
|
|
101
102
|
"build_booth_demo",
|
|
102
103
|
"build_carelink_workbench",
|
|
104
|
+
"build_evidence_bundle",
|
|
103
105
|
"ClinicalMetricsCalculator",
|
|
104
106
|
"ClinicalMetricsResult",
|
|
105
107
|
"ClinicalReportGenerator",
|
|
108
|
+
"EVIDENCE_SCOPE",
|
|
106
109
|
"compute_metrics",
|
|
107
110
|
"compare_studies",
|
|
108
111
|
"build_eucys_abstract_draft_markdown",
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from iints.utils.run_io import compute_sha256
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
EVIDENCE_SCOPE = (
|
|
16
|
+
"Research and education only. This bundle is not clinical evidence, "
|
|
17
|
+
"not a medical-device submission, and not dosing advice."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class EvidenceRun:
|
|
23
|
+
label: str
|
|
24
|
+
source_dir: Path
|
|
25
|
+
bundle_dir: Path
|
|
26
|
+
artifacts: Dict[str, str] = field(default_factory=dict)
|
|
27
|
+
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
warnings: List[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"label": self.label,
|
|
33
|
+
"source_dir": str(self.source_dir),
|
|
34
|
+
"bundle_dir": str(self.bundle_dir),
|
|
35
|
+
"artifacts": self.artifacts,
|
|
36
|
+
"metrics": self.metrics,
|
|
37
|
+
"warnings": self.warnings,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _safe_slug(value: str) -> str:
|
|
42
|
+
slug = "".join(ch.lower() if ch.isalnum() else "_" for ch in value.strip())
|
|
43
|
+
slug = "_".join(part for part in slug.split("_") if part)
|
|
44
|
+
return slug or "run"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _read_json(path: Path) -> Dict[str, Any]:
|
|
48
|
+
try:
|
|
49
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
except Exception:
|
|
51
|
+
return {}
|
|
52
|
+
return payload if isinstance(payload, dict) else {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _copy_if_exists(source: Path, target_dir: Path, name: Optional[str] = None) -> Optional[Path]:
|
|
56
|
+
if not source.is_file():
|
|
57
|
+
return None
|
|
58
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
target = target_dir / (name or source.name)
|
|
60
|
+
shutil.copy2(source, target)
|
|
61
|
+
return target
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_first(root: Path, names: Iterable[str]) -> Optional[Path]:
|
|
65
|
+
for name in names:
|
|
66
|
+
candidate = root / name
|
|
67
|
+
if candidate.is_file():
|
|
68
|
+
return candidate
|
|
69
|
+
for name in names:
|
|
70
|
+
matches = sorted(root.glob(f"**/{name}"))
|
|
71
|
+
if matches:
|
|
72
|
+
return matches[0]
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _glucose_metrics(results_csv: Optional[Path]) -> Dict[str, Any]:
|
|
77
|
+
if results_csv is None or not results_csv.is_file():
|
|
78
|
+
return {}
|
|
79
|
+
try:
|
|
80
|
+
df = pd.read_csv(results_csv)
|
|
81
|
+
except Exception:
|
|
82
|
+
return {}
|
|
83
|
+
glucose_column = None
|
|
84
|
+
for candidate in ("glucose_actual_mgdl", "glucose", "glucose_mgdl", "current_glucose"):
|
|
85
|
+
if candidate in df.columns:
|
|
86
|
+
glucose_column = candidate
|
|
87
|
+
break
|
|
88
|
+
if glucose_column is None or df.empty:
|
|
89
|
+
return {"rows": int(len(df))}
|
|
90
|
+
glucose = pd.to_numeric(df[glucose_column], errors="coerce").dropna()
|
|
91
|
+
if glucose.empty:
|
|
92
|
+
return {"rows": int(len(df))}
|
|
93
|
+
time_column = "time_minutes" if "time_minutes" in df.columns else "timestamp" if "timestamp" in df.columns else None
|
|
94
|
+
duration_minutes = None
|
|
95
|
+
if time_column is not None:
|
|
96
|
+
time_values = pd.to_numeric(df[time_column], errors="coerce").dropna()
|
|
97
|
+
if not time_values.empty:
|
|
98
|
+
duration_minutes = float(time_values.max() - time_values.min())
|
|
99
|
+
return {
|
|
100
|
+
"rows": int(len(df)),
|
|
101
|
+
"duration_minutes": duration_minutes,
|
|
102
|
+
"mean_glucose_mgdl": round(float(glucose.mean()), 3),
|
|
103
|
+
"min_glucose_mgdl": round(float(glucose.min()), 3),
|
|
104
|
+
"max_glucose_mgdl": round(float(glucose.max()), 3),
|
|
105
|
+
"tir_70_180_pct": round(float(((glucose >= 70) & (glucose <= 180)).mean() * 100.0), 3),
|
|
106
|
+
"tir_below_70_pct": round(float((glucose < 70).mean() * 100.0), 3),
|
|
107
|
+
"tir_below_54_pct": round(float((glucose < 54).mean() * 100.0), 3),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _bundle_one_run(label: str, run_dir: Path, output_runs_dir: Path) -> EvidenceRun:
|
|
112
|
+
source = run_dir.expanduser().resolve()
|
|
113
|
+
warnings: list[str] = []
|
|
114
|
+
run_target = output_runs_dir / _safe_slug(label)
|
|
115
|
+
artifacts_dir = run_target / "artifacts"
|
|
116
|
+
run_target.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
results_csv = _find_first(source, ("results.csv", "sim_results.csv"))
|
|
119
|
+
summary_json = _find_first(source, ("summary.json", "demo_summary.json", "run_summary.json"))
|
|
120
|
+
manifest_json = _find_first(source, ("run_manifest.json", "manifest.json"))
|
|
121
|
+
safety_json = _find_first(source, ("safety_report.json", "safety.json"))
|
|
122
|
+
report_pdf = _find_first(source, ("report.pdf", "clinical_report.pdf", "research_report.pdf"))
|
|
123
|
+
|
|
124
|
+
artifacts: dict[str, str] = {}
|
|
125
|
+
for key, path in {
|
|
126
|
+
"results_csv": results_csv,
|
|
127
|
+
"summary_json": summary_json,
|
|
128
|
+
"run_manifest_json": manifest_json,
|
|
129
|
+
"safety_report_json": safety_json,
|
|
130
|
+
"report_pdf": report_pdf,
|
|
131
|
+
}.items():
|
|
132
|
+
if path is None:
|
|
133
|
+
continue
|
|
134
|
+
copied = _copy_if_exists(path, artifacts_dir)
|
|
135
|
+
if copied is not None:
|
|
136
|
+
artifacts[key] = str(copied)
|
|
137
|
+
artifacts[f"{key}_sha256"] = compute_sha256(copied)
|
|
138
|
+
|
|
139
|
+
if results_csv is None:
|
|
140
|
+
warnings.append("No results.csv-style file found; metrics are limited.")
|
|
141
|
+
if manifest_json is None:
|
|
142
|
+
warnings.append("No run manifest found; reproducibility evidence is incomplete.")
|
|
143
|
+
if safety_json is None:
|
|
144
|
+
warnings.append("No safety report found; safety evidence is incomplete.")
|
|
145
|
+
|
|
146
|
+
metrics = _glucose_metrics(results_csv)
|
|
147
|
+
if safety_json is not None:
|
|
148
|
+
safety_payload = _read_json(safety_json)
|
|
149
|
+
for key in ("terminated_early", "critical_events", "supervisor_interventions"):
|
|
150
|
+
if key in safety_payload:
|
|
151
|
+
metrics[key] = safety_payload[key]
|
|
152
|
+
if summary_json is not None:
|
|
153
|
+
summary_payload = _read_json(summary_json)
|
|
154
|
+
for key in ("status", "completion_ratio", "completed_duration_minutes", "requested_duration_minutes"):
|
|
155
|
+
if key in summary_payload:
|
|
156
|
+
metrics[key] = summary_payload[key]
|
|
157
|
+
|
|
158
|
+
run_card = {
|
|
159
|
+
"label": label,
|
|
160
|
+
"source_dir": str(source),
|
|
161
|
+
"artifacts": artifacts,
|
|
162
|
+
"metrics": metrics,
|
|
163
|
+
"warnings": warnings,
|
|
164
|
+
"scope": EVIDENCE_SCOPE,
|
|
165
|
+
}
|
|
166
|
+
(run_target / "RUN_CARD.json").write_text(json.dumps(run_card, indent=2), encoding="utf-8")
|
|
167
|
+
return EvidenceRun(label=label, source_dir=source, bundle_dir=run_target, artifacts=artifacts, metrics=metrics, warnings=warnings)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _load_optional_json(root: Optional[Path], names: Iterable[str]) -> Dict[str, Any]:
|
|
171
|
+
if root is None:
|
|
172
|
+
return {}
|
|
173
|
+
path = _find_first(root.expanduser().resolve(), names)
|
|
174
|
+
return _read_json(path) if path is not None else {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _write_markdown(path: Path, title: str, payload: Dict[str, Any]) -> None:
|
|
178
|
+
lines = [
|
|
179
|
+
f"# {title}",
|
|
180
|
+
"",
|
|
181
|
+
EVIDENCE_SCOPE,
|
|
182
|
+
"",
|
|
183
|
+
"## Summary",
|
|
184
|
+
"",
|
|
185
|
+
f"- Created UTC: `{payload['created_utc']}`",
|
|
186
|
+
f"- Runs: `{len(payload['runs'])}`",
|
|
187
|
+
f"- Local AI evidence: `{'yes' if payload.get('local_ai') else 'no'}`",
|
|
188
|
+
f"- Pump bench evidence: `{'yes' if payload.get('pump_bench') else 'no'}`",
|
|
189
|
+
"",
|
|
190
|
+
"## Runs",
|
|
191
|
+
"",
|
|
192
|
+
]
|
|
193
|
+
if not payload["runs"]:
|
|
194
|
+
lines.append("No simulation runs were attached.")
|
|
195
|
+
lines.append("")
|
|
196
|
+
for run in payload["runs"]:
|
|
197
|
+
metrics = run.get("metrics", {})
|
|
198
|
+
lines.extend(
|
|
199
|
+
[
|
|
200
|
+
f"### {run['label']}",
|
|
201
|
+
"",
|
|
202
|
+
f"- Source: `{run['source_dir']}`",
|
|
203
|
+
f"- Bundle: `{run['bundle_dir']}`",
|
|
204
|
+
f"- Rows: `{metrics.get('rows', 'n/a')}`",
|
|
205
|
+
f"- Mean glucose: `{metrics.get('mean_glucose_mgdl', 'n/a')}` mg/dL",
|
|
206
|
+
f"- TIR 70-180: `{metrics.get('tir_70_180_pct', 'n/a')}`%",
|
|
207
|
+
f"- Time <70: `{metrics.get('tir_below_70_pct', 'n/a')}`%",
|
|
208
|
+
f"- Time <54: `{metrics.get('tir_below_54_pct', 'n/a')}`%",
|
|
209
|
+
"",
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
for warning in run.get("warnings", []):
|
|
213
|
+
lines.append(f"- Warning: {warning}")
|
|
214
|
+
if run.get("warnings"):
|
|
215
|
+
lines.append("")
|
|
216
|
+
lines.extend(
|
|
217
|
+
[
|
|
218
|
+
"## Required Interpretation",
|
|
219
|
+
"",
|
|
220
|
+
"- Use this as reproducibility and engineering evidence.",
|
|
221
|
+
"- Do not present this as clinical performance evidence.",
|
|
222
|
+
"- Keep local AI and pump exports behind deterministic safety gates.",
|
|
223
|
+
"",
|
|
224
|
+
]
|
|
225
|
+
)
|
|
226
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _write_model_card(path: Path, payload: Dict[str, Any]) -> None:
|
|
230
|
+
local_ai = payload.get("local_ai") or {}
|
|
231
|
+
gate = local_ai.get("training_safety_gate") or local_ai.get("closed_loop_evaluation", {}).get("safety_gate") or {}
|
|
232
|
+
lines = [
|
|
233
|
+
"# IINTS Research Model Card",
|
|
234
|
+
"",
|
|
235
|
+
EVIDENCE_SCOPE,
|
|
236
|
+
"",
|
|
237
|
+
"## Intended Use",
|
|
238
|
+
"",
|
|
239
|
+
"Local AI models generated by IINTS are for simulator research, reproducibility studies, and bench-only demonstrations.",
|
|
240
|
+
"",
|
|
241
|
+
"## Not Intended For",
|
|
242
|
+
"",
|
|
243
|
+
"- real insulin delivery",
|
|
244
|
+
"- diagnosis or treatment",
|
|
245
|
+
"- autonomous therapy without certified medical-device controls",
|
|
246
|
+
"",
|
|
247
|
+
"## Evidence Inputs",
|
|
248
|
+
"",
|
|
249
|
+
f"- Attached runs: `{len(payload['runs'])}`",
|
|
250
|
+
f"- Local AI summary present: `{'yes' if local_ai else 'no'}`",
|
|
251
|
+
"",
|
|
252
|
+
"## Safety Gate",
|
|
253
|
+
"",
|
|
254
|
+
f"- Status: `{gate.get('status', 'not_available')}`",
|
|
255
|
+
f"- Passed: `{gate.get('passed', 'not_available')}`",
|
|
256
|
+
f"- Score: `{gate.get('score', 'not_available')}`",
|
|
257
|
+
"",
|
|
258
|
+
]
|
|
259
|
+
if gate.get("critical_failures"):
|
|
260
|
+
lines.extend(["### Critical Failures", ""])
|
|
261
|
+
lines.extend(f"- {item}" for item in gate["critical_failures"])
|
|
262
|
+
lines.append("")
|
|
263
|
+
lines.extend(
|
|
264
|
+
[
|
|
265
|
+
"## Promotion Rule",
|
|
266
|
+
"",
|
|
267
|
+
"A model can only move toward hardware bench testing after realism checks, closed-loop evaluation, MDMP/evidence review, and deterministic supervisor gates pass.",
|
|
268
|
+
"",
|
|
269
|
+
]
|
|
270
|
+
)
|
|
271
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def build_evidence_bundle(
|
|
275
|
+
run_dirs: Iterable[tuple[str, Path]],
|
|
276
|
+
*,
|
|
277
|
+
output_dir: Path,
|
|
278
|
+
title: str = "IINTS Research Evidence Bundle",
|
|
279
|
+
local_ai_dir: Optional[Path] = None,
|
|
280
|
+
pump_bundle_dir: Optional[Path] = None,
|
|
281
|
+
) -> Dict[str, Any]:
|
|
282
|
+
"""Create a compact evidence pack for demos, reviews, and EUCYS-style explanation."""
|
|
283
|
+
|
|
284
|
+
root = output_dir.expanduser().resolve()
|
|
285
|
+
runs_dir = root / "runs"
|
|
286
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
287
|
+
|
|
288
|
+
runs = [_bundle_one_run(label, Path(run_dir), runs_dir).to_dict() for label, run_dir in run_dirs]
|
|
289
|
+
local_ai = _load_optional_json(local_ai_dir, ("LOCAL_AI_RESEARCH_SUMMARY.json", "summary.json"))
|
|
290
|
+
pump_bench = _load_optional_json(pump_bundle_dir, ("manifest.json", "iints_pump_manifest.json"))
|
|
291
|
+
|
|
292
|
+
payload: Dict[str, Any] = {
|
|
293
|
+
"title": title,
|
|
294
|
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
|
295
|
+
"scope": EVIDENCE_SCOPE,
|
|
296
|
+
"runs": runs,
|
|
297
|
+
"local_ai": local_ai,
|
|
298
|
+
"pump_bench": pump_bench,
|
|
299
|
+
"artifacts": {
|
|
300
|
+
"summary_json": str(root / "evidence_summary.json"),
|
|
301
|
+
"readme_md": str(root / "README.md"),
|
|
302
|
+
"model_card_md": str(root / "MODEL_CARD.md"),
|
|
303
|
+
"run_index_csv": str(root / "run_index.csv"),
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
(root / "evidence_summary.json").write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
308
|
+
_write_markdown(root / "README.md", title, payload)
|
|
309
|
+
_write_model_card(root / "MODEL_CARD.md", payload)
|
|
310
|
+
|
|
311
|
+
rows = []
|
|
312
|
+
for run in runs:
|
|
313
|
+
metrics = run.get("metrics", {})
|
|
314
|
+
rows.append(
|
|
315
|
+
{
|
|
316
|
+
"label": run["label"],
|
|
317
|
+
"source_dir": run["source_dir"],
|
|
318
|
+
"bundle_dir": run["bundle_dir"],
|
|
319
|
+
"rows": metrics.get("rows"),
|
|
320
|
+
"mean_glucose_mgdl": metrics.get("mean_glucose_mgdl"),
|
|
321
|
+
"tir_70_180_pct": metrics.get("tir_70_180_pct"),
|
|
322
|
+
"tir_below_70_pct": metrics.get("tir_below_70_pct"),
|
|
323
|
+
"tir_below_54_pct": metrics.get("tir_below_54_pct"),
|
|
324
|
+
"warnings": "; ".join(run.get("warnings", [])),
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
pd.DataFrame(rows).to_csv(root / "run_index.csv", index=False)
|
|
328
|
+
return payload
|