iints-sdk-python35 1.4.0__py3-none-any.whl → 1.5.1__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,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.4.0"
14
+ __version__ = "1.5.1"
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.
@@ -70,11 +70,13 @@ from .data.nightscout import NightscoutConfig, import_nightscout
70
70
  from .data.tidepool import TidepoolClient, load_openapi_spec
71
71
  from .data.guardians import mdmp_gate, MDMPGateError
72
72
  from .data.synthetic_mirror import generate_synthetic_mirror, SyntheticMirrorArtifact
73
+ from .data.study_corruption import AVAILABLE_STUDY_CORRUPTIONS, apply_study_corruptions, write_corrupted_study_csv
73
74
  from .analysis.metrics import generate_benchmark_metrics # Added for benchmark
74
75
  from .analysis.booth_demo import build_booth_demo
75
76
  from .analysis.carelink_workbench import build_carelink_workbench
76
77
  from .analysis.poster import generate_results_poster
77
78
  from .analysis.reporting import ClinicalReportGenerator
79
+ from .analysis.study_protocol import build_study_protocol_payload, render_study_protocol_markdown, write_study_protocol_bundle
78
80
  from .analysis.edge_efficiency import EnergyEstimate, estimate_energy_per_decision
79
81
  from .ai import AIResponse, IINTSAssistant, MDMPGuard
80
82
  from .highlevel import run_simulation, run_full, run_population
@@ -184,10 +186,16 @@ __all__ = [
184
186
  "MDMPGateError",
185
187
  "generate_synthetic_mirror",
186
188
  "SyntheticMirrorArtifact",
189
+ "AVAILABLE_STUDY_CORRUPTIONS",
190
+ "apply_study_corruptions",
191
+ "write_corrupted_study_csv",
187
192
  # Analysis Metrics
188
193
  "generate_benchmark_metrics",
189
194
  "build_booth_demo",
190
195
  "build_carelink_workbench",
196
+ "build_study_protocol_payload",
197
+ "render_study_protocol_markdown",
198
+ "write_study_protocol_bundle",
191
199
  "ClinicalReportGenerator",
192
200
  "EnergyEstimate",
193
201
  "estimate_energy_per_decision",
iints/ai/assistant.py CHANGED
@@ -122,3 +122,6 @@ class IINTSAssistant:
122
122
 
123
123
  def generate_report(self, run: dict[str, Any]) -> AIResponse:
124
124
  return self._run_task("generate_report", run)
125
+
126
+ def review_realism(self, run: dict[str, Any]) -> AIResponse:
127
+ return self._run_task("review_realism", run)
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
@@ -69,6 +70,15 @@ def _resolve_cli_inputs(
69
70
 
70
71
  if input_path.is_dir():
71
72
  ai_dir = input_path / "ai"
73
+ if (
74
+ not ai_dir.exists()
75
+ or not any((ai_dir / candidate).is_file() for candidate in ("report_payload.json", "trends_payload.json", "anomalies_payload.json", "step_riskiest.json", "review_payload.json"))
76
+ or (resolved_cert is None and not (ai_dir / "report.signed.mdmp").is_file())
77
+ ):
78
+ prepare_ai_ready_artifacts(
79
+ input_path,
80
+ create_dev_mdmp_cert=resolved_cert is None,
81
+ )
72
82
  resolved_input = _default_prepared_payload(task, ai_dir)
73
83
  if resolved_cert is None:
74
84
  candidate_cert = ai_dir / "report.signed.mdmp"
@@ -95,6 +105,14 @@ def _write_output(path: Path | None, response: AIResponse) -> None:
95
105
  path.write_text(response.text + "\n", encoding="utf-8")
96
106
 
97
107
 
108
+ def _default_output_for_review(input_path: Path, output: Path | None) -> Path | None:
109
+ if output is not None:
110
+ return output
111
+ if input_path.is_dir():
112
+ return input_path / "ai" / "realism_review.md"
113
+ return None
114
+
115
+
98
116
  def _render_response(console: Console, title: str, response: AIResponse, output: Path | None) -> None:
99
117
  console.print(Panel(response.text, title=title, border_style="cyan"))
100
118
  console.print(
@@ -438,3 +456,45 @@ def report(
438
456
  except Exception as exc:
439
457
  console.print(f"[bold red]Error:[/bold red] {exc}")
440
458
  raise typer.Exit(code=1)
459
+
460
+
461
+ @app.command("review")
462
+ def review(
463
+ input_json: Annotated[Path, typer.Argument(help="Prepared run directory or JSON file with run-level simulation outputs.")],
464
+ mdmp_cert: Annotated[Optional[Path], typer.Option(help="Signed MDMP artifact required before AI analysis can run.")] = None,
465
+ mode: Annotated[str, typer.Option(help="AI backend mode. Use 'local' for Ollama/Ministral.")] = "auto",
466
+ model: Annotated[str, typer.Option(help="Ollama model name to use.")] = DEFAULT_MINISTRAL_MODEL,
467
+ minimum_grade: Annotated[str, typer.Option(help="Minimum MDMP grade required to allow analysis.")] = "research_grade",
468
+ public_key: Annotated[Optional[Path], typer.Option(help="Explicit MDMP public key for verification.")] = None,
469
+ trust_store: Annotated[Optional[Path], typer.Option(help="MDMP trust store for verification.")] = None,
470
+ ollama_host: Annotated[Optional[str], typer.Option(help="Override the Ollama base URL.")] = None,
471
+ timeout_seconds: Annotated[float, typer.Option(help="HTTP timeout for Ollama generation requests.")] = 120.0,
472
+ output: Annotated[Optional[Path], typer.Option(help="Optional file path to save the realism review.")] = None,
473
+ ) -> None:
474
+ console = Console()
475
+ try:
476
+ resolved_input, resolved_cert, resolved_public_key = _resolve_cli_inputs(
477
+ task="review",
478
+ input_path=input_json,
479
+ mdmp_cert=mdmp_cert,
480
+ public_key=public_key,
481
+ trust_store=trust_store,
482
+ )
483
+ payload = _load_json_payload(resolved_input, "Input JSON")
484
+ assistant = _build_assistant(
485
+ mdmp_cert=resolved_cert,
486
+ mode=mode,
487
+ model=model,
488
+ minimum_grade=minimum_grade,
489
+ public_key=resolved_public_key,
490
+ trust_store=trust_store,
491
+ ollama_host=ollama_host,
492
+ timeout_seconds=timeout_seconds,
493
+ )
494
+ resolved_output = _default_output_for_review(input_json, output)
495
+ response = assistant.review_realism(payload)
496
+ _write_output(resolved_output, response)
497
+ _render_response(console, "IINTS AI Realism Review", response, resolved_output)
498
+ except Exception as exc:
499
+ console.print(f"[bold red]Error:[/bold red] {exc}")
500
+ raise typer.Exit(code=1)
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,
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["explain_decision", "analyze_trends", "detect_anomalies", "generate_report"]
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,19 @@ 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. What looks realistic\n"
67
+ "3. What looks suspicious\n"
68
+ "4. Priority fixes\n"
69
+ "5. What to improve next\n"
70
+ "6. Suggested follow-up validation checks\n\n"
71
+ "Focus on glycemic ranges, excursion patterns, insulin behavior, safety overrides, and whether the data looks internally coherent.\n\n"
72
+ "Input JSON:\n{data}"
73
+ ),
55
74
  }
56
75
 
57
76
 
@@ -4,15 +4,43 @@ from .booth_demo import build_booth_demo
4
4
  from .carelink_workbench import build_carelink_workbench
5
5
  from .poster import generate_results_poster
6
6
  from .reporting import ClinicalReportGenerator
7
+ from .study_poster import generate_study_poster
8
+ from .study_protocol import (
9
+ build_study_protocol_payload,
10
+ render_study_protocol_markdown,
11
+ write_study_protocol_bundle,
12
+ )
13
+ from .study_analysis import (
14
+ analyze_run_directory,
15
+ analyze_study_directory,
16
+ compare_studies,
17
+ load_study_summary,
18
+ quality_badges_for_metrics,
19
+ StudyComparison,
20
+ StudyRunSummary,
21
+ StudySummary,
22
+ )
7
23
 
8
24
  __all__ = [
25
+ "analyze_run_directory",
26
+ "analyze_study_directory",
9
27
  "build_booth_demo",
10
28
  "build_carelink_workbench",
11
29
  "ClinicalMetricsCalculator",
12
30
  "ClinicalMetricsResult",
13
31
  "ClinicalReportGenerator",
14
32
  "compute_metrics",
33
+ "compare_studies",
15
34
  "generate_results_poster",
35
+ "generate_study_poster",
36
+ "build_study_protocol_payload",
37
+ "render_study_protocol_markdown",
38
+ "write_study_protocol_bundle",
39
+ "load_study_summary",
40
+ "quality_badges_for_metrics",
16
41
  "run_baseline_comparison",
42
+ "StudyComparison",
43
+ "StudyRunSummary",
44
+ "StudySummary",
17
45
  "write_baseline_comparison",
18
46
  ]
@@ -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": [