File size: 6,022 Bytes
437dbfa |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
import os
import numpy as np
import plotly.graph_objects as go
import gradio as gr
def _cos_blend(x):
x = np.clip(x, 0.0, 1.0)
return 0.5 - 0.5*np.cos(np.pi*x)
def generate_cycle(HR=72, preload=1.0, afterload=1.0, inotropy=1.0):
T = 60.0 / HR
n = 1600
t = np.linspace(0, T, n)
tn = t / T
EDV0, ESV0 = 120.0, 50.0
EDV = EDV0 * (0.9 + 0.25*preload)
ESV = max(10.0, ESV0 * (1.1 + 0.35*afterload - 0.45*inotropy))
SV = max(5.0, EDV - ESV)
P_lv_peak0 = 120.0
P_lv_peak = P_lv_peak0 * (0.85 + 0.25*inotropy + 0.10*afterload)
P_ao_sys0 = 120.0
P_ao_sys = P_ao_sys0 * (0.9 + 0.25*afterload + 0.10*inotropy)
runoff = 0.75 + 0.25*(HR/72.0)
P_ao_dia0 = 80.0
P_ao_dia = max(45.0, P_ao_dia0 * (0.8 + 0.4*afterload) * (0.9 + 0.2*runoff))
P_la_mean0 = 10.0
P_la_mean = P_la_mean0 * (0.9 + 0.2*preload)
dur_isoC = max(0.05, 0.08 - 0.015*(HR-72)/72)
dur_ej = max(0.18, 0.26 - 0.02*(afterload-1) + 0.03*(inotropy-1))
dur_isoR = max(0.05, 0.08 - 0.015*(HR-72)/72)
dur_fill = max(0.25, 0.42 - (dur_isoC+dur_ej+dur_isoR))
dur_as = min(0.18, 0.12 + 0.04*(preload-1))
total = dur_isoC + dur_ej + dur_isoR + dur_fill
s = 1.0 / total
dur_isoC *= s; dur_ej *= s; dur_isoR *= s; dur_fill *= s
dur_fill_early = max(0.05, dur_fill - dur_as)
t0 = 0.0
t_fill_early_end = t0 + dur_fill_early
t_as_end = t_fill_early_end + dur_as
t_isoC_end = t_as_end + dur_isoC
t_ej_end = t_isoC_end + dur_ej
t_isoR_end = t_ej_end + dur_isoR
t_AV_open = (t_isoR_end) % 1.0
t_AV_close = t_as_end
t_SL_open = t_isoC_end
t_SL_close = t_ej_end
m_fillE = (tn >= 0.0) & (tn < t_fill_early_end)
m_as = (tn >= t_fill_early_end) & (tn < t_as_end)
m_isoC = (tn >= t_as_end) & (tn < t_isoC_end)
m_ej = (tn >= t_isoC_end) & (tn < t_ej_end)
m_isoR = (tn >= t_ej_end) & (tn < t_isoR_end)
Vlv = np.full_like(tn, EDV)
if m_ej.any():
xe = (tn[m_ej] - t_isoC_end) / max(1e-6, (t_ej_end - t_isoC_end))
Vlv[m_ej] = EDV - SV * _cos_blend(xe)
if m_fillE.any():
xf = (tn[m_fillE] - 0.0) / max(1e-6, t_fill_early_end - 0.0)
Vstart, Vend = ESV, EDV - 0.12*SV
Vlv[m_fillE] = Vstart + (Vend - Vstart) * _cos_blend(xf)
if m_as.any():
xa = (tn[m_as] - t_fill_early_end) / max(1e-6, dur_as)
Vstart, Vend = EDV - 0.12*SV, EDV
Vlv[m_as] = Vstart + (Vend - Vstart) * _cos_blend(xa)
Plv = 5 + 7*(Vlv-ESV)/max(1.0, (EDV-ESV))
if m_isoC.any():
xc = (tn[m_isoC] - t_as_end) / max(1e-6, (t_isoC_end - t_as_end))
Plv[m_isoC] = np.maximum(Plv[m_isoC], 20 + (P_lv_peak-20)*_cos_blend(xc))
if m_ej.any():
xe = (tn[m_ej] - t_isoC_end) / max(1e-6, (t_ej_end - t_isoC_end))
Plv[m_ej] = np.maximum(Plv[m_ej], P_lv_peak * (1 - 0.35*xe))
if m_isoR.any():
xr = (tn[m_isoR] - t_ej_end) / max(1e-6, (t_isoR_end - t_ej_end))
Plv[m_isoR] = np.maximum(5 + 7*(Vlv[m_isoR]-ESV)/max(1.0,(EDV-ESV)),
P_lv_peak*(1 - _cos_blend(xr)) + 5*_cos_blend(xr))
Pao = np.full_like(tn, P_ao_dia)
if m_ej.any():
Pao[m_ej] = np.minimum(Plv[m_ej] - 2.0, P_ao_sys)
Pao[~m_ej] = np.maximum(Pao[~m_ej], P_ao_dia)
Pla = np.full_like(tn, P_la_mean)
if m_as.any():
xa = (tn[m_as] - t_fill_early_end) / max(1e-6, dur_as)
Pla[m_as] += 3.0*np.sin(np.pi*xa)**2
if m_isoC.any():
xc = (tn[m_isoC] - t_as_end) / max(1e-6, (t_isoC_end - t_as_end))
Pla[m_isoC] += 1.4*np.sin(np.pi*xc)**2
if m_ej.any():
xv = (tn[m_ej] - t_isoC_end) / max(1e-6, (t_ej_end - t_isoC_end))
Pla[m_ej] += 3.0*(xv**2)
events_s = {
"AV_open": (t_AV_open*T) % T,
"AV_close": (t_AV_close*T) % T,
"SL_open": (t_SL_open*T) % T,
"SL_close": (t_SL_close*T) % T,
}
return t, Plv, Pao, Pla, events_s
def make_plot(HR, preload, afterload, inotropy, show_events):
t, Plv, Pao, Pla, ev = generate_cycle(HR, preload, afterload, inotropy)
fig = go.Figure()
fig.add_trace(go.Scatter(x=t, y=Pao, mode="lines", line=dict(color="black", width=3)))
fig.add_trace(go.Scatter(x=t, y=Plv, mode="lines", line=dict(color="red", width=3)))
fig.add_trace(go.Scatter(x=t, y=Pla, mode="lines", line=dict(color="blue", width=3)))
if show_events:
markers = [("1", ev["AV_close"]), ("2", ev["SL_open"]), ("3", ev["SL_close"]), ("4", ev["AV_open"])]
ymax = max(Pao.max(), Plv.max(), Pla.max())
for label, x in markers:
fig.add_vline(x=x, line=dict(width=1, dash="dot", color="#777"))
fig.add_annotation(x=x, y=ymax*1.02, text=label, showarrow=False, font=dict(size=14, color="#444"), yanchor="bottom")
ymax = max(Pao.max(), Plv.max(), Pla.max())
fig.update_yaxes(range=[0, max(140, ymax*1.1)], tickfont=dict(size=12), showline=True, mirror=True)
fig.update_xaxes(tickfont=dict(size=12), showline=True, mirror=True)
fig.update_layout(template="simple_white", height=420, margin=dict(l=40, r=10, t=10, b=40), showlegend=False)
return fig
with gr.Blocks(title="Wiggers Diagram (Minimal)") as demo:
gr.Markdown("### Wiggers Diagram (Minimal)")
with gr.Row():
HR = gr.Slider(40, 180, value=72, step=1, label="HR (bpm)")
preload = gr.Slider(0.6, 1.6, value=1.0, step=0.05, label="Preload")
afterload = gr.Slider(0.6, 1.6, value=1.0, step=0.05, label="Afterload")
inotropy = gr.Slider(0.5, 1.8, value=1.0, step=0.05, label="Contractility")
show_events = gr.Checkbox(value=False, label="Show event numbers (1–4)")
plot = gr.Plot(value=make_plot(72, 1.0, 1.0, 1.0, False))
for w in (HR, preload, afterload, inotropy, show_events):
w.change(make_plot, [HR, preload, afterload, inotropy, show_events], plot)
if __name__ == "__main__":
# Required for Spaces
demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
|