# app.py import math import tempfile from pathlib import Path import gradio as gr # ========================================== # CARPL Multi-Use-Case ROI Calculators (MMG / FFR-CT / MSK AI) # - Consistent UI/UX across use cases # - Waterfall now colors COSTS in red automatically # - Monthly/Annual toggle (default: Annual) # - MSK hides zero/NA metrics # - ROI/Payback shown only when finite # ========================================== USE_CASES = ["Mammography AI (MMG)", "FFR-CT AI", "MSK AI (ER/Trauma)"] CTA_URL = "https://carpl.ai/contact-us" CTA_LABEL = "Book a 15-min walkthrough" MMG_VENDOR_PRESETS = { "Custom": {}, "Lunit": { "base_recall_rate": 0.028, "ai_recall_rate": 0.025, "base_ppr": 0.100, "ai_ppr": 0.095, "ai_audit_rate": 0.050, "base_audit_rate": 0.000, "read_reduction_pct": 0.15, "followup_uplift_pct": 0.0095, "early_detect_uplift_per_1000": 0.7, }, "Therapixel (MammoScreen)": { "base_recall_rate": 0.028, "ai_recall_rate": 0.024, "base_ppr": 0.100, "ai_ppr": 0.094, "ai_audit_rate": 0.040, "base_audit_rate": 0.000, "read_reduction_pct": 0.15, "followup_uplift_pct": 0.010, "early_detect_uplift_per_1000": 0.9, }, "MammoScreen (Alt)": { "base_recall_rate": 0.030, "ai_recall_rate": 0.026, "base_ppr": 0.100, "ai_ppr": 0.093, "ai_audit_rate": 0.045, "base_audit_rate": 0.000, "read_reduction_pct": 0.18, "followup_uplift_pct": 0.011, "early_detect_uplift_per_1000": 0.8, }, "MedCognetics": { "base_recall_rate": 0.029, "ai_recall_rate": 0.026, "base_ppr": 0.100, "ai_ppr": 0.096, "ai_audit_rate": 0.035, "base_audit_rate": 0.000, "read_reduction_pct": 0.14, "followup_uplift_pct": 0.009, "early_detect_uplift_per_1000": 0.6, }, } # ---------- Helpers ---------- def usd(x: float, digits: int = 0) -> str: if x == math.inf: return "∞" try: return "$" + f"{x:,.{digits}f}" except Exception: return "$0" def pct(x: float, digits: int = 1) -> str: try: return f"{x*100:.{digits}f}%" except Exception: return "0.0%" def clamp_nonneg(x: float) -> float: return max(0.0, float(x)) def safe_fraction(x: float) -> float: return max(0.0, min(1.0, float(x))) def write_csv(rows, title: str = "roi_results") -> str: """Write rows [(label, value), ...] to a temp CSV and return file path.""" csv_dir = Path(tempfile.gettempdir()) path = csv_dir / f"{title.replace(' ','_').lower()}.csv" with open(path, "w", encoding="utf-8") as f: f.write("Metric,Value\n") for lab, val in rows: val = str(val).replace("", "").replace("", "") f.write(f"\"{lab}\",\"{val}\"\n") return str(path) # ---------- MMG (Mammography) ---------- def compute_mmg( monthly_volume: float, read_minutes: float, radiologist_hourly_cost: float, # Sensitivity (advanced) base_ppr: float, ai_ppr: float, base_audit_rate: float, ai_audit_rate: float, base_recall_rate: float, ai_recall_rate: float, recall_cost_per_case: float, read_reduction_pct: float, base_cost_per_scan: float, cost_reduction_pct: float, followup_price: float, followup_uplift_pct: float, early_detect_uplift_per_1000: float, treatment_cost_delta_early_vs_late: float, # Backend program costs (hidden) vendor_per_case_fee: float, platform_annual_fee: float, integration_overhead_monthly: float, cloud_compute_monthly: float, ): monthly_ai_cases = clamp_nonneg(monthly_volume) annual_ai_cases = monthly_ai_cases * 12.0 # Clinical errors_reduced = clamp_nonneg(monthly_ai_cases * (base_ppr - ai_ppr)) discrepant_flags = clamp_nonneg(monthly_ai_cases * (ai_audit_rate - base_audit_rate)) recalls_avoided = clamp_nonneg(monthly_ai_cases * (base_recall_rate - ai_recall_rate)) earlier_detections = clamp_nonneg(monthly_ai_cases * (early_detect_uplift_per_1000 / 1000.0)) # Ops base_read_seconds = read_minutes * 60.0 hours_saved = clamp_nonneg(monthly_ai_cases * (base_read_seconds * read_reduction_pct) / 3600.0) workload_reduction_pct = read_reduction_pct fte_saved = hours_saved / 160.0 capacity_increase_pct = (1.0 / max(1e-6, (1.0 - read_reduction_pct)) - 1.0) value_time_saved_month = hours_saved * radiologist_hourly_cost # $$ baseline_monthly_cost = monthly_ai_cases * base_cost_per_scan new_monthly_cost = baseline_monthly_cost * (1.0 - cost_reduction_pct) per_scan_cost_savings_month = baseline_monthly_cost - new_monthly_cost addl_followups = monthly_ai_cases * followup_uplift_pct addl_followup_revenue_month = addl_followups * followup_price recall_cost_savings_month = recalls_avoided * recall_cost_per_case early_detection_savings_month = earlier_detections * treatment_cost_delta_early_vs_late vendor_cost_month = monthly_ai_cases * vendor_per_case_fee platform_cost_month = platform_annual_fee / 12.0 other_costs_month = integration_overhead_monthly + cloud_compute_monthly incr_revenue_month = addl_followup_revenue_month incr_costs_month = vendor_cost_month + platform_cost_month + other_costs_month ops_value_month = value_time_saved_month + per_scan_cost_savings_month + recall_cost_savings_month + early_detection_savings_month net_impact_month = incr_revenue_month - incr_costs_month + ops_value_month roi_pct_annual = ( (net_impact_month*12) / max(1e-6, (incr_costs_month*12)) ) months_to_payback = ( (platform_annual_fee + vendor_per_case_fee*annual_ai_cases + other_costs_month*12.0) / max(1e-6, (net_impact_month / max(1.0, monthly_ai_cases))) / max(1.0, monthly_ai_cases) ) evidence = """ """ clinical_bullet = ( f"~{int(round(recalls_avoided))} recalls avoided, " f"{earlier_detections:.1f} earlier cancers detected, " f"{int(round(errors_reduced))} fewer missed positives" ) return { "summary": f"For your practice with {int(monthly_volume):,} mammography scans/month, modeled net benefit is {usd(net_impact_month)} per month. Clinical: {clinical_bullet}.", "financial": { "rows": [ ("Additional follow-up scans (count/mo)", f"{int(round(addl_followups))}"), ("Additional follow-up revenue (mo)", usd(addl_followup_revenue_month)), ("Value of time saved (mo)", usd(value_time_saved_month)), ("Per-scan radiologist cost savings (mo)", usd(per_scan_cost_savings_month)), ("Savings from avoided recalls (mo)", usd(recall_cost_savings_month)), ("Savings from earlier detection (mo)", usd(early_detection_savings_month)), ("AI vendor fees (mo)", usd(vendor_cost_month)), ("Platform license (mo)", usd(platform_cost_month)), ("Integration & cloud (mo)", usd(other_costs_month)), ("Net impact (mo)", f"{usd(net_impact_month)}"), ("Net impact (annual)", f"{usd(net_impact_month*12)}"), ("ROI % (annual)", f"{roi_pct_annual*100:.1f}%"), ("Months to payback", f"{months_to_payback:.1f}"), ] }, "clinical": { "rows": [ ("Fewer missed positives (Δ pickup)", f"{int(round(errors_reduced))} /mo"), ("Discrepant cases flagged (audit uplift)", f"{int(round(discrepant_flags))} /mo"), ("Earlier cancers detected", f"{earlier_detections:.1f} /mo"), ("Recalls avoided", f"{int(round(recalls_avoided))} /mo"), ], "bars": [ ("Recall reduction", max(0.0, base_recall_rate - ai_recall_rate)), ("Pickup improvement", max(0.0, base_ppr - ai_ppr)), ] }, "operational": { "rows": [ ("Hours saved / month", f"{hours_saved:.1f}"), ("Workload reduction", pct(workload_reduction_pct)), ("Approx. FTE-month saved", f"{fte_saved:.2f}"), ("Effective capacity increase", pct(capacity_increase_pct)), ] }, "waterfall_monthly": [("Incremental revenue", incr_revenue_month), ("Incremental costs", -incr_costs_month), ("Operational value", ops_value_month)], "annual_card": { "incr_rev": incr_revenue_month * 12.0, "incr_costs": incr_costs_month * 12.0, "ops_value": ops_value_month * 12.0, "net": net_impact_month * 12.0, "roi_pct": roi_pct_annual, "payback": months_to_payback, }, "evidence": evidence, } # ---------- FFR-CT ---------- def compute_ffrct( site_type: str, monthly_eligible_ccta: float, uptake_pct: float, avg_time_to_decision_today_hours: float, baseline_clinician_touch_min: float, reimb_ccta: float, reimb_ffrct: float, reimb_ai_qpa: float, pct_billed_ai_qpa: float, one_test_dx_pct: float, dec_unnec_ica_pct: float, more_likely_revasc_pct: float, revasc_prevalence_pct: float, vendor_per_case_cost: float, platform_annual_cost: float, stress_test_cost: float, bed_hour_value: float, clinician_hour_cost: float, ai_time_to_decision_min: float, clinician_touch_reduction_pct: float, baseline_diag_ica_rate_pct: float, baseline_additional_testing_rate_pct: float, sens_uptake_factor_pct: float, sens_dec_unnec_ica_factor_pct: float, sens_vendor_cost_factor_pct: float, ): if site_type == "Hospital / Health System": net_cost_per_diag_ica = 5000.0 elif site_type == "Imaging Center": net_cost_per_diag_ica = 2000.0 else: net_cost_per_diag_ica = 4000.0 monthly_eligible_ccta = clamp_nonneg(monthly_eligible_ccta) uptake = safe_fraction(uptake_pct/100.0) * safe_fraction(sens_uptake_factor_pct/100.0) annual_eligible = monthly_eligible_ccta * 12.0 annual_ai_cases = annual_eligible * uptake pct_ai_qpa = safe_fraction(pct_billed_ai_qpa/100.0) one_test_dx = safe_fraction(one_test_dx_pct/100.0) dec_unnec_ica = safe_fraction(dec_unnec_ica_pct/100.0) * safe_fraction(sens_dec_unnec_ica_factor_pct/100.0) more_likely_revasc = safe_fraction(more_likely_revasc_pct/100.0) revasc_prev = safe_fraction(revasc_prevalence_pct/100.0) vendor_cost = float(vendor_per_case_cost) * safe_fraction(sens_vendor_cost_factor_pct/100.0) platform_annual_cost = float(platform_annual_cost) stress_test_cost = float(stress_test_cost) # Baseline (annual) baseline_revenue = annual_eligible * reimb_ccta baseline_additional_tests = annual_eligible * safe_fraction(baseline_additional_testing_rate_pct/100.0) baseline_additional_tests_cost = baseline_additional_tests * stress_test_cost baseline_diag_ica_total = annual_eligible * safe_fraction(baseline_diag_ica_rate_pct/100.0) baseline_revasc_true = annual_eligible * revasc_prev baseline_unnecessary_ica = max(0.0, baseline_diag_ica_total - baseline_revasc_true) baseline_unnecessary_ica_cost = baseline_unnecessary_ica * net_cost_per_diag_ica baseline_ops_value = 0.0 baseline_costs = baseline_additional_tests_cost + baseline_unnecessary_ica_cost # With AI (annual) with_ai_revenue = ( annual_eligible * reimb_ccta + annual_ai_cases * reimb_ffrct + annual_ai_cases * pct_ai_qpa * reimb_ai_qpa ) with_ai_vendor_costs = annual_ai_cases * vendor_cost with_ai_platform_costs = platform_annual_cost baseline_addl_tests_in_ai_cohort = annual_ai_cases * safe_fraction(baseline_additional_testing_rate_pct/100.0) with_ai_additional_tests = annual_ai_cases * (1.0 - one_test_dx) with_ai_additional_tests_cost = with_ai_additional_tests * stress_test_cost avoided_additional_tests = max(0.0, baseline_addl_tests_in_ai_cohort - with_ai_additional_tests) avoided_unnec_ica = baseline_unnecessary_ica * dec_unnec_ica * (annual_ai_cases / annual_eligible if annual_eligible > 0 else 0.0) with_ai_unnecessary_ica = max(0.0, baseline_unnecessary_ica - avoided_unnec_ica) with_ai_unnecessary_ica_cost = with_ai_unnecessary_ica * net_cost_per_diag_ica ai_saved_hours_per_case = min(max(0.0, ai_time_to_decision_min/60.0), max(0.0, float(avg_time_to_decision_today_hours))) bed_hours_saved = annual_ai_cases * ai_saved_hours_per_case bed_hours_value = bed_hours_saved * bed_hour_value clinician_hours_saved = annual_ai_cases * max(0.0, float(baseline_clinician_touch_min)/60.0) * safe_fraction(clinician_touch_reduction_pct/100.0) clinician_hours_value = clinician_hours_saved * clinician_hour_cost with_ai_ops_value = bed_hours_value + clinician_hours_value with_ai_costs = with_ai_vendor_costs + with_ai_platform_costs + with_ai_additional_tests_cost + with_ai_unnecessary_ica_cost incr_revenue = with_ai_revenue - baseline_revenue incr_costs = with_ai_costs - baseline_costs incr_ops = with_ai_ops_value - baseline_ops_value net_impact = incr_revenue - incr_costs + incr_ops ai_program_costs = with_ai_vendor_costs + with_ai_platform_costs roi_pct_val = net_impact / max(1e-6, ai_program_costs) per_case_net_impact = (net_impact / annual_ai_cases) if annual_ai_cases > 0 else 0.0 cases_to_payback = (ai_program_costs / max(1e-6, per_case_net_impact)) if per_case_net_impact > 0 else math.inf months_to_payback = (cases_to_payback / (monthly_eligible_ccta * uptake)) if (monthly_eligible_ccta * uptake) > 0 else math.inf evidence = """ """ return { "summary": f"For your program with {int(annual_ai_cases):,} AI cases/year, modeled net impact is {usd(net_impact)} annually.", "financial": { "rows": [ ("Incremental revenue (annual)", usd(incr_revenue)), ("Incremental costs (annual)", usd(incr_costs)), ("Operational value (annual)", usd(incr_ops)), ("AI program costs (annual)", usd(ai_program_costs)), ("Net impact (annual)", f"{usd(net_impact)}"), ("ROI % (annual on AI program)", f"{roi_pct_val*100:.1f}%"), ("Months to payback", f"{'∞' if months_to_payback==math.inf else f'{months_to_payback:.1f}'}"), ] }, "clinical": { "rows": [ ("Avoided unnecessary ICAs (est.)", f"{int(round(avoided_unnec_ica)):,} /yr"), ("One-test diagnosis rate (AI cohort)", f"{one_test_dx*100:.0f}%"), ("Added revasc candidates (est.)", f"{int(round(annual_eligible * revasc_prev * uptake * more_likely_revasc)):,} /yr"), ("Avoided extra tests (est.)", f"{int(round(avoided_additional_tests)):,} /yr"), ], "bars": [ ("Unnecessary ICA reduction", dec_unnec_ica), ("One-test diagnosis", one_test_dx), ] }, "operational": { "rows": [ ("Avg hours saved per case", f"{ai_saved_hours_per_case:.2f}"), ("Bed-hours saved", f"{int(round(bed_hours_saved)):,} hrs/yr"), ("Value of bed-hours", usd(bed_hours_value)), ("Clinician hours saved", f"{int(round(clinician_hours_saved)):,} hrs/yr"), ("Value of clinician time", usd(clinician_hours_value)), ] }, "waterfall_annual": [("Incremental revenue", incr_revenue), ("Incremental costs", -incr_costs), ("Operational value", incr_ops)], "annual_card": { "incr_rev": incr_revenue, "incr_costs": incr_costs, "ops_value": incr_ops, "net": net_impact, "roi_pct": roi_pct_val, "payback": months_to_payback, }, "evidence": evidence, } # ---------- MSK (ER/Trauma) ---------- def compute_msk( scans_per_day: float, reading_time_min: float, er_time_to_treatment_min: float, radiologist_hourly_cost: float = 180.0, ): scans_per_month = clamp_nonneg(scans_per_day) * 30.0 # Clinical volumes (illustrative) errors_reduced_per_month = int(scans_per_month * 0.05 * 0.20) # incidence × improvement discrepant_cases_flagged = int(scans_per_month * 0.05) hrs_saved_per_visit = max(0.0, er_time_to_treatment_min) * 0.50 / 60.0 # Operational value time_saved_per_scan_min = max(0.0, reading_time_min) * 0.30 total_time_saved_hours = (scans_per_month * time_saved_per_scan_min) / 60.0 value_time_saved_month = total_time_saved_hours * radiologist_hourly_cost radiologist_cost_savings = scans_per_month * 4.0 # conservative proxy ops_value_month = value_time_saved_month + radiologist_cost_savings # Financial: by default not counted incr_revenue_month = 0.0 incr_costs_month = 0.0 net_impact_month = incr_revenue_month - incr_costs_month + ops_value_month evidence = """ """ # Conditional rows (hide zeros) fin_rows = [] if incr_revenue_month != 0: fin_rows.append(("Incremental revenue (mo)", usd(incr_revenue_month))) if incr_costs_month != 0: fin_rows.append(("Incremental costs (mo)", usd(incr_costs_month))) fin_rows.append(("Net impact (mo)", f"{usd(net_impact_month)}")) clin_rows = [] if errors_reduced_per_month > 0: clin_rows.append(("Errors reduced (est.)", f"{errors_reduced_per_month} /mo")) if discrepant_cases_flagged > 0: clin_rows.append(("Discrepant cases flagged (est.)", f"{discrepant_cases_flagged} /mo")) if hrs_saved_per_visit > 0: clin_rows.append(("Hours saved per ED visit (modeled)", f"{hrs_saved_per_visit:.2f}")) op_rows = [] if total_time_saved_hours > 0: op_rows.append(("Radiologist hours saved / month", f"{total_time_saved_hours:.1f}")) if value_time_saved_month > 0: op_rows.append(("Value of radiologist time saved (mo)", usd(value_time_saved_month))) if radiologist_cost_savings > 0: op_rows.append(("Radiologist cost proxy savings (mo)", usd(radiologist_cost_savings))) # Waterfall (monthly), include only nonzero components wf_rows = [] if incr_revenue_month != 0: wf_rows.append(("Incremental revenue", incr_revenue_month)) if incr_costs_month != 0: wf_rows.append(("Incremental costs", -incr_costs_month)) if ops_value_month != 0: wf_rows.append(("Operational value", ops_value_month)) if not wf_rows: wf_rows = [("Operational value", ops_value_month)] annual_card = { "incr_rev": incr_revenue_month * 12.0, "incr_costs": incr_costs_month * 12.0, "ops_value": ops_value_month * 12.0, "net": net_impact_month * 12.0, "roi_pct": None, # hidden "payback": None, # hidden } return { "summary": f"For your ED with ~{int(scans_per_month):,} MSK scans/month, modeled net benefit is {usd(net_impact_month)} per month.", "financial": {"rows": fin_rows}, "clinical": {"rows": clin_rows, "bars": [("Touch-time reduction", 0.30)] if time_saved_per_scan_min > 0 else []}, "operational": {"rows": op_rows}, "waterfall_monthly": wf_rows, "annual_card": annual_card, "evidence": evidence, } # ---------- Card / HTML builders ---------- def build_overall_card(title: str, summary_line: str, annual: dict): rows = [] if "incr_rev" in annual and annual["incr_rev"] != 0: rows.append(("Incremental revenue (annual)", f"{usd(annual['incr_rev'])}")) if "incr_costs" in annual and annual["incr_costs"] != 0: rows.append(("Incremental costs (annual)", f"{usd(annual['incr_costs'])}")) if "ops_value" in annual and annual["ops_value"] != 0: rows.append(("Operational value (annual)", f"{usd(annual['ops_value'])}")) if "net" in annual: rows.append(("Net impact (annual)", f"{usd(annual['net'])}")) roi = annual.get("roi_pct", None) if isinstance(roi, (int, float)) and math.isfinite(roi): rows.append(("ROI %", f"{roi*100:.1f}%")) payback = annual.get("payback", None) if isinstance(payback, (int, float)) and math.isfinite(payback): rows.append(("Months to payback", f"{payback:.1f}")) items = "".join(f"
{lab}
{val}
" for lab, val in rows) return f"""
{title}
Clinical · Financial · Operational
{summary_line}
{items}
""" def build_rows_card(title: str, rows): items = "".join(f"
{lab}
{val}
" for lab, val in rows) return f"
{title}
{items}
" def build_clinical_card(rows, bars): rows_html = "".join(f"
{lab}
{val}
" for lab, val in rows) bars_html = "" for lab, frac in (bars or []): frac = max(0.0, min(1.0, float(frac))) if frac <= 0: continue bars_html += f"""
{lab}
{frac*100:.1f}%
{frac*100:.1f}%
""" bars_section = f"
{bars_html}
" if bars_html else "" return f"
Clinical
{rows_html}
{bars_section}
" def build_waterfall(wf_rows, period_label="Annual"): """ wf_rows: list of (label, value) where value is signed. Colors: positive=green, negative=red (costs). """ # remove exact zero bars wf_rows = [(l, v) for (l, v) in wf_rows if abs(float(v)) > 1e-9] if not wf_rows: return "
Waterfall ({})
".format(period_label) denom = sum(abs(v) for _, v in wf_rows) or 1.0 def row(label, val): width = min(100, max(2, int(abs(val)/denom * 100))) cls = "wf-pos" if val >= 0 else "wf-neg" return f"
{label}
{usd(val)}
" net_total = sum(v for _, v in wf_rows) return "
Waterfall ({})
{}
Net impact: {}
".format( period_label, "".join(row(l, v) for l, v in wf_rows), usd(net_total) ) def rows_to_csv(fin_rows, clin_rows, op_rows) -> str: all_rows = [("Section","—")] + [("Financial","—")] + fin_rows + [("Clinical","—")] + clin_rows + [("Operational","—")] + op_rows return write_csv(all_rows, title="roi_results") # ---------- UI ---------- def build_ui(): with gr.Blocks(theme=gr.themes.Soft(), css=""" @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap'); * { font-family: Inter, ui-sans-serif, system-ui; } .gradio-container { max-width: 1120px !important; } .card{background:#fff;border:1px solid #eef2f7;border-radius:18px;padding:18px;box-shadow:0 8px 24px rgba(0,0,0,.08);margin-bottom:12px} .card-title{font-weight:700;margin-bottom:6px} .pill{background:#ecfdf5;color:#065f46;padding:4px 10px;border-radius:999px;font-weight:700;font-size:.75rem} .kpi-grid{display:grid;grid-template-columns:1fr auto;gap:6px 12px} .neg{color:#b91c1c} .sumline{margin-bottom:8px;opacity:.9} .small-note{opacity:.75;font-size:.9em;margin-top:6px} .bars .bar-row{display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:center;margin:6px 0} .bars .bar{height:12px;background:#f1f5f9;border-radius:999px;position:relative;overflow:hidden;width:100%} .bars .bar span{display:block;height:100%;background:linear-gradient(90deg,#14b8a6,#22d3ee);position:relative} .bars .bar span em{position:absolute;right:6px;top:-18px;font-size:.85em;opacity:.8} .wf-row{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;margin:8px 0} .wf-bar{height:22px;border-radius:6px;display:flex;align-items:center;justify-content:flex-end;padding-right:8px;color:#0b1727;min-width:80px} .wf-bar .wf-val{font-weight:700} .wf-pos{background:linear-gradient(90deg,#a7f3d0,#34d399)} /* green */ .wf-neg{background:linear-gradient(90deg,#fecaca,#f87171)} /* red for costs/negatives */ .wf-total{margin-top:8px;border-top:1px solid #e5e7eb;padding-top:8px;font-weight:700} .cta{display:flex;justify-content:space-between;align-items:center} .cta-btn{background:#0ea5e9;color:#fff;text-decoration:none;padding:10px 14px;border-radius:12px;font-weight:700} .header{display:flex;justify-content:space-between;align-items:center} .header .title{font-weight:800;font-size:1.1rem} .header .pill{margin-left:8px} /* Use-case chooser as cards */ #uc [role="radiogroup"] { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 12px; } @media (max-width: 820px) { #uc [role="radiogroup"] { grid-template-columns: 1fr; } } #uc [role="radiogroup"] label { border: 1px solid #eef2f7; border-radius: 16px; background: #fff; padding: 14px 16px; box-shadow: 0 8px 24px rgba(0,0,0,.06); cursor: pointer; display: flex; align-items: center; gap: 10px; transition: transform .06s ease, box-shadow .12s ease, border-color .12s ease; } #uc [role="radiogroup"] input[type="radio"] { accent-color: #14b8a6; } #uc [role="radiogroup"] label:hover { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(0,0,0,.10); } #uc [role="radiogroup"] input[type="radio"]:checked + span { font-weight: 700; } #uc .uc-hint { margin-top: 6px; opacity: .7; font-size: .9em; } """) as demo: gr.Markdown("""
CARPL ROI Calculator · Multi-Use-Case
Clinical · Financial · Operational
""") with gr.Row(): with gr.Column(scale=1): # Use case chooser with gr.Column(elem_id="uc"): use_case = gr.Radio( choices=[ "🩺 Mammography AI (MMG)", "❤️‍🩹 FFR-CT AI", "🦴 MSK AI (ER/Trauma)" ], value="🩺 Mammography AI (MMG)", label="Choose your use case", interactive=True ) period = gr.Radio( choices=["Annual", "Monthly"], value="Annual", label="Reporting period", interactive=True ) gr.Markdown("
Pick a calculator and reporting period. You can switch anytime. Click the 'Calculate' button when you are ready!
") # --- MMG Inputs --- vendor_preset = gr.Dropdown( choices=list(MMG_VENDOR_PRESETS.keys()), value="Custom", label="MMG vendor preset", info="Prefills hidden sensitivity. You can still override in Sensitivity.", visible=True ) mmg_monthly_volume = gr.Slider(0, 20000, 7500, step=50, label="Monthly volume (MMG)", info="Mammography exams per month", visible=True) mmg_read_minutes = gr.Number(label="Avg reading time today (minutes)", value=1.7, visible=True) mmg_rdx_hr_cost = gr.Number(label="Radiologist cost (USD/hr)", value=180, visible=True) with gr.Accordion("Sensitivity (MMG)", open=False, visible=True) as mmg_sens: mmg_base_ppr = gr.Slider(0, 1, value=0.10, step=0.001, label="Baseline positive pickup rate") mmg_ai_ppr = gr.Slider(0, 1, value=0.095, step=0.001, label="With-AI positive pickup rate") mmg_base_audit = gr.Slider(0, 1, value=0.00, step=0.001, label="Baseline audit flag rate") mmg_ai_audit = gr.Slider(0, 1, value=0.05, step=0.001, label="With-AI audit flag rate") mmg_base_recall = gr.Slider(0, 1, value=0.028, step=0.001, label="Baseline recall rate") mmg_ai_recall = gr.Slider(0, 1, value=0.025, step=0.001, label="With-AI recall rate") mmg_recall_cost = gr.Number(value=250.0, label="Cost per recall case (USD)") mmg_read_redux = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Reading time reduction with AI (fraction)") mmg_cost_per_scan = gr.Number(value=15, label="Radiologist cost per scan (USD)") mmg_cost_redux = gr.Slider(0, 0.8, value=0.15, step=0.005, label="Per-scan radiologist cost reduction") mmg_follow_price = gr.Number(value=200, label="Price per follow-up scan (USD)") mmg_follow_uplift = gr.Slider(0, 0.2, value=0.0095, step=0.0005, label="Follow-up uplift (fraction of AI scans)") mmg_early_uplift_per_1000 = gr.Number(value=0.7, label="Earlier cancers detected (+/1000 AI scans)") mmg_tx_delta = gr.Number(value=15000.0, label="Treatment cost savings per earlier case (USD)") mmg_vendor_fee = gr.State(2.5) mmg_platform_annual = gr.State(12000.0) mmg_integration_mo = gr.State(0.0) mmg_cloud_mo = gr.State(0.0) # --- FFR-CT Inputs --- f_site = gr.Dropdown(["Hospital / Health System","Imaging Center","Academic Medical Center"], value="Hospital / Health System", label="Site type (FFR-CT)", visible=False) f_monthly_eligible = gr.Slider(0, 5000, 100, step=10, label="Monthly eligible CCTA", visible=False) f_uptake = gr.Slider(0, 100, 60, step=1, label="Uptake (%)", visible=False) f_ttd_hours = gr.Number(label="Avg time-to-decision today (hours)", value=8, visible=False) f_touch_min = gr.Number(label="Clinician touch-time per case (min)", value=30, visible=False) with gr.Accordion("Assumptions (FFR-CT)", open=False, visible=False) as f_assump: f_reimb_ccta = gr.Number(value=400, label="CCTA reimbursement (USD)") f_reimb_ffr = gr.Number(value=1017, label="FFR-CT reimbursement (USD)") f_reimb_aiqpa= gr.Number(value=950, label="AI-QPA reimbursement (USD)") f_pct_aiqpa = gr.Slider(0, 100, 60, step=1, label="% billed AI-QPA") f_one_test = gr.Slider(0, 100, 97, step=1, label="One-test Dx (%)") f_dec_unnec_ica = gr.Slider(0, 100, 69, step=1, label="Unnecessary ICA reduction (%)") f_more_revasc = gr.Slider(0, 100, 78, step=1, label="More likely revasc (%)") f_revasc_prev = gr.Slider(0, 100, 10, step=1, label="Revasc prevalence (%)") f_vendor_cost = gr.Number(value=350, label="Vendor per-case cost (USD)") f_platform_annual = gr.Number(value=12000, label="Platform annual (USD)") f_stress_cost = gr.Number(value=400, label="Non-invasive test cost (USD)") f_bed_hr_val = gr.Number(value=100, label="Bed-hour value (USD)") f_clin_hr_cost = gr.Number(value=150, label="Clinician hr cost (USD)") f_ai_ttd_min = gr.Number(value=90, label="AI time-to-decision saved (min)") f_touch_redux = gr.Slider(0, 100, 30, step=1, label="Clinician touch reduction (%)") f_base_diag_ica = gr.Slider(0, 100, 30, step=1, label="Baseline diagnostic ICA rate (%)") f_base_addl_test= gr.Slider(0, 100, 30, step=1, label="Baseline additional testing rate (%)") f_sens_uptake = gr.Slider(0, 200, 100, step=5, label="Sensitivity: Uptake factor (%)") f_sens_dec_ica = gr.Slider(0, 200, 100, step=5, label="Sensitivity: ICA reduction factor (%)") f_sens_vendor = gr.Slider(0, 200, 100, step=5, label="Sensitivity: Vendor cost factor (%)") # --- MSK Inputs --- msk_scans_day = gr.Number(label="Scans per day (MSK)", value=100, visible=False) msk_read_min = gr.Number(label="Radiologist time per scan (min)", value=3, visible=False) msk_er_ttt_min= gr.Number(label="ED time to treatment (min)", value=60, visible=False) msk_rdx_hr_cost = gr.State(180.0) run_btn = gr.Button("Calculate", variant="primary") with gr.Column(scale=1): overall_card = gr.HTML() with gr.Tabs(): with gr.Tab("Financial"): financial_card = gr.HTML() with gr.Tab("Clinical"): clinical_card = gr.HTML() with gr.Tab("Operational"): operational_card = gr.HTML() waterfall_panel = gr.HTML() evidence_panel = gr.HTML() cta_panel = gr.HTML(visible=False) csv_file = gr.File(label="Download CSV", visible=False) def normalize_uc(uclabel: str) -> str: if "Mammography" in uclabel: return USE_CASES[0] if "FFR" in uclabel: return USE_CASES[1] return USE_CASES[2] def _on_use_case_change(uc_label): uc = normalize_uc(uc_label) mmg_vis = (uc == USE_CASES[0]) f_vis = (uc == USE_CASES[1]) msk_vis = (uc == USE_CASES[2]) return ( gr.update(visible=mmg_vis), # vendor preset gr.update(visible=mmg_vis), gr.update(visible=mmg_vis), gr.update(visible=mmg_vis), gr.update(visible=mmg_vis), # accordion gr.update(visible=f_vis), gr.update(visible=f_vis), gr.update(visible=f_vis), gr.update(visible=f_vis), gr.update(visible=f_vis), gr.update(visible=f_vis), # accordion gr.update(visible=msk_vis), gr.update(visible=msk_vis), gr.update(visible=msk_vis), ) use_case.change( _on_use_case_change, inputs=[use_case], outputs=[ vendor_preset, mmg_monthly_volume, mmg_read_minutes, mmg_rdx_hr_cost, mmg_sens, f_site, f_monthly_eligible, f_uptake, f_ttd_hours, f_touch_min, f_assump, msk_scans_day, msk_read_min, msk_er_ttt_min ], ) def _apply_vendor_preset(preset_name, base_ppr, ai_ppr, base_audit, ai_audit, base_recall, ai_recall, read_redux, follow_uplift, early_uplift): p = MMG_VENDOR_PRESETS.get(preset_name, {}) return ( gr.update(value=p.get("base_ppr", base_ppr)), gr.update(value=p.get("ai_ppr", ai_ppr)), gr.update(value=p.get("base_audit_rate", base_audit)), gr.update(value=p.get("ai_audit_rate", ai_audit)), gr.update(value=p.get("base_recall_rate", base_recall)), gr.update(value=p.get("ai_recall_rate", ai_recall)), gr.update(value=p.get("read_reduction_pct", read_redux)), gr.update(value=p.get("followup_uplift_pct", follow_uplift)), gr.update(value=p.get("early_detect_uplift_per_1000", early_uplift)), ) vendor_preset.change( _apply_vendor_preset, inputs=[vendor_preset, mmg_base_ppr, mmg_ai_ppr, mmg_base_audit, mmg_ai_audit, mmg_base_recall, mmg_ai_recall, mmg_read_redux, mmg_follow_uplift, mmg_early_uplift_per_1000], outputs=[mmg_base_ppr, mmg_ai_ppr, mmg_base_audit, mmg_ai_audit, mmg_base_recall, mmg_ai_recall, mmg_read_redux, mmg_follow_uplift, mmg_early_uplift_per_1000], ) def _compute( uclabel, period_sel, # MMG mv, rm, rhr, base_ppr, ai_ppr, base_audit, ai_audit, base_recall, ai_recall, recall_cost, read_redux, cps, cps_redux, fol_price, fol_uplift, early_uplift, tx_delta, v_fee, p_annual, integ_mo, cloud_mo, # FFR-CT s_type, ccta_mo, uptake, ttd_h, touch_min, r_ccta, r_ffr, r_aiqpa, pct_aiqpa, one_test, dec_ica, more_revasc, revasc_prev, v_per_case, p_ann, stress_cost, bed_hr_val, clin_hr_cost, ai_ttd_min, touch_redux, base_diag_ica, base_addl_test, sens_upt, sens_dec, sens_vendor, # MSK msk_day, msk_read, msk_ttt, msk_rhr, ): uc = normalize_uc(uclabel) annual = (period_sel == "Annual") if uc == USE_CASES[0]: res = compute_mmg( mv, rm, rhr, base_ppr, ai_ppr, base_audit, ai_audit, base_recall, ai_recall, recall_cost, read_redux, cps, cps_redux, fol_price, fol_uplift, early_uplift, tx_delta, v_fee, p_annual, integ_mo, cloud_mo, ) title = "Overall Impact — Mammography AI" # Waterfall source (monthly), scale if annual wf = res["waterfall_monthly"] wf = [(l, v*12.0) for (l, v) in wf] if annual else wf elif uc == USE_CASES[1]: res = compute_ffrct( s_type, ccta_mo, uptake, ttd_h, touch_min, r_ccta, r_ffr, r_aiqpa, pct_aiqpa, one_test, dec_ica, more_revasc, revasc_prev, v_per_case, p_ann, stress_cost, bed_hr_val, clin_hr_cost, ai_ttd_min, touch_redux, base_diag_ica, base_addl_test, sens_upt, sens_dec, sens_vendor, ) title = "Overall Impact — FFR-CT AI" # Waterfall source (annual), scale if monthly wf = res["waterfall_annual"] wf = [(l, v/12.0) for (l, v) in wf] if not annual else wf else: res = compute_msk(msk_day, msk_read, msk_ttt, msk_rhr) title = "Overall Impact — MSK AI" wf = res["waterfall_monthly"] wf = [(l, v*12.0) for (l, v) in wf] if annual else wf overall_html = build_overall_card(title, res["summary"], res["annual_card"]) financial_html = build_rows_card("Financial", res["financial"]["rows"]) clinical_html = build_clinical_card(res["clinical"]["rows"], res["clinical"].get("bars")) operational_html = build_rows_card("Operational", res["operational"]["rows"]) water = build_waterfall(wf, period_label=("Annual" if annual else "Monthly")) evidence = f"
Evidence snapshot
{res['evidence']}
Neutral claims; update with site citations.
" cta = f"
Want to see this in your workflow?
{CTA_LABEL}
" # CSV export fin_rows = [(lab, val) for lab, val in res["financial"]["rows"]] clin_rows = [(lab, val) for lab, val in res["clinical"]["rows"]] op_rows = [(lab, val) for lab, val in res["operational"]["rows"]] csv_path = rows_to_csv(fin_rows, clin_rows, op_rows) return overall_html, financial_html, clinical_html, operational_html, water, evidence, gr.update(value=cta, visible=True), gr.update(value=csv_path, visible=True) inputs = [ use_case, period, # MMG inputs mmg_monthly_volume, mmg_read_minutes, mmg_rdx_hr_cost, mmg_base_ppr, mmg_ai_ppr, mmg_base_audit, mmg_ai_audit, mmg_base_recall, mmg_ai_recall, mmg_recall_cost, mmg_read_redux, mmg_cost_per_scan, mmg_cost_redux, mmg_follow_price, mmg_follow_uplift, mmg_early_uplift_per_1000, mmg_tx_delta, mmg_vendor_fee, mmg_platform_annual, mmg_integration_mo, mmg_cloud_mo, # FFR-CT inputs f_site, f_monthly_eligible, f_uptake, f_ttd_hours, f_touch_min, f_reimb_ccta, f_reimb_ffr, f_reimb_aiqpa, f_pct_aiqpa, f_one_test, f_dec_unnec_ica, f_more_revasc, f_revasc_prev, f_vendor_cost, f_platform_annual, f_stress_cost, f_bed_hr_val, f_clin_hr_cost, f_ai_ttd_min, f_touch_redux, f_base_diag_ica, f_base_addl_test, f_sens_uptake, f_sens_dec_ica, f_sens_vendor, # MSK inputs msk_scans_day, msk_read_min, msk_er_ttt_min, msk_rdx_hr_cost, ] outputs = [overall_card, financial_card, clinical_card, operational_card, waterfall_panel, evidence_panel, cta_panel, csv_file] run_btn.click(_compute, inputs=inputs, outputs=outputs) demo.load(_compute, inputs=inputs, outputs=outputs) return demo def main(): return build_ui() if __name__ == "__main__": app = build_ui() app.launch()