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 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.1"
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",
@@ -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
+ )