Antimicrobial resistance in hospitals — two-strain colonisation → bloodstream infection

Methodology card. This is the primary human- and agent-legible description of the model. The runnable stub beside it (stub.go) is the type-checked generative demonstration; this card carries the structure, assumptions, and validity regime that the Go code does not spell out.

System

The spread of antibiotic resistance through a hospital patient population, and the resulting burden of resistant bloodstream infections (BSI). Concretely: two competing strains of E. coli — cephalosporin-susceptible (S) and cephalosporin-resistant (R) — colonising patients, where the hospital’s aggregate cephalosporin prescribing rate applies selective pressure that shifts the S/R balance. A downstream Poisson process converts colonisation load into observed BSI counts.

The generative core is two coupled partitions:

Partition Iteration State Role
colonisation ColonisationDynamicsIteration [S_frac, R_frac] Two-strain colonisation SDE (Euler–Maruyama)
infection InfectionProcessIteration [S_bsi, R_bsi] Poisson BSI counts driven by colonisation

The colonisation dynamics per unit time dt:

dS = [ turnover·(S_comm − S) + transmission·S·U − selection·ceph·S + fitness·R ] dt + noise·√S dW
dR = [ turnover·(R_comm − R) + transmission·R·U + selection·ceph·S − fitness·R ] dt + noise·√R dW

where U = 1 − S − R is the uncolonised fraction and ceph is the prescribing rate. The selection term selection·ceph·S is the causal heart of the model: cephalosporin use converts susceptible colonisation into resistant colonisation. BSI counts are Poisson(infection_probability · population · fraction · dt) per strain.

Partition wiring

The partition dependency graph, derived statically from the stub’s BuildStub wiring by pkg/graph. Solid arrows are within-step params_from_upstream wiring (which imposes a computation order); dashed arrows leaving a shaded past-copy node are lag reads of a partition’s committed state from an earlier step — drawn as separate source nodes so the graph stays a DAG.

flowchart TB
  n0["colonisation"]
  n1["infection"]
  n0past["colonisation"]
  n0past -.->|colonisation_partition| n1
  classDef pastcopy fill:#F5F5F5,stroke:#4a7ba6,color:#000;
  class n0past pastcopy;

Ingests (in the stub: nothing)

The stub is data-free — every input is a literal constant in stub.go. The single exogenous driver is the scalar prescribing_rate. In the downstream application these constants become calibrated quantities and the prescribing rate is produced by a policy partition rather than fixed; see Downstream below. The model’s real-world ingests there are UKHSA surveillance series (resistant BSI incidence) and prescribing volumes.

Assumptions

Validity regime

Failure modes

Question answered

Given a hospital’s cephalosporin prescribing rate, what resistant colonisation fraction and resistant bloodstream-infection burden does it sustain — and which direction, and roughly how much, does changing that prescribing rate move them?

Generative behaviour under test

stub_test.go asserts, beyond “it runs”: 1. Harness — no NaNs, correct state widths, no params mutation, no statefulness residue across a repeated run (simulator.RunWithHarnesses). 2. Structural invariantsS, R ∈ [0,1], S + R ≤ 1 every step; BSI counts ≥ 0. 3. Correct direction of parameter response — raising prescribing_rate raises both the steady-state resistant colonisation fraction and the total resistant BSI burden (the observed prescribing sweep is the first row of the generated Observed behaviour table below).

The expected-behaviour suite (behaviour_test.go) adds named, plain-language response claims, with the observed number for each emitted by the test run into the Observed behaviour table below (never hand-typed):

Observed behaviour

Every row below is one bound object: a plain-language response claim, the test subtest that enforces it, and the number that test produced (ensemble values rounded to 2 dp). Nothing here is hand-written — the claims and their numbers are emitted by TestAMRExpectedBehaviour (via go run ./cmd/model-graphs), so a claim cannot drift from its test or its result. If the model’s behaviour changes, either the binding test fails (a claim’s assertion broke) or TestCardsUpToDate fails (a number moved) — a broken claim cannot reach the card silently.

Response claim Enforced by Observed
Higher cephalosporin prescribing raises resistance (headline lever) TestAMRExpectedBehaviour/prescribing_raises_resistance ensemble-mean resistant fraction — rate 0.02 0.13 · 0.3 0.20 · 0.8 0.25
Prescribing moves resistance only through selection (off: no effect; on: it moves) TestAMRExpectedBehaviour/prescribing_acts_only_through_selection resistant-fraction change over the 0.02→0.8 prescribing sweep — selection off Δ 0.00 · selection on Δ 0.12 (asserts selection off Δ < 0.01)
Higher fitness cost of resistance lowers resistance TestAMRExpectedBehaviour/higher_fitness_cost_lowers_resistance ensemble-mean resistant fraction — base 0.20 · fitness_cost=0.15 0.10
Higher within-hospital transmission raises total colonisation TestAMRExpectedBehaviour/higher_transmission_raises_total_colonisation ensemble-mean total colonised fraction (S+R) — base 0.35 · transmission_rate=0.09 0.49
Higher infection probability raises resistant BSI burden TestAMRExpectedBehaviour/higher_infection_probability_raises_bsi ensemble-mean total resistant BSI count — base 190.00 · infection_probability=0.03 587.50

Bespoke extensions (staged beside the stub)

ColonisationDynamicsIteration (colonisation.go) and InfectionProcessIteration (infection.go) are custom simulator.Iteration implementations, lifted verbatim from the downstream repo. They live here rather than in the engine core because the catalogue is the staging ground for the “should this be promoted into core?” question. A generic two-strain competition SDE or a partition-driven Poisson-thinning process recurring across other models would be the signal to promote — but that decision waits for the recurrence, not for this single case.

Downstream

Data ingestion (UKHSA surveillance), simulation-based inference / calibration, and the prescribing-policy decision layer (baseline / cycling / threshold / restriction) live in the project repo:

https://github.com/umbralcalc/antimicrobial-resistance