Show Code
import os
import sys
sys.path.append("..")
import report_utils as ru
from IPython.display import display, HTML
display(HTML(f"<div style='margin-bottom: 20px;'>{ru.nav_bar('estimation')}</div>"))Distribution of block-IF2 parameter estimates: R panelPomp vs. pypomp
import os
import sys
sys.path.append("..")
import report_utils as ru
from IPython.display import display, HTML
display(HTML(f"<div style='margin-bottom: 20px;'>{ru.nav_bar('estimation')}</div>"))This report compares block-IF2 parameter estimates for the mixed panel measles model. Both implementations run the same number of iterations from the same committed starting points (../starting_parameters.csv) with the same perturbation sizes, so the distribution of final estimates should agree.
The parameter structure is mixed across the panel’s 4 units — London, Halesworth, Hastings and Cardiff:
R0, sigma, gamma, sigmaSE, cohort, amplitude.iota, rho, psi, S_0, E_0, I_0, R_0.pypomp reports the shared parameters on every unit’s row; they are collapsed here to a single shared entry so a shared density is not drawn four times over.
pomp
The python and R smoothing spline implementations interpolate the birthrate and population covariates differently — noticeably so for Cardiff — which on its own shifts the estimates. The final interpolated covariate grid is therefore exported from pomp into ../R_covariates.csv and injected into the pypomp objects, so both packages search over the identical model.
The pypomp run continues past the search into the panel-specific part of the workflow: mix-and-match across units, then pruning to the single best combination. R panelPomp has no equivalent, so the like-for-like comparison below uses results_final.csv, captured before that step. The mix-and-match outcome is reported separately as a pypomp-only result.
The table below summarizes algorithmic parameters, software environments, and compute hardware recorded in latest.json for each configuration.
PLATFORMS = {
"R panelPomp": os.path.join("results", "R"),
"pypomp (GPU)": ru.platform_dir(),
}
runs = ru.load_timing_data(PLATFORMS)
display(HTML(ru.build_settings_comparison_html(runs, is_panel=True)))| Setting / Parameter | R panelPomp | pypomp (GPU) |
|---|---|---|
| Algorithmic & Workload Settings | ||
| Run Level | 4 | 4 |
| Starting Searches ($N_{starts}$) | 360 | 360 |
| MPIF Iterations ($N_{iter}$) | 100 | 100 |
| MPIF Particles / Unit ($N_{p,fit}$) | 5,000 | 5,000 |
| Pfilter Particles / Unit ($N_{p,eval}$) | 5,000 | 5,000 |
| Evaluation Replicates ($N_{reps}$) | 36 | 36 |
| Unit(s) | London, Halesworth, Hastings, Cardiff (4 units) | London, Halesworth, Hastings, Cardiff (4 units) |
| Shared Parameters | — | R0, sigma, gamma, sigmaSE, cohort, amplitude (6 params) |
| Random Seed | — | 594709947 |
| Software & Environment | ||
| Pomp Framework | panelPomp 1.7.0.0 (pomp 6.3) | pypomp 1.0.0rc1 |
| Backend / Engine | R 4.4.0 | JAX 0.11.1 |
| Quant Git Commit | e6ac592 |
630cc23 |
| Run Timestamp | 2026-08-14 17:06:50 | 2026-09-02 20:18:19 |
| Hardware & Compute | ||
| Compute Device | Intel(R) Xeon(R) Gold 6154 CPU @ 3.00GHz (36 cores) | NVIDIA RTX PRO 6000 Blackwell Server Edition (1 GPU) |
| Slurm Partition | standard | gpu-rtx6000 |
| Slurm Job ID | 57274684 | 59687801 |
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_density, facet_wrap, labs
os.environ["JAX_PLATFORMS"] = "cpu"
PY_DIR = ru.platform_dir()
R_DIR = os.path.join("results", "R")
py_results = ru.read_if_exists(os.path.join(PY_DIR, "results_final.csv"))
r_coefs = ru.read_if_exists(os.path.join(R_DIR, "mif_coefs.csv"))long_frames = []
if py_results is not None:
long_frames.append(
ru.split_shared_specific(ru.wide_results_to_long(py_results, "pypomp"))
)
if r_coefs is not None:
long_frames.append(ru.r_coefs_to_long(r_coefs))
combined = pd.concat(long_frames, ignore_index=True) if long_frames else pd.DataFrame(
columns=["unit", "param", "value", "source"]
)
combined = combined[combined["param"].isin(ru.LOG_PARAMS + ru.LOGIT_PARAMS)]
transformed = ru.to_estimation_scale(combined)Reported on the estimation scale (log for rates, logit for probabilities and initial-state fractions), which is the scale block-IF2 perturbs on.
if transformed.empty:
display(HTML("<p><em>No parameter estimates available.</em></p>"))
else:
summary = (
transformed.groupby(["unit", "source", "param"])["value"]
.agg(n="count", mean="mean", sd="std", min="min", max="max")
.reset_index()
.sort_values(["unit", "param", "source"])
)
display(HTML(summary.round(4).to_html(classes="table table-striped table-hover", index=False)))| unit | source | param | n | mean | sd | min | max |
|---|---|---|---|---|---|---|---|
| Cardiff | R panelPomp | log(iota) | 360 | -1.3846 | 0.8071 | -8.5191 | 1.3889 |
| Cardiff | pypomp | log(iota) | 360 | -1.3865 | 0.6724 | -3.0666 | 2.1724 |
| Cardiff | R panelPomp | log(psi) | 360 | -1.3274 | 0.0797 | -1.5598 | -1.1275 |
| Cardiff | pypomp | log(psi) | 360 | -1.3289 | 0.0874 | -1.6340 | -1.1045 |
| Cardiff | R panelPomp | logit(E_0) | 360 | -12.2716 | 1.2066 | -17.7719 | -10.1121 |
| Cardiff | pypomp | logit(E_0) | 360 | -12.0875 | 1.1604 | -16.7719 | -9.9929 |
| Cardiff | R panelPomp | logit(I_0) | 360 | -11.9284 | 0.9089 | -15.9739 | -10.1714 |
| Cardiff | pypomp | logit(I_0) | 360 | -11.9939 | 0.8656 | -14.9956 | -10.3695 |
| Cardiff | R panelPomp | logit(R_0) | 360 | 3.1835 | 0.4680 | -1.3559 | 4.0635 |
| Cardiff | pypomp | logit(R_0) | 360 | 3.1413 | 0.4445 | 0.0176 | 4.1495 |
| Cardiff | R panelPomp | logit(S_0) | 360 | -3.1841 | 0.4683 | -4.0656 | 1.3558 |
| Cardiff | pypomp | logit(S_0) | 360 | -3.1419 | 0.4448 | -4.1509 | -0.0177 |
| Cardiff | R panelPomp | logit(rho) | 360 | 0.4290 | 0.1165 | 0.1532 | 1.3888 |
| Cardiff | pypomp | logit(rho) | 360 | 0.4334 | 0.1091 | 0.1737 | 1.0183 |
| Halesworth | R panelPomp | log(iota) | 360 | -3.5921 | 0.5524 | -5.2829 | -2.3170 |
| Halesworth | pypomp | log(iota) | 360 | -3.6110 | 0.5878 | -5.4775 | -2.1657 |
| Halesworth | R panelPomp | log(psi) | 360 | -0.2210 | 0.3414 | -3.2855 | 0.5357 |
| Halesworth | pypomp | log(psi) | 360 | -0.2513 | 0.3436 | -2.4590 | 0.4673 |
| Halesworth | R panelPomp | logit(E_0) | 360 | -9.9699 | 1.8703 | -16.2223 | -6.2764 |
| Halesworth | pypomp | logit(E_0) | 360 | -10.0486 | 1.7792 | -15.4198 | -6.6043 |
| Halesworth | R panelPomp | logit(I_0) | 360 | -8.0779 | 1.0023 | -15.8802 | -6.0016 |
| Halesworth | pypomp | logit(I_0) | 360 | -8.0852 | 1.0159 | -14.3419 | -6.4938 |
| Halesworth | R panelPomp | logit(R_0) | 360 | 2.9819 | 0.4057 | -0.6082 | 3.8129 |
| Halesworth | pypomp | logit(R_0) | 360 | 2.9529 | 0.4120 | -0.2377 | 3.9218 |
| Halesworth | R panelPomp | logit(S_0) | 360 | -2.9964 | 0.4116 | -3.8271 | 0.5992 |
| Halesworth | pypomp | logit(S_0) | 360 | -2.9662 | 0.4168 | -3.9579 | 0.2376 |
| Halesworth | R panelPomp | logit(rho) | 360 | 1.1668 | 0.5406 | 0.1084 | 4.3023 |
| Halesworth | pypomp | logit(rho) | 360 | 1.2369 | 0.6357 | 0.0478 | 4.6529 |
| Hastings | R panelPomp | log(iota) | 360 | -1.8849 | 0.7394 | -3.6612 | 5.6518 |
| Hastings | pypomp | log(iota) | 360 | -1.9034 | 0.6519 | -3.7708 | 0.0863 |
| Hastings | R panelPomp | log(psi) | 360 | -0.7913 | 0.1037 | -1.0533 | -0.4748 |
| Hastings | pypomp | log(psi) | 360 | -0.7867 | 0.1079 | -1.1025 | -0.4372 |
| Hastings | R panelPomp | logit(E_0) | 360 | -12.7748 | 1.1655 | -18.6498 | -9.8697 |
| Hastings | pypomp | logit(E_0) | 360 | -12.8158 | 1.0917 | -16.6527 | -10.2459 |
| Hastings | R panelPomp | logit(I_0) | 360 | -12.2414 | 0.9971 | -15.7087 | -9.9430 |
| Hastings | pypomp | logit(I_0) | 360 | -12.3074 | 0.9222 | -15.3185 | -9.6045 |
| Hastings | R panelPomp | logit(R_0) | 360 | 3.5908 | 0.5538 | 0.9506 | 5.9753 |
| Hastings | pypomp | logit(R_0) | 360 | 3.5425 | 0.5765 | 0.1325 | 4.9867 |
| Hastings | R panelPomp | logit(S_0) | 360 | -3.5914 | 0.5542 | -5.9783 | -0.9506 |
| Hastings | pypomp | logit(S_0) | 360 | -3.5430 | 0.5767 | -4.9900 | -0.1326 |
| Hastings | R panelPomp | logit(rho) | 360 | 0.9042 | 0.3588 | 0.3281 | 4.4942 |
| Hastings | pypomp | logit(rho) | 360 | 0.9499 | 0.4138 | 0.2682 | 3.8125 |
| London | R panelPomp | log(iota) | 360 | -0.3036 | 2.5104 | -12.2456 | 5.5178 |
| London | pypomp | log(iota) | 360 | -0.5266 | 2.4967 | -11.1779 | 4.5633 |
| London | R panelPomp | log(psi) | 360 | -2.1882 | 0.0690 | -2.4044 | -2.0400 |
| London | pypomp | log(psi) | 360 | -2.1926 | 0.0747 | -2.4554 | -2.0119 |
| London | R panelPomp | logit(E_0) | 360 | -11.0339 | 1.3433 | -16.7741 | -9.0569 |
| London | pypomp | logit(E_0) | 360 | -10.9341 | 1.1250 | -15.9268 | -9.2685 |
| London | R panelPomp | logit(I_0) | 360 | -10.7906 | 0.7941 | -13.7053 | -8.6269 |
| London | pypomp | logit(I_0) | 360 | -10.8029 | 0.8057 | -14.5075 | -9.2203 |
| London | R panelPomp | logit(R_0) | 360 | 3.1025 | 0.4073 | -0.5147 | 3.8509 |
| London | pypomp | logit(R_0) | 360 | 3.0676 | 0.4275 | -0.4781 | 3.9755 |
| London | R panelPomp | logit(S_0) | 360 | -3.1041 | 0.4082 | -3.8558 | 0.5137 |
| London | pypomp | logit(S_0) | 360 | -3.0692 | 0.4284 | -3.9829 | 0.4776 |
| London | R panelPomp | logit(rho) | 360 | -0.0349 | 0.0821 | -1.1646 | 0.1578 |
| London | pypomp | logit(rho) | 360 | -0.0243 | 0.0709 | -0.2981 | 0.9426 |
| shared | R panelPomp | log(R0) | 360 | 3.3543 | 0.4566 | 0.8974 | 4.3441 |
| shared | pypomp | log(R0) | 360 | 3.3177 | 0.4703 | 0.9048 | 4.5728 |
| shared | R panelPomp | log(gamma) | 360 | 4.4236 | 0.9585 | 2.9219 | 12.3555 |
| shared | pypomp | log(gamma) | 360 | 4.4408 | 0.9347 | 3.0492 | 9.3928 |
| shared | R panelPomp | log(sigma) | 360 | 4.1788 | 0.6636 | 3.0463 | 6.8606 |
| shared | pypomp | log(sigma) | 360 | 4.1773 | 0.6354 | 2.9454 | 6.7107 |
| shared | R panelPomp | log(sigmaSE) | 360 | -2.9606 | 0.6861 | -4.6905 | -1.0842 |
| shared | pypomp | log(sigmaSE) | 360 | -2.9796 | 0.7168 | -4.9314 | -1.2002 |
| shared | R panelPomp | logit(amplitude) | 360 | -1.0288 | 0.9994 | -9.7369 | 4.6968 |
| shared | pypomp | logit(amplitude) | 360 | -1.0263 | 0.8966 | -2.6614 | 2.6985 |
| shared | R panelPomp | logit(cohort) | 360 | -0.2529 | 2.5117 | -9.0060 | 8.3077 |
| shared | pypomp | logit(cohort) | 360 | -0.2546 | 2.7761 | -9.5456 | 8.2214 |
specific = transformed[transformed["unit"] != "shared"]
if specific.empty:
display(HTML("<p><em>No unit-specific parameter estimates available.</em></p>"))
else:
for unit in sorted(specific["unit"].dropna().unique()):
unit_data = specific[specific["unit"] == unit]
display(
ggplot(unit_data, aes(x="value", fill="source", color="source"))
+ geom_density(alpha=0.35)
+ facet_wrap("~param", scales="free")
+ labs(
title=f"Block-IF2 estimates of the unit-specific parameters: {unit}",
subtitle="Estimation scale; one density per implementation",
x="Parameter value",
y="Density",
fill="Source",
color="Source",
)
+ ru.scale_fill_premium()
+ ru.scale_color_premium()
+ ru.theme_premium
)The pypomp and panelPomp densities should overlap heavily for each parameter, both shared and unit-specific.
Each start’s final parameter vector is re-evaluated by particle filter. The distribution over starts shows how consistently the search reaches the same region.
py_ll = ru.read_if_exists(os.path.join(PY_DIR, "pfilter_logliks.csv"))
r_ll = ru.read_if_exists(os.path.join(R_DIR, "pfilter_logliks.csv"))
ll_frames = []
if py_ll is not None:
py_ll = py_ll.copy()
py_ll["source"] = "pypomp"
ll_frames.append(py_ll)
if r_ll is not None:
r_ll = r_ll.copy()
r_ll["source"] = "R panelPomp"
ll_frames.append(r_ll)
missing_ll = []
if py_ll is None:
missing_ll.append(("pypomp endpoint log-likelihoods", os.path.join(PY_DIR, "pfilter_logliks.csv")))
if r_ll is None:
missing_ll.append(("R panelPomp endpoint log-likelihoods", os.path.join(R_DIR, "pfilter_logliks.csv")))
for label, path in missing_ll:
display(HTML(ru.missing_note(label, path)))
if ll_frames:
combined_ll = pd.concat(ll_frames, ignore_index=True)
per_unit = (
combined_ll.groupby(["source", "unit", "theta_idx"])["logLik"].agg(logmeanexp=ru.logmeanexp).reset_index()
if "theta_idx" in combined_ll.columns
else combined_ll.groupby(["source", "unit"])["logLik"].agg(logmeanexp=ru.logmeanexp).reset_index()
)
display(
ggplot(per_unit, aes(x="logmeanexp", fill="source", color="source"))
+ geom_density(alpha=0.35)
+ facet_wrap("~unit", scales="free")
+ labs(
title="Per-unit log-likelihood across search endpoints",
subtitle="One logmeanexp value per start per implementation",
x="Log-likelihood",
y="Density",
fill="Source",
color="Source",
)
+ ru.scale_fill_premium()
+ ru.scale_color_premium()
+ ru.theme_premium
)panelPomp fits each start as one coherent parameter vector. pypomp can additionally recombine the best unit-specific blocks across starts and re-fit, which has no counterpart in the R baseline. The table below is the pruned best combination after that step.
pruned = ru.read_if_exists(os.path.join(PY_DIR, "results.csv"))
if pruned is None:
display(HTML(ru.missing_note("pypomp mix-and-match result",
os.path.join(PY_DIR, "results.csv"))))
else:
display(HTML(pruned.round(4).to_html(classes="table table-striped table-hover", index=False)))| theta_idx | shared logLik | shared logLik se | unit | unit logLik | unit logLik se | R0 | sigma | gamma | sigmaSE | cohort | amplitude | iota | rho | psi | S_0 | E_0 | I_0 | R_0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -10037.0817 | 8.8164 | London | -5701.3041 | 8.7045 | 18.6382 | 66.4044 | 240.4364 | 0.0229 | 0.4636 | 0.1717 | 1.7311 | 0.4913 | 0.1014 | 0.0350 | 0.0001 | 0.0000 | 0.9649 |
| 0 | -10037.0817 | 8.8164 | Halesworth | -349.6454 | 0.2334 | 18.6382 | 66.4044 | 240.4364 | 0.0229 | 0.4636 | 0.1717 | 0.0525 | 0.7474 | 0.6346 | 0.0475 | 0.0000 | 0.0007 | 0.9519 |
| 0 | -10037.0817 | 8.8164 | Hastings | -1591.5398 | 0.3840 | 18.6382 | 66.4044 | 240.4364 | 0.0229 | 0.4636 | 0.1717 | 0.0953 | 0.7627 | 0.4007 | 0.0439 | 0.0000 | 0.0000 | 0.9561 |
| 0 | -10037.0817 | 8.8164 | Cardiff | -2394.5925 | 1.3257 | 18.6382 | 66.4044 | 240.4364 | 0.0229 | 0.4636 | 0.1717 | 0.2448 | 0.6076 | 0.2455 | 0.0484 | 0.0000 | 0.0000 | 0.9516 |
from plotnine import geom_line
target_cols = ["unit", "theta_idx", "replicate", "iteration", "method", "logLik", "loglik"] + ru.LOG_PARAMS + ru.LOGIT_PARAMS
py_traces = ru.load_traces(os.path.join(PY_DIR, "traces.csv.gz"), "pypomp", usecols=target_cols, max_iters=20)
r_trace_path = os.path.join(R_DIR, "mif_traces.csv.gz")
if not os.path.exists(r_trace_path):
r_trace_path = os.path.join(R_DIR, "traces.csv.gz")
r_traces = ru.load_traces(r_trace_path, "R panelPomp", usecols=target_cols, max_iters=20)
trace_frames = []
if py_traces is not None:
if "method" in py_traces.columns:
py_traces = py_traces[py_traces["method"] == "mif"]
py_traces = ru.split_traces_shared_specific(py_traces)
py_long = ru.to_long(py_traces, max_iters=20)
trace_frames.append(py_long)
if r_traces is not None:
if "method" in r_traces.columns:
r_traces = r_traces[r_traces["method"] == "mif"]
r_traces = ru.split_traces_shared_specific(r_traces)
r_long = ru.to_long(r_traces, max_iters=20)
trace_frames.append(r_long)
missing_traces = []
if py_traces is None:
missing_traces.append(("pypomp traces", os.path.join(PY_DIR, "traces.csv.gz")))
if r_traces is None:
missing_traces.append(("R panelPomp traces", os.path.join(R_DIR, "mif_traces.csv.gz")))
for label, path in missing_traces:
display(HTML(ru.missing_note(label, path)))
if trace_frames:
long = pd.concat(trace_frames, ignore_index=True)
long = long.dropna(subset=["param_value"])
long = ru.to_estimation_scale(long, param_col="quantity", value_col="param_value")
long["group_id"] = long["source"].astype(str) + "_" + long["rep"].astype(str)
unit_col = "unit" if "unit" in long.columns else None
groups = sorted(long[unit_col].dropna().unique()) if unit_col else [None]
for unit in groups:
data = long[long[unit_col] == unit] if unit_col else long
if data.empty:
continue
data = data.copy()
data["quantity"] = data["quantity"].astype(str)
display(
ggplot(data, aes(x="iter", y="param_value", group="group_id", color="source"))
+ geom_line(alpha=0.2, size=0.4)
+ facet_wrap("~quantity", scales="free_y")
+ labs(
title=f"Block-IF2 iteration traces{f': {unit}' if unit else ''}",
x="IF2 iteration",
y="Value",
color="Source",
)
+ ru.scale_color_premium()
+ ru.theme_premium
)Traces from independent starts should converge toward a common region rather than each settling at its own starting neighbourhood.