iints-sdk-python35 1.3.1__py3-none-any.whl → 1.4.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 (73) hide show
  1. iints/__init__.py +1 -1
  2. iints/ai/backends/ollama.py +32 -2
  3. iints/ai/mdmp_guard.py +2 -3
  4. iints/ai/prepare.py +1 -1
  5. iints/analysis/booth_demo.py +3 -1
  6. iints/analysis/clinical_metrics.py +13 -7
  7. iints/analysis/validator.py +10 -4
  8. iints/cli/cli.py +34 -0
  9. iints/core/patient/bergman_model.py +3 -2
  10. iints/core/patient/models.py +1 -1
  11. iints/core/patient/patient_factory.py +8 -8
  12. iints/core/patient/profile.py +1 -1
  13. iints/core/safety/config.py +13 -4
  14. iints/core/safety/input_validator.py +12 -7
  15. iints/data/adapter.py +7 -2
  16. iints/data/ingestor.py +7 -2
  17. iints/data/quality_checker.py +12 -4
  18. iints/data/registry.py +23 -1
  19. iints/demo_assets.py +50 -0
  20. iints/highlevel.py +1 -1
  21. iints/learning/autonomous_optimizer.py +13 -1
  22. iints/mdmp/backend.py +2 -3
  23. iints/research/predictor.py +9 -1
  24. iints/templates/demos/__init__.py +1 -0
  25. iints/templates/demos/live_stage_demo.py +410 -0
  26. {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/METADATA +88 -24
  27. {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/RECORD +72 -28
  28. {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.0.dist-info}/entry_points.txt +1 -0
  29. iints_sdk_python35-1.4.0.dist-info/licenses/LICENSE +173 -0
  30. iints_sdk_python35-1.3.1.dist-info/licenses/LICENSE → iints_sdk_python35-1.4.0.dist-info/licenses/LICENSE-MIT-IINTS-LEGACY +0 -7
  31. iints_sdk_python35-1.4.0.dist-info/licenses/NOTICE +17 -0
  32. iints_sdk_python35-1.4.0.dist-info/top_level.txt +5 -0
  33. mdmp_ai/__init__.py +3 -0
  34. mdmp_ai/lineage.py +198 -0
  35. mdmp_core/__init__.py +133 -0
  36. mdmp_core/audit.py +166 -0
  37. mdmp_core/bias_hooks.py +36 -0
  38. mdmp_core/bundle.py +142 -0
  39. mdmp_core/certification.py +38 -0
  40. mdmp_core/cli.py +1351 -0
  41. mdmp_core/compare.py +56 -0
  42. mdmp_core/conformance.py +429 -0
  43. mdmp_core/contracts.py +164 -0
  44. mdmp_core/crypto.py +372 -0
  45. mdmp_core/data/conformance/vectors/delegation.json +56 -0
  46. mdmp_core/data/conformance/vectors/fingerprint.json +24 -0
  47. mdmp_core/data/conformance/vectors/grading.json +83 -0
  48. mdmp_core/data/conformance/vectors/signing.json +37 -0
  49. mdmp_core/delegate.py +476 -0
  50. mdmp_core/diffing.py +128 -0
  51. mdmp_core/drift.py +203 -0
  52. mdmp_core/exceptions.py +33 -0
  53. mdmp_core/fingerprint.py +92 -0
  54. mdmp_core/fingerprint_store.py +42 -0
  55. mdmp_core/hf.py +60 -0
  56. mdmp_core/keys/mdmp_pub_v1.pem +3 -0
  57. mdmp_core/llm_provenance.py +34 -0
  58. mdmp_core/migrate.py +182 -0
  59. mdmp_core/policy.py +185 -0
  60. mdmp_core/prov.py +31 -0
  61. mdmp_core/registry.py +350 -0
  62. mdmp_core/runner.py +292 -0
  63. mdmp_core/schema_export.py +92 -0
  64. mdmp_core/synthetic.py +30 -0
  65. mdmp_core/trust.py +216 -0
  66. mdmp_core/visualizer.py +61 -0
  67. mdmp_flavors/__init__.py +146 -0
  68. mdmp_integrations/__init__.py +9 -0
  69. mdmp_integrations/dvc.py +30 -0
  70. mdmp_integrations/mlflow.py +14 -0
  71. mdmp_integrations/wandb.py +19 -0
  72. iints_sdk_python35-1.3.1.dist-info/top_level.txt +0 -1
  73. {iints_sdk_python35-1.3.1.dist-info → iints_sdk_python35-1.4.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.1"
14
+ __version__ = "1.4.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.
@@ -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/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
@@ -245,7 +245,7 @@ def _load_mdmp_signer_tools() -> tuple[type[Any], Any]:
245
245
  module = importlib.import_module("mdmp_core")
246
246
  except Exception as exc:
247
247
  raise ImportError(
248
- "Local AI certification requires the optional standalone MDMP package.\n"
248
+ "Local AI certification requires the bundled MDMP crypto support.\n"
249
249
  "Install with: pip install 'iints-sdk-python35[mdmp]'"
250
250
  ) from exc
251
251
 
@@ -233,7 +233,7 @@ def _build_commands_markdown(
233
233
  "# Booth Demo Commands\n\n"
234
234
  "## Showable live demo script\n\n"
235
235
  "```bash\n"
236
- f"PYTHONPATH=src python3 {example_script} --output-dir {output_dir}\n"
236
+ f"python3 {example_script} --output-dir {output_dir}\n"
237
237
  "```\n\n"
238
238
  "## Run from source tree\n\n"
239
239
  "```bash\n"
@@ -264,6 +264,8 @@ def _build_live_demo_script_text(
264
264
  "1. WHAT CODE TO SHOW FIRST\n"
265
265
  "- Show examples/demos/07_live_stage_demo.py first.\n"
266
266
  " Reason: the top of that file exposes the patient profile, output folder, duration, and seed on one screen.\n"
267
+ "- Point out the visible SDK feature calls in that script:\n"
268
+ " run_full(...), generate_results_poster(...), and prepare_ai_ready_artifacts(...).\n"
267
269
  "- Point out that you can swap PATIENT_CONFIG to another packaged profile such as patient_559_config or clinic_safe_hypo_prone.\n"
268
270
  "- If someone asks how the full bundle is generated, open examples/demos/06_booth_demo.py and then src/iints/analysis/booth_demo.py.\n"
269
271
  " Reason: those files define the three scenarios and write the poster, talk track, and run bundle outputs.\n\n"
@@ -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
+ }
iints/cli/cli.py CHANGED
@@ -53,6 +53,7 @@ from iints.data.registry import (
53
53
  )
54
54
  from iints.data.contracts import load_contract_yaml
55
55
  from iints.data.synthetic_mirror import generate_synthetic_mirror
56
+ from iints.demo_assets import export_live_stage_demo
56
57
  from iints.mdmp.backend import (
57
58
  MDMP_GRADE_ORDER,
58
59
  active_mdmp_backend,
@@ -2740,6 +2741,39 @@ def demo_booth(
2740
2741
  )
2741
2742
 
2742
2743
 
2744
+ @app.command("demo-export")
2745
+ def demo_export(
2746
+ output_dir: Annotated[
2747
+ Path,
2748
+ typer.Option(help="Directory where the bundled live stage demo files should be written."),
2749
+ ] = Path("./iints_demo"),
2750
+ overwrite: Annotated[
2751
+ bool,
2752
+ typer.Option("--overwrite/--no-overwrite", help="Allow overwriting exported demo files."),
2753
+ ] = False,
2754
+ ) -> None:
2755
+ """Export the showable live demo code from the installed SDK."""
2756
+ console = Console()
2757
+ try:
2758
+ outputs = export_live_stage_demo(output_dir=output_dir, overwrite=overwrite)
2759
+ except FileExistsError as exc:
2760
+ console.print(f"[bold red]Demo export stopped:[/bold red] {exc}")
2761
+ raise typer.Exit(code=1)
2762
+ except Exception as exc:
2763
+ console.print(f"[bold red]Demo export failed:[/bold red] {exc}")
2764
+ raise typer.Exit(code=1)
2765
+
2766
+ table = Table(title="IINTS Demo Export")
2767
+ table.add_column("Artifact", style="cyan")
2768
+ table.add_column("Path", overflow="fold")
2769
+ table.add_row("script", outputs["script_path"])
2770
+ table.add_row("notes", outputs["notes_path"])
2771
+ console.print(table)
2772
+ console.print(
2773
+ "[green]Next:[/green] open `07_live_stage_demo.py`, explain the visible SDK calls, then run `python 07_live_stage_demo.py`."
2774
+ )
2775
+
2776
+
2743
2777
  @app.command()
2744
2778
  def report(
2745
2779
  results_csv: Annotated[Path, typer.Option(help="Path to a simulation results CSV")],
@@ -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/demo_assets.py ADDED
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.resources import files
4
+ from pathlib import Path
5
+
6
+
7
+ def export_live_stage_demo(output_dir: str | Path = '.', *, overwrite: bool = False) -> dict[str, str]:
8
+ """Export the bundled live stage demo script for users running the installed SDK."""
9
+ resolved_output = Path(output_dir).expanduser().resolve()
10
+ resolved_output.mkdir(parents=True, exist_ok=True)
11
+
12
+ script_target = resolved_output / '07_live_stage_demo.py'
13
+ notes_target = resolved_output / 'RUN_ME_FIRST.txt'
14
+
15
+ if not overwrite and script_target.exists():
16
+ raise FileExistsError(f'Demo script already exists: {script_target}')
17
+ if not overwrite and notes_target.exists():
18
+ raise FileExistsError(f'Instruction file already exists: {notes_target}')
19
+
20
+ script_content = files('iints.templates.demos').joinpath('live_stage_demo.py').read_text(encoding='utf-8')
21
+ notes_content = (
22
+ 'IINTS LIVE DEMO EXPORT\n'
23
+ '======================\n\n'
24
+ 'This folder was exported from the installed IINTS SDK.\n\n'
25
+ '1. Activate the virtual environment that contains IINTS.\n'
26
+ '2. Open 07_live_stage_demo.py and point to:\n'
27
+ ' - PATIENT_CONFIG\n'
28
+ ' - OUTPUT_DIR\n'
29
+ ' - DURATION_MINUTES\n'
30
+ ' - TIME_STEP_MINUTES\n'
31
+ ' - SEED\n'
32
+ '3. Explain that the script visibly calls:\n'
33
+ ' - run_full(...)\n'
34
+ ' - generate_results_poster(...)\n'
35
+ ' - prepare_ai_ready_artifacts(...)\n'
36
+ '4. Run:\n'
37
+ ' python 07_live_stage_demo.py\n'
38
+ '5. Open the generated files under results/booth_demo_live/.\n\n'
39
+ 'Tip: if you also cloned the SDK repo, you can run the repo wrapper instead:\n'
40
+ ' ./scripts/run_live_stage_demo.sh\n'
41
+ )
42
+
43
+ script_target.write_text(script_content, encoding='utf-8')
44
+ notes_target.write_text(notes_content, encoding='utf-8')
45
+
46
+ return {
47
+ 'output_dir': str(resolved_output),
48
+ 'script_path': str(script_target),
49
+ 'notes_path': str(notes_target),
50
+ }
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),
@@ -15,6 +15,18 @@ except Exception: # pragma: no cover
15
15
  _TORCH_AVAILABLE = False
16
16
 
17
17
 
18
+ def _safe_torch_load_weights(path: str):
19
+ if torch is None: # pragma: no cover
20
+ raise ImportError("Torch required for model loading.")
21
+ try:
22
+ return torch.load(path, map_location="cpu", weights_only=True)
23
+ except TypeError as exc:
24
+ raise RuntimeError(
25
+ "This PyTorch build does not support secure weights-only loading. "
26
+ "Upgrade torch to a version that supports `weights_only=True`."
27
+ ) from exc
28
+
29
+
18
30
  @dataclass
19
31
  class ClinicalConstraints:
20
32
  """Physiological constraints based on medical literature."""
@@ -98,7 +110,7 @@ if _TORCH_AVAILABLE:
98
110
  model = LSTMModel(input_size=7, hidden_size=50, output_size=1)
99
111
 
100
112
  if os.path.exists(self.model_path):
101
- model.load_state_dict(torch.load(self.model_path))
113
+ model.load_state_dict(_safe_torch_load_weights(self.model_path))
102
114
 
103
115
  improved_model = self._clinical_fine_tuning(model, clinical_X, clinical_y)
104
116
  safety_score = self._validate_clinical_safety(improved_model, clinical_X, clinical_y)
iints/mdmp/backend.py CHANGED
@@ -114,9 +114,8 @@ def get_backend() -> str:
114
114
  if requested in {BACKEND_MDMP, "mdmp", "external"}:
115
115
  if not is_mdmp_available():
116
116
  raise ImportError(
117
- "mdmp_core not found.\n"
118
- "Install with: pip install iints-sdk-python35[mdmp]\n"
119
- "or: pip install 'mdmp-protocol>=0.3.0'"
117
+ "Bundled MDMP support is not available in this environment.\n"
118
+ "Install with: pip install iints-sdk-python35[mdmp]"
120
119
  )
121
120
  return BACKEND_MDMP
122
121
  if requested == "auto" and is_mdmp_available():
@@ -293,7 +293,15 @@ def load_predictor(model_path: Path) -> Tuple["LSTMPredictor", dict]:
293
293
  raise ImportError(
294
294
  "Torch is required for predictor loading. Install with `pip install iints-sdk-python35[research]`."
295
295
  ) from _IMPORT_ERROR
296
- payload = torch.load(model_path, map_location="cpu", weights_only=False)
296
+ try:
297
+ payload = torch.load(model_path, map_location="cpu", weights_only=True)
298
+ except TypeError as exc:
299
+ raise RuntimeError(
300
+ "This PyTorch build does not support secure checkpoint loading. "
301
+ "Upgrade torch to a version with `weights_only=True` support."
302
+ ) from exc
303
+ if not isinstance(payload, dict):
304
+ raise RuntimeError("Predictor checkpoint must decode to a dictionary payload.")
297
305
  config = payload["config"]
298
306
  model = LSTMPredictor(
299
307
  input_size=config["input_size"],
@@ -0,0 +1 @@
1
+ """Bundled demo script templates for installed IINTS users."""