FA-64841 / Epidemic compartment models / Open access
SIR Euler integrator with daily sampling: transmission scaling · case 01
Outbreak size explodes with population size and susceptibles are exhausted within the first substep.
ROOT CAUSE
Infections use density-dependent mass action beta*S*I instead of the frequency-dependent beta*S*I/pop.
VERIFIED REPAIR
Restore the transmission scaling rule: `beta * s * i / pop * dt`.
Unsuccessful approach: Normalising by S+I ignores the recovered in the mixing pool, so late-epidemic force of infection is inflated.
Case contract
Frequency-dependent SIR integrated with forward Euler at dt=1/substeps; new infections per substep are beta*S*I/pop*dt capped at S; peak prevalence and its day are sampled at day ends (day 0 is the seed); return [S, peak I, peak day, R] rounded to 3 decimals, or None for invalid input.
Why this case matters
Compartmental epidemic calculations drive outbreak forecasts, vaccine targets and hospital planning; a single wrong flow, rate conversion or boundary silently changes every downstream number.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(beta, gamma, pop, i0, days, substeps):
if pop <= 0 or i0 < 0 or i0 > pop or substeps < 1:
return None
dt = 1.0 / substeps
s = float(pop - i0)
i = float(i0)
r = 0.0
peak_i = i
peak_day = 0
for day in range(1, days + 1):
for _ in range(substeps):
new_inf = min(beta * s * i * dt, s)
new_rec = gamma * i * dt
s -= new_inf
i += new_inf - new_rec
r += new_rec
if i > peak_i:
peak_i = i
peak_day = day
return [round(s, 3), round(peak_i, 3), peak_day, round(r, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0])],
[('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639])],
[('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: mild outbreak in 1000 | [0.0, 948.797, 1, 997.589] | [85.915, 303.71, 39, 810.961] | Failed |
| regression: fast outbreak daily steps | [0.0, 499.0, 1, 499.917] | [2.433, 249.994, 10, 497.014] | Failed |
| regression: subcritical seed decays | [0.0, 958.5, 1, 982.509] | [984.011, 10.0, 0, 15.167] | Failed |
| control: overshoot-prone large beta single step | [0.0, 75.0, 1, 95.312] | [0.0, 75.0, 1, 95.312] | Passed |
| regression: overshoot-prone large beta two steps | [0.0, 159.8, 1, 168.539] | [0.0, 156.8, 1, 166.339] | Failed |
| control: boundary zero days | [97.0, 3.0, 0, 0.0] | [97.0, 3.0, 0, 0.0] | Passed |
| control: boundary no seed | [100.0, 0.0, 0, 0.0] | [100.0, 0.0, 0, 0.0] | Passed |
SHA-256 / 83d242804db02e19939c86282f6b519f796ab57071d4fdaf4c1ca35b48eaabe8
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(beta, gamma, pop, i0, days, substeps):
if pop <= 0 or i0 < 0 or i0 > pop or substeps < 1:
return None
dt = 1.0 / substeps
s = float(pop - i0)
i = float(i0)
r = 0.0
peak_i = i
peak_day = 0
for day in range(1, days + 1):
for _ in range(substeps):
new_inf = min(beta * s * i / (s + i) * dt, s)
new_rec = gamma * i * dt
s -= new_inf
i += new_inf - new_rec
r += new_rec
if i > peak_i:
peak_i = i
peak_day = day
return [round(s, 3), round(peak_i, 3), peak_day, round(r, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0])],
[('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639])],
[('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: mild outbreak in 1000 | [0.453, 387.948, 39, 918.98] | [85.915, 303.71, 39, 810.961] | Failed |
| regression: fast outbreak daily steps | [0.0, 281.911, 10, 499.584] | [2.433, 249.994, 10, 497.014] | Failed |
| regression: subcritical seed decays | [983.948, 10.0, 0, 15.216] | [984.011, 10.0, 0, 15.167] | Failed |
| control: overshoot-prone large beta single step | [0.0, 75.0, 1, 95.312] | [0.0, 75.0, 1, 95.312] | Passed |
| regression: overshoot-prone large beta two steps | [0.0, 159.864, 1, 166.457] | [0.0, 156.8, 1, 166.339] | Failed |
| control: boundary zero days | [97.0, 3.0, 0, 0.0] | [97.0, 3.0, 0, 0.0] | Passed |
| control: boundary no seed | [100.0, 0.0, 0, 0.0] | [100.0, 0.0, 0, 0.0] | Passed |
SHA-256 / 9148bdebb69a04adc4ab563fe8a88583409797c085ee07ead554c36fd7eae083
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(beta, gamma, pop, i0, days, substeps):
if pop <= 0 or i0 < 0 or i0 > pop or substeps < 1:
return None
dt = 1.0 / substeps
s = float(pop - i0)
i = float(i0)
r = 0.0
peak_i = i
peak_day = 0
for day in range(1, days + 1):
for _ in range(substeps):
new_inf = min(beta * s * i / pop * dt, s)
new_rec = gamma * i * dt
s -= new_inf
i += new_inf - new_rec
r += new_rec
if i > peak_i:
peak_i = i
peak_day = day
return [round(s, 3), round(peak_i, 3), peak_day, round(r, 3)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0])],
[('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639])],
[('control: boundary no seed', (0.5, 0.1, 100, 0, 10, 2), [100.0, 0.0, 0, 0.0]),
('control: invalid seed above population', (0.5, 0.1, 100, 150, 10, 2), None),
('control: invalid zero substeps', (0.5, 0.1, 100, 1, 10, 0), None),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('regression: slow recovery long run', (0.25, 0.05, 2000, 2, 90, 3), [19.331, 965.817, 43, 1829.639]),
('regression: large city fine steps',
(0.4, 0.15, 100000, 20, 50, 8),
[13105.196, 25867.476, 37, 75829.968]),
('regression: small village', (0.6, 0.25, 50, 1, 30, 2), [5.845, 11.78, 12, 43.206]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('regression: mild outbreak in 1000', (0.3, 0.1, 1000, 1, 60, 4), [85.915, 303.71, 39, 810.961]),
('regression: fast outbreak daily steps', (0.9, 0.2, 500, 5, 40, 1), [2.433, 249.994, 10, 497.014]),
('regression: subcritical seed decays', (0.08, 0.2, 1000, 10, 20, 2), [984.011, 10.0, 0, 15.167]),
('control: overshoot-prone large beta single step', (3.0, 0.5, 100, 50, 5, 1), [0.0, 75.0, 1, 95.312]),
('regression: overshoot-prone large beta two steps', (2.5, 0.3, 200, 80, 6, 2), [0.0, 156.8, 1, 166.339]),
('control: boundary zero days', (0.5, 0.1, 100, 3, 0, 2), [97.0, 3.0, 0, 0.0]),
('control: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])]]
for label, args, expected in fixtures[N - 1]:
check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression: mild outbreak in 1000 | [85.915, 303.71, 39, 810.961] | [85.915, 303.71, 39, 810.961] | Passed |
| regression: fast outbreak daily steps | [2.433, 249.994, 10, 497.014] | [2.433, 249.994, 10, 497.014] | Passed |
| regression: subcritical seed decays | [984.011, 10.0, 0, 15.167] | [984.011, 10.0, 0, 15.167] | Passed |
| control: overshoot-prone large beta single step | [0.0, 75.0, 1, 95.312] | [0.0, 75.0, 1, 95.312] | Passed |
| regression: overshoot-prone large beta two steps | [0.0, 156.8, 1, 166.339] | [0.0, 156.8, 1, 166.339] | Passed |
| control: boundary zero days | [97.0, 3.0, 0, 0.0] | [97.0, 3.0, 0, 0.0] | Passed |
| control: boundary no seed | [100.0, 0.0, 0, 0.0] | [100.0, 0.0, 0, 0.0] | Passed |
SHA-256 / b653d7e315e36a65de3dd00d83feb43dc988a6d24b453a0ac175bfb1970cea0b
Verification & scope
Deterministic bounded teaching model with a stipulated contract; not a validated scientific or public-health modelling library. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:47:28.279669+00:00.
Case digest / 7d9f66cdaa47bd0d1a87a91d6fa08648f3f8197c116c9f25b988b6130c61c9a1