iints-sdk-python35 1.3.2__py3-none-any.whl → 1.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. iints/__init__.py +1 -1
  2. iints/ai/assistant.py +3 -0
  3. iints/ai/backends/ollama.py +32 -2
  4. iints/ai/cli.py +51 -0
  5. iints/ai/mdmp_guard.py +2 -3
  6. iints/ai/prepare.py +28 -1
  7. iints/ai/prompts.py +19 -1
  8. iints/analysis/booth_demo.py +3 -0
  9. iints/analysis/carelink_workbench.py +17 -0
  10. iints/analysis/clinical_metrics.py +13 -7
  11. iints/analysis/validator.py +10 -4
  12. iints/core/patient/bergman_model.py +3 -2
  13. iints/core/patient/models.py +1 -1
  14. iints/core/patient/patient_factory.py +8 -8
  15. iints/core/patient/profile.py +1 -1
  16. iints/core/safety/config.py +13 -4
  17. iints/core/safety/input_validator.py +12 -7
  18. iints/data/adapter.py +7 -2
  19. iints/data/ingestor.py +7 -2
  20. iints/data/quality_checker.py +12 -4
  21. iints/data/registry.py +23 -1
  22. iints/highlevel.py +1 -1
  23. iints/learning/autonomous_optimizer.py +13 -1
  24. iints/mdmp/backend.py +2 -3
  25. iints/research/predictor.py +9 -1
  26. iints/templates/demos/live_stage_demo.py +2 -0
  27. {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/METADATA +26 -18
  28. {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/RECORD +73 -32
  29. {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/entry_points.txt +1 -0
  30. iints_sdk_python35-1.5.0.dist-info/licenses/LICENSE +173 -0
  31. iints_sdk_python35-1.3.2.dist-info/licenses/LICENSE → iints_sdk_python35-1.5.0.dist-info/licenses/LICENSE-MIT-IINTS-LEGACY +0 -7
  32. iints_sdk_python35-1.5.0.dist-info/licenses/NOTICE +17 -0
  33. iints_sdk_python35-1.5.0.dist-info/top_level.txt +5 -0
  34. mdmp_ai/__init__.py +3 -0
  35. mdmp_ai/lineage.py +198 -0
  36. mdmp_core/__init__.py +133 -0
  37. mdmp_core/audit.py +166 -0
  38. mdmp_core/bias_hooks.py +36 -0
  39. mdmp_core/bundle.py +142 -0
  40. mdmp_core/certification.py +38 -0
  41. mdmp_core/cli.py +1352 -0
  42. mdmp_core/compare.py +56 -0
  43. mdmp_core/conformance.py +429 -0
  44. mdmp_core/contracts.py +164 -0
  45. mdmp_core/crypto.py +372 -0
  46. mdmp_core/data/conformance/vectors/delegation.json +56 -0
  47. mdmp_core/data/conformance/vectors/fingerprint.json +24 -0
  48. mdmp_core/data/conformance/vectors/grading.json +83 -0
  49. mdmp_core/data/conformance/vectors/signing.json +37 -0
  50. mdmp_core/delegate.py +476 -0
  51. mdmp_core/diffing.py +128 -0
  52. mdmp_core/drift.py +203 -0
  53. mdmp_core/exceptions.py +33 -0
  54. mdmp_core/fingerprint.py +92 -0
  55. mdmp_core/fingerprint_store.py +42 -0
  56. mdmp_core/hf.py +60 -0
  57. mdmp_core/keys/mdmp_pub_v1.pem +3 -0
  58. mdmp_core/llm_provenance.py +34 -0
  59. mdmp_core/migrate.py +182 -0
  60. mdmp_core/policy.py +185 -0
  61. mdmp_core/prov.py +31 -0
  62. mdmp_core/registry.py +350 -0
  63. mdmp_core/runner.py +292 -0
  64. mdmp_core/schema_export.py +92 -0
  65. mdmp_core/synthetic.py +30 -0
  66. mdmp_core/trust.py +216 -0
  67. mdmp_core/visualizer.py +61 -0
  68. mdmp_flavors/__init__.py +146 -0
  69. mdmp_integrations/__init__.py +9 -0
  70. mdmp_integrations/dvc.py +30 -0
  71. mdmp_integrations/mlflow.py +14 -0
  72. mdmp_integrations/wandb.py +19 -0
  73. iints_sdk_python35-1.3.2.dist-info/top_level.txt +0 -1
  74. {iints_sdk_python35-1.3.2.dist-info → iints_sdk_python35-1.5.0.dist-info}/WHEEL +0 -0
iints/__init__.py CHANGED
@@ -11,7 +11,7 @@ except ImportError: # pragma: no cover - Python < 3.8 fallback
11
11
  try:
12
12
  __version__ = version("iints-sdk-python35")
13
13
  except PackageNotFoundError: # pragma: no cover - source tree fallback
14
- __version__ = "1.3.2"
14
+ __version__ = "1.5.0"
15
15
 
16
16
  # Note to developers: this SDK is currently maintained by a single author.
17
17
  # Please report bugs via GitHub issues and feel free to contribute fixes via PRs.
iints/ai/assistant.py CHANGED
@@ -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)
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ from ipaddress import ip_address
5
6
  from http.client import IncompleteRead, RemoteDisconnected
6
7
  from time import sleep
7
8
  from urllib import error, request
9
+ from urllib.parse import urlparse
8
10
 
9
11
 
10
12
  DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"
@@ -35,10 +37,38 @@ class OllamaBackend:
35
37
  timeout_seconds: float = 120.0,
36
38
  ) -> None:
37
39
  self.model_name = model_name
38
- self.base_url = (base_url or os.getenv("OLLAMA_HOST") or DEFAULT_OLLAMA_HOST).rstrip("/")
40
+ raw_base_url = base_url or os.getenv("OLLAMA_HOST") or DEFAULT_OLLAMA_HOST
41
+ self.base_url = self._validate_base_url(raw_base_url)
39
42
  self.timeout_seconds = timeout_seconds
40
43
  self.resolved_model_name: str | None = None
41
44
 
45
+ @staticmethod
46
+ def _is_loopback_host(hostname: str) -> bool:
47
+ if hostname == "localhost":
48
+ return True
49
+ try:
50
+ return ip_address(hostname).is_loopback
51
+ except ValueError:
52
+ return False
53
+
54
+ @classmethod
55
+ def _validate_base_url(cls, raw_base_url: str) -> str:
56
+ parsed = urlparse(raw_base_url)
57
+ if parsed.scheme not in {"http", "https"}:
58
+ raise ValueError(
59
+ "OLLAMA_HOST/base_url must use http or https. Other URL schemes are blocked."
60
+ )
61
+ if not parsed.hostname:
62
+ raise ValueError("OLLAMA_HOST/base_url must include a hostname.")
63
+ if parsed.path not in {"", "/"}:
64
+ raise ValueError("OLLAMA_HOST/base_url must not include a path component.")
65
+ if not cls._is_loopback_host(parsed.hostname) and os.getenv("IINTS_ALLOW_REMOTE_OLLAMA") != "1":
66
+ raise ValueError(
67
+ "Remote Ollama endpoints are disabled by default. "
68
+ "Use localhost/127.0.0.1 or set IINTS_ALLOW_REMOTE_OLLAMA=1 explicitly."
69
+ )
70
+ return raw_base_url.rstrip("/")
71
+
42
72
  def _pull_hint(self) -> str:
43
73
  return f"ollama pull {self.model_name}"
44
74
 
@@ -102,7 +132,7 @@ class OllamaBackend:
102
132
  headers["Content-Type"] = "application/json"
103
133
  req = request.Request(url, data=body, headers=headers, method=method)
104
134
  try:
105
- with request.urlopen(req, timeout=self.timeout_seconds) as response:
135
+ with request.urlopen(req, timeout=self.timeout_seconds) as response: # nosec B310 - base_url is scheme/host validated
106
136
  text = response.read().decode("utf-8")
107
137
  except error.HTTPError as exc:
108
138
  detail = exc.read().decode("utf-8", errors="replace").strip()
iints/ai/cli.py CHANGED
@@ -43,6 +43,7 @@ def _default_prepared_payload(task: str, ai_dir: Path) -> Path:
43
43
  "trends": ["trends_payload.json"],
44
44
  "anomalies": ["anomalies_payload.json"],
45
45
  "report": ["report_payload.json"],
46
+ "review": ["review_payload.json", "report_payload.json"],
46
47
  }.get(task, [])
47
48
  for filename in candidates:
48
49
  candidate = ai_dir / filename
@@ -95,6 +96,14 @@ def _write_output(path: Path | None, response: AIResponse) -> None:
95
96
  path.write_text(response.text + "\n", encoding="utf-8")
96
97
 
97
98
 
99
+ def _default_output_for_review(input_path: Path, output: Path | None) -> Path | None:
100
+ if output is not None:
101
+ return output
102
+ if input_path.is_dir():
103
+ return input_path / "ai" / "realism_review.md"
104
+ return None
105
+
106
+
98
107
  def _render_response(console: Console, title: str, response: AIResponse, output: Path | None) -> None:
99
108
  console.print(Panel(response.text, title=title, border_style="cyan"))
100
109
  console.print(
@@ -438,3 +447,45 @@ def report(
438
447
  except Exception as exc:
439
448
  console.print(f"[bold red]Error:[/bold red] {exc}")
440
449
  raise typer.Exit(code=1)
450
+
451
+
452
+ @app.command("review")
453
+ def review(
454
+ input_json: Annotated[Path, typer.Argument(help="Prepared run directory or JSON file with run-level simulation outputs.")],
455
+ mdmp_cert: Annotated[Optional[Path], typer.Option(help="Signed MDMP artifact required before AI analysis can run.")] = None,
456
+ mode: Annotated[str, typer.Option(help="AI backend mode. Use 'local' for Ollama/Ministral.")] = "auto",
457
+ model: Annotated[str, typer.Option(help="Ollama model name to use.")] = DEFAULT_MINISTRAL_MODEL,
458
+ minimum_grade: Annotated[str, typer.Option(help="Minimum MDMP grade required to allow analysis.")] = "research_grade",
459
+ public_key: Annotated[Optional[Path], typer.Option(help="Explicit MDMP public key for verification.")] = None,
460
+ trust_store: Annotated[Optional[Path], typer.Option(help="MDMP trust store for verification.")] = None,
461
+ ollama_host: Annotated[Optional[str], typer.Option(help="Override the Ollama base URL.")] = None,
462
+ timeout_seconds: Annotated[float, typer.Option(help="HTTP timeout for Ollama generation requests.")] = 120.0,
463
+ output: Annotated[Optional[Path], typer.Option(help="Optional file path to save the realism review.")] = None,
464
+ ) -> None:
465
+ console = Console()
466
+ try:
467
+ resolved_input, resolved_cert, resolved_public_key = _resolve_cli_inputs(
468
+ task="review",
469
+ input_path=input_json,
470
+ mdmp_cert=mdmp_cert,
471
+ public_key=public_key,
472
+ trust_store=trust_store,
473
+ )
474
+ payload = _load_json_payload(resolved_input, "Input JSON")
475
+ assistant = _build_assistant(
476
+ mdmp_cert=resolved_cert,
477
+ mode=mode,
478
+ model=model,
479
+ minimum_grade=minimum_grade,
480
+ public_key=resolved_public_key,
481
+ trust_store=trust_store,
482
+ ollama_host=ollama_host,
483
+ timeout_seconds=timeout_seconds,
484
+ )
485
+ resolved_output = _default_output_for_review(input_json, output)
486
+ response = assistant.review_realism(payload)
487
+ _write_output(resolved_output, response)
488
+ _render_response(console, "IINTS AI Realism Review", response, resolved_output)
489
+ except Exception as exc:
490
+ console.print(f"[bold red]Error:[/bold red] {exc}")
491
+ raise typer.Exit(code=1)
iints/ai/mdmp_guard.py CHANGED
@@ -14,9 +14,8 @@ def _load_mdmp_verifier() -> type[Any]:
14
14
  module = importlib.import_module("mdmp_core")
15
15
  except Exception as exc:
16
16
  raise ImportError(
17
- "MDMP verification requires the optional standalone package.\n"
18
- "Install with: pip install iints-sdk-python35[mdmp]\n"
19
- "or: pip install 'mdmp-protocol>=0.3.0'"
17
+ "MDMP verification requires the bundled MDMP crypto support.\n"
18
+ "Install with: pip install iints-sdk-python35[mdmp]"
20
19
  ) from exc
21
20
  verifier_cls = getattr(module, "MDMPVerifier", None)
22
21
  if verifier_cls is None:
iints/ai/prepare.py CHANGED
@@ -97,6 +97,13 @@ def _sample_trace(df: pd.DataFrame, *, max_rows: int = 48) -> list[dict[str, Any
97
97
  return [_normalize_series_record(record) for record in sampled.to_dict(orient="records")]
98
98
 
99
99
 
100
+ def _max_glucose_delta_per_step(df: pd.DataFrame, glucose_column: str) -> float:
101
+ clean = pd.to_numeric(df[glucose_column], errors="coerce").dropna()
102
+ if clean.empty or len(clean) < 2:
103
+ return 0.0
104
+ return float(clean.diff().abs().dropna().max())
105
+
106
+
100
107
  def _position_for_label(df: pd.DataFrame, label: Any) -> int:
101
108
  location = df.index.get_loc(label)
102
109
  if isinstance(location, int):
@@ -166,9 +173,12 @@ def _build_summary(df: pd.DataFrame, run_metadata: dict[str, Any], audit_summary
166
173
  "mean_glucose_mgdl": _normalize_value(float(glucose.mean())),
167
174
  "min_glucose_mgdl": _normalize_value(float(glucose.min())),
168
175
  "max_glucose_mgdl": _normalize_value(float(glucose.max())),
176
+ "max_glucose_delta_per_step_mgdl": _normalize_value(_max_glucose_delta_per_step(df, glucose_column)),
169
177
  "time_in_range_70_180_pct": _normalize_value(_time_in_band_pct(glucose, 70.0, 180.0)),
170
178
  "time_below_70_pct": _normalize_value(float((glucose < 70.0).mean() * 100.0)),
179
+ "time_below_54_pct": _normalize_value(float((glucose < 54.0).mean() * 100.0)),
171
180
  "time_above_180_pct": _normalize_value(float((glucose > 180.0).mean() * 100.0)),
181
+ "time_above_250_pct": _normalize_value(float((glucose > 250.0).mean() * 100.0)),
172
182
  "delivered_insulin_total_units": _normalize_value(_safe_sum(df, "delivered_insulin_units")),
173
183
  "recommended_insulin_total_units": _normalize_value(_safe_sum(df, "algo_recommended_insulin_units")),
174
184
  "safety_trigger_count": _bool_sum(df, "safety_triggered"),
@@ -228,6 +238,23 @@ def _build_payloads(
228
238
  "trace_sample": trace_sample,
229
239
  "baseline_comparison": baseline_comparison,
230
240
  },
241
+ "review_payload.json": {
242
+ **common,
243
+ "audit_summary": audit_summary,
244
+ "baseline_comparison": baseline_comparison,
245
+ "trace_sample": trace_sample,
246
+ "review_focus": {
247
+ "goal": "Judge whether the simulation or imported dataset looks physiologically plausible and internally coherent.",
248
+ "checks": [
249
+ "glucose range plausibility",
250
+ "excursion size and recovery behavior",
251
+ "time in range and severe hypo/hyper exposure",
252
+ "insulin delivery realism relative to observed glucose behavior",
253
+ "safety trigger and override consistency",
254
+ ],
255
+ },
256
+ "run_manifest": run_manifest,
257
+ },
231
258
  "step_riskiest.json": {
232
259
  **common,
233
260
  **risk_payload,
@@ -245,7 +272,7 @@ def _load_mdmp_signer_tools() -> tuple[type[Any], Any]:
245
272
  module = importlib.import_module("mdmp_core")
246
273
  except Exception as exc:
247
274
  raise ImportError(
248
- "Local AI certification requires the optional standalone MDMP package.\n"
275
+ "Local AI certification requires the bundled MDMP crypto support.\n"
249
276
  "Install with: pip install 'iints-sdk-python35[mdmp]'"
250
277
  ) from exc
251
278
 
iints/ai/prompts.py CHANGED
@@ -4,7 +4,13 @@ import json
4
4
  from typing import Any, Literal
5
5
 
6
6
 
7
- TaskName = Literal["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,18 @@ TASK_TEMPLATES: dict[TaskName, str] = {
52
58
  "5. Research-only conclusion\n\n"
53
59
  "Input JSON:\n{data}"
54
60
  ),
61
+ "review_realism": (
62
+ "Review this simulation or imported-data payload and judge whether the results look physiologically plausible for research use.\n"
63
+ "Be conservative and do not overclaim. If the payload is incomplete, say so clearly.\n"
64
+ "Respond in markdown with these sections:\n"
65
+ "1. Overall realism verdict (Likely realistic / Needs review / Likely unrealistic)\n"
66
+ "2. Strong realism signals\n"
67
+ "3. Questionable or unrealistic patterns\n"
68
+ "4. Concrete feedback points the SDK developer can improve\n"
69
+ "5. Suggested follow-up validation checks\n\n"
70
+ "Focus on glycemic ranges, excursion patterns, insulin behavior, safety overrides, and whether the data looks internally coherent.\n\n"
71
+ "Input JSON:\n{data}"
72
+ ),
55
73
  }
56
74
 
57
75
 
@@ -196,6 +196,7 @@ def _build_jury_brief(
196
196
  "```bash",
197
197
  "iints ai local-check --model ministral-3:3b",
198
198
  f"iints ai report {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
199
+ f"iints ai review {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
199
200
  f"iints ai explain {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b",
200
201
  "```",
201
202
  "",
@@ -247,6 +248,7 @@ def _build_commands_markdown(
247
248
  "```bash\n"
248
249
  "iints ai local-check --model ministral-3:3b\n"
249
250
  f"iints ai report {supervisor_dir} --model ministral-3:3b\n"
251
+ f"iints ai review {supervisor_dir} --model ministral-3:3b\n"
250
252
  f"iints ai explain {supervisor_dir} --model ministral-3:3b\n"
251
253
  "```\n"
252
254
  )
@@ -296,6 +298,7 @@ def _build_live_demo_script_text(
296
298
  "- If Ollama is ready, run:\n"
297
299
  " iints ai local-check --model ministral-3:3b\n"
298
300
  f" iints ai report {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
301
+ f" iints ai review {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
299
302
  f" iints ai explain {run_outputs['03_supervisor_override']['output_dir']} --model ministral-3:3b\n"
300
303
  "- Say: the local model explains the result, but only after the SDK has prepared the run artifacts.\n\n"
301
304
  "7. IF THE JURY ASKS WHY THIS MATTERS\n"
@@ -202,6 +202,23 @@ def _prepare_ai_payloads(
202
202
  "profile_24h": profile_records,
203
203
  "trace_sample": trace_sample,
204
204
  },
205
+ "review_payload.json": {
206
+ **common,
207
+ "daily_summary": daily_records,
208
+ "profile_24h": profile_records,
209
+ "trace_sample": trace_sample,
210
+ "top_alerts": alert_counts,
211
+ "top_sensor_exceptions": sensor_exception_counts,
212
+ "review_focus": {
213
+ "goal": "Judge whether the imported glucose history looks internally coherent and physiologically plausible.",
214
+ "checks": [
215
+ "time in range versus extreme exposure",
216
+ "daily variability and day-to-day stability",
217
+ "consistency between glucose, carbs, and insulin logs",
218
+ "frequency of alerts and sensor exceptions",
219
+ ],
220
+ },
221
+ },
205
222
  "anomalies_payload.json": {
206
223
  **common,
207
224
  "lowest_readings": [
@@ -144,6 +144,12 @@ class ClinicalMetricsCalculator:
144
144
  """
145
145
  self.target_range = target_range
146
146
  self.tight_range = tight_range
147
+
148
+ @staticmethod
149
+ def _percentage(mask: pd.Series) -> float:
150
+ if len(mask) == 0:
151
+ return 0.0
152
+ return float(mask.sum()) / float(len(mask)) * 100.0
147
153
 
148
154
  def calculate_tir(self,
149
155
  glucose: pd.Series,
@@ -169,13 +175,13 @@ class ClinicalMetricsCalculator:
169
175
  def calculate_all_tir_metrics(self, glucose: pd.Series) -> Dict[str, float]:
170
176
  """Calculate all TIR-related metrics"""
171
177
  return {
172
- 'tir_70_180': self.calculate_tir(glucose, 70, 180),
173
- 'tir_70_140': self.calculate_tir(glucose, 70, 140),
174
- 'tir_70_110': self.calculate_tir(glucose, 70, 110),
175
- 'tir_below_70': self.calculate_tir(glucose, 0, 70),
176
- 'tir_below_54': self.calculate_tir(glucose, 0, 54),
177
- 'tir_above_180': self.calculate_tir(glucose, 180, 600),
178
- 'tir_above_250': self.calculate_tir(glucose, 250, 600)
178
+ 'tir_70_180': self._percentage((glucose >= 70) & (glucose <= 180)),
179
+ 'tir_70_140': self._percentage((glucose >= 70) & (glucose <= 140)),
180
+ 'tir_70_110': self._percentage((glucose >= 70) & (glucose <= 110)),
181
+ 'tir_below_70': self._percentage(glucose < 70),
182
+ 'tir_below_54': self._percentage(glucose < 54),
183
+ 'tir_above_180': self._percentage(glucose > 180),
184
+ 'tir_above_250': self._percentage(glucose > 250)
179
185
  }
180
186
 
181
187
  def calculate_gmi(self, glucose: pd.Series) -> float:
@@ -4,6 +4,12 @@ from typing import Dict, List, Tuple, Optional, Any
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
6
 
7
+ from iints.core.safety.config import (
8
+ SENSOR_GLUCOSE_MAX_MGDL,
9
+ SENSOR_GLUCOSE_MIN_MGDL,
10
+ SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL,
11
+ )
12
+
7
13
  class ReliabilityLevel(Enum):
8
14
  HIGH = "high"
9
15
  MEDIUM = "medium"
@@ -22,9 +28,9 @@ class DataIntegrityValidator:
22
28
  """Validates data integrity for reverse engineering analysis."""
23
29
 
24
30
  def __init__(self):
25
- # Physiological limits
26
- self.max_glucose_rate = 10 # mg/dL per minute
27
- self.glucose_range = (20, 600) # Physiologically possible range
31
+ # Broad CGM/sensor fail-soft limits
32
+ self.max_glucose_rate = SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL
33
+ self.glucose_range = (SENSOR_GLUCOSE_MIN_MGDL, SENSOR_GLUCOSE_MAX_MGDL)
28
34
  self.max_insulin_per_step = 5.0 # Units per 5-min step
29
35
 
30
36
  def validate_glucose_data(self, glucose_values: List[float], timestamps: List[float]) -> ValidationResult:
@@ -270,4 +276,4 @@ class ReverseEngineeringValidator:
270
276
  "issues": all_issues,
271
277
  "warnings": all_warnings,
272
278
  "category_scores": {category: result.reliability_score for category, result in validation_results.items()}
273
- }
279
+ }
@@ -46,7 +46,7 @@ class BergmanParameters:
46
46
  n: float = 0.23 # 1/min — fractional insulin degradation
47
47
  Ib: float = 7.0 # mU/L — basal plasma insulin
48
48
  Vi: float = 0.05 # L/kg — insulin distribution volume
49
- gamma: float = 0.004 # (mU/L)/(mg/dL)/min — endogenous secretion gain
49
+ gamma: float = 0.0 # (mU/L)/(mg/dL)/min — endogenous secretion gain (0 for T1D default)
50
50
  h: float = 80.0 # mg/dL — secretion glucose threshold
51
51
 
52
52
  # --- Gut absorption ---
@@ -330,7 +330,8 @@ class BergmanPatientModel:
330
330
  dXdt = -p.p2 * X + p.p3 * max(I - p.Ib, 0.0)
331
331
 
332
332
  # --- dI/dt ---
333
- # Endogenous pancreatic secretion (blunted in T1D, but kept for generality)
333
+ # T1D defaults keep endogenous secretion disabled (gamma=0). Override
334
+ # gamma explicitly if you intentionally want residual beta-cell function.
334
335
  secretion = p.gamma * max(G - p.h, 0.0)
335
336
  dIdt = -p.n * (I - p.Ib) + secretion + u_insulin_mu_per_min / Vi_abs
336
337
 
@@ -14,7 +14,7 @@ class CustomPatientModel:
14
14
  This model is intended for educational and stress-testing purposes, not for clinical accuracy.
15
15
  """
16
16
  def __init__(self, basal_insulin_rate: float = 0.8, insulin_sensitivity: float = 50.0,
17
- carb_factor: float = 10.0, glucose_decay_rate: float = 0.002,
17
+ carb_factor: float = 10.0, glucose_decay_rate: float = 0.05,
18
18
  initial_glucose: float = 120.0, glucose_absorption_rate: float = 0.03,
19
19
  basal_glucose_target: Optional[float] = None,
20
20
  insulin_action_duration: float = 300.0, # minutes, e.g., 5 hours
@@ -63,14 +63,14 @@ class PatientFactory:
63
63
  """Get a diverse set of patients for population studies."""
64
64
  if not SIMGLUCOSE_AVAILABLE:
65
65
  # Create diverse custom patients with different parameters
66
- return [
67
- CustomPatientModel(initial_glucose=120, insulin_sensitivity=40), # High sensitivity
68
- CustomPatientModel(initial_glucose=120, insulin_sensitivity=60), # Low sensitivity
69
- CustomPatientModel(initial_glucose=120, carb_factor=8), # Fast carb absorption
70
- CustomPatientModel(initial_glucose=120, carb_factor=12), # Slow carb absorption
71
- CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.0015), # Slow metabolism
72
- CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.0035), # Fast metabolism
73
- ]
66
+ return [
67
+ CustomPatientModel(initial_glucose=120, insulin_sensitivity=40), # High sensitivity
68
+ CustomPatientModel(initial_glucose=120, insulin_sensitivity=60), # Low sensitivity
69
+ CustomPatientModel(initial_glucose=120, carb_factor=8), # Fast carb absorption
70
+ CustomPatientModel(initial_glucose=120, carb_factor=12), # Slow carb absorption
71
+ CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.03), # Slower homeostatic drift
72
+ CustomPatientModel(initial_glucose=120, glucose_decay_rate=0.07), # Faster homeostatic drift
73
+ ]
74
74
  else:
75
75
  # Use FDA-approved virtual patients
76
76
  selected_patients = [
@@ -18,7 +18,7 @@ class PatientProfile:
18
18
  dawn_end_hour: float = 8.0
19
19
 
20
20
  # Advanced knobs (optional)
21
- glucose_decay_rate: float = 0.002
21
+ glucose_decay_rate: float = 0.05
22
22
  glucose_absorption_rate: float = 0.03
23
23
  insulin_action_duration: float = 300.0
24
24
  insulin_peak_time: float = 75.0
@@ -2,16 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
+ SENSOR_GLUCOSE_MIN_MGDL = 40.0
6
+ SENSOR_GLUCOSE_MAX_MGDL = 500.0
7
+ SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL = 20.0
8
+ SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL = 4.0
9
+ SIMULATION_GLUCOSE_FLOOR_MGDL = 20.0
10
+ SIMULATION_GLUCOSE_CEILING_MGDL = 600.0
11
+
5
12
 
6
13
  @dataclass
7
14
  class SafetyConfig:
8
15
  """
9
16
  Central safety configuration for simulator, input validation, and supervisor.
10
17
  """
11
- # Input validation limits
12
- min_glucose: float = 20.0
13
- max_glucose: float = 600.0
14
- max_glucose_delta_per_5_min: float = 35.0
18
+ # Broad CGM/sensor plausibility limits.
19
+ # These are intentionally device-aware fail-soft bounds, not a claim about
20
+ # the full physiologic envelope of blood glucose inside the simulator.
21
+ min_glucose: float = SENSOR_GLUCOSE_MIN_MGDL
22
+ max_glucose: float = SENSOR_GLUCOSE_MAX_MGDL
23
+ max_glucose_delta_per_5_min: float = SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL
15
24
 
16
25
  # Supervisor thresholds
17
26
  hypoglycemia_threshold: float = 70.0
@@ -1,6 +1,11 @@
1
1
  from typing import Any, Dict, Optional
2
2
 
3
- from iints.core.safety.config import SafetyConfig
3
+ from iints.core.safety.config import (
4
+ SENSOR_GLUCOSE_MAX_MGDL,
5
+ SENSOR_GLUCOSE_MIN_MGDL,
6
+ SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL,
7
+ SafetyConfig,
8
+ )
4
9
 
5
10
  class InputValidator:
6
11
  """
@@ -9,16 +14,16 @@ class InputValidator:
9
14
  This component makes the system robust against common sensor errors.
10
15
  """
11
16
  def __init__(self,
12
- min_glucose: float = 20.0,
13
- max_glucose: float = 600.0,
14
- max_glucose_delta_per_5_min: float = 35.0,
17
+ min_glucose: float = SENSOR_GLUCOSE_MIN_MGDL,
18
+ max_glucose: float = SENSOR_GLUCOSE_MAX_MGDL,
19
+ max_glucose_delta_per_5_min: float = SENSOR_MAX_GLUCOSE_DELTA_PER_5_MIN_MGDL,
15
20
  safety_config: Optional[SafetyConfig] = None):
16
21
  """
17
22
  Initializes the validator with plausible biological limits.
18
23
 
19
24
  Args:
20
- min_glucose (float): The absolute minimum plausible glucose value (mg/dL).
21
- max_glucose (float): The absolute maximum plausible glucose value (mg/dL).
25
+ min_glucose (float): Broad fail-soft lower bound for incoming CGM/sensor values (mg/dL).
26
+ max_glucose (float): Broad fail-soft upper bound for incoming CGM/sensor values (mg/dL).
22
27
  max_glucose_delta_per_5_min (float): The maximum plausible change in glucose
23
28
  over a 5-minute period (mg/dL).
24
29
  """
@@ -62,7 +67,7 @@ class InputValidator:
62
67
  Raises:
63
68
  ValueError: If the value is outside biological plausibility limits.
64
69
  """
65
- # 1. Absolute biological plausibility check
70
+ # 1. Broad CGM/sensor plausibility check
66
71
  if not (self.min_glucose <= glucose_value <= self.max_glucose):
67
72
  raise ValueError(
68
73
  f"BIOLOGICAL_PLAUSIBILITY_ERROR: Glucose {glucose_value} mg/dL is outside the "
iints/data/adapter.py CHANGED
@@ -10,6 +10,8 @@ import numpy as np
10
10
  from pathlib import Path
11
11
  from typing import Dict, List, Optional, Union, Any
12
12
 
13
+ from iints.core.safety.config import SENSOR_GLUCOSE_MAX_MGDL, SENSOR_GLUCOSE_MIN_MGDL
14
+
13
15
  class DataAdapter:
14
16
  """Universal data adapter for IINTS-AF framework"""
15
17
 
@@ -51,8 +53,11 @@ class DataAdapter:
51
53
  raise ValueError(f"Missing required columns: {missing_cols}")
52
54
 
53
55
  # Validate glucose range
54
- if df['glucose'].min() < 20 or df['glucose'].max() > 600:
55
- raise ValueError("Glucose values outside physiological range (20-600 mg/dL)")
56
+ if df['glucose'].min() < SENSOR_GLUCOSE_MIN_MGDL or df['glucose'].max() > SENSOR_GLUCOSE_MAX_MGDL:
57
+ raise ValueError(
58
+ "Glucose values outside broad CGM/sensor-valid range "
59
+ f"({int(SENSOR_GLUCOSE_MIN_MGDL)}-{int(SENSOR_GLUCOSE_MAX_MGDL)} mg/dL)"
60
+ )
56
61
 
57
62
  def load_ohio_dataset(self, patient_id: str) -> pd.DataFrame:
58
63
  """Load Ohio T1DM dataset with clinical benchmarks"""
iints/data/ingestor.py CHANGED
@@ -3,6 +3,8 @@ from pathlib import Path
3
3
  from typing import Dict, Any, Union, Optional
4
4
  import yaml
5
5
 
6
+ from iints.core.safety.config import SENSOR_GLUCOSE_MAX_MGDL, SENSOR_GLUCOSE_MIN_MGDL
7
+
6
8
  class DataIngestor:
7
9
  """
8
10
  Standardized Data Bridge for ingesting various diabetes datasets into a
@@ -64,8 +66,11 @@ class DataIngestor:
64
66
 
65
67
  # Basic quality checks (from DATA_SCHEMA.md)
66
68
  if 'glucose' in df.columns:
67
- if not ((df['glucose'] >= 20) & (df['glucose'] <= 600)).all():
68
- raise ValueError("Glucose values outside acceptable range (20-600 mg/dL)")
69
+ if not ((df['glucose'] >= SENSOR_GLUCOSE_MIN_MGDL) & (df['glucose'] <= SENSOR_GLUCOSE_MAX_MGDL)).all():
70
+ raise ValueError(
71
+ "Glucose values outside acceptable CGM/sensor range "
72
+ f"({int(SENSOR_GLUCOSE_MIN_MGDL)}-{int(SENSOR_GLUCOSE_MAX_MGDL)} mg/dL)"
73
+ )
69
74
  if 'insulin' in df.columns and not df['insulin'].isna().all():
70
75
  if not ((df['insulin'] >= 0) & (df['insulin'] <= 50)).all():
71
76
  raise ValueError("Insulin values outside acceptable range (0-50 units)")
@@ -10,6 +10,12 @@ from datetime import datetime, timedelta
10
10
  import pandas as pd
11
11
  import numpy as np
12
12
 
13
+ from iints.core.safety.config import (
14
+ SENSOR_GLUCOSE_MAX_MGDL,
15
+ SENSOR_GLUCOSE_MIN_MGDL,
16
+ SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL,
17
+ )
18
+
13
19
 
14
20
  @dataclass
15
21
  class QualityReport:
@@ -100,14 +106,16 @@ class DataQualityChecker:
100
106
 
101
107
  # Physiological limits for glucose values
102
108
  GLUCOSE_LIMITS = {
103
- 'minimum': 20, # mg/dL - physiologically possible minimum
104
- 'maximum': 600, # mg/dL - physiologically possible maximum
109
+ 'minimum': SENSOR_GLUCOSE_MIN_MGDL, # mg/dL - broad CGM/sensor-valid minimum
110
+ 'maximum': SENSOR_GLUCOSE_MAX_MGDL, # mg/dL - broad CGM/sensor-valid maximum
105
111
  'critical_low': 54, # mg/dL - clinically significant low
106
112
  'critical_high': 350 # mg/dL - clinically significant high
107
113
  }
108
-
114
+
109
115
  PHYSIOLOGICAL_RATES = {
110
- 'max_glucose_change_per_min': 19.9 # mg/dL/min - Detecting changes of 20 mg/dL/min or more
116
+ # Broad fail-soft CGM plausibility cap; common CGM trend-arrow systems
117
+ # already treat >2 mg/dL/min as "rapid", so this keeps a conservative margin.
118
+ 'max_glucose_change_per_min': SENSOR_MAX_GLUCOSE_RATE_PER_MIN_MGDL
111
119
  }
112
120
 
113
121
  # Expected sampling intervals (in minutes)
iints/data/registry.py CHANGED
@@ -5,8 +5,10 @@ import urllib.request
5
5
  import zipfile
6
6
  import hashlib
7
7
  import shutil
8
+ from ipaddress import ip_address
8
9
  from pathlib import Path
9
10
  from typing import Any, Dict, List, Optional, IO, cast
11
+ from urllib.parse import urlparse
10
12
 
11
13
  try: # Python 3.9+
12
14
  from importlib.resources import files
@@ -54,10 +56,30 @@ def list_dataset_ids() -> List[str]:
54
56
  return ids
55
57
 
56
58
 
59
+ def _is_loopback_host(hostname: str) -> bool:
60
+ if hostname == "localhost":
61
+ return True
62
+ try:
63
+ return ip_address(hostname).is_loopback
64
+ except ValueError:
65
+ return False
66
+
67
+
68
+ def _validate_download_url(url: str) -> str:
69
+ parsed = urlparse(url)
70
+ if parsed.scheme == "https":
71
+ return url
72
+ if parsed.scheme == "http" and parsed.hostname and _is_loopback_host(parsed.hostname):
73
+ return url
74
+ raise DatasetFetchError(
75
+ "Dataset download URL must use https, or http only for localhost/loopback development mirrors."
76
+ )
77
+
78
+
57
79
  def _download_file(url: str, output_path: Path) -> Path:
58
80
  output_path.parent.mkdir(parents=True, exist_ok=True)
59
81
  try:
60
- urllib.request.urlretrieve(url, output_path)
82
+ urllib.request.urlretrieve(_validate_download_url(url), output_path) # nosec B310 - URL is scheme validated before download
61
83
  except Exception as exc:
62
84
  raise DatasetFetchError(f"Failed to download {url}: {exc}") from exc
63
85
  return output_path
iints/highlevel.py CHANGED
@@ -442,7 +442,7 @@ def run_population(
442
442
  dawn_phenomenon_strength=patient_params.get("dawn_phenomenon_strength", 0.0),
443
443
  dawn_start_hour=patient_params.get("dawn_start_hour", 4.0),
444
444
  dawn_end_hour=patient_params.get("dawn_end_hour", 8.0),
445
- glucose_decay_rate=patient_params.get("glucose_decay_rate", 0.002),
445
+ glucose_decay_rate=patient_params.get("glucose_decay_rate", 0.05),
446
446
  glucose_absorption_rate=patient_params.get("glucose_absorption_rate", 0.03),
447
447
  insulin_peak_time=patient_params.get("insulin_peak_time", 75.0),
448
448
  meal_mismatch_epsilon=patient_params.get("meal_mismatch_epsilon", 1.0),