Building a composable Julia ecosystem for infectious disease modelling

A roadmap, challenges, and questions

Sam Abbott

London School of Hygiene & Tropical Medicine

14 August 2026

Talk plan

  • Bundibugyo virus disease in the Democratic Republic of the Congo
  • The ways we are trying to build it
  • Six years of trying this in R
  • Before and after agents
  • Infrastructure, autodiff, docs, community, governance
  • Please send help!

samabbott.co.uk/JuliaCon2026/roadmap

JuliaCon 2026, Muschel — N3, Friday 14 August 2026, 14:30.

The 2026 Bundibugyo virus disease outbreak in the Democratic Republic of the Congo

A situation report page from the DRC national public health institute, in French, with cumulative confirmed cases, deaths, isolation occupancy, recoveries and suspected cases in boxes.

Posterior densities for cumulative infections, one curve per data stream and one for the joint fit, which sits inside the spread of the single-stream fits.

Posterior reproduction number trajectories over the established outbreak, with most of the mass between 1.5 and 2.

  • A replication of the McCabe et al. report of 18 May, in a few hours the next day
  • Updated up to daily since, and the model has evolved with the outbreak
  • From replicating their model to a renewal process fitted to the suspected, confirmed, death, export and bed-occupancy counts
  • The situation reports are French PDFs. Agents ingest them directly
  • Aiming to follow our infectious disease modelling workflow, and to run in free CI so anyone could use it

BVDOutbreakSize, first commit 2026-05-19 with 33 that day, read 2026-08-13 from the clone at 90df658d · figures from the WHO collaboratory talk of 10 June 2026, fitted to data as of 7 June · situation reports from insp.cd · McCabe et al., Estimation of the size of the outbreak of Ebola disease caused by Bundibugyo virus in DRC, Imperial College London, 18 May 2026, doi:10.25560/130007.

Where does Julia come in?

  • All of it in Julia, with Turing.jl and Mooncake as the autodiff backend
  • Our R and Stan tools were not expressive enough
  • Nothing in Julia for infectious disease modelling that made sense to use, so all of it from scratch. About 35,000 lines
  • The good. Heavy use of submodels makes it easy to follow the workflow
  • The bad. Really quite slow. I cannot get Enzyme to work, and Mooncake compile times are very slow
  • Some of it took a lot of work to run under autodiff at all, the integrals especially

The Julia language logo

The Mooncake.jl logo

About 35,000 lines of Julia across src, test, scripts, ext and docs, counted 2026-08-13 · the Enzyme comparison is BVDOutbreakSize#445, open at 2026-08-12.

epiaware.org

  • Composable tools for infectious disease modelling in Julia
  • Models built from interchangeable parts, not one system rebuilt for each outbreak
  • A specialist contributes the part they know, without holding the whole model in their head
  • Uncertainty carried from the priors through infection dynamics to observation
  • Nowcasting, forecasting and scenarios from one model definition

The EpiAware logo

epiaware.org, read 2026-08-13.

Approach one, ComposableTuringIDModels.jl

  • A library of parts over Turing.jl. Transmission, latent process and observation are separate objects that nest into one model
  • Joint inference and the rest of the Turing toolchain come with it
  • Registered, and the proof of concept the 16:45 talk demonstrates
using ComposableTuringIDModels, Distributions, Turing, Mooncake
using ADTypes: AutoMooncake

renewal = Renewal(;
    generation_time = Gamma(1.4, 1 / 0.38),
    rt = AR(; ϵ_t = HierarchicalNormal(std = HalfNormal(0.1))),
    initialisation = Normal(log(1.0), 1.0))

obs = LatentDelay(                    # delay → ascertainment → noise
    Ascertainment(NegativeBinomialError(),
        Intercept(Normal(log(0.4), 0.1))),
    truncated(Normal(5.0, 2.0), 0.0, Inf))

model = IDModel(renewal, obs)

chain = sample(as_turing_model(model, cases, length(cases)),
    NUTS(0.95; adtype = AutoMooncake(; config = nothing)),
    MCMCThreads(), 100, 2)

Approach two, ComposedDistributions.jl

  • Model the relationships between events with Distributions.jl
  • Those relationships form an event tree
  • Modify the tree, and the distributions in it, for an intervention. Split the tree, compete distributions against each other
  • convolve_series pushes a time series through the tree, and it marginalises across the tree where that is cheaper
  • lower turns a delay into a compartmental generator, over Catalyst, SciMLBase, JumpProcesses and AlgebraicPetri

epiaware.org/approaches/composed-distributions, read 2026-08-13, comments trimmed to fit · ComposedDistributions.jl · LoweredDistributions.jl README, read 2026-08-10.

using ComposedDistributions, Distributions

tree = @uncertain compose((          # parallel pathways, nested
    clinical = sequential(
        :onset_admit => Gamma(Normal(1.2, 0.2), 3.0),
        :admit_resolve => resolve(
            :death => (Gamma(2.0, 3.5), 0.3),
            :discharge => (Gamma(1.0, 8.0), 0.7))),
    surveillance = sequential(
        :onset_notif => Gamma(0.7, 20.0),
        :notif_compete => compete(
            :confirmed => Gamma(2.0, 1.0),
            :lost => Gamma(5.0, 0.5))),))

event_tree(tree)              # the tree,
params_table(tree)            # its parameters,
priors = param_priors(tree)   # and its default priors

case = rand(tree)
logpdf(tree, case)

Some inspiration

Six years of trying this in R and Stan

Package Since Stars CRAN
EpiNow2 2020 141 58,073
epinowcast 2021 67 not on CRAN
epidist 2023 16 not on CRAN
primarycensored 2024 9 11,294
baselinenowcast 2025 10 1,678

Stars and creation dates from the GitHub API, downloads are CRAN totals to 2026-08-11 from cranlogs, retrieved 2026-08-12, and baselinenowcast 2026-08-13.

  • All of them are used by public health departments, including CDC, UKHSA, RKI, ECDC and WHO
  • epinowcast was the attempt at community developed tools
  • In many cases the package is too complex for anyone else to use
  • We see fragmentation, and a lot of small mistakes in outbreak modelling that add up to real biases

Trying, but not in software

A nine step workflow for infectious disease modelling, from research questions through process and observation DAGs, modularisation, inference choices, implementation, validation and data integration, with feedback arrows running backwards from later steps to earlier ones.

What do we need to implement this?

Before skynet

  • Funding. Several goes at grants of a few million, and a lot of scepticism about Julia. None of them funded so far
  • A team. A few people
  • Buy-in. Other groups have to want to build on it

“We have been applying for funding to do it. So far without success”, the EpiAware announcement post, news/posts/hello-epiaware.qmd on epiaware.org, published 2026-07-10, read 2026-08-12.

After skynet, what does robot driven dev look like? 🤖

Monthly counts of pull requests opened by the bot account across all of my organisations, January to July 2026.

All of my organisations, seabbs-bot only, January to July 2026 · EpiAware counts from gh api search/issues, read 2026-08-10.

  • I was travelling for summer schools, teaching and workshops, so decided to have a go with agents
  • 966 merged pull requests in the EpiAware org from the bot account, and 119 closed unmerged
  • A Claude Max subscription
  • Well, actually, we still need funding

Our aims

  1. All the composability requirements we outlined in the other talk
  2. As much infrastructure as we can build
  3. Automatic differentiation as a first class citizen
  4. Good to great docs, and checking that they work
  5. A community of contributors
  6. Governance

.github

  • Every package’s workflows are thin callers into 22 reusable workflows in one .github repository, as SciML does it
  • Each caller pins the reusable workflow to a commit SHA
  • The more interesting ones: the per-backend autodiff matrix, downstream testing, and the weekly template sync
  • release-nudge opens a tracking issue when a package has unreleased changes, and closes it again when it does not
# MANAGED by EpiAwarePackageTools.scaffold
# — do not edit by hand.
name: Test

jobs:
  test:
    uses: EpiAware/.github/.github/workflows/tests.yml@d024dc6

try-this-pr comments install instructions on every pull request

import Pkg
Pkg.activate(temp = true)
Pkg.add(url = "...", rev = "my-branch")
using CensoredDistributions

.github/workflows/test.yaml in ComposableTuringIDModels.jl, SHA abridged, read 2026-08-12 · 22 of the 23 workflows in EpiAware/.github declare workflow_call, counted from the contents API 2026-08-13 · template-sync.yaml runs Monday 04:00 UTC.

EpiAwarePackageTools.jl

  • Rigid template implementing and enforcing community standards
  • scaffold writes the tooling into a package once. update re-applies it and reports drift
  • Aqua, JET, formatting, docstrings and benchmarks
  • Sixteen written standards, and checks for them
  • It injects AGENTS.md and CLAUDE.md so a model can find the docs

Important

We should probably have built this on BestieTemplate.jl’s Copier template rather than writing our own scaffolder.

using EpiAwarePackageTools

scaffold(pkgdir(MyPackage))

# again on every sync
EpiAwarePackageTools.update(pkgdir(MyPackage))

src/ and templates/ in EpiAwarePackageTools.jl, counted at commit 427d4f5 on main, 2026-08-12 · ten of the eleven Julia packages carry template-sync.yaml on main, checked through the contents API 2026-08-12 · BestieTemplate.jl and PkgTemplates.jl.

Package checks

  • CRAN has more checks than comparable package servers
  • As part of this there is a set of automated checks that looks at documentation, examples, dependencies and portability
  • In Julia you can opt in to Aqua, which is great
  • But we want to go further, so we added JET, docstring format, doctests, README sections, import centralisation, formatting and benchmarks
@testitem "Quality: Aqua"
@testitem "Quality: ExplicitImports"
@testitem "Quality: import centralisation"
@testitem "Quality: docstring format"
@testitem "Quality: README sections"
@testitem "Quality: doctest"
@testitem "Quality: formatting"
@testitem "Quality: linting (JET)"
@testitem "Quality: extension ambiguities"

templates/test/package/quality.jl in EpiAwarePackageTools.jl, written into each adopting package and overwritten on every sync, read 2026-08-12.

Reverse dependency checks

  • Nothing in Julia checks the packages downstream of you
  • CRAN policy asks you to check that packages depending on yours still pass R CMD check before you update. Julia has no equivalent
  • SciML runs downstream CI against two packages it does not own
  • Our scaffold writes a downstream job into all ten packages, run by a label on a pull request

CRAN Repository Policy, read 2026-08-12 · downstream.yaml and the downgrade-compat job read through the contents API across the ten packages that carry them, 2026-08-12 · SciML’s Downstream.yml in OrdinaryDiffEq.jl runs ProbNumDiffEq.jl and PositiveIntegrators.jl, read 2026-08-10.

What does it mean to say a package supports automatic differentiation?

  • We do not think there is a community standard for what supporting automatic differentiation means
  • So we aimed to build our own, using DifferentiationInterfaceTest
  • In SpecialFunctions.jl the shape-parameter partials of gamma_inc and beta_inc are not implemented, so support is janky across backends
  • EpiAwareADTools.jl stores our org-wide automatic differentiation fixes, and aims to delete each one when upstream ships it

Note

Is there a guide for any of this anywhere in the Julia ecosystem? I went looking and did not find one.

DIT.test_differentiation(
    [backend], ok;
    correctness = true,
    type_stability = :none,
    scenario_intact = scenario_intact,
    rtol = rtol, atol = atol
)

check_broken(broken_scens, backend; rtol, atol)

Guillaume Dalle, author of DifferentiationInterface.jl

src/ad_harness.jl:157-168 in EpiAwarePackageTools.jl, read 2026-08-13 · DifferentiationInterface.jl is by Guillaume Dalle, with Adrian Hill · DifferentiationInterfaceTest.jl · the missing rules are SpecialFunctions.jl issue #531, and the replacements are in src/gamma_ad.jl in EpiAwareADTools.jl, read 2026-08-13.

Documentation

  • Julia’s default is a docstring if you feel like it
  • DocStringExtensions gets us close to R’s standard. I would like more
  • The README’s Julia blocks run at docs build, so a broken example fails the build
  • A tutorial for each key feature, developed alongside the feature and checked in CI
  • SciML puts something interesting on the front page and keeps the tutorials short. We copy that
@template (FUNCTIONS, METHODS, MACROS) = """
$(TYPEDSIGNATURES)
$(DOCSTRING)
"""

@template TYPES = """
$(TYPEDEF)
$(DOCSTRING)

---
## Fields
$(TYPEDFIELDS)
"""

templates/src/docstrings.jl in EpiAwarePackageTools.jl, written into every package and paired with test_docstring_format, read 2026-08-13 · DocStringExtensions.jl.

Community, in the age of robots

  • Nobody from outside the team has committed since February, so the other party on an issue is usually one of my robots. Answering a robot is dispiriting
  • It is not clear which repositories want robot-filed issues at all
  • Do 🤖 shut down collaboration?
  • EpiAwareAgents is one possible answer, a community bot any maintainer could point at their own repository
  • 🌍 We want contributors who are not Julia developers, and not in well resourced settings

🤖 🤖 🤖 🤖 🤖 🤖
🤖 🤖 🤖 🤖 🤖 🤖
🤖 🤖 🧑‍💻 👩‍💻 🤖 🤖
🤖 🤖 🤖 🧑‍🔬 🤖 🤖
🤖 🤖 🤖 🤖 🤖 🤖
🤖 🤖 🤖 🤖 🤖 🤖

EpiAwareAgents README and DESIGN.md, read 2026-08-12 · no open good first issue in the org from gh search issues, and the most recent outside commit is 2026-02-03 from git log on main, both 2026-08-12.

Governance, and current whales 🐋

  • JuMP and SciML wrote their governance down after they were already large, and Turing has contributing guidance instead
  • None of them says how an org starts, or how a package joins one (according to @seabbs-bot)
  • 🐋 Every project I have worked on has ended up with one person doing most of it
  • Here the whale is me and a subscription
  • 🐳 Julia’s composability does not fix that. The hope is that modularity makes it easier for the 🐋 to work together
  • What are good examples of 🐋 working together in Julia?
  • 🌍 Who can do this work at all is narrowing. Agents make that worse

JuMP governance, SciML governance and TuringLang’s contributing guidance, read 2026-08-10, with no governance document in any TuringLang repository, searched 2026-08-12.

Governance, what do users want

  • The people I build for work in R in the middle of an outbreak response
  • They don’t care about fancy language features
  • They need their tools to work
  • Julia often doesn’t
  • They need to be able to trust it, and to see who is behind it
  • Who do they ask when it breaks?
  • How do we move users to contributors?

Thank you

Please send help!