iints-sdk-python35 1.5.1__py3-none-any.whl → 1.5.2__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 +65 -5
- iints/analysis/__init__.py +54 -5
- iints/cli/cli.py +346 -5
- iints/cli/patient_cli.py +450 -0
- iints/live_patient/__init__.py +30 -0
- iints/live_patient/api.py +331 -0
- iints/live_patient/daemon.py +62 -0
- iints/live_patient/edge_benchmark.py +160 -0
- iints/live_patient/edge_ops.py +391 -0
- iints/live_patient/runtime.py +964 -0
- iints/live_patient/service_export.py +71 -0
- iints/live_patient/uno_q.py +37 -0
- iints/templates/uno_q/README.md +21 -0
- iints/templates/uno_q/iints_supervisor_bridge.ino +66 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/METADATA +28 -7
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/RECORD +22 -11
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/WHEEL +0 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/entry_points.txt +0 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/licenses/LICENSE +0 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/licenses/LICENSE-MIT-IINTS-LEGACY +0 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/licenses/NOTICE +0 -0
- {iints_sdk_python35-1.5.1.dist-info → iints_sdk_python35-1.5.2.dist-info}/top_level.txt +0 -0
iints/__init__.py
CHANGED
|
@@ -11,11 +11,18 @@ 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.2"
|
|
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.
|
|
18
18
|
|
|
19
|
+
|
|
20
|
+
def _missing_reports_dependency(feature: str, exc: Exception) -> None:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
f"{feature} requires the optional reporting stack. Install "
|
|
23
|
+
f"'iints-sdk-python35[reports]' or 'iints-sdk-python35[full]'."
|
|
24
|
+
) from exc
|
|
25
|
+
|
|
19
26
|
# API Components for Algorithm Development
|
|
20
27
|
from .api.base_algorithm import (
|
|
21
28
|
InsulinAlgorithm,
|
|
@@ -72,16 +79,58 @@ from .data.guardians import mdmp_gate, MDMPGateError
|
|
|
72
79
|
from .data.synthetic_mirror import generate_synthetic_mirror, SyntheticMirrorArtifact
|
|
73
80
|
from .data.study_corruption import AVAILABLE_STUDY_CORRUPTIONS, apply_study_corruptions, write_corrupted_study_csv
|
|
74
81
|
from .analysis.metrics import generate_benchmark_metrics # Added for benchmark
|
|
75
|
-
from .analysis.booth_demo import build_booth_demo
|
|
76
|
-
from .analysis.carelink_workbench import build_carelink_workbench
|
|
77
|
-
from .analysis.poster import generate_results_poster
|
|
78
|
-
from .analysis.reporting import ClinicalReportGenerator
|
|
79
82
|
from .analysis.study_protocol import build_study_protocol_payload, render_study_protocol_markdown, write_study_protocol_bundle
|
|
80
83
|
from .analysis.edge_efficiency import EnergyEstimate, estimate_energy_per_decision
|
|
81
84
|
from .ai import AIResponse, IINTSAssistant, MDMPGuard
|
|
85
|
+
from .live_patient import (
|
|
86
|
+
create_edge_bundle,
|
|
87
|
+
export_edge_setup,
|
|
88
|
+
LivePatientDaemon,
|
|
89
|
+
PatientRuntimeConfig,
|
|
90
|
+
create_patient_app,
|
|
91
|
+
export_uno_q_bridge,
|
|
92
|
+
get_runtime_scenario_profile,
|
|
93
|
+
list_runtime_scenario_profiles,
|
|
94
|
+
run_edge_benchmark,
|
|
95
|
+
summarize_edge_workspace,
|
|
96
|
+
write_edge_update_script,
|
|
97
|
+
)
|
|
82
98
|
from .highlevel import run_simulation, run_full, run_population
|
|
83
99
|
from .scenarios import ScenarioGeneratorConfig, generate_random_scenario
|
|
84
100
|
|
|
101
|
+
try:
|
|
102
|
+
from .analysis.booth_demo import build_booth_demo
|
|
103
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
104
|
+
_build_booth_demo_exc = exc
|
|
105
|
+
|
|
106
|
+
def build_booth_demo(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
107
|
+
_missing_reports_dependency("build_booth_demo()", _build_booth_demo_exc)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
from .analysis.carelink_workbench import build_carelink_workbench
|
|
111
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
112
|
+
_build_carelink_workbench_exc = exc
|
|
113
|
+
|
|
114
|
+
def build_carelink_workbench(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
115
|
+
_missing_reports_dependency("build_carelink_workbench()", _build_carelink_workbench_exc)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from .analysis.poster import generate_results_poster
|
|
119
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
120
|
+
_generate_results_poster_exc = exc
|
|
121
|
+
|
|
122
|
+
def generate_results_poster(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
123
|
+
_missing_reports_dependency("generate_results_poster()", _generate_results_poster_exc)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
from .analysis.reporting import ClinicalReportGenerator
|
|
127
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
128
|
+
_clinical_report_generator_exc = exc
|
|
129
|
+
|
|
130
|
+
class ClinicalReportGenerator: # type: ignore[no-redef]
|
|
131
|
+
def __init__(self, *args, **kwargs):
|
|
132
|
+
_missing_reports_dependency("ClinicalReportGenerator", _clinical_report_generator_exc)
|
|
133
|
+
|
|
85
134
|
# Population testing
|
|
86
135
|
from .population import (
|
|
87
136
|
PopulationGenerator,
|
|
@@ -202,6 +251,17 @@ __all__ = [
|
|
|
202
251
|
"AIResponse",
|
|
203
252
|
"IINTSAssistant",
|
|
204
253
|
"MDMPGuard",
|
|
254
|
+
"create_edge_bundle",
|
|
255
|
+
"export_edge_setup",
|
|
256
|
+
"LivePatientDaemon",
|
|
257
|
+
"PatientRuntimeConfig",
|
|
258
|
+
"create_patient_app",
|
|
259
|
+
"export_uno_q_bridge",
|
|
260
|
+
"get_runtime_scenario_profile",
|
|
261
|
+
"list_runtime_scenario_profiles",
|
|
262
|
+
"run_edge_benchmark",
|
|
263
|
+
"summarize_edge_workspace",
|
|
264
|
+
"write_edge_update_script",
|
|
205
265
|
# Reporting
|
|
206
266
|
"generate_report",
|
|
207
267
|
"generate_quickstart_report",
|
iints/analysis/__init__.py
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
from .clinical_metrics import ClinicalMetricsCalculator, ClinicalMetricsResult
|
|
2
2
|
from .baseline import compute_metrics, run_baseline_comparison, write_baseline_comparison
|
|
3
|
-
from .booth_demo import build_booth_demo
|
|
4
|
-
from .carelink_workbench import build_carelink_workbench
|
|
5
|
-
from .poster import generate_results_poster
|
|
6
|
-
from .reporting import ClinicalReportGenerator
|
|
7
|
-
from .study_poster import generate_study_poster
|
|
8
3
|
from .study_protocol import (
|
|
9
4
|
build_study_protocol_payload,
|
|
10
5
|
render_study_protocol_markdown,
|
|
@@ -21,6 +16,60 @@ from .study_analysis import (
|
|
|
21
16
|
StudySummary,
|
|
22
17
|
)
|
|
23
18
|
|
|
19
|
+
|
|
20
|
+
def _missing_reports_dependency(feature: str, exc: Exception) -> None:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
f"{feature} requires the optional reporting stack. Install "
|
|
23
|
+
f"'iints-sdk-python35[reports]' or 'iints-sdk-python35[full]'."
|
|
24
|
+
) from exc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from .booth_demo import build_booth_demo
|
|
29
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
30
|
+
_build_booth_demo_exc = exc
|
|
31
|
+
|
|
32
|
+
def build_booth_demo(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
33
|
+
_missing_reports_dependency("build_booth_demo()", _build_booth_demo_exc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from .carelink_workbench import build_carelink_workbench
|
|
38
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
39
|
+
_build_carelink_workbench_exc = exc
|
|
40
|
+
|
|
41
|
+
def build_carelink_workbench(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
42
|
+
_missing_reports_dependency("build_carelink_workbench()", _build_carelink_workbench_exc)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from .poster import generate_results_poster
|
|
47
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
48
|
+
_generate_results_poster_exc = exc
|
|
49
|
+
|
|
50
|
+
def generate_results_poster(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
51
|
+
_missing_reports_dependency("generate_results_poster()", _generate_results_poster_exc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from .reporting import ClinicalReportGenerator
|
|
56
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
57
|
+
_clinical_report_generator_exc = exc
|
|
58
|
+
|
|
59
|
+
class ClinicalReportGenerator: # type: ignore[no-redef]
|
|
60
|
+
def __init__(self, *args, **kwargs):
|
|
61
|
+
_missing_reports_dependency("ClinicalReportGenerator", _clinical_report_generator_exc)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from .study_poster import generate_study_poster
|
|
66
|
+
except Exception as exc: # pragma: no cover - optional reports stack
|
|
67
|
+
_generate_study_poster_exc = exc
|
|
68
|
+
|
|
69
|
+
def generate_study_poster(*args, **kwargs): # type: ignore[misc,no-redef]
|
|
70
|
+
_missing_reports_dependency("generate_study_poster()", _generate_study_poster_exc)
|
|
71
|
+
|
|
72
|
+
|
|
24
73
|
__all__ = [
|
|
25
74
|
"analyze_run_directory",
|
|
26
75
|
"analyze_study_directory",
|
iints/cli/cli.py
CHANGED
|
@@ -24,12 +24,9 @@ from rich.panel import Panel # type: ignore # For nicer auto-doc output
|
|
|
24
24
|
import iints # Import the top-level SDK package
|
|
25
25
|
from iints.ai import prepare_ai_ready_artifacts
|
|
26
26
|
from iints.ai.cli import app as ai_app
|
|
27
|
+
from iints.cli.patient_cli import app as patient_app
|
|
27
28
|
from iints.analysis.baseline import run_baseline_comparison, write_baseline_comparison
|
|
28
|
-
from iints.analysis.booth_demo import build_booth_demo
|
|
29
|
-
from iints.analysis.carelink_workbench import build_carelink_workbench
|
|
30
|
-
from iints.analysis.poster import generate_results_poster
|
|
31
29
|
from iints.analysis.study_analysis import analyze_study_directory, compare_studies, load_study_summary
|
|
32
|
-
from iints.analysis.study_poster import generate_study_poster
|
|
33
30
|
from iints.analysis.study_protocol import write_study_protocol_bundle
|
|
34
31
|
from iints.api.registry import list_algorithm_plugins
|
|
35
32
|
from iints.core.patient.profile import PatientProfile
|
|
@@ -65,6 +62,14 @@ from iints.data.registry import (
|
|
|
65
62
|
from iints.data.contracts import load_contract_yaml
|
|
66
63
|
from iints.data.synthetic_mirror import generate_synthetic_mirror
|
|
67
64
|
from iints.demo_assets import export_live_stage_demo
|
|
65
|
+
from iints.live_patient.edge_benchmark import run_edge_benchmark
|
|
66
|
+
from iints.live_patient.edge_ops import (
|
|
67
|
+
create_edge_bundle,
|
|
68
|
+
export_edge_setup,
|
|
69
|
+
summarize_edge_workspace,
|
|
70
|
+
write_edge_update_script,
|
|
71
|
+
)
|
|
72
|
+
from iints.live_patient.uno_q import export_uno_q_bridge
|
|
68
73
|
from iints.mdmp.backend import (
|
|
69
74
|
MDMP_GRADE_ORDER,
|
|
70
75
|
active_mdmp_backend,
|
|
@@ -105,6 +110,22 @@ from iints.validation import (
|
|
|
105
110
|
)
|
|
106
111
|
|
|
107
112
|
|
|
113
|
+
def _require_reports_feature(console: Console, module_path: str, attribute: str, feature_name: str):
|
|
114
|
+
try:
|
|
115
|
+
module = importlib.import_module(module_path)
|
|
116
|
+
return getattr(module, attribute)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
console.print(
|
|
119
|
+
f"[bold red]{feature_name} is not available in the minimal edge install.[/bold red]\n"
|
|
120
|
+
"Install the optional reporting stack with:\n"
|
|
121
|
+
" [cyan]python -m pip install -U \"iints-sdk-python35[reports]\"[/cyan]\n"
|
|
122
|
+
"or:\n"
|
|
123
|
+
" [cyan]python -m pip install -U \"iints-sdk-python35[full]\"[/cyan]\n"
|
|
124
|
+
f"Details: {exc}"
|
|
125
|
+
)
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
|
|
108
129
|
IINTS_ASCII_LOGO = r"""
|
|
109
130
|
/$$$$$$ /$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$ /$$ /$$
|
|
110
131
|
|_ $$_/|_ $$_/| $$$ | $$|__ $$__//$$__ $$ /$$__ $$| $$__ $$| $$ /$$/
|
|
@@ -139,6 +160,7 @@ data_app = typer.Typer(help="Data import, certification, and public data packs."
|
|
|
139
160
|
mdmp_app = typer.Typer(help="Legacy MDMP namespace kept for backwards compatibility.")
|
|
140
161
|
scenarios_app = typer.Typer(help="Scenario generation and utilities.")
|
|
141
162
|
algorithms_app = typer.Typer(help="Algorithm registry and plugins.")
|
|
163
|
+
edge_app = typer.Typer(help="Single-board computer and edge deployment tools.")
|
|
142
164
|
app.add_typer(docs_app, name="docs")
|
|
143
165
|
app.add_typer(presets_app, name="presets")
|
|
144
166
|
app.add_typer(profiles_app, name="profiles")
|
|
@@ -147,6 +169,8 @@ app.add_typer(mdmp_app, name="mdmp", hidden=True, deprecated=True)
|
|
|
147
169
|
app.add_typer(ai_app, name="ai")
|
|
148
170
|
app.add_typer(scenarios_app, name="scenarios")
|
|
149
171
|
app.add_typer(algorithms_app, name="algorithms")
|
|
172
|
+
app.add_typer(edge_app, name="edge")
|
|
173
|
+
app.add_typer(patient_app, name="patient")
|
|
150
174
|
|
|
151
175
|
def _load_algorithm_instance(algo: Path, console: Console) -> iints.InsulinAlgorithm:
|
|
152
176
|
if not algo.is_file():
|
|
@@ -1383,6 +1407,12 @@ def poster_study(
|
|
|
1383
1407
|
) -> None:
|
|
1384
1408
|
"""Generate a poster-style visual summary from study results."""
|
|
1385
1409
|
console = Console()
|
|
1410
|
+
generate_study_poster = _require_reports_feature(
|
|
1411
|
+
console,
|
|
1412
|
+
"iints.analysis.study_poster",
|
|
1413
|
+
"generate_study_poster",
|
|
1414
|
+
"Study poster generation",
|
|
1415
|
+
)
|
|
1386
1416
|
try:
|
|
1387
1417
|
outputs = generate_study_poster(study_input, output_path=output_path, title=title, subtitle=subtitle)
|
|
1388
1418
|
except Exception as exc:
|
|
@@ -1401,6 +1431,18 @@ def demo_expo(
|
|
|
1401
1431
|
) -> None:
|
|
1402
1432
|
"""Build the public expo bundle: three runs, study summary, study poster, and evidence tables."""
|
|
1403
1433
|
console = Console()
|
|
1434
|
+
build_booth_demo = _require_reports_feature(
|
|
1435
|
+
console,
|
|
1436
|
+
"iints.analysis.booth_demo",
|
|
1437
|
+
"build_booth_demo",
|
|
1438
|
+
"Expo demo bundle generation",
|
|
1439
|
+
)
|
|
1440
|
+
generate_study_poster = _require_reports_feature(
|
|
1441
|
+
console,
|
|
1442
|
+
"iints.analysis.study_poster",
|
|
1443
|
+
"generate_study_poster",
|
|
1444
|
+
"Study poster generation",
|
|
1445
|
+
)
|
|
1404
1446
|
try:
|
|
1405
1447
|
booth_outputs = build_booth_demo(
|
|
1406
1448
|
output_dir=output_dir,
|
|
@@ -1468,6 +1510,12 @@ def run_eucys_study(
|
|
|
1468
1510
|
) -> None:
|
|
1469
1511
|
"""Run the fixed EUCYS study matrix and generate summaries, comparisons, and posters."""
|
|
1470
1512
|
console = Console()
|
|
1513
|
+
generate_study_poster = _require_reports_feature(
|
|
1514
|
+
console,
|
|
1515
|
+
"iints.analysis.study_poster",
|
|
1516
|
+
"generate_study_poster",
|
|
1517
|
+
"Study poster generation",
|
|
1518
|
+
)
|
|
1471
1519
|
parsed_seeds = [int(item.strip()) for item in seeds.split(",") if item.strip()]
|
|
1472
1520
|
if not parsed_seeds:
|
|
1473
1521
|
console.print("[bold red]Please provide at least one seed.[/bold red]")
|
|
@@ -3389,6 +3437,12 @@ def poster(
|
|
|
3389
3437
|
):
|
|
3390
3438
|
"""Generate a poster-style PNG from one to three IINTS run bundles."""
|
|
3391
3439
|
console = Console()
|
|
3440
|
+
generate_results_poster = _require_reports_feature(
|
|
3441
|
+
console,
|
|
3442
|
+
"iints.analysis.poster",
|
|
3443
|
+
"generate_results_poster",
|
|
3444
|
+
"Run poster generation",
|
|
3445
|
+
)
|
|
3392
3446
|
try:
|
|
3393
3447
|
outputs = generate_results_poster(
|
|
3394
3448
|
run_dirs=run_dir,
|
|
@@ -3450,6 +3504,12 @@ def demo_booth(
|
|
|
3450
3504
|
) -> None:
|
|
3451
3505
|
"""Build a full expo/jury demo bundle with runs, poster, and talk track."""
|
|
3452
3506
|
console = Console()
|
|
3507
|
+
build_booth_demo = _require_reports_feature(
|
|
3508
|
+
console,
|
|
3509
|
+
"iints.analysis.booth_demo",
|
|
3510
|
+
"build_booth_demo",
|
|
3511
|
+
"Booth demo generation",
|
|
3512
|
+
)
|
|
3453
3513
|
try:
|
|
3454
3514
|
outputs = build_booth_demo(
|
|
3455
3515
|
output_dir=output_dir,
|
|
@@ -3547,9 +3607,11 @@ def report(
|
|
|
3547
3607
|
output_path = bundle_dir / "clinical_report.pdf"
|
|
3548
3608
|
audit_output_dir = bundle_dir / "audit"
|
|
3549
3609
|
plots_dir = bundle_dir / "plots"
|
|
3610
|
+
_require_reports_feature(console, "iints.analysis.reporting", "ClinicalReportGenerator", "Clinical PDF reporting")
|
|
3550
3611
|
generator = iints.ClinicalReportGenerator()
|
|
3551
3612
|
generator.export_plots(results_df, str(plots_dir))
|
|
3552
3613
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3614
|
+
_require_reports_feature(console, "iints.analysis.reporting", "ClinicalReportGenerator", "Clinical PDF reporting")
|
|
3553
3615
|
iints.generate_report(results_df, str(output_path), safety_report)
|
|
3554
3616
|
console.print(f"PDF report saved to: [link=file://{output_path}]{output_path}[/link]")
|
|
3555
3617
|
|
|
@@ -5036,6 +5098,12 @@ def carelink_workbench(
|
|
|
5036
5098
|
console.print(f"[bold red]Error: Input CSV '{input_csv}' not found.[/bold red]")
|
|
5037
5099
|
raise typer.Exit(code=1)
|
|
5038
5100
|
|
|
5101
|
+
build_carelink_workbench = _require_reports_feature(
|
|
5102
|
+
console,
|
|
5103
|
+
"iints.analysis.carelink_workbench",
|
|
5104
|
+
"build_carelink_workbench",
|
|
5105
|
+
"CareLink workbench",
|
|
5106
|
+
)
|
|
5039
5107
|
try:
|
|
5040
5108
|
outputs = build_carelink_workbench(
|
|
5041
5109
|
input_csv,
|
|
@@ -5337,7 +5405,7 @@ def docs_algo(
|
|
|
5337
5405
|
console.print(f"[bold red]Error loading algorithm module {algo_path}: {e}[/bold red]")
|
|
5338
5406
|
raise typer.Exit(code=1)
|
|
5339
5407
|
|
|
5340
|
-
algorithm_class = None
|
|
5408
|
+
algorithm_class: Optional[type[iints.InsulinAlgorithm]] = None
|
|
5341
5409
|
for name_in_module, obj in module.__dict__.items():
|
|
5342
5410
|
if isinstance(obj, type) and issubclass(obj, iints.InsulinAlgorithm) and obj is not iints.InsulinAlgorithm:
|
|
5343
5411
|
algorithm_class = obj
|
|
@@ -5353,6 +5421,7 @@ def docs_algo(
|
|
|
5353
5421
|
|
|
5354
5422
|
# Ensure algorithm_class is not None (it shouldn't be if algorithm_instance is not None)
|
|
5355
5423
|
assert algorithm_class is not None
|
|
5424
|
+
algorithm_class = cast(type[iints.InsulinAlgorithm], algorithm_class)
|
|
5356
5425
|
|
|
5357
5426
|
# Extract class docstring
|
|
5358
5427
|
class_doc = algorithm_class.__doc__ if algorithm_class.__doc__ else "No class docstring available."
|
|
@@ -5639,3 +5708,275 @@ def benchmark(
|
|
|
5639
5708
|
console.print(f"Run manifest signature: {signature_path}")
|
|
5640
5709
|
else:
|
|
5641
5710
|
console.print("[yellow]No benchmark results were generated.[/yellow]")
|
|
5711
|
+
|
|
5712
|
+
|
|
5713
|
+
@app.command("edge-benchmark")
|
|
5714
|
+
def edge_benchmark(
|
|
5715
|
+
algo: Annotated[Path, typer.Option(help="Path to the insulin algorithm Python file used for the edge benchmark.")],
|
|
5716
|
+
output_json: Annotated[Path, typer.Option(help="Output JSON path for the hardware benchmark results.")] = Path("results/edge_benchmark.json"),
|
|
5717
|
+
patient_config: Annotated[str, typer.Option(help="Patient configuration name or YAML path.")] = "default_patient",
|
|
5718
|
+
patient_model: Annotated[str, typer.Option("--patient-model", help="Patient model type.")] = "auto",
|
|
5719
|
+
scenario_profile: Annotated[str, typer.Option(help="Digital patient scenario profile.")] = "normal_day",
|
|
5720
|
+
steps: Annotated[int, typer.Option(help="Number of simulated steps used for throughput measurement.")] = 72,
|
|
5721
|
+
platform_name: Annotated[str, typer.Option("--platform", help="Platform label written into the benchmark report. Use 'auto' to detect locally.")] = "auto",
|
|
5722
|
+
api_host: Annotated[str, typer.Option(help="Host used for the local dashboard probe.")] = "127.0.0.1",
|
|
5723
|
+
api_port: Annotated[int, typer.Option(help="Port used for the local dashboard probe.")] = 8766,
|
|
5724
|
+
seed: Annotated[Optional[int], typer.Option(help="Optional deterministic seed override.")] = None,
|
|
5725
|
+
) -> None:
|
|
5726
|
+
"""Measure digital-patient throughput, memory use, and dashboard response time on edge hardware."""
|
|
5727
|
+
console = Console()
|
|
5728
|
+
if not algo.is_file():
|
|
5729
|
+
console.print(f"[bold red]Algorithm file '{algo}' not found.[/bold red]")
|
|
5730
|
+
raise typer.Exit(code=1)
|
|
5731
|
+
|
|
5732
|
+
try:
|
|
5733
|
+
payload = run_edge_benchmark(
|
|
5734
|
+
algo_path=algo,
|
|
5735
|
+
patient_config=patient_config,
|
|
5736
|
+
patient_model_type=patient_model,
|
|
5737
|
+
scenario_profile=scenario_profile,
|
|
5738
|
+
steps=steps,
|
|
5739
|
+
platform_name=platform_name,
|
|
5740
|
+
api_host=api_host,
|
|
5741
|
+
api_port=api_port,
|
|
5742
|
+
seed=seed,
|
|
5743
|
+
)
|
|
5744
|
+
except Exception as exc:
|
|
5745
|
+
console.print(f"[bold red]Edge benchmark failed:[/bold red] {exc}")
|
|
5746
|
+
raise typer.Exit(code=1)
|
|
5747
|
+
|
|
5748
|
+
output_json.parent.mkdir(parents=True, exist_ok=True)
|
|
5749
|
+
output_json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
5750
|
+
|
|
5751
|
+
runtime = payload["runtime"]
|
|
5752
|
+
dashboard = payload["dashboard"]
|
|
5753
|
+
table = Table(title="IINTS Edge Benchmark")
|
|
5754
|
+
table.add_column("Metric", style="cyan")
|
|
5755
|
+
table.add_column("Value")
|
|
5756
|
+
table.add_row("Platform", str(payload["platform"]))
|
|
5757
|
+
table.add_row("Scenario", str(payload["scenario_profile"]))
|
|
5758
|
+
table.add_row("Seed", str(payload["seed"]))
|
|
5759
|
+
table.add_row("Steps / second", f"{runtime['steps_per_second']:.2f}")
|
|
5760
|
+
table.add_row("Mean step latency", f"{runtime['mean_step_latency_ms']:.2f} ms")
|
|
5761
|
+
table.add_row("Peak process memory", f"{runtime['peak_process_memory_mb']:.2f} MB")
|
|
5762
|
+
table.add_row("Dashboard response", f"{dashboard['dashboard_response_ms']['mean_ms']:.2f} ms")
|
|
5763
|
+
table.add_row("Status response", f"{dashboard['status_response_ms']['mean_ms']:.2f} ms")
|
|
5764
|
+
console.print(table)
|
|
5765
|
+
console.print(f"[green]Edge benchmark JSON written:[/green] {output_json}")
|
|
5766
|
+
|
|
5767
|
+
|
|
5768
|
+
def _parse_edge_speed(value: str | float) -> float:
|
|
5769
|
+
if isinstance(value, (int, float)):
|
|
5770
|
+
parsed = float(value)
|
|
5771
|
+
else:
|
|
5772
|
+
text = str(value).strip().lower()
|
|
5773
|
+
if text.endswith("x"):
|
|
5774
|
+
text = text[:-1]
|
|
5775
|
+
parsed = float(text)
|
|
5776
|
+
if parsed <= 0.0:
|
|
5777
|
+
raise typer.BadParameter("Speed must be a positive number such as 60 or 60x.")
|
|
5778
|
+
return parsed
|
|
5779
|
+
|
|
5780
|
+
|
|
5781
|
+
@edge_app.command("setup")
|
|
5782
|
+
def edge_setup(
|
|
5783
|
+
output_dir: Annotated[Path, typer.Option(help="Directory where the edge-ready project scaffold should be written.")] = Path("iints_edge_demo"),
|
|
5784
|
+
board: Annotated[str, typer.Option(help="Edge board target: raspberry_pi or uno_q.")] = "raspberry_pi",
|
|
5785
|
+
workspace_name: Annotated[str, typer.Option(help="Workspace folder name used for the persistent patient runtime.")] = "patient_runtime",
|
|
5786
|
+
scenario_profile: Annotated[str, typer.Option(help="Initial live scenario profile.")] = "normal_day",
|
|
5787
|
+
patient_config: Annotated[str, typer.Option(help="Patient configuration name or YAML path.")] = "default_patient",
|
|
5788
|
+
patient_model: Annotated[str, typer.Option("--patient-model", help="Patient model type.")] = "auto",
|
|
5789
|
+
mode: Annotated[str, typer.Option(help="Clock mode for the generated edge project.")] = "demo-time",
|
|
5790
|
+
speed: Annotated[str, typer.Option(help="Acceleration factor for demo-time mode. Accepts 60 or 60x.")] = "60x",
|
|
5791
|
+
api_host: Annotated[str, typer.Option(help="Dashboard host to bake into the generated runtime config.")] = "127.0.0.1",
|
|
5792
|
+
api_port: Annotated[int, typer.Option(help="Dashboard port to bake into the generated runtime config.")] = 8765,
|
|
5793
|
+
seed: Annotated[Optional[int], typer.Option(help="Optional deterministic seed override.")] = None,
|
|
5794
|
+
service_name: Annotated[str, typer.Option(help="systemd service name without the .service suffix.")] = "iints-digital-patient",
|
|
5795
|
+
user_name: Annotated[Optional[str], typer.Option(help="Linux user that should own the generated systemd service.")] = None,
|
|
5796
|
+
) -> None:
|
|
5797
|
+
console = Console()
|
|
5798
|
+
normalized_board = board.strip().lower()
|
|
5799
|
+
if normalized_board not in {"raspberry_pi", "uno_q"}:
|
|
5800
|
+
console.print("[bold red]Unsupported board. Use `raspberry_pi` or `uno_q`.[/bold red]")
|
|
5801
|
+
raise typer.Exit(code=1)
|
|
5802
|
+
|
|
5803
|
+
outputs = export_edge_setup(
|
|
5804
|
+
output_dir,
|
|
5805
|
+
board=normalized_board,
|
|
5806
|
+
workspace_name=workspace_name,
|
|
5807
|
+
scenario_profile=scenario_profile,
|
|
5808
|
+
patient_config=patient_config,
|
|
5809
|
+
patient_model_type=patient_model,
|
|
5810
|
+
mode=mode,
|
|
5811
|
+
speed=_parse_edge_speed(speed),
|
|
5812
|
+
api_host=api_host,
|
|
5813
|
+
api_port=api_port,
|
|
5814
|
+
seed=seed,
|
|
5815
|
+
service_name=service_name,
|
|
5816
|
+
user_name=user_name,
|
|
5817
|
+
include_uno_bridge=normalized_board == "uno_q",
|
|
5818
|
+
)
|
|
5819
|
+
|
|
5820
|
+
table = Table(title="IINTS Edge Setup")
|
|
5821
|
+
table.add_column("Artifact", style="cyan")
|
|
5822
|
+
table.add_column("Path", overflow="fold")
|
|
5823
|
+
for key in [
|
|
5824
|
+
"root",
|
|
5825
|
+
"algorithm",
|
|
5826
|
+
"workspace",
|
|
5827
|
+
"config",
|
|
5828
|
+
"run_script",
|
|
5829
|
+
"kiosk_script",
|
|
5830
|
+
"update_script",
|
|
5831
|
+
"service_file",
|
|
5832
|
+
"service_notes",
|
|
5833
|
+
"setup_guide",
|
|
5834
|
+
]:
|
|
5835
|
+
table.add_row(key, outputs[key])
|
|
5836
|
+
if "uno_q_bridge" in outputs:
|
|
5837
|
+
table.add_row("uno_q_bridge", outputs["uno_q_bridge"])
|
|
5838
|
+
console.print(table)
|
|
5839
|
+
console.print(
|
|
5840
|
+
Panel(
|
|
5841
|
+
"\n".join(
|
|
5842
|
+
[
|
|
5843
|
+
f"Board profile: {normalized_board}",
|
|
5844
|
+
f"Start script: {outputs['run_script']}",
|
|
5845
|
+
f"Kiosk launcher: {outputs['kiosk_script']}",
|
|
5846
|
+
f"Setup guide: {outputs['setup_guide']}",
|
|
5847
|
+
]
|
|
5848
|
+
),
|
|
5849
|
+
title="Edge Setup Ready",
|
|
5850
|
+
border_style="green",
|
|
5851
|
+
)
|
|
5852
|
+
)
|
|
5853
|
+
|
|
5854
|
+
|
|
5855
|
+
@edge_app.command("status")
|
|
5856
|
+
def edge_status(
|
|
5857
|
+
workspace: Annotated[Path, typer.Option(help="Workspace directory for the persistent digital patient state.")] = Path("./digital_patient_runtime"),
|
|
5858
|
+
) -> None:
|
|
5859
|
+
console = Console()
|
|
5860
|
+
summary = summarize_edge_workspace(workspace)
|
|
5861
|
+
if not summary:
|
|
5862
|
+
console.print(f"[bold red]No edge runtime found in {workspace}.[/bold red]")
|
|
5863
|
+
raise typer.Exit(code=1)
|
|
5864
|
+
|
|
5865
|
+
certification = summary.get("certification") or {}
|
|
5866
|
+
review = summary.get("review") or {}
|
|
5867
|
+
|
|
5868
|
+
table = Table(title="IINTS Edge Runtime Status")
|
|
5869
|
+
table.add_column("Field", style="cyan")
|
|
5870
|
+
table.add_column("Value", overflow="fold")
|
|
5871
|
+
rows = [
|
|
5872
|
+
("daemon_status", summary.get("daemon_status", "-")),
|
|
5873
|
+
("pid_alive", summary.get("pid_alive", "-")),
|
|
5874
|
+
("algorithm_name", summary.get("algorithm_name", "-")),
|
|
5875
|
+
("scenario_profile", summary.get("scenario_profile", "-")),
|
|
5876
|
+
("active_seed", summary.get("active_seed", "-")),
|
|
5877
|
+
("simulated_clock", summary.get("simulated_clock", "-")),
|
|
5878
|
+
("last_glucose_mgdl", summary.get("last_glucose_mgdl", "-")),
|
|
5879
|
+
("dashboard_url", summary.get("dashboard_url", "-")),
|
|
5880
|
+
("kiosk_url", summary.get("kiosk_url", "-")),
|
|
5881
|
+
("certification", certification.get("label", "-")),
|
|
5882
|
+
("review", review.get("label", "-")),
|
|
5883
|
+
("workspace_size_mb", summary.get("workspace_size_mb", "-")),
|
|
5884
|
+
("bundle_size_mb", summary.get("bundle_size_mb", "-")),
|
|
5885
|
+
("last_heartbeat_utc", summary.get("last_heartbeat_utc", "-")),
|
|
5886
|
+
]
|
|
5887
|
+
for field, value in rows:
|
|
5888
|
+
table.add_row(str(field), str(value))
|
|
5889
|
+
console.print(table)
|
|
5890
|
+
|
|
5891
|
+
|
|
5892
|
+
@edge_app.command("bundle")
|
|
5893
|
+
def edge_bundle(
|
|
5894
|
+
workspace: Annotated[Path, typer.Option(help="Workspace directory for the persistent digital patient state.")] = Path("./digital_patient_runtime"),
|
|
5895
|
+
output: Annotated[Path, typer.Option(help="ZIP archive written for workstation-side analysis.")] = Path("results/edge_runtime_bundle.zip"),
|
|
5896
|
+
include_log: Annotated[bool, typer.Option(help="Include the patient log in the archive.")] = True,
|
|
5897
|
+
include_database: Annotated[bool, typer.Option(help="Include the SQLite runtime database in the archive.")] = True,
|
|
5898
|
+
) -> None:
|
|
5899
|
+
console = Console()
|
|
5900
|
+
payload = create_edge_bundle(
|
|
5901
|
+
workspace,
|
|
5902
|
+
output_path=output,
|
|
5903
|
+
include_log=include_log,
|
|
5904
|
+
include_database=include_database,
|
|
5905
|
+
)
|
|
5906
|
+
summary = payload["summary"]
|
|
5907
|
+
console.print(
|
|
5908
|
+
Panel(
|
|
5909
|
+
"\n".join(
|
|
5910
|
+
[
|
|
5911
|
+
f"Archive: {payload['archive']}",
|
|
5912
|
+
f"Scenario: {summary.get('scenario_profile', '-')}",
|
|
5913
|
+
f"Certification: {(summary.get('certification') or {}).get('label', '-')}",
|
|
5914
|
+
f"Review: {(summary.get('review') or {}).get('label', '-')}",
|
|
5915
|
+
]
|
|
5916
|
+
),
|
|
5917
|
+
title="Edge Bundle Ready",
|
|
5918
|
+
border_style="cyan",
|
|
5919
|
+
)
|
|
5920
|
+
)
|
|
5921
|
+
|
|
5922
|
+
|
|
5923
|
+
@edge_app.command("update")
|
|
5924
|
+
def edge_update(
|
|
5925
|
+
output_script: Annotated[Path, typer.Option(help="Where to write the edge update shell script.")] = Path("update_edge_runtime.sh"),
|
|
5926
|
+
profile: Annotated[str, typer.Option(help="Install profile to upgrade: edge or full.")] = "edge",
|
|
5927
|
+
version_pin: Annotated[Optional[str], typer.Option(help="Optional exact SDK version pin, for example 1.5.2.")] = None,
|
|
5928
|
+
) -> None:
|
|
5929
|
+
console = Console()
|
|
5930
|
+
normalized = profile.strip().lower()
|
|
5931
|
+
if normalized not in {"edge", "full"}:
|
|
5932
|
+
console.print("[bold red]Profile must be `edge` or `full`.[/bold red]")
|
|
5933
|
+
raise typer.Exit(code=1)
|
|
5934
|
+
script_path = write_edge_update_script(output_script, profile=normalized, version_pin=version_pin)
|
|
5935
|
+
console.print(f"[green]Edge update script written:[/green] {script_path}")
|
|
5936
|
+
|
|
5937
|
+
|
|
5938
|
+
@edge_app.command("hardware-bridge")
|
|
5939
|
+
def edge_hardware_bridge(
|
|
5940
|
+
board: Annotated[str, typer.Option(help="Hardware bridge target. Currently supported: uno_q.")] = "uno_q",
|
|
5941
|
+
output_dir: Annotated[Path, typer.Option(help="Directory where the hardware bridge scaffold should be written.")] = Path("uno_q_bridge"),
|
|
5942
|
+
) -> None:
|
|
5943
|
+
console = Console()
|
|
5944
|
+
normalized = board.strip().lower()
|
|
5945
|
+
if normalized != "uno_q":
|
|
5946
|
+
console.print("[bold red]Only the `uno_q` hardware bridge is currently implemented.[/bold red]")
|
|
5947
|
+
raise typer.Exit(code=1)
|
|
5948
|
+
outputs = export_uno_q_bridge(output_dir)
|
|
5949
|
+
table = Table(title="IINTS Edge Hardware Bridge")
|
|
5950
|
+
table.add_column("Artifact", style="cyan")
|
|
5951
|
+
table.add_column("Path", overflow="fold")
|
|
5952
|
+
table.add_row("sketch", outputs["sketch"])
|
|
5953
|
+
table.add_row("readme", outputs["readme"])
|
|
5954
|
+
table.add_row("protocol", outputs["protocol"])
|
|
5955
|
+
console.print(table)
|
|
5956
|
+
|
|
5957
|
+
|
|
5958
|
+
@edge_app.command("benchmark")
|
|
5959
|
+
def edge_benchmark_alias(
|
|
5960
|
+
algo: Annotated[Path, typer.Option(help="Path to the insulin algorithm Python file used for the edge benchmark.")],
|
|
5961
|
+
output_json: Annotated[Path, typer.Option(help="Output JSON path for the hardware benchmark results.")] = Path("results/edge_benchmark.json"),
|
|
5962
|
+
patient_config: Annotated[str, typer.Option(help="Patient configuration name or YAML path.")] = "default_patient",
|
|
5963
|
+
patient_model: Annotated[str, typer.Option("--patient-model", help="Patient model type.")] = "auto",
|
|
5964
|
+
scenario_profile: Annotated[str, typer.Option(help="Digital patient scenario profile.")] = "normal_day",
|
|
5965
|
+
steps: Annotated[int, typer.Option(help="Number of simulated steps used for throughput measurement.")] = 72,
|
|
5966
|
+
platform_name: Annotated[str, typer.Option("--platform", help="Platform label written into the benchmark report. Use 'auto' to detect locally.")] = "auto",
|
|
5967
|
+
api_host: Annotated[str, typer.Option(help="Host used for the local dashboard probe.")] = "127.0.0.1",
|
|
5968
|
+
api_port: Annotated[int, typer.Option(help="Port used for the local dashboard probe.")] = 8766,
|
|
5969
|
+
seed: Annotated[Optional[int], typer.Option(help="Optional deterministic seed override.")] = None,
|
|
5970
|
+
) -> None:
|
|
5971
|
+
edge_benchmark(
|
|
5972
|
+
algo=algo,
|
|
5973
|
+
output_json=output_json,
|
|
5974
|
+
patient_config=patient_config,
|
|
5975
|
+
patient_model=patient_model,
|
|
5976
|
+
scenario_profile=scenario_profile,
|
|
5977
|
+
steps=steps,
|
|
5978
|
+
platform_name=platform_name,
|
|
5979
|
+
api_host=api_host,
|
|
5980
|
+
api_port=api_port,
|
|
5981
|
+
seed=seed,
|
|
5982
|
+
)
|