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.
- iints/__init__.py +183 -0
- iints/analysis/__init__.py +12 -0
- iints/analysis/algorithm_xray.py +387 -0
- iints/analysis/baseline.py +92 -0
- iints/analysis/clinical_benchmark.py +198 -0
- iints/analysis/clinical_metrics.py +551 -0
- iints/analysis/clinical_tir_analyzer.py +136 -0
- iints/analysis/diabetes_metrics.py +43 -0
- iints/analysis/edge_efficiency.py +33 -0
- iints/analysis/edge_performance_monitor.py +315 -0
- iints/analysis/explainability.py +94 -0
- iints/analysis/explainable_ai.py +232 -0
- iints/analysis/hardware_benchmark.py +221 -0
- iints/analysis/metrics.py +117 -0
- iints/analysis/population_report.py +188 -0
- iints/analysis/reporting.py +345 -0
- iints/analysis/safety_index.py +311 -0
- iints/analysis/sensor_filtering.py +54 -0
- iints/analysis/validator.py +273 -0
- iints/api/__init__.py +0 -0
- iints/api/base_algorithm.py +307 -0
- iints/api/registry.py +103 -0
- iints/api/template_algorithm.py +195 -0
- iints/assets/iints_logo.png +0 -0
- iints/cli/__init__.py +0 -0
- iints/cli/cli.py +2598 -0
- iints/core/__init__.py +1 -0
- iints/core/algorithms/__init__.py +0 -0
- iints/core/algorithms/battle_runner.py +138 -0
- iints/core/algorithms/correction_bolus.py +95 -0
- iints/core/algorithms/discovery.py +92 -0
- iints/core/algorithms/fixed_basal_bolus.py +58 -0
- iints/core/algorithms/hybrid_algorithm.py +92 -0
- iints/core/algorithms/lstm_algorithm.py +138 -0
- iints/core/algorithms/mock_algorithms.py +162 -0
- iints/core/algorithms/pid_controller.py +88 -0
- iints/core/algorithms/standard_pump_algo.py +64 -0
- iints/core/device.py +0 -0
- iints/core/device_manager.py +64 -0
- iints/core/devices/__init__.py +3 -0
- iints/core/devices/models.py +160 -0
- iints/core/patient/__init__.py +9 -0
- iints/core/patient/bergman_model.py +341 -0
- iints/core/patient/models.py +285 -0
- iints/core/patient/patient_factory.py +117 -0
- iints/core/patient/profile.py +41 -0
- iints/core/safety/__init__.py +12 -0
- iints/core/safety/config.py +37 -0
- iints/core/safety/input_validator.py +95 -0
- iints/core/safety/supervisor.py +39 -0
- iints/core/simulation/__init__.py +0 -0
- iints/core/simulation/scenario_parser.py +61 -0
- iints/core/simulator.py +874 -0
- iints/core/supervisor.py +367 -0
- iints/data/__init__.py +53 -0
- iints/data/adapter.py +142 -0
- iints/data/column_mapper.py +398 -0
- iints/data/datasets.json +132 -0
- iints/data/demo/__init__.py +1 -0
- iints/data/demo/demo_cgm.csv +289 -0
- iints/data/importer.py +275 -0
- iints/data/ingestor.py +162 -0
- iints/data/nightscout.py +128 -0
- iints/data/quality_checker.py +550 -0
- iints/data/registry.py +166 -0
- iints/data/tidepool.py +38 -0
- iints/data/universal_parser.py +813 -0
- iints/data/virtual_patients/clinic_safe_baseline.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hyper_challenge.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_hypo_prone.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_midnight.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_pizza.yaml +9 -0
- iints/data/virtual_patients/clinic_safe_stress_meal.yaml +9 -0
- iints/data/virtual_patients/default_patient.yaml +11 -0
- iints/data/virtual_patients/patient_559_config.yaml +11 -0
- iints/emulation/__init__.py +80 -0
- iints/emulation/legacy_base.py +414 -0
- iints/emulation/medtronic_780g.py +337 -0
- iints/emulation/omnipod_5.py +367 -0
- iints/emulation/tandem_controliq.py +393 -0
- iints/highlevel.py +451 -0
- iints/learning/__init__.py +3 -0
- iints/learning/autonomous_optimizer.py +194 -0
- iints/learning/learning_system.py +122 -0
- iints/metrics.py +34 -0
- iints/population/__init__.py +11 -0
- iints/population/generator.py +131 -0
- iints/population/runner.py +327 -0
- iints/presets/__init__.py +28 -0
- iints/presets/presets.json +114 -0
- iints/research/__init__.py +30 -0
- iints/research/config.py +68 -0
- iints/research/dataset.py +319 -0
- iints/research/losses.py +73 -0
- iints/research/predictor.py +329 -0
- iints/scenarios/__init__.py +3 -0
- iints/scenarios/generator.py +92 -0
- iints/templates/__init__.py +0 -0
- iints/templates/default_algorithm.py +91 -0
- iints/templates/scenarios/__init__.py +0 -0
- iints/templates/scenarios/chaos_insulin_stacking.json +29 -0
- iints/templates/scenarios/chaos_runaway_ai.json +25 -0
- iints/templates/scenarios/example_scenario.json +35 -0
- iints/templates/scenarios/exercise_stress.json +30 -0
- iints/utils/__init__.py +3 -0
- iints/utils/plotting.py +50 -0
- iints/utils/run_io.py +152 -0
- iints/validation/__init__.py +133 -0
- iints/validation/schemas.py +94 -0
- iints/visualization/__init__.py +34 -0
- iints/visualization/cockpit.py +691 -0
- iints/visualization/uncertainty_cloud.py +612 -0
- iints_sdk_python35-0.0.18.dist-info/METADATA +225 -0
- iints_sdk_python35-0.0.18.dist-info/RECORD +118 -0
- iints_sdk_python35-0.0.18.dist-info/WHEEL +5 -0
- iints_sdk_python35-0.0.18.dist-info/entry_points.txt +10 -0
- iints_sdk_python35-0.0.18.dist-info/licenses/LICENSE +28 -0
- iints_sdk_python35-0.0.18.dist-info/top_level.txt +1 -0
iints/utils/plotting.py
ADDED
|
@@ -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
|
+
|