Show Code
import os
import sys
sys.path.append("..")
import report_utils as ru
from IPython.display import display, HTML, Markdown
display(HTML(f"<div style='margin-bottom: 20px;'>{ru.nav_bar('algorithms')}</div>"))Which algorithm reaches the higher likelihood for a comparable budget
import os
import sys
sys.path.append("..")
import report_utils as ru
from IPython.display import display, HTML, Markdown
display(HTML(f"<div style='margin-bottom: 20px;'>{ru.nav_bar('algorithms')}</div>"))This report compares two pypomp fitting pipelines on the UK measles model:
Both arms start from the same points drawn from the same box, and IF2 is given enough extra iterations that the two arms cost roughly the same wall clock. The question is therefore not which is faster but which gets further for the money.
Unlike the other measles reports, this one uses the continuous-time model (003) on London alone, not the discrete 001b model the R comparisons use. Gradient training needs a differentiable measurement model, which 001b does not provide. There is consequently no R baseline on this page.
R0: Basic reproduction number.sigma: Rate of leaving the exposed class (1 / latent period).gamma: Recovery rate (1 / infectious period).iota: Rate of imported infections.rho: Reporting probability.sigmaSE: Intensity of extrademographic (environmental) noise.psi: Reporting overdispersion.cohort: Fraction of births entering the susceptible pool as a school-entry cohort.amplitude: Amplitude of term-time seasonal forcing.S_0, E_0, I_0, R_0: Initial compartment fractions.The table below summarizes algorithmic parameters, software environments, and compute hardware recorded in latest.json for each configuration.
PLATFORMS = {
"IF2": os.path.join("results", "if2"),
"IFAD": os.path.join("results", "ifad"),
}
runs = ru.load_timing_data(PLATFORMS)
display(HTML(ru.build_settings_comparison_html(runs, is_panel=False)))| Setting / Parameter | IF2 | IFAD |
|---|---|---|
| Algorithmic & Workload Settings | ||
| Run Level | 4 | 4 |
| Starting Searches ($N_{starts}$) | 36 | 36 |
| IF2 Iterations ($N_{iter}$) | 350 | 300 |
| Training Iterations ($N_{train}$) | 0 | 50 |
| IF2 Particles ($N_p$) | 5,000 | 5,000 |
| Pfilter Particles ($N_{p,eval}$) | 5,000 | 5,000 |
| Evaluation Replicates ($N_{reps}$) | 36 | 36 |
| Random Seed | 594709947 | 594709947 |
| Software & Environment | ||
| Pomp Framework | pypomp 1.0.0rc1 | pypomp 1.0.0rc1 |
| Backend / Engine | JAX 0.11.1 | JAX 0.11.1 |
| Quant Git Commit | 630cc23 |
630cc23 |
| Run Timestamp | 2026-09-02 19:29:44 | 2026-09-02 19:33:56 |
| Hardware & Compute | ||
| Compute Device | NVIDIA RTX PRO 6000 Blackwell Server Edition (1 GPU) | NVIDIA RTX PRO 6000 Blackwell Server Edition (1 GPU) |
| Slurm Partition | gpu-rtx6000 | gpu-rtx6000 |
| Slurm Job ID | 59687777 | 59687781 |
missing = [(label, r["dir"]) for label, r in runs.items() if not r["available"]]
for label, path in missing:
display(HTML(f"<div class='alert alert-warning'><strong>Missing results for {label}:</strong> Expected at <code>{path}</code>.</div>"))import numpy as np
import pandas as pd
from plotnine import (
ggplot, aes, geom_line, geom_density, facet_wrap, labs, theme
)
os.environ["JAX_PLATFORMS"] = "cpu"
ARMS = {"if2": "IF2", "ifad": "IFAD"}
meta = {arm: ru.load_json(os.path.join("results", arm, "latest.json")) for arm in ARMS}
final = {arm: ru.read_if_exists(os.path.join("results", arm, "results_final.csv")) for arm in ARMS}
timings = {arm: ru.read_if_exists(os.path.join("results", arm, "timings.csv")) for arm in ARMS}
traces = {
arm: ru.load_traces(os.path.join("results", arm, "traces.csv.gz"), label)
for arm, label in ARMS.items()
}
missing = [
(label, os.path.join("results", arm, "results_final.csv"))
for arm, label in ARMS.items()
if final[arm] is None
]
have_final = not missingIF2 alone should take a little longer than IFAD overall — the point of the comparison is that IFAD reaches a better likelihood in the same or less time.
rows = []
for arm, label in ARMS.items():
tm = timings[arm]
if tm is None:
continue
cfg = meta[arm].get("run_config", {})
time_col = "time" if "time" in tm.columns else tm.columns[-1]
per_stage = tm.groupby("method", sort=False)[time_col].sum() if "method" in tm.columns else {}
n_mif = cfg.get("NFITR")
n_train = cfg.get("NTRAIN") or 0
rows.append({
"Pipeline": label,
"mif (s)": f"{per_stage.get('mif', np.nan):.1f}",
"train (s)": f"{per_stage.get('train', 0.0):.1f}",
"pfilter (s)": f"{per_stage.get('pfilter', np.nan):.1f}",
"Total (s)": f"{tm[time_col].sum():.1f}",
"mif iters": n_mif,
"train iters": n_train,
"s / mif iter": f"{per_stage.get('mif', np.nan) / n_mif:.3f}" if n_mif else "—",
"s / train iter": f"{per_stage.get('train', 0.0) / n_train:.3f}" if n_train else "—",
})
if rows:
display(HTML(pd.DataFrame(rows).to_html(classes="table table-striped table-hover", index=False)))
else:
display(Markdown("_No timings on disk._"))| Pipeline | mif (s) | train (s) | pfilter (s) | Total (s) | mif iters | train iters | s / mif iter | s / train iter |
|---|---|---|---|---|---|---|---|---|
| IF2 | 100.9 | 0.0 | 20.3 | 121.2 | 350 | 0 | 0.288 | — |
| IFAD | 88.1 | 122.8 | 21.0 | 231.9 | 300 | 50 | 0.294 | 2.457 |
if have_final:
summary = pd.DataFrame([
{
"Pipeline": label,
"Starts": len(final[arm]),
"Mean": final[arm]["logLik"].mean(),
"Std": final[arm]["logLik"].std(),
"Min": final[arm]["logLik"].min(),
"Median": final[arm]["logLik"].median(),
"Max": final[arm]["logLik"].max(),
}
for arm, label in ARMS.items()
]).round(2)
display(HTML(summary.to_html(classes="table table-striped table-hover", index=False)))
else:
display(Markdown("_No final results on disk._"))| Pipeline | Starts | Mean | Std | Min | Median | Max |
|---|---|---|---|---|---|---|
| IF2 | 36 | -3817.56 | 3.67 | -3831.57 | -3816.90 | -3812.13 |
| IFAD | 36 | -3843.08 | 36.98 | -3970.35 | -3830.19 | -3812.59 |
if have_final:
ll_df = pd.concat(
[
pd.DataFrame({"logLik": final[arm]["logLik"], "source": label})
for arm, label in ARMS.items()
],
ignore_index=True,
)
# A handful of starts stall far below the mode; clipping keeps the KDE readable.
cutoff = ll_df["logLik"].median() - 5 * ll_df["logLik"].std()
ll_df = ll_df[ll_df["logLik"] >= cutoff]
display(
ggplot(ll_df, aes(x="logLik", fill="source", color="source"))
+ geom_density(alpha=0.4)
+ labs(
title="Final logLik density: IF2 vs IFAD",
subtitle="Evaluated likelihood at each start's final parameter vector",
x="Log-Likelihood", y="Density", fill="Pipeline", color="Pipeline",
)
+ ru.scale_fill_premium()
+ ru.scale_color_premium()
+ ru.theme_premium
)IFAD tends to yield parameter estimates with lower Monte Carlo variance than IF2 alone, so its density curves should be the narrower ones.
if have_final:
param_cols = [
c for c in final["if2"].columns if c not in ("theta_idx", "logLik", "se", "unit")
]
params_df = pd.concat(
[
final[arm][param_cols]
.melt(var_name="quantity", value_name="param_value")
.assign(source=label)
for arm, label in ARMS.items()
],
ignore_index=True,
)
display(
ggplot(params_df, aes(x="param_value", fill="source", color="source"))
+ geom_density(alpha=0.4)
+ facet_wrap("quantity", scales="free", ncol=3)
+ labs(
title="Final parameter estimates: IF2 vs IFAD",
subtitle="Density across all starts",
x="Estimated value", y="Density", fill="Pipeline", color="Pipeline",
)
+ ru.scale_fill_premium()
+ ru.scale_color_premium()
+ ru.theme_premium
+ theme(figure_size=(10, 10), panel_spacing=0.02)
)have_traces = all(t is not None for t in traces.values())
if have_traces:
long = {arm: ru.to_long(traces[arm]) for arm in ARMS}Under IFAD, most parameters should tighten once the search switches from mif to train. Several parameters in this model are weakly identified, so tight convergence is not expected everywhere.
if have_traces:
display(
ggplot(long["if2"], aes(x="iter", y="param_value", group="rep", color="factor(rep)"))
+ geom_line(show_legend=False, alpha=0.5)
+ facet_wrap("quantity", scales="free_y", ncol=3)
+ labs(
title="IF2 parameter traces",
subtitle="Individual start paths across iterations",
x="Iteration", y="Parameter value",
)
+ ru.theme_premium
+ theme(figure_size=(10, 10), panel_spacing=0.02)
)
else:
display(Markdown("_No traces on disk._"))if have_traces:
display(
ggplot(long["ifad"], aes(x="iter", y="param_value", group="rep", color="factor(rep)"))
+ geom_line(show_legend=False, alpha=0.5)
+ facet_wrap("quantity", scales="free_y", ncol=3)
+ labs(
title="IFAD parameter traces",
subtitle="Individual start paths across mif and train iterations",
x="Iteration", y="Parameter value",
)
+ ru.theme_premium
+ theme(figure_size=(10, 10), panel_spacing=0.02)
)
else:
display(Markdown("_No traces on disk._"))if have_traces:
ll_traces = pd.concat(
[
ru.thin(traces[arm])[["rep", "iter", "logLik", "source"]].dropna(subset=["logLik"])
for arm in ARMS
],
ignore_index=True,
)
display(
ggplot(ll_traces, aes(x="iter", y="logLik", group="rep", color="factor(rep)"))
+ geom_line(show_legend=False, alpha=0.6)
+ facet_wrap("source", ncol=1, scales="free_x")
+ labs(
title="logLik traces",
subtitle="Log-likelihood trajectory of each start",
x="Iteration", y="Log-Likelihood",
)
+ ru.theme_premium
+ theme(figure_size=(8, 8))
)
else:
display(Markdown("_No traces on disk._"))