Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from dataclasses import dataclass, asdict
|
| 3 |
+
import gradio as gr
|
| 4 |
+
|
| 5 |
+
"""
|
| 6 |
+
FFR-CT AI ROI Calculator
|
| 7 |
+
- Built for Hugging Face Spaces (Gradio)
|
| 8 |
+
- Focus: Clinical, Financial, and Operational ROI
|
| 9 |
+
|
| 10 |
+
Assumptions & Notes
|
| 11 |
+
- "With AI" refers to adopting FFR-CT + (optionally) AI-QPA billing.
|
| 12 |
+
- "Without AI" baseline assumes a CCTA-first pathway without FFR-CT.
|
| 13 |
+
- Savings from reduced unnecessary diagnostic ICA leverage the provided 69% reduction figure,
|
| 14 |
+
scaled by adoption (uptake).
|
| 15 |
+
- Additional testing costs are approximated via stress-test costs when one-test diagnosis fails.
|
| 16 |
+
- Downstream revascularization (PCI/CABG) revenue is optional (off by default), given it depends on site type.
|
| 17 |
+
- Ops value accounts for 2 parts: (a) faster decision-making (bed-hours value) and (b) clinician touch-time reduction.
|
| 18 |
+
|
| 19 |
+
You can tune any assumptions in the Advanced section.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class Defaults:
|
| 24 |
+
# Volumes
|
| 25 |
+
monthly_eligible_ccta: int = 100 # Monthly eligible CCTA patients
|
| 26 |
+
uptake_pct: float = 60.0 # % of eligible getting FFR-CT (policy/site dependent)
|
| 27 |
+
|
| 28 |
+
# Reimbursement (USD)
|
| 29 |
+
reimb_ffrct: float = 1017.0
|
| 30 |
+
reimb_ai_qpa: float = 950.0
|
| 31 |
+
pct_billed_ai_qpa: float = 60.0 # % of FFR-CT also billed with AI-QPA
|
| 32 |
+
|
| 33 |
+
# Core performance
|
| 34 |
+
one_test_dx_pct: float = 97.0 # with FFR-CT (i.e., 3% need add'l testing)
|
| 35 |
+
dec_unnec_ica_pct: float = 69.0 # reduction in unnecessary diagnostic ICAs
|
| 36 |
+
more_likely_revasc_pct: float = 78.0 # relative lift in identifying revasc candidates
|
| 37 |
+
revasc_prevalence_pct: float = 10.0 # true prevalence among eligible
|
| 38 |
+
|
| 39 |
+
# Costs (USD)
|
| 40 |
+
vendor_per_case_cost: float = 350.0
|
| 41 |
+
platform_monthly_cost: float = 1000.0 # = $12k/yr
|
| 42 |
+
stress_test_cost: float = 400.0
|
| 43 |
+
net_cost_per_diag_ica: float = 5000.0 # allow negative if ICA is profit-positive
|
| 44 |
+
|
| 45 |
+
# Ops economics (USD)
|
| 46 |
+
bed_hour_value: float = 100.0
|
| 47 |
+
clinician_hour_cost: float = 150.0
|
| 48 |
+
|
| 49 |
+
# Time effects
|
| 50 |
+
ai_time_to_decision_min: float = 90.0 # faster time from CCTA -> decision (minutes)
|
| 51 |
+
clinician_touch_reduction_pct: float = 30.0
|
| 52 |
+
baseline_clinician_touch_min: float = 30.0
|
| 53 |
+
|
| 54 |
+
# Baseline (without AI) assumptions
|
| 55 |
+
baseline_diag_ica_rate_pct: float = 30.0 # % of eligible that go to diagnostic ICA
|
| 56 |
+
baseline_additional_testing_rate_pct: float = 30.0 # % needing add'l testing without FFR-CT
|
| 57 |
+
|
| 58 |
+
# Optional downstream value
|
| 59 |
+
include_revasc_value: bool = False
|
| 60 |
+
value_per_revasc: float = 0.0 # Only used if include_revasc_value is True
|
| 61 |
+
|
| 62 |
+
DEFAULTS = Defaults()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _pct(x):
|
| 66 |
+
return max(0.0, min(100.0, float(x)))
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _pos(x):
|
| 70 |
+
return max(0.0, float(x))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def compute_roi(
|
| 74 |
+
monthly_eligible_ccta,
|
| 75 |
+
uptake_pct,
|
| 76 |
+
reimb_ffrct,
|
| 77 |
+
reimb_ai_qpa,
|
| 78 |
+
pct_billed_ai_qpa,
|
| 79 |
+
one_test_dx_pct,
|
| 80 |
+
dec_unnec_ica_pct,
|
| 81 |
+
more_likely_revasc_pct,
|
| 82 |
+
revasc_prevalence_pct,
|
| 83 |
+
vendor_per_case_cost,
|
| 84 |
+
platform_monthly_cost,
|
| 85 |
+
stress_test_cost,
|
| 86 |
+
net_cost_per_diag_ica,
|
| 87 |
+
bed_hour_value,
|
| 88 |
+
clinician_hour_cost,
|
| 89 |
+
ai_time_to_decision_min,
|
| 90 |
+
clinician_touch_reduction_pct,
|
| 91 |
+
baseline_clinician_touch_min,
|
| 92 |
+
baseline_diag_ica_rate_pct,
|
| 93 |
+
baseline_additional_testing_rate_pct,
|
| 94 |
+
include_revasc_value,
|
| 95 |
+
value_per_revasc,
|
| 96 |
+
):
|
| 97 |
+
# Sanitize
|
| 98 |
+
monthly_eligible_ccta = _pos(monthly_eligible_ccta)
|
| 99 |
+
uptake = _pct(uptake_pct) / 100.0
|
| 100 |
+
reimb_ffrct = float(reimb_ffrct)
|
| 101 |
+
reimb_ai_qpa = float(reimb_ai_qpa)
|
| 102 |
+
pct_billed_ai_qpa = _pct(pct_billed_ai_qpa) / 100.0
|
| 103 |
+
one_test_dx = _pct(one_test_dx_pct) / 100.0
|
| 104 |
+
need_addl_with_ai = 1.0 - one_test_dx
|
| 105 |
+
dec_unnec_ica = _pct(dec_unnec_ica_pct) / 100.0
|
| 106 |
+
more_likely_revasc = _pct(more_likely_revasc_pct) / 100.0
|
| 107 |
+
revasc_prev = _pct(revasc_prevalence_pct) / 100.0
|
| 108 |
+
|
| 109 |
+
vendor_per_case_cost = float(vendor_per_case_cost)
|
| 110 |
+
platform_monthly_cost = float(platform_monthly_cost)
|
| 111 |
+
stress_test_cost = float(stress_test_cost)
|
| 112 |
+
net_cost_per_diag_ica = float(net_cost_per_diag_ica)
|
| 113 |
+
|
| 114 |
+
bed_hour_value = float(bed_hour_value)
|
| 115 |
+
clinician_hour_cost = float(clinician_hour_cost)
|
| 116 |
+
ai_time_to_decision_min = _pos(ai_time_to_decision_min)
|
| 117 |
+
clinician_touch_reduction_pct = _pct(clinician_touch_reduction_pct) / 100.0
|
| 118 |
+
baseline_clinician_touch_min = _pos(baseline_clinician_touch_min)
|
| 119 |
+
|
| 120 |
+
baseline_diag_ica_rate = _pct(baseline_diag_ica_rate_pct) / 100.0
|
| 121 |
+
baseline_additional_testing_rate = _pct(baseline_additional_testing_rate_pct) / 100.0
|
| 122 |
+
|
| 123 |
+
include_revasc_value = bool(include_revasc_value)
|
| 124 |
+
value_per_revasc = float(value_per_revasc)
|
| 125 |
+
|
| 126 |
+
# Volumes
|
| 127 |
+
annual_eligible = monthly_eligible_ccta * 12.0
|
| 128 |
+
annual_uptake_cases = annual_eligible * uptake
|
| 129 |
+
|
| 130 |
+
# Baseline (without AI)
|
| 131 |
+
baseline_additional_tests = annual_eligible * baseline_additional_testing_rate
|
| 132 |
+
baseline_additional_tests_cost = baseline_additional_tests * stress_test_cost
|
| 133 |
+
|
| 134 |
+
baseline_diag_ica_total = annual_eligible * baseline_diag_ica_rate
|
| 135 |
+
baseline_revasc_true = annual_eligible * revasc_prev
|
| 136 |
+
baseline_unnecessary_ica = max(0.0, baseline_diag_ica_total - baseline_revasc_true)
|
| 137 |
+
baseline_unnecessary_ica_cost = baseline_unnecessary_ica * net_cost_per_diag_ica
|
| 138 |
+
|
| 139 |
+
baseline_revenue = 0.0 # no FFR-CT/AI-QPA billing without AI
|
| 140 |
+
baseline_costs = baseline_additional_tests_cost + baseline_unnecessary_ica_cost
|
| 141 |
+
baseline_ops_value = 0.0
|
| 142 |
+
baseline_net = baseline_revenue - baseline_costs + baseline_ops_value
|
| 143 |
+
|
| 144 |
+
# With AI
|
| 145 |
+
with_ai_revenue = (
|
| 146 |
+
annual_uptake_cases * reimb_ffrct
|
| 147 |
+
+ annual_uptake_cases * pct_billed_ai_qpa * reimb_ai_qpa
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
with_ai_vendor_costs = annual_uptake_cases * vendor_per_case_cost
|
| 151 |
+
with_ai_platform_costs = platform_monthly_cost * 12.0
|
| 152 |
+
|
| 153 |
+
with_ai_additional_tests = annual_uptake_cases * need_addl_with_ai
|
| 154 |
+
with_ai_additional_tests_cost = with_ai_additional_tests * stress_test_cost
|
| 155 |
+
|
| 156 |
+
# Unnecessary ICA reduction scaled by uptake
|
| 157 |
+
avoided_unnec_ica = baseline_unnecessary_ica * dec_unnec_ica * uptake
|
| 158 |
+
with_ai_unnecessary_ica = max(0.0, baseline_unnecessary_ica - avoided_unnec_ica)
|
| 159 |
+
with_ai_unnecessary_ica_cost = with_ai_unnecessary_ica * net_cost_per_diag_ica
|
| 160 |
+
|
| 161 |
+
# Downstream revasc capture (optional; conservative default = off)
|
| 162 |
+
extra_revasc_identified = annual_eligible * revasc_prev * more_likely_revasc * uptake
|
| 163 |
+
revasc_value = extra_revasc_identified * value_per_revasc if include_revasc_value else 0.0
|
| 164 |
+
|
| 165 |
+
# Ops value
|
| 166 |
+
time_value = (annual_uptake_cases * (ai_time_to_decision_min / 60.0) * bed_hour_value)
|
| 167 |
+
clinician_time_saved_hours = annual_uptake_cases * (baseline_clinician_touch_min / 60.0) * clinician_touch_reduction_pct
|
| 168 |
+
clinician_time_value = clinician_time_saved_hours * clinician_hour_cost
|
| 169 |
+
|
| 170 |
+
with_ai_ops_value = time_value + clinician_time_value
|
| 171 |
+
|
| 172 |
+
with_ai_costs = with_ai_vendor_costs + with_ai_platform_costs + with_ai_additional_tests_cost + with_ai_unnecessary_ica_cost
|
| 173 |
+
with_ai_net = with_ai_revenue - with_ai_costs + with_ai_ops_value + revasc_value
|
| 174 |
+
|
| 175 |
+
# Incremental view (AI program impact)
|
| 176 |
+
incr_revenue = with_ai_revenue - baseline_revenue
|
| 177 |
+
incr_costs = with_ai_costs - baseline_costs
|
| 178 |
+
incr_ops_value = with_ai_ops_value - baseline_ops_value
|
| 179 |
+
incr_revasc_value = revasc_value
|
| 180 |
+
|
| 181 |
+
net_impact = incr_revenue - incr_costs + incr_ops_value + incr_revasc_value
|
| 182 |
+
|
| 183 |
+
ai_program_costs = with_ai_vendor_costs + with_ai_platform_costs # denominator for ROI
|
| 184 |
+
roi_pct = (net_impact / ai_program_costs * 100.0) if ai_program_costs > 0 else 0.0
|
| 185 |
+
|
| 186 |
+
# Payback
|
| 187 |
+
per_case_net_impact = (net_impact / annual_uptake_cases) if annual_uptake_cases > 0 else 0.0
|
| 188 |
+
cases_to_payback = (ai_program_costs / per_case_net_impact) if per_case_net_impact > 0 else math.inf
|
| 189 |
+
months_to_payback = (cases_to_payback / (monthly_eligible_ccta * uptake)) if (monthly_eligible_ccta * uptake) > 0 and cases_to_payback != math.inf else math.inf
|
| 190 |
+
|
| 191 |
+
# Build summaries
|
| 192 |
+
with_ai_summary = {
|
| 193 |
+
"Annual revenue (FFR-CT + AI-QPA)": with_ai_revenue,
|
| 194 |
+
"Program costs (vendor + platform)": with_ai_vendor_costs + with_ai_platform_costs,
|
| 195 |
+
"Add'l testing cost": with_ai_additional_tests_cost,
|
| 196 |
+
"Unnecessary ICA cost": with_ai_unnecessary_ica_cost,
|
| 197 |
+
"Ops value (time + clinician)": with_ai_ops_value,
|
| 198 |
+
"Downstream revasc value": revasc_value,
|
| 199 |
+
"Net (with AI)": with_ai_net,
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
without_ai_summary = {
|
| 203 |
+
"Annual revenue": baseline_revenue,
|
| 204 |
+
"Add'l testing cost": baseline_additional_tests_cost,
|
| 205 |
+
"Unnecessary ICA cost": baseline_unnecessary_ica_cost,
|
| 206 |
+
"Ops value": baseline_ops_value,
|
| 207 |
+
"Net (without AI)": baseline_net,
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
overall = {
|
| 211 |
+
"Incremental revenue": incr_revenue,
|
| 212 |
+
"Incremental costs (incl. tests/ICA)": incr_costs,
|
| 213 |
+
"Incremental ops value": incr_ops_value,
|
| 214 |
+
"Downstream revasc value": incr_revasc_value,
|
| 215 |
+
"Net impact": net_impact,
|
| 216 |
+
"ROI % (on AI program costs)": roi_pct,
|
| 217 |
+
"Cases to payback": cases_to_payback,
|
| 218 |
+
"Months to payback": months_to_payback,
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
# Pretty text blocks
|
| 222 |
+
def money(x):
|
| 223 |
+
if x == math.inf:
|
| 224 |
+
return "∞"
|
| 225 |
+
return f"${x:,.0f}"
|
| 226 |
+
|
| 227 |
+
def num(x):
|
| 228 |
+
if x == math.inf:
|
| 229 |
+
return "∞"
|
| 230 |
+
return f"{x:,.1f}"
|
| 231 |
+
|
| 232 |
+
with_ai_md = f"""
|
| 233 |
+
### With AI
|
| 234 |
+
- **Annual revenue (FFR-CT + AI-QPA):** {money(with_ai_revenue)}
|
| 235 |
+
- **Program costs (vendor + platform):** {money(with_ai_vendor_costs + with_ai_platform_costs)}
|
| 236 |
+
- **Add'l testing cost:** {money(with_ai_additional_tests_cost)}
|
| 237 |
+
- **Unnecessary ICA cost:** {money(with_ai_unnecessary_ica_cost)}
|
| 238 |
+
- **Ops value (time + clinician):** {money(with_ai_ops_value)}
|
| 239 |
+
- **Downstream revasc value:** {money(revasc_value)}
|
| 240 |
+
- **Net (with AI):** **{money(with_ai_net)}**
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
without_ai_md = f"""
|
| 244 |
+
### Without AI
|
| 245 |
+
- **Annual revenue:** {money(baseline_revenue)}
|
| 246 |
+
- **Add'l testing cost:** {money(baseline_additional_tests_cost)}
|
| 247 |
+
- **Unnecessary ICA cost:** {money(baseline_unnecessary_ica_cost)}
|
| 248 |
+
- **Ops value:** {money(baseline_ops_value)}
|
| 249 |
+
- **Net (without AI):** **{money(baseline_net)}**
|
| 250 |
+
"""
|
| 251 |
+
|
| 252 |
+
overall_md = f"""
|
| 253 |
+
### Overall Impact
|
| 254 |
+
- **Incremental revenue:** {money(incr_revenue)}
|
| 255 |
+
- **Incremental costs (incl. tests/ICA):** {money(incr_costs)}
|
| 256 |
+
- **Incremental ops value:** {money(incr_ops_value)}
|
| 257 |
+
- **Downstream revasc value:** {money(incr_revasc_value)}
|
| 258 |
+
- **Net impact:** **{money(net_impact)}**
|
| 259 |
+
- **ROI % (on AI program costs):** **{roi_pct:,.1f}%**
|
| 260 |
+
- **Cases to payback:** {num(cases_to_payback)}
|
| 261 |
+
- **Months to payback:** {num(months_to_payback)}
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
return with_ai_md, without_ai_md, overall_md
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def build_ui():
|
| 268 |
+
with gr.Blocks(theme=gr.themes.Soft(), css="""
|
| 269 |
+
.card {border: 1px solid #e5e7eb; border-radius: 16px; padding: 16px; box-shadow: 0 1px 2px rgba(0,0,0,0.05);}
|
| 270 |
+
.title {font-weight: 700; font-size: 1.1rem;}
|
| 271 |
+
.muted {color: #6b7280;}
|
| 272 |
+
""") as demo:
|
| 273 |
+
gr.Markdown("""
|
| 274 |
+
# FFR‑CT AI ROI Calculator
|
| 275 |
+
**For providers, imaging centers, and AMCs.**
|
| 276 |
+
Use your site's numbers or try the defaults. Results show **With AI**, **Without AI**, and **Overall** impact across **clinical, financial, and operational** dimensions.
|
| 277 |
+
""")
|
| 278 |
+
|
| 279 |
+
with gr.Row():
|
| 280 |
+
with gr.Column():
|
| 281 |
+
monthly_eligible_ccta = gr.Slider(
|
| 282 |
+
label="Monthly eligible CCTA volume",
|
| 283 |
+
minimum=0,
|
| 284 |
+
maximum=2000,
|
| 285 |
+
step=10,
|
| 286 |
+
value=DEFAULTS.monthly_eligible_ccta,
|
| 287 |
+
info="Approx. number of CCTA patients per month who would be eligible for FFR‑CT"
|
| 288 |
+
)
|
| 289 |
+
uptake_pct = gr.Slider(
|
| 290 |
+
label="Uptake (eligible getting FFR‑CT, %)",
|
| 291 |
+
minimum=0,
|
| 292 |
+
maximum=100,
|
| 293 |
+
step=1,
|
| 294 |
+
value=DEFAULTS.uptake_pct,
|
| 295 |
+
info="Share of eligible patients who actually get FFR‑CT"
|
| 296 |
+
)
|
| 297 |
+
with gr.Accordion("Reimbursement & Costs", open=True):
|
| 298 |
+
reimb_ffrct = gr.Number(label="Reimbursement: FFR‑CT ($)", value=DEFAULTS.reimb_ffrct, info="CMS 2025")
|
| 299 |
+
reimb_ai_qpa = gr.Number(label="Reimbursement: AI‑QPA ($)", value=DEFAULTS.reimb_ai_qpa, info="CMS 2025")
|
| 300 |
+
pct_billed_ai_qpa = gr.Slider(label="% billed AI‑QPA",
|
| 301 |
+
minimum=0, maximum=100, step=5, value=DEFAULTS.pct_billed_ai_qpa,
|
| 302 |
+
info="Share of FFR‑CT cases where AI‑QPA is billed")
|
| 303 |
+
vendor_per_case_cost = gr.Number(label="Vendor per‑case cost ($)", value=DEFAULTS.vendor_per_case_cost)
|
| 304 |
+
platform_monthly_cost = gr.Number(label="Platform cost / month ($)", value=DEFAULTS.platform_monthly_cost)
|
| 305 |
+
stress_test_cost = gr.Number(label="Stress test cost ($)", value=DEFAULTS.stress_test_cost)
|
| 306 |
+
net_cost_per_diag_ica = gr.Number(label="Net cost per diagnostic ICA ($)", value=DEFAULTS.net_cost_per_diag_ica,
|
| 307 |
+
info="Allow negative if ICA is net profit‑positive at your site")
|
| 308 |
+
|
| 309 |
+
with gr.Accordion("Clinical Performance & Ops (Advanced)", open=False):
|
| 310 |
+
one_test_dx_pct = gr.Slider(label="One‑test diagnosis with FFR‑CT (%)",
|
| 311 |
+
minimum=80, maximum=100, step=1, value=DEFAULTS.one_test_dx_pct,
|
| 312 |
+
info="Percent of cases resolved with CCTA+FFR‑CT, no extra tests")
|
| 313 |
+
dec_unnec_ica_pct = gr.Slider(label="Decrease in unnecessary diagnostic ICAs (%)",
|
| 314 |
+
minimum=0, maximum=100, step=1, value=DEFAULTS.dec_unnec_ica_pct,
|
| 315 |
+
info="Relative reduction vs baseline, scaled by uptake")
|
| 316 |
+
more_likely_revasc_pct = gr.Slider(label="More likely to identify revasc candidates (%)",
|
| 317 |
+
minimum=0, maximum=200, step=1, value=DEFAULTS.more_likely_revasc_pct,
|
| 318 |
+
info="Relative lift, used only if downstream value is enabled")
|
| 319 |
+
revasc_prevalence_pct = gr.Slider(label="True revasc prevalence among eligible (%)",
|
| 320 |
+
minimum=0, maximum=100, step=1, value=DEFAULTS.revasc_prevalence_pct)
|
| 321 |
+
|
| 322 |
+
bed_hour_value = gr.Number(label="Bed‑hour value ($/hour)", value=DEFAULTS.bed_hour_value)
|
| 323 |
+
clinician_hour_cost = gr.Number(label="Clinician hour cost ($/hour)", value=DEFAULTS.clinician_hour_cost)
|
| 324 |
+
ai_time_to_decision_min = gr.Number(label="AI time CCTA → decision saved (min)", value=DEFAULTS.ai_time_to_decision_min)
|
| 325 |
+
clinician_touch_reduction_pct = gr.Slider(label="Clinician touch‑time reduction with AI (%)",
|
| 326 |
+
minimum=0, maximum=100, step=5, value=DEFAULTS.clinician_touch_reduction_pct)
|
| 327 |
+
baseline_clinician_touch_min = gr.Number(label="Baseline clinician touch‑time per case (min)",
|
| 328 |
+
value=DEFAULTS.baseline_clinician_touch_min)
|
| 329 |
+
|
| 330 |
+
baseline_diag_ica_rate_pct = gr.Slider(label="Baseline diagnostic ICA rate (%)",
|
| 331 |
+
minimum=0, maximum=100, step=1, value=DEFAULTS.baseline_diag_ica_rate_pct,
|
| 332 |
+
info="Share of eligible patients who go to diagnostic ICA without AI")
|
| 333 |
+
baseline_additional_testing_rate_pct = gr.Slider(label="Baseline additional testing rate (%)",
|
| 334 |
+
minimum=0, maximum=100, step=1, value=DEFAULTS.baseline_additional_testing_rate_pct,
|
| 335 |
+
info="Share needing extra tests without AI")
|
| 336 |
+
|
| 337 |
+
include_revasc_value = gr.Checkbox(label="Include downstream revascularization value", value=DEFAULTS.include_revasc_value)
|
| 338 |
+
value_per_revasc = gr.Number(label="Value per revascularization captured ($)", value=DEFAULTS.value_per_revasc,
|
| 339 |
+
info="Set only if your site captures revasc value; else leave 0")
|
| 340 |
+
|
| 341 |
+
with gr.Column():
|
| 342 |
+
with gr.Row():
|
| 343 |
+
with_ai_card = gr.Markdown(elem_classes=["card"]) # With AI
|
| 344 |
+
without_ai_card = gr.Markdown(elem_classes=["card"]) # Without AI
|
| 345 |
+
overall_card = gr.Markdown(elem_classes=["card"]) # Overall impact
|
| 346 |
+
|
| 347 |
+
inputs = [
|
| 348 |
+
monthly_eligible_ccta,
|
| 349 |
+
uptake_pct,
|
| 350 |
+
reimb_ffrct,
|
| 351 |
+
reimb_ai_qpa,
|
| 352 |
+
pct_billed_ai_qpa,
|
| 353 |
+
one_test_dx_pct,
|
| 354 |
+
dec_unnec_ica_pct,
|
| 355 |
+
more_likely_revasc_pct,
|
| 356 |
+
revasc_prevalence_pct,
|
| 357 |
+
vendor_per_case_cost,
|
| 358 |
+
platform_monthly_cost,
|
| 359 |
+
stress_test_cost,
|
| 360 |
+
net_cost_per_diag_ica,
|
| 361 |
+
bed_hour_value,
|
| 362 |
+
clinician_hour_cost,
|
| 363 |
+
ai_time_to_decision_min,
|
| 364 |
+
clinician_touch_reduction_pct,
|
| 365 |
+
baseline_clinician_touch_min,
|
| 366 |
+
baseline_diag_ica_rate_pct,
|
| 367 |
+
baseline_additional_testing_rate_pct,
|
| 368 |
+
include_revasc_value,
|
| 369 |
+
value_per_revasc,
|
| 370 |
+
]
|
| 371 |
+
|
| 372 |
+
def _update(*args):
|
| 373 |
+
return compute_roi(*args)
|
| 374 |
+
|
| 375 |
+
# Initial compute & reactive updates
|
| 376 |
+
for comp in inputs:
|
| 377 |
+
comp.change(_update, inputs=inputs, outputs=[with_ai_card, without_ai_card, overall_card])
|
| 378 |
+
|
| 379 |
+
# Run once on load with defaults
|
| 380 |
+
demo.load(_update, inputs=inputs, outputs=[with_ai_card, without_ai_card, overall_card])
|
| 381 |
+
|
| 382 |
+
gr.Markdown(
|
| 383 |
+
"""
|
| 384 |
+
---
|
| 385 |
+
**How to use this app**
|
| 386 |
+
- Start with your **monthly eligible** volume and **uptake**.
|
| 387 |
+
- Check or edit **reimbursement** and **costs** if your site differs.
|
| 388 |
+
- Optionally expand **Advanced** to fine-tune baseline and ops assumptions.
|
| 389 |
+
- The cards update live with **With AI**, **Without AI**, and **Overall** impact, including **ROI** and **payback**.
|
| 390 |
+
"""
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
return demo
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def main():
|
| 397 |
+
demo = build_ui()
|
| 398 |
+
demo.queue().launch()
|
| 399 |
+
|
| 400 |
+
if __name__ == "__main__":
|
| 401 |
+
main()
|