-
Notifications
You must be signed in to change notification settings - Fork 0
Add Case Study 3: ODE-based SIR model with deterministic and stochastic variants #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add ODE-based SIR compartmental model case study demonstrating: - ODEProcess with SciML ODE solvers for SIR dynamics - TransformObservationModel for population scaling - Deterministic model with direct Poisson link - Stochastic model with AR(1) time-varying ascertainment - 2x3 comparison figure panel following established format Update index.qmd with figure reference and description.
- Move ODE packages to case study section (not global setup) - Restructure model section with equations together - Introduce Ascertainment as new observation model modifier - Use @set from Accessors.jl for cleaner model composition - Simplify sol2infs: ODE returns proportions, obs model handles counts - Update index.qmd to reflect Ascertainment introduction
Comparison with Draft PR #79This PR builds on the work in draft PR #79, addressing the outstanding tasks whilst making structural changes to better fit the established case study format. Key Differences from PR #79Structural Changes
Code Architecture
Content
Tasks from PR #79 Addressed
What This PR Preserves from PR #79
Design Decision: sol2infs in Observation ModelThe key architectural change is moving population scaling from the infection process to the observation model: # PR #79: Scaling in ODEProcess
sir_process = ODEProcess(
params = sir_params,
sol2infs = sol -> softplus.(N .* sol[2, :]), # Combined here
...
)
# This PR: Scaling in TransformObservationModel
sir_process = ODEProcess(
params = sir_params,
sol2infs = sol -> sol[2, :], # Returns proportions
...
)
transform_pois = TransformObservationModel(pois, x -> log1pexp.(N .* x))This separation means the ODE model returns epidemiologically meaningful quantities (infected proportions), whilst the observation model handles the transformation to expected counts - demonstrating cleaner compositional design. |
- Load ODE packages inline with data, not separate chunk - Consolidate equations: deterministic and stochastic together - Add figure references when defining components (like other case studies) - Explain proportions choice: numerical stability across population sizes - Rename ascert_ar to ascertainment_ar/ascertainment_model for clarity - Reference inference_method from sec-example1 - Define both EpiProblems together, then fit sequentially - Hide R0 computation code (echo: false) - Reword index.qmd intro to match other case study style - Add discussion note: ODE composition via AlgebraicPetri/Catalyst
case-studies.qmd: - Add complete mathematical model with R0 equation - Expand explanation of continuous/discrete time connection - Add softplus explanation (numerical stability for small negative values) - Add OU to AR(1) connection note - Add SciML compositional approach context - Add SciMLSensitivity requirement note - Update figure caption with detailed panel descriptions - Add inline computed R0 values with 95% CIs - Add calibration comparison between models index.qmd: - Add OU to AR(1) derivation note - Add softplus stability note - Add calibration comparison finding - Add submodel conditioning flexibility note in discussion
- Fix I_t_sol field access error by solving ODE directly from parameters - Add solve_sir helper to solve ODE with given β, γ, I₀ - Add posterior_sir_solutions to generate ODE solutions from chain samples - Add compartment_quantiles for computing quantile ribbons - Reuse post_predictive_yt! from Case Study 1 instead of duplicate - Simplify figure code by reusing existing helper functions
Restructure figure layout to 2x3 grid with stacked S/I compartment subplots for clearer presentation. Fix R0 outlier issues caused by numerical instability and improve statistical reporting. Changes: - Create stacked S/I subplots for prior (Panel A) and posterior (Panel D) compartments - Filter extreme R0 outliers (>100) from γ≈0 numerical instability - Change R0 statistics from mean to median for proper Bayesian reporting - Update credible interval notation from CI to CrI - Combine both models in posterior panels D, E, F with blue/orange color scheme - Add posterior Rt over time plot (Panel F) with Rt=1 reference line - Modify post_predictive_yt! to support multiple models on same plot - Remove all plot titles, relying on caption for descriptions - Update figure caption to reflect new 6-panel layout
Correct Rt formula to Rt = (β/γ) × S(t) = R0 × S(t) instead of just β × S(t). Create helper function calculate_Rt to eliminate code duplication between deterministic and stochastic model calculations.
Keep only the R0 legend in Panel C since the blue/orange color scheme for deterministic/stochastic is established there and consistent across all posterior panels.
Plot individual posterior trajectories for S and I compartments to match Panel A style, replacing median lines and credible interval bands with transparent trajectory lines.
Add details about deterministic vs stochastic SIR formulations, parameter inference, and model comparison. Also expand explanation of SciML ecosystem advantages.
- Update equations to show S(t), I(t), R(t) are proportions of population N - Fix infection rate to r_SI(t) = β*S(t)*I(t) (proportion-based) - Fix expected observations to softplus(N·I(t)) to show scaling step - Condense softplus explanation to single sentence mentioning N scaling
- Remove redundant latent_prefix parameter (uses default) - Clarify ascertainment is log-scale AR(1) process applied multiplicatively via exp(κ_t) - Add context about AR process prior specifications (damping, initial state, innovation std)
Explain that AR latent model type is reused from earlier case studies but for observation process rather than infection dynamics. Clarify Ascertainment takes latent_model as argument (unlike earlier direct generate_latent calls) and handles generation internally.
Replace log1pexp with softplus from LogExpFunctions to match mathematical notation in text. Both are equivalent but softplus is more explicit.
Show generated code for generate_latent_infs (ODEProcess) and generate_observations (observation model composition), matching pattern from case study 2.
|
Also not I have tried both the solver used in the original and the default EpiAware solver and both have issues. |
- Panel B: Use longer time scale (70 days) with new infection pattern - Panel C: Swap density plot order (stochastic first, deterministic second) - Panels D, E, F: Maintain blue first, orange second for layering
Use ylims!() function instead of invalid ylims attribute for Makie Axis
Change from Beta(0.5, 0.5) to Beta(1, 99) for initial_prop_infected. This concentrates prior mass on small initial outbreaks, ensuring S₀ stays near 1 as expected epidemiologically.
Change from LogNormal(0, 1.0) to LogNormal(0, 0.5) for infectiousness prior. This reduces prior mass on extreme β values while maintaining flexibility (95% CI: [0.37, 2.7] vs [0.14, 7.1]).
Plot deterministic density first, then stochastic, ensuring orange appears on top of blue.
Change from 0.1 to 0.05 to reduce visual density while maintaining same number of lines.
I've just run this branch locally and not found a convergence problem... thats a bit odd. EDIT: running cell by cell gets a different result to running the task which calls |
Add xticklabelrotation=π/4 (45 degrees) to date tick labels in case studies 2 and 3 to prevent month/year labels from overlapping.
Change from (1 + cospi(...)) to (1 + 0.3 * cospi(...)) so infections oscillate between 350-650 instead of 0-1000, making the ascertainment variation more visible.
I tuned the priors since posting to get this to work |
From the rendered artefact downloaded from CI, this has done the job. |
Remove duplicate SIR solving code by using internal _generate_ode_solution model function. Add sir_R0 helper function to avoid code duplication.
Extract calculate_Rt as reusable helper function that uses sir_R0 internally.
…e names - Reference @fig-epinow2 D when introducing delay distributions in case study 2 - Fix deterministic_R0/stochastic_R0 variable names (remove _raw suffix)
Modify EpiNow2 case study to use more informative prior (Normal(0.3, 0.3)) for initial log reproduction number, favouring outbreak scenarios. Add explanations of what initialization priors mean on the natural Rt scale for both Mishra and EpiNow2 examples. Include transitional text before broadcasting to improve flow.
Create ar2_mishra with ρ₁ ~ N(0.8, 0.05) as specified in Mishra et al., reflecting strong autocorrelation in log R_t process. Previous ar2 used ρ₁ ~ N(0.2, 0.2) which did not match paper. Update case-studies.qmd to use ar2_mishra in Mishra case study. Fix misleading claim in index.qmd that illustration priors were based on Mishra specification.
SamuelBrand1
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. In particular I like the pattern of recursively using @SubModel inside the ODE model.
Summary
TransformObservationModelto compose population scaling with observation modelsKey implementation details
ODEProcesswith SciML ODE solvers for SIR dynamicsTransformObservationModelwraps observation models with softplus population scalingAscertainmentwith AR(1) latent process for time-varying ascertainment in stochastic modelTest plan
task render-case-studiesto verify case study renderstask render-indexto verify main document renders with figurefigures/directory