Spaces:
Sleeping
Sleeping
| import numpy as np | |
| import pandas as pd | |
| import gradio as gr | |
| import plotly.graph_objects as go | |
| from scipy.integrate import odeint | |
| PRESETS = { | |
| "Malaria (Nigeria)": {"beta": 0.30, "gamma": 0.10, "sigma": 0.14, "model": "SEIR"}, | |
| "Malaria (Ghana)": {"beta": 0.25, "gamma": 0.10, "sigma": 0.14, "model": "SEIR"}, | |
| "Dengue Fever": {"beta": 0.40, "gamma": 0.14, "sigma": 0.20, "model": "SEIR"}, | |
| "Cholera": {"beta": 0.50, "gamma": 0.25, "sigma": 0.50, "model": "SEIR"}, | |
| "Lassa Fever": {"beta": 0.20, "gamma": 0.18, "sigma": 0.17, "model": "SEIR"}, | |
| "Measles": {"beta": 1.40, "gamma": 0.14, "sigma": 0.20, "model": "SEIR"}, | |
| "Mpox": {"beta": 0.18, "gamma": 0.10, "sigma": 0.13, "model": "SEIR"}, | |
| } | |
| DISEASE_INFO = { | |
| "Malaria (Nigeria)": "R0 approx 3.0 - Nigeria has the world's highest malaria burden. Peaks in rainy season.", | |
| "Malaria (Ghana)": "R0 approx 2.5 - High seasonal transmission in northern and coastal regions.", | |
| "Dengue Fever": "R0 approx 2.9 - Re-emerging in West Africa via Aedes aegypti mosquitoes.", | |
| "Cholera": "R0 approx 2.0 - Frequent outbreaks during floods and poor-sanitation events.", | |
| "Lassa Fever": "R0 approx 1.1 - Endemic in Nigeria; spread via rodent contact.", | |
| "Measles": "R0 approx 10-18 - Highly contagious; leading cause of child mortality in West Africa.", | |
| "Mpox": "R0 approx 1.8 - Endemic in parts of Nigeria; human-to-human spread increasing.", | |
| } | |
| POPULATIONS = { | |
| "Nigeria - National (220M)": 220000000, | |
| "Nigeria - Lagos (15M)": 15000000, | |
| "Nigeria - Kano (13M)": 13000000, | |
| "Nigeria - Rivers (7M)": 7000000, | |
| "Ghana - National (33M)": 33000000, | |
| "Ghana - Greater Accra (5M)": 5000000, | |
| "Ghana - Ashanti (5M)": 5000000, | |
| } | |
| def sir_ode(y, t, beta, gamma, N): | |
| S, I, R = y | |
| n = beta * S * I / N | |
| return [-n, n - gamma * I, gamma * I] | |
| def seir_ode(y, t, beta, gamma, sigma, N): | |
| S, E, I, R = y | |
| n = beta * S * I / N | |
| return [-n, n - sigma * E, sigma * E - gamma * I, gamma * I] | |
| def fig_to_html(fig): | |
| return fig.to_html(include_plotlyjs="cdn", full_html=False, | |
| config={"displayModeBar": False}) | |
| def _card(label, value, sub): | |
| return ( | |
| "<div style='background:white;border-radius:10px;padding:14px 16px;" | |
| "border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.06);'>" | |
| f"<div style='font-size:11px;color:#64748b;font-weight:600;text-transform:uppercase;'>{label}</div>" | |
| f"<div style='font-size:24px;font-weight:800;color:#0f172a;margin-top:4px;'>{value}</div>" | |
| f"<div style='font-size:12px;color:#94a3b8;margin-top:2px;'>{sub}</div>" | |
| "</div>" | |
| ) | |
| def run_simulation(disease, population_label, model_type, | |
| beta, gamma, sigma, days, | |
| i0_pct, vax_pct, | |
| add_intervention, interv_day, interv_reduction): | |
| N = POPULATIONS[population_label] | |
| t = np.linspace(0, int(days), int(days) * 4) | |
| I0 = max(1, int(N * i0_pct / 100)) | |
| R_init = int(N * vax_pct / 100) | |
| S0 = max(0, N - I0 - R_init) | |
| def solve(b, t_arr, y0): | |
| if model_type == "SIR": | |
| return odeint(sir_ode, y0[:3], t_arr, args=(b, gamma, N)) | |
| return odeint(seir_ode, y0, t_arr, args=(b, gamma, sigma, N)) | |
| y0 = [S0, I0, R_init] if model_type == "SIR" else [S0, 0, I0, R_init] | |
| sol = solve(beta, t, y0) | |
| if model_type == "SIR": | |
| S, I, R = sol[:, 0], sol[:, 1], sol[:, 2] | |
| E = None | |
| else: | |
| S, E, I, R = sol[:, 0], sol[:, 1], sol[:, 2], sol[:, 3] | |
| I_int = None | |
| if add_intervention and int(interv_day) < int(days): | |
| t1 = t[t <= interv_day] | |
| t2 = t[t > interv_day] - interv_day | |
| beta2 = beta * (1 - interv_reduction / 100) | |
| sol1 = solve(beta, t1, y0) | |
| sol2 = solve(beta2, t2, sol1[-1]) | |
| full = np.concatenate([sol1, sol2], axis=0) | |
| I_int = full[:, 1] if model_type == "SIR" else full[:, 2] | |
| R0 = beta / gamma | |
| peak_I = int(I.max()) | |
| peak_day = int(t[np.argmax(I)]) | |
| total_inf = int(R[-1] - R_init) | |
| herd = max(0.0, (1 - 1 / R0) * 100) if R0 > 1 else 0.0 | |
| inf_p = round(1 / gamma, 1) | |
| inc_p = round(1 / sigma, 1) if model_type == "SEIR" else "N/A" | |
| epidemic = "Epidemic will grow" if R0 > 1 else "Epidemic will die out" | |
| cards = "".join([ | |
| _card("Basic R0", f"{R0:.2f}", epidemic), | |
| _card("Peak Infectious", f"{peak_I:,}", f"on day {peak_day}"), | |
| _card("Total Infected", f"{total_inf:,}", f"{total_inf/N*100:.1f}% of population"), | |
| _card("Herd Immunity", f"{herd:.1f}%", "vaccination needed"), | |
| _card("Infectious Period",f"{inf_p}d", f"Incubation: {inc_p}{'d' if inc_p != 'N/A' else ''}"), | |
| ]) | |
| metrics_html = ( | |
| "<div style='display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));" | |
| f"gap:12px;margin-bottom:12px;'>{cards}</div>" | |
| "<div style='background:#eff6ff;border-left:4px solid #3b82f6;border-radius:0 8px 8px 0;" | |
| f"padding:10px 14px;font-size:13px;color:#1e40af;'>{DISEASE_INFO[disease]}</div>" | |
| ) | |
| COLORS = {"S": "#94a3b8", "E": "#f59e0b", "I": "#ef4444", "R": "#22c55e"} | |
| fig1 = go.Figure() | |
| curves = [("Susceptible", S, COLORS["S"]), | |
| ("Infectious", I, COLORS["I"]), | |
| ("Recovered", R, COLORS["R"])] | |
| if E is not None: | |
| curves.insert(1, ("Exposed", E, COLORS["E"])) | |
| for name, arr, color in curves: | |
| fig1.add_trace(go.Scatter( | |
| x=t, y=arr / N * 100, name=name, | |
| line=dict(color=color, width=2.5), | |
| hovertemplate=f"<b>{name}</b><br>Day %{{x:.0f}}<br>%{{y:.2f}}%<extra></extra>", | |
| )) | |
| if I_int is not None: | |
| fig1.add_trace(go.Scatter( | |
| x=t, y=I_int / N * 100, | |
| name="Infectious (+ intervention)", | |
| line=dict(color="#7c3aed", width=2, dash="dash"), | |
| )) | |
| fig1.add_vline(x=interv_day, line_dash="dot", line_color="#7c3aed", | |
| annotation_text=f"Intervention day {int(interv_day)}", | |
| annotation_position="top right", annotation_font_size=11) | |
| fig1.add_vline(x=peak_day, line_dash="dot", line_color="#ef4444", | |
| annotation_text=f"Peak day {peak_day}", | |
| annotation_position="top left", annotation_font_size=11) | |
| fig1.update_layout( | |
| title=f"{model_type} Epidemic Curve | {disease} | {population_label}", | |
| xaxis_title="Days", yaxis_title="% of Population", | |
| height=430, plot_bgcolor="white", paper_bgcolor="white", | |
| hovermode="x unified", font=dict(size=13), | |
| legend=dict(orientation="h", y=-0.25, x=0), | |
| margin=dict(l=10, r=10, t=50, b=90), | |
| ) | |
| fig1.update_xaxes(showgrid=True, gridcolor="#f1f5f9", linecolor="#e2e8f0") | |
| fig1.update_yaxes(showgrid=True, gridcolor="#f1f5f9", linecolor="#e2e8f0", range=[0, 105]) | |
| beta_range = np.round(np.arange(0.05, 1.55, 0.05), 2) | |
| peaks = [] | |
| for b in beta_range: | |
| s = solve(b, t, y0) | |
| col = 1 if model_type == "SIR" else 2 | |
| peaks.append(s[:, col].max() / N * 100) | |
| r0_range = beta_range / gamma | |
| bar_colors = ["#ef4444" if r > 1 else "#22c55e" for r in r0_range] | |
| fig2 = go.Figure(go.Bar( | |
| x=[f"{r:.1f}" for r in r0_range], y=peaks, | |
| marker_color=bar_colors, | |
| hovertemplate="R0=%{x}<br>Peak infected: %{y:.1f}%<extra></extra>", | |
| )) | |
| closest_idx = int(np.argmin(np.abs(r0_range - R0))) | |
| fig2.add_vline(x=f"{r0_range[closest_idx]:.1f}", | |
| line_dash="dash", line_color="#0f172a", line_width=2, | |
| annotation_text=f"Current R0 = {R0:.2f}", | |
| annotation_font_size=11, annotation_position="top right") | |
| fig2.update_layout( | |
| title="Peak Infected (%) across R0 values | Red=epidemic grows Green=dies out", | |
| xaxis_title="R0 (beta / gamma)", yaxis_title="Peak Infected (%)", | |
| height=320, plot_bgcolor="white", paper_bgcolor="white", | |
| showlegend=False, font=dict(size=12), | |
| margin=dict(l=10, r=10, t=50, b=50), | |
| ) | |
| fig2.update_xaxes(tickangle=-45, tickfont=dict(size=9), | |
| showgrid=True, gridcolor="#f1f5f9") | |
| fig2.update_yaxes(showgrid=True, gridcolor="#f1f5f9") | |
| step = max(1, len(t) // 100) | |
| rowdict = { | |
| "Day": t[::step].astype(int), | |
| "Susceptible %": (S[::step] / N * 100).round(2), | |
| "Infectious %": (I[::step] / N * 100).round(2), | |
| "Recovered %": (R[::step] / N * 100).round(2), | |
| } | |
| if E is not None: | |
| rowdict["Exposed %"] = (E[::step] / N * 100).round(2) | |
| df_out = pd.DataFrame(rowdict) | |
| return metrics_html, fig_to_html(fig1), fig_to_html(fig2), df_out | |
| def load_preset(disease): | |
| p = PRESETS[disease] | |
| return p["beta"], p["gamma"], p["sigma"], p["model"] | |
| with gr.Blocks(title="Disease Modelling - Nigeria & Ghana", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| "# Infectious Disease Modelling - Nigeria & Ghana\n" | |
| "SIR / SEIR compartmental model. Adjust parameters then click **Run Simulation**." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=270): | |
| gr.Markdown("### Settings") | |
| disease_dd = gr.Dropdown(list(PRESETS.keys()), | |
| value="Malaria (Nigeria)", label="Disease Preset") | |
| pop_dd = gr.Dropdown(list(POPULATIONS.keys()), | |
| value="Nigeria - National (220M)", label="Population / Location") | |
| model_dd = gr.Radio(["SIR", "SEIR"], value="SEIR", label="Model type", | |
| info="SEIR adds an Exposed (latent) compartment") | |
| gr.Markdown("### Parameters") | |
| beta_sl = gr.Slider(0.01, 2.00, value=0.30, step=0.01, label="Beta - transmission rate") | |
| gamma_sl = gr.Slider(0.01, 1.00, value=0.10, step=0.01, label="Gamma - recovery rate (1/gamma = infectious days)") | |
| sigma_sl = gr.Slider(0.01, 1.00, value=0.14, step=0.01, label="Sigma - incubation rate (SEIR only)") | |
| gr.Markdown("### Simulation") | |
| days_sl = gr.Slider(30, 730, value=180, step=10, label="Days to simulate") | |
| i0_sl = gr.Slider(0.001, 2.0, value=0.01, step=0.001, label="Initial infected (%)") | |
| vax_sl = gr.Slider(0, 80, value=0, step=1, label="Pre-existing immunity / vaccinated (%)") | |
| gr.Markdown("### Intervention") | |
| interv_cb = gr.Checkbox(value=False, label="Add intervention (bed nets, treatment, quarantine)") | |
| interv_day = gr.Slider(1, 180, value=30, step=1, label="Intervention start day") | |
| interv_red = gr.Slider(10, 90, value=50, step=5, label="Transmission reduction (%)") | |
| run_btn = gr.Button("Run Simulation", variant="primary", size="lg") | |
| with gr.Column(scale=2): | |
| metrics_out = gr.HTML(label="Key Metrics") | |
| curve_out = gr.HTML(label="Epidemic Curve") | |
| sens_out = gr.HTML(label="R0 Sensitivity") | |
| table_out = gr.Dataframe(label="Simulation Data (% of population)", interactive=False) | |
| inputs = [disease_dd, pop_dd, model_dd, | |
| beta_sl, gamma_sl, sigma_sl, days_sl, | |
| i0_sl, vax_sl, | |
| interv_cb, interv_day, interv_red] | |
| outputs = [metrics_out, curve_out, sens_out, table_out] | |
| disease_dd.change(fn=load_preset, inputs=[disease_dd], | |
| outputs=[beta_sl, gamma_sl, sigma_sl, model_dd]) | |
| run_btn.click(fn=run_simulation, inputs=inputs, outputs=outputs) | |
| demo.load(fn=run_simulation, inputs=inputs, outputs=outputs) | |
| gr.Markdown( | |
| "---\n" | |
| "S = Susceptible | E = Exposed | I = Infectious | R = Recovered\n\n" | |
| "R0 = beta / gamma. R0 > 1 means epidemic grows. R0 < 1 means it dies out.\n\n" | |
| "Sources: WHO AFRO - NCDC Nigeria - Ghana Health Service" | |
| ) | |
| demo.launch() |