iints-sdk-python35 0.0.18__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 (118) hide show
  1. iints/__init__.py +183 -0
  2. iints/analysis/__init__.py +12 -0
  3. iints/analysis/algorithm_xray.py +387 -0
  4. iints/analysis/baseline.py +92 -0
  5. iints/analysis/clinical_benchmark.py +198 -0
  6. iints/analysis/clinical_metrics.py +551 -0
  7. iints/analysis/clinical_tir_analyzer.py +136 -0
  8. iints/analysis/diabetes_metrics.py +43 -0
  9. iints/analysis/edge_efficiency.py +33 -0
  10. iints/analysis/edge_performance_monitor.py +315 -0
  11. iints/analysis/explainability.py +94 -0
  12. iints/analysis/explainable_ai.py +232 -0
  13. iints/analysis/hardware_benchmark.py +221 -0
  14. iints/analysis/metrics.py +117 -0
  15. iints/analysis/population_report.py +188 -0
  16. iints/analysis/reporting.py +345 -0
  17. iints/analysis/safety_index.py +311 -0
  18. iints/analysis/sensor_filtering.py +54 -0
  19. iints/analysis/validator.py +273 -0
  20. iints/api/__init__.py +0 -0
  21. iints/api/base_algorithm.py +307 -0
  22. iints/api/registry.py +103 -0
  23. iints/api/template_algorithm.py +195 -0
  24. iints/assets/iints_logo.png +0 -0
  25. iints/cli/__init__.py +0 -0
  26. iints/cli/cli.py +2598 -0
  27. iints/core/__init__.py +1 -0
  28. iints/core/algorithms/__init__.py +0 -0
  29. iints/core/algorithms/battle_runner.py +138 -0
  30. iints/core/algorithms/correction_bolus.py +95 -0
  31. iints/core/algorithms/discovery.py +92 -0
  32. iints/core/algorithms/fixed_basal_bolus.py +58 -0
  33. iints/core/algorithms/hybrid_algorithm.py +92 -0
  34. iints/core/algorithms/lstm_algorithm.py +138 -0
  35. iints/core/algorithms/mock_algorithms.py +162 -0
  36. iints/core/algorithms/pid_controller.py +88 -0
  37. iints/core/algorithms/standard_pump_algo.py +64 -0
  38. iints/core/device.py +0 -0
  39. iints/core/device_manager.py +64 -0
  40. iints/core/devices/__init__.py +3 -0
  41. iints/core/devices/models.py +160 -0
  42. iints/core/patient/__init__.py +9 -0
  43. iints/core/patient/bergman_model.py +341 -0
  44. iints/core/patient/models.py +285 -0
  45. iints/core/patient/patient_factory.py +117 -0
  46. iints/core/patient/profile.py +41 -0
  47. iints/core/safety/__init__.py +12 -0
  48. iints/core/safety/config.py +37 -0
  49. iints/core/safety/input_validator.py +95 -0
  50. iints/core/safety/supervisor.py +39 -0
  51. iints/core/simulation/__init__.py +0 -0
  52. iints/core/simulation/scenario_parser.py +61 -0
  53. iints/core/simulator.py +874 -0
  54. iints/core/supervisor.py +367 -0
  55. iints/data/__init__.py +53 -0
  56. iints/data/adapter.py +142 -0
  57. iints/data/column_mapper.py +398 -0
  58. iints/data/datasets.json +132 -0
  59. iints/data/demo/__init__.py +1 -0
  60. iints/data/demo/demo_cgm.csv +289 -0
  61. iints/data/importer.py +275 -0
  62. iints/data/ingestor.py +162 -0
  63. iints/data/nightscout.py +128 -0
  64. iints/data/quality_checker.py +550 -0
  65. iints/data/registry.py +166 -0
  66. iints/data/tidepool.py +38 -0
  67. iints/data/universal_parser.py +813 -0
  68. iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
  69. iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
  70. iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
  71. iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
  72. iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
  73. iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
  74. iints/data/virtual_patients/default_patient.yaml +11 -0
  75. iints/data/virtual_patients/patient_559_config.yaml +11 -0
  76. iints/emulation/__init__.py +80 -0
  77. iints/emulation/legacy_base.py +414 -0
  78. iints/emulation/medtronic_780g.py +337 -0
  79. iints/emulation/omnipod_5.py +367 -0
  80. iints/emulation/tandem_controliq.py +393 -0
  81. iints/highlevel.py +451 -0
  82. iints/learning/__init__.py +3 -0
  83. iints/learning/autonomous_optimizer.py +194 -0
  84. iints/learning/learning_system.py +122 -0
  85. iints/metrics.py +34 -0
  86. iints/population/__init__.py +11 -0
  87. iints/population/generator.py +131 -0
  88. iints/population/runner.py +327 -0
  89. iints/presets/__init__.py +28 -0
  90. iints/presets/presets.json +114 -0
  91. iints/research/__init__.py +30 -0
  92. iints/research/config.py +68 -0
  93. iints/research/dataset.py +319 -0
  94. iints/research/losses.py +73 -0
  95. iints/research/predictor.py +329 -0
  96. iints/scenarios/__init__.py +3 -0
  97. iints/scenarios/generator.py +92 -0
  98. iints/templates/__init__.py +0 -0
  99. iints/templates/default_algorithm.py +91 -0
  100. iints/templates/scenarios/__init__.py +0 -0
  101. iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
  102. iints/templates/scenarios/chaos_runaway_ai.json +25 -0
  103. iints/templates/scenarios/example_scenario.json +35 -0
  104. iints/templates/scenarios/exercise_stress.json +30 -0
  105. iints/utils/__init__.py +3 -0
  106. iints/utils/plotting.py +50 -0
  107. iints/utils/run_io.py +152 -0
  108. iints/validation/__init__.py +133 -0
  109. iints/validation/schemas.py +94 -0
  110. iints/visualization/__init__.py +34 -0
  111. iints/visualization/cockpit.py +691 -0
  112. iints/visualization/uncertainty_cloud.py +612 -0
  113. iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
  114. iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
  115. iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
  116. iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
  117. iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
  118. iints_sdk_python35-0.0.18.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Iterable, List, Optional
4
+
5
+
6
+ IINTS_BLUE = "#00798c"
7
+ IINTS_RED = "#d1495b"
8
+ IINTS_ORANGE = "#f4a261"
9
+ IINTS_TEAL = "#2a9d8f"
10
+ IINTS_NAVY = "#264653"
11
+ IINTS_GOLD = "#e9c46a"
12
+
13
+
14
+ def apply_plot_style(
15
+ dpi: int = 150,
16
+ font_scale: float = 1.1,
17
+ palette: Optional[Iterable[str]] = None,
18
+ ) -> List[str]:
19
+ """
20
+ Apply IINTS paper-ready plotting defaults (Matplotlib/Seaborn).
21
+
22
+ Returns the palette used.
23
+ """
24
+ try:
25
+ import matplotlib as mpl
26
+ import seaborn as sns
27
+ except Exception as exc: # pragma: no cover - optional dependency
28
+ raise ImportError("Plot styling requires matplotlib and seaborn.") from exc
29
+
30
+ colors = list(
31
+ palette
32
+ if palette is not None
33
+ else [IINTS_BLUE, IINTS_RED, IINTS_ORANGE, IINTS_TEAL, IINTS_NAVY, IINTS_GOLD]
34
+ )
35
+
36
+ sns.set_theme(context="paper", style="whitegrid", palette=colors, font_scale=font_scale)
37
+ mpl.rcParams.update(
38
+ {
39
+ "figure.dpi": dpi,
40
+ "savefig.dpi": dpi,
41
+ "axes.titlesize": 14,
42
+ "axes.labelsize": 12,
43
+ "legend.fontsize": 10,
44
+ "xtick.labelsize": 10,
45
+ "ytick.labelsize": 10,
46
+ "axes.spines.top": False,
47
+ "axes.spines.right": False,
48
+ }
49
+ )
50
+ return colors
iints/utils/run_io.py ADDED
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, is_dataclass
4
+ from datetime import datetime, timezone
5
+ import json
6
+ import hashlib
7
+ import platform
8
+ import random
9
+ import sys
10
+ import uuid
11
+ import subprocess
12
+ import os
13
+ from importlib import metadata as importlib_metadata
14
+ from typing import Mapping, cast
15
+ from pathlib import Path
16
+ from typing import Any, Dict, Optional, Union
17
+
18
+ try:
19
+ from importlib.metadata import version as pkg_version
20
+ except Exception: # pragma: no cover - stdlib fallback
21
+ pkg_version = None # type: ignore[assignment]
22
+
23
+
24
+ def resolve_seed(seed: Optional[int]) -> int:
25
+ if seed is None:
26
+ seed = random.SystemRandom().randint(0, 2**31 - 1)
27
+ return int(seed)
28
+
29
+
30
+ def generate_run_id(seed: int) -> str:
31
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
32
+ token = uuid.uuid4().hex[:6]
33
+ return f"{timestamp}-{seed}-{token}"
34
+
35
+
36
+ def resolve_output_dir(output_dir: Optional[Union[str, Path]], run_id: str) -> Path:
37
+ if output_dir is None:
38
+ output_path = Path.cwd() / "results" / run_id
39
+ else:
40
+ output_path = Path(output_dir).expanduser()
41
+ if not output_path.is_absolute():
42
+ output_path = (Path.cwd() / output_path).resolve()
43
+ else:
44
+ output_path = output_path.resolve()
45
+ output_path.mkdir(parents=True, exist_ok=True)
46
+ return output_path
47
+
48
+
49
+ def _serialize_payload(payload: Any) -> Any:
50
+ if is_dataclass(payload) and not isinstance(payload, type):
51
+ return asdict(payload)
52
+ if isinstance(payload, Path):
53
+ return str(payload)
54
+ return payload
55
+
56
+
57
+ def write_json(path: Path, payload: Dict[str, Any]) -> None:
58
+ safe_payload = {key: _serialize_payload(value) for key, value in payload.items()}
59
+ path.write_text(json.dumps(safe_payload, indent=2, sort_keys=True))
60
+
61
+
62
+ def get_sdk_version(package_name: str = "iints-sdk-python35") -> str:
63
+ if pkg_version is None:
64
+ return "unknown"
65
+ try:
66
+ return pkg_version(package_name)
67
+ except Exception:
68
+ return "unknown"
69
+
70
+
71
+ def build_run_metadata(run_id: str, seed: int, config: Dict[str, Any], output_dir: Path) -> Dict[str, Any]:
72
+ dependencies = []
73
+ try:
74
+ for dist in importlib_metadata.distributions():
75
+ metadata = cast(Mapping[str, str], dist.metadata)
76
+ name = metadata.get("Name") or metadata.get("Summary") or metadata.get("name")
77
+ if name:
78
+ dependencies.append({"name": name, "version": dist.version})
79
+ dependencies = sorted(dependencies, key=lambda item: item["name"].lower())
80
+ except Exception:
81
+ dependencies = []
82
+
83
+ git_sha = "unknown"
84
+ try:
85
+ result = subprocess.run(
86
+ ["git", "rev-parse", "HEAD"],
87
+ capture_output=True,
88
+ text=True,
89
+ check=False,
90
+ )
91
+ if result.returncode == 0:
92
+ git_sha = result.stdout.strip()
93
+ except Exception:
94
+ git_sha = "unknown"
95
+
96
+ return {
97
+ "run_id": run_id,
98
+ "created_at_utc": datetime.now(timezone.utc).isoformat(),
99
+ "seed": seed,
100
+ "output_dir": str(output_dir),
101
+ "sdk_version": get_sdk_version(),
102
+ "python_version": sys.version.split()[0],
103
+ "platform": platform.platform(),
104
+ "git_sha": git_sha,
105
+ "dependencies": dependencies,
106
+ "config": config,
107
+ }
108
+
109
+
110
+ def compute_sha256(path: Path) -> str:
111
+ digest = hashlib.sha256()
112
+ with path.open("rb") as handle:
113
+ for chunk in iter(lambda: handle.read(8192), b""):
114
+ digest.update(chunk)
115
+ return digest.hexdigest()
116
+
117
+
118
+ def build_run_manifest(output_dir: Path, files: Dict[str, Path]) -> Dict[str, Any]:
119
+ manifest: Dict[str, Any] = {
120
+ "generated_at_utc": datetime.now(timezone.utc).isoformat(),
121
+ "output_dir": str(output_dir),
122
+ "files": {},
123
+ }
124
+ for label, path in files.items():
125
+ entry: Dict[str, Any] = {"path": str(path)}
126
+ if path.exists():
127
+ entry["sha256"] = compute_sha256(path)
128
+ entry["size_bytes"] = path.stat().st_size
129
+ else:
130
+ entry["missing"] = True
131
+ manifest["files"][label] = entry
132
+ return manifest
133
+
134
+
135
+ def maybe_sign_manifest(manifest_path: Path) -> Optional[Path]:
136
+ """Optionally sign a manifest if IINTS_ATTEST_KEY is set."""
137
+ key_path = os.getenv("IINTS_ATTEST_KEY")
138
+ if not key_path:
139
+ return None
140
+ sig_path = manifest_path.with_suffix(".sig")
141
+ try:
142
+ result = subprocess.run(
143
+ ["openssl", "dgst", "-sha256", "-sign", key_path, "-out", str(sig_path), str(manifest_path)],
144
+ capture_output=True,
145
+ text=True,
146
+ check=False,
147
+ )
148
+ if result.returncode != 0:
149
+ return None
150
+ return sig_path
151
+ except Exception:
152
+ return None
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple, Union
7
+
8
+ import yaml
9
+ from pydantic import ValidationError
10
+
11
+ from iints.core.simulator import StressEvent
12
+ from iints.validation.schemas import ScenarioModel, StressEventModel, PatientConfigModel, LATEST_SCHEMA_VERSION
13
+
14
+
15
+ def _convert_legacy_scenario(data: Dict[str, Any]) -> Dict[str, Any]:
16
+ if "events" in data and "stress_events" not in data:
17
+ events = []
18
+ for event in data.get("events", []):
19
+ event_type = event.get("type")
20
+ if event_type == "carb_error":
21
+ event = dict(event)
22
+ event["reported_value"] = 0
23
+ event_type = "meal"
24
+ events.append(
25
+ {
26
+ "start_time": event.get("time"),
27
+ "event_type": event_type,
28
+ "value": event.get("value"),
29
+ "reported_value": event.get("reported_value"),
30
+ "absorption_delay_minutes": event.get("absorption_delay_minutes", 0),
31
+ "duration": event.get("duration", 0),
32
+ }
33
+ )
34
+ return {
35
+ "scenario_name": data.get("scenario_name", "Legacy Scenario"),
36
+ "scenario_version": data.get("scenario_version", "legacy"),
37
+ "description": data.get("description", ""),
38
+ "stress_events": events,
39
+ }
40
+ return data
41
+
42
+
43
+ def migrate_scenario_dict(data: Dict[str, Any]) -> Dict[str, Any]:
44
+ """Normalize legacy schemas into the latest scenario schema."""
45
+ data = _convert_legacy_scenario(data)
46
+ if "schema_version" not in data or not data.get("schema_version"):
47
+ data["schema_version"] = LATEST_SCHEMA_VERSION
48
+ if "scenario_version" not in data or not data.get("scenario_version"):
49
+ data["scenario_version"] = "1.0"
50
+ if "scenario_name" not in data or not data.get("scenario_name"):
51
+ data["scenario_name"] = "Unnamed Scenario"
52
+ return data
53
+
54
+
55
+ def load_scenario(path: Union[str, Path]) -> ScenarioModel:
56
+ scenario_path = Path(path)
57
+ data = json.loads(scenario_path.read_text())
58
+ data = migrate_scenario_dict(data)
59
+ return ScenarioModel.model_validate(data)
60
+
61
+
62
+ def validate_scenario_dict(data: Dict[str, Any]) -> ScenarioModel:
63
+ data = migrate_scenario_dict(data)
64
+ return ScenarioModel.model_validate(data)
65
+
66
+
67
+ def scenario_warnings(model: ScenarioModel) -> List[str]:
68
+ warnings: List[str] = []
69
+ for idx, event in enumerate(model.stress_events):
70
+ prefix = f"event[{idx}]"
71
+ if event.event_type in {"meal", "missed_meal"} and event.value is not None:
72
+ if event.value > 200:
73
+ warnings.append(f"{prefix}: meal value {event.value}g is unusually high")
74
+ if event.absorption_delay_minutes > 120:
75
+ warnings.append(f"{prefix}: absorption_delay_minutes {event.absorption_delay_minutes} is unusual")
76
+ if event.duration > 240:
77
+ warnings.append(f"{prefix}: duration {event.duration} is unusual")
78
+ return warnings
79
+
80
+
81
+ def build_stress_events(payloads: List[Dict[str, Any]]) -> List[StressEvent]:
82
+ events: List[StressEvent] = []
83
+ for event_data in payloads:
84
+ events.append(
85
+ StressEvent(
86
+ start_time=event_data["start_time"],
87
+ event_type=event_data["event_type"],
88
+ value=event_data.get("value"),
89
+ reported_value=event_data.get("reported_value"),
90
+ absorption_delay_minutes=event_data.get("absorption_delay_minutes", 0),
91
+ duration=event_data.get("duration", 0),
92
+ isf=event_data.get("isf"),
93
+ icr=event_data.get("icr"),
94
+ basal_rate=event_data.get("basal_rate"),
95
+ dia_minutes=event_data.get("dia_minutes"),
96
+ )
97
+ )
98
+ return events
99
+
100
+
101
+ def scenario_to_payloads(model: ScenarioModel) -> List[Dict[str, Any]]:
102
+ return [event.model_dump() for event in model.stress_events]
103
+
104
+
105
+ def validate_patient_config_dict(data: Dict[str, Any]) -> PatientConfigModel:
106
+ return PatientConfigModel.model_validate(data)
107
+
108
+
109
+ def load_patient_config(path: Union[str, Path]) -> PatientConfigModel:
110
+ config_path = Path(path)
111
+ data = yaml.safe_load(config_path.read_text())
112
+ return validate_patient_config_dict(data)
113
+
114
+
115
+ def load_patient_config_by_name(name: str) -> PatientConfigModel:
116
+ filename = f"{name}.yaml" if not name.endswith(".yaml") else name
117
+ if sys.version_info >= (3, 9):
118
+ from importlib.resources import files
119
+ content = files("iints.data.virtual_patients").joinpath(filename).read_text()
120
+ else:
121
+ from importlib import resources
122
+ content = resources.read_text("iints.data.virtual_patients", filename)
123
+ data = yaml.safe_load(content)
124
+ return validate_patient_config_dict(data)
125
+
126
+
127
+ def format_validation_error(error: ValidationError) -> List[str]:
128
+ lines: List[str] = []
129
+ for entry in error.errors():
130
+ loc = ".".join(str(item) for item in entry.get("loc", []))
131
+ msg = entry.get("msg", "Invalid value")
132
+ lines.append(f"{loc}: {msg}")
133
+ return lines
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, Literal
4
+
5
+ LATEST_SCHEMA_VERSION = "1.1"
6
+
7
+ from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
8
+
9
+
10
+ class StressEventModel(BaseModel):
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ start_time: int = Field(ge=0)
14
+ event_type: Literal["meal", "missed_meal", "sensor_error", "exercise", "exercise_end", "ratio_change"]
15
+ value: Optional[float] = None
16
+ reported_value: Optional[float] = None
17
+ absorption_delay_minutes: int = Field(default=0, ge=0)
18
+ duration: int = Field(default=0, ge=0)
19
+ isf: Optional[float] = Field(default=None, gt=0)
20
+ icr: Optional[float] = Field(default=None, gt=0)
21
+ basal_rate: Optional[float] = Field(default=None, ge=0)
22
+ dia_minutes: Optional[float] = Field(default=None, gt=0)
23
+
24
+ @model_validator(mode="after")
25
+ def _check_required_fields(self) -> "StressEventModel":
26
+ if self.event_type in {"meal", "missed_meal"}:
27
+ if self.value is None or self.value <= 0:
28
+ raise ValueError("meal value must be > 0 grams")
29
+ if self.event_type == "exercise":
30
+ if self.value is None or not (0.0 <= self.value <= 1.0):
31
+ raise ValueError("exercise value must be between 0.0 and 1.0")
32
+ if self.event_type == "sensor_error":
33
+ if self.value is None:
34
+ raise ValueError("sensor_error requires a value")
35
+ if self.event_type == "ratio_change":
36
+ if all(
37
+ val is None
38
+ for val in (self.isf, self.icr, self.basal_rate, self.dia_minutes)
39
+ ):
40
+ raise ValueError("ratio_change requires at least one ratio value (isf/icr/basal_rate/dia_minutes)")
41
+ return self
42
+
43
+
44
+ class ScenarioModel(BaseModel):
45
+ model_config = ConfigDict(extra="forbid")
46
+
47
+ scenario_name: str = Field(min_length=1)
48
+ schema_version: str = Field(default=LATEST_SCHEMA_VERSION, min_length=1)
49
+ scenario_version: str = Field(min_length=1)
50
+ description: Optional[str] = None
51
+ stress_events: List[StressEventModel] = Field(default_factory=list)
52
+
53
+ @field_validator("schema_version", mode="before")
54
+ @classmethod
55
+ def _normalize_schema_version(cls, value: Any) -> str:
56
+ if isinstance(value, (int, float)):
57
+ return str(value)
58
+ if value is None:
59
+ return LATEST_SCHEMA_VERSION
60
+ return str(value)
61
+
62
+ @field_validator("scenario_version", mode="before")
63
+ @classmethod
64
+ def _normalize_version(cls, value: Any) -> str:
65
+ if isinstance(value, (int, float)):
66
+ return str(value)
67
+ if value is None:
68
+ raise ValueError("scenario_version is required")
69
+ return str(value)
70
+
71
+
72
+ class PatientConfigModel(BaseModel):
73
+ model_config = ConfigDict(extra="forbid")
74
+
75
+ basal_insulin_rate: float = Field(default=0.8, ge=0.0, le=3.0)
76
+ insulin_sensitivity: float = Field(default=50.0, ge=10.0, le=200.0)
77
+ carb_factor: float = Field(default=10.0, ge=3.0, le=30.0)
78
+ glucose_decay_rate: float = Field(default=0.05, ge=0.0, le=0.2)
79
+ initial_glucose: float = Field(default=120.0, ge=40.0, le=400.0)
80
+ glucose_absorption_rate: float = Field(default=0.03, ge=0.0, le=0.2)
81
+ insulin_action_duration: float = Field(default=300.0, ge=60.0, le=720.0)
82
+ insulin_peak_time: float = Field(default=75.0, ge=15.0, le=240.0)
83
+ meal_mismatch_epsilon: float = Field(default=1.0, ge=0.5, le=1.5)
84
+ dawn_phenomenon_strength: float = Field(default=0.0, ge=0.0, le=50.0)
85
+ dawn_start_hour: float = Field(default=4.0, ge=0.0, le=23.0)
86
+ dawn_end_hour: float = Field(default=8.0, ge=0.0, le=24.0)
87
+
88
+ @model_validator(mode="after")
89
+ def _check_peak_vs_duration(self) -> "PatientConfigModel":
90
+ if self.insulin_peak_time >= self.insulin_action_duration:
91
+ raise ValueError("insulin_peak_time must be less than insulin_action_duration")
92
+ if self.dawn_end_hour <= self.dawn_start_hour:
93
+ raise ValueError("dawn_end_hour must be greater than dawn_start_hour")
94
+ return self
@@ -0,0 +1,34 @@
1
+ """
2
+ IINTS-AF Visualization Module
3
+ Professional medical visualizations for diabetes algorithm research.
4
+
5
+ Includes:
6
+ - Uncertainty Cloud: AI confidence visualization
7
+ - Clinical Cockpit: Full dashboard for research
8
+ - Algorithm comparison charts
9
+ """
10
+
11
+ from .uncertainty_cloud import (
12
+ UncertaintyCloud,
13
+ UncertaintyData,
14
+ VisualizationConfig
15
+ )
16
+
17
+ from .cockpit import (
18
+ ClinicalCockpit,
19
+ CockpitConfig,
20
+ DashboardState
21
+ )
22
+
23
+ __all__ = [
24
+ # Uncertainty Cloud
25
+ 'UncertaintyCloud',
26
+ 'UncertaintyData',
27
+ 'VisualizationConfig',
28
+
29
+ # Clinical Cockpit
30
+ 'ClinicalCockpit',
31
+ 'CockpitConfig',
32
+ 'DashboardState'
33
+ ]
34
+