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 ( "
" f"
{label}
" f"
{value}
" f"
{sub}
" "
" ) 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 = ( "
{cards}
" "
{DISEASE_INFO[disease]}
" ) 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"{name}
Day %{{x:.0f}}
%{{y:.2f}}%", )) 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}
Peak infected: %{y:.1f}%", )) 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()