LIBRE / src /domain /entities /prediction.py
RyZ
feat: adding predicted ecg
9cabacf
Raw
History Blame Contribute Delete
6.05 kB
"""
domain/entities/prediction.py
──────────────────────────────
BPPrediction β€” domain entity representing a blood pressure prediction result.
Design decisions:
β€’ Pure Python @dataclass β€” no ORM, no Pydantic.
β€’ SBP and DBP are validated against physiological bounds in ``validate()``.
β€’ MAP (Mean Arterial Pressure) is a computed property β€” not stored.
"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
from src.domain.exceptions.domain_exceptions import PredictionOutOfRangeError
from src.shared.constants import (
BP_DBP_MAX,
BP_DBP_MIN,
BP_SBP_MAX,
BP_SBP_MIN,
MODEL_VERSION_MOCK,
)
@dataclass
class BPPrediction:
"""
Represents the output of an AI blood pressure prediction for one PPG signal.
Attributes:
ppg_signal_id: Foreign key β€” which PPGSignal was the input.
predicted_sbp: Predicted Systolic Blood Pressure (mmHg).
predicted_dbp: Predicted Diastolic Blood Pressure (mmHg).
predicted_ecg: Synthetic ECG signal segments produced by CardioGAN
(list of segments, each segment is a list of float samples).
model_version: Version string of the model that produced this result.
inference_time_ms: Wall-clock time spent on inference (milliseconds).
id: Auto-generated UUID.
created_at: UTC timestamp of when the prediction was made.
"""
ppg_signal_id: str
predicted_sbp: float
predicted_dbp: float
predicted_ecg: Optional[list] = None # list[list[float]] β€” GAN-generated ECG windows
model_version: str = MODEL_VERSION_MOCK
inference_time_ms: float = 0.0
sa_log: Optional[dict] = None
# Populated by repository on persist
id: str = field(default_factory=lambda: str(uuid.uuid4()))
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
# ── Computed Properties ───────────────────────────────────────────────────
@property
def mean_arterial_pressure(self) -> float:
"""
MAP = DBP + (SBP - DBP) / 3.
Clinically used as a single summary of perfusion pressure.
"""
return self.predicted_dbp + (self.predicted_sbp - self.predicted_dbp) / 3.0
@property
def pulse_pressure(self) -> float:
"""Pulse pressure = SBP - DBP (should be positive)."""
return self.predicted_sbp - self.predicted_dbp
@property
def hypertension_stage(self) -> str:
"""
Simple hypertension classification based on American Heart Association tiers.
Returns one of: 'Normal', 'Elevated', 'Stage 1', 'Stage 2', 'Crisis'.
"""
sbp = self.predicted_sbp
dbp = self.predicted_dbp
if sbp >= 180 or dbp >= 120:
return "Crisis"
if sbp >= 140 or dbp >= 90:
return "Stage 2"
if sbp >= 130 or dbp >= 80:
return "Stage 1"
if sbp >= 120:
return "Elevated"
return "Normal"
@property
def is_valid(self) -> bool:
"""Quick boolean check β€” does not raise."""
try:
self.validate()
return True
except PredictionOutOfRangeError:
return False
# ── Domain Validation ─────────────────────────────────────────────────────
def validate(self) -> None:
"""
Enforce physiological bounds on the predicted values.
Raises:
PredictionOutOfRangeError: If SBP or DBP is physiologically implausible.
"""
if not (BP_SBP_MIN <= self.predicted_sbp <= BP_SBP_MAX):
raise PredictionOutOfRangeError(
self.predicted_sbp,
self.predicted_dbp,
f"SBP must be between {BP_SBP_MIN} and {BP_SBP_MAX} mmHg",
)
if not (BP_DBP_MIN <= self.predicted_dbp <= BP_DBP_MAX):
raise PredictionOutOfRangeError(
self.predicted_sbp,
self.predicted_dbp,
f"DBP must be between {BP_DBP_MIN} and {BP_DBP_MAX} mmHg",
)
if self.predicted_sbp <= self.predicted_dbp:
raise PredictionOutOfRangeError(
self.predicted_sbp,
self.predicted_dbp,
"SBP must be greater than DBP (pulse pressure cannot be ≀ 0)",
)
# ── Dunder Methods ────────────────────────────────────────────────────────
def __repr__(self) -> str:
return (
f"BPPrediction(id={self.id!r}, "
f"ppg_signal_id={self.ppg_signal_id!r}, "
f"SBP={self.predicted_sbp} mmHg, "
f"DBP={self.predicted_dbp} mmHg, "
f"MAP={self.mean_arterial_pressure:.1f} mmHg, "
f"stage={self.hypertension_stage!r}, "
f"model={self.model_version!r})"
)
def to_dict(self) -> dict:
"""Serialise to plain dict."""
return {
"id": self.id,
"ppg_signal_id": self.ppg_signal_id,
"predicted_sbp": self.predicted_sbp,
"predicted_dbp": self.predicted_dbp,
"predicted_ecg": self.predicted_ecg,
"mean_arterial_pressure": self.mean_arterial_pressure,
"pulse_pressure": self.pulse_pressure,
"hypertension_stage": self.hypertension_stage,
"model_version": self.model_version,
"inference_time_ms": self.inference_time_ms,
"sa_log": self.sa_log,
"created_at": self.created_at.isoformat(),
}