tavi-backend / src /tavi_api /simulation /per_device.py
Jainish Solanki
Initial backend Space
84da040
Raw
History Blame Contribute Delete
4.86 kB
"""Per-device predicted complication rates.
This is a transparent, rule-based simulator inspired by FEops HEARTguide.
Real digital-twin tools run patient-specific finite-element simulation on
the segmented aortic root; ours combines published baseline rates with
CT-derived modifiers. Useful as a hackathon decision-support visualization,
not a substitute for FEops.
Baseline rates from VARC-3 contemporary literature
(`research/01-clinical-foundations.md`, table "Valve Types"):
| Family | 30-day PPM | Mod+ PVL |
| Balloon (SAPIEN 3 / Ultra) | 6–10% | 1–3% |
| Self-expanding supra (Evolut) | 12–17% | 3–5% |
| Self-expanding intra (Navitor) | 13–19% | 1–4% |
"""
from __future__ import annotations
from dataclasses import dataclass
from tavi_api.schemas import DeviceSimulation, PatientInput
@dataclass(frozen=True)
class _Profile:
valve: str
valve_class: str # one of the schema literals
ppm_baseline: float
pvl_baseline: float
mort_adjust: float
notes_static: tuple[str, ...]
_PROFILES: list[_Profile] = [
_Profile(
valve="Edwards SAPIEN 3 Ultra",
valve_class="balloon-expandable",
ppm_baseline=0.07,
pvl_baseline=0.02,
mort_adjust=1.00,
notes_static=(
"Intra-annular balloon-expandable; outer skirt reduces PVL.",
"PARTNER 3 (low risk) and PARTNER 2 (intermediate) trial heritage.",
),
),
_Profile(
valve="Medtronic Evolut FX+",
valve_class="self-expanding-supra-annular",
ppm_baseline=0.13,
pvl_baseline=0.04,
mort_adjust=1.00,
notes_static=(
"Supra-annular self-expanding; lower residual gradients.",
"Higher PPM rate due to deeper LVOT seating.",
),
),
_Profile(
valve="Abbott Navitor",
valve_class="self-expanding-intra-annular",
ppm_baseline=0.14,
pvl_baseline=0.02,
mort_adjust=1.05,
notes_static=(
"Intra-annular self-expanding with NaviSeal active sealing cuff.",
"Approved Jan 2023 for high/extreme-risk patients.",
),
),
]
def simulate_devices(
patient: PatientInput, mort_baseline: float
) -> list[DeviceSimulation]:
"""Return one DeviceSimulation per FDA-cleared TAVI family."""
results: list[DeviceSimulation] = []
has_ct = (
patient.annular_area_mm2 is not None
or patient.calcium_volume_au is not None
or patient.membranous_septum_length_mm is not None
)
for prof in _PROFILES:
ppm = prof.ppm_baseline
pvl = prof.pvl_baseline
mort = mort_baseline * prof.mort_adjust
notes: list[str] = list(prof.notes_static)
# CT-derived modifiers
if patient.membranous_septum_length_mm is not None:
ms = patient.membranous_septum_length_mm
if ms < 5.0 and prof.valve_class.startswith("self-expanding"):
ppm *= 1.5
notes.append(
f"Membranous septum {ms:.1f} mm < 5 mm β€” elevated PPM risk for "
"self-expanding deployment."
)
elif ms < 5.0:
ppm *= 1.2
notes.append(
f"Membranous septum {ms:.1f} mm < 5 mm β€” modestly elevated PPM risk."
)
if patient.calcium_volume_au is not None and patient.calcium_volume_au > 1000:
pvl *= 1.4
notes.append(
f"Calcium volume {patient.calcium_volume_au:.0f} AU β€” elevated PVL risk."
)
if (
patient.distance_to_left_main_mm is not None
and patient.distance_to_left_main_mm < 10
):
mort *= 1.10
notes.append(
f"Left-main height {patient.distance_to_left_main_mm:.1f} mm < 10 mm β€” "
"consider coronary protection."
)
# Clinical modifiers (used regardless of CT availability)
if patient.lvef_pct < 30:
mort *= 1.30
if patient.atrial_fibrillation:
ppm *= 1.05 # mild
results.append(
DeviceSimulation(
valve=prof.valve,
valve_class=prof.valve_class, # type: ignore[arg-type]
predicted_mortality_30d=round(min(mort, 0.50), 4),
predicted_ppm_30d=round(min(ppm, 0.60), 4),
predicted_pvl_moderate=round(min(pvl, 0.20), 4),
notes=notes,
)
)
if not has_ct:
for r in results:
r.notes.append(
"CT-derived features not provided β€” predictions based on clinical "
"variables and published baseline rates only."
)
return results