FA-64851 / Epidemic compartment models / Open access
SIR Euler integrator with daily sampling: recovery step size · case 01
Recovery runs substeps times too fast whenever the day is subdivided.
ROOT CAUSE
The recovery flow omits the dt factor while infection applies it.
VERIFIED REPAIR
Restore the recovery step size rule: `new_rec = gamma * i * dt`.
Unsuccessful approach: Using the seed prevalence i0 makes recovery independent of current prevalence.
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 / pop * dt, s)
new_rec = gamma * i
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]),
('regression: 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]),
('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)],
[('regression: 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]),
('regression: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('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),
('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]),
('regression: 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: 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),
('regression: 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 | [996.036, 1.0, 0, 3.962] | [85.915, 303.71, 39, 810.961] | Failed |
| regression: fast outbreak daily steps | [2.433, 249.994, 10, 497.014] | [2.433, 249.994, 10, 497.014] | Passed |
| regression: subcritical seed decays | [987.537, 10.0, 0, 12.454] | [984.011, 10.0, 0, 15.167] | Failed |
| regression: overshoot-prone large beta single step | [0.0, 75.0, 1, 95.312] | [0.0, 75.0, 1, 95.312] | 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 |
| control: invalid seed above population | None | None | Passed |
SHA-256 / 5fbd4abfc8bbd8b37575741dd6d92572cd36ce533e486f648ff20a5f4c4a16b7
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 / pop * dt, s)
new_rec = gamma * i0 * 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]),
('regression: 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]),
('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)],
[('regression: 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]),
('regression: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('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),
('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]),
('regression: 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: 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),
('regression: 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.021, 994.557, 51, 6.0] | [85.915, 303.71, 39, 810.961] | Failed |
| regression: fast outbreak daily steps | [0.0, 487.812, 12, 40.0] | [2.433, 249.994, 10, 497.014] | Failed |
| regression: subcritical seed decays | [1007.013, 10.0, 0, 40.0] | [984.011, 10.0, 0, 15.167] | Failed |
| regression: overshoot-prone large beta single step | [0.0, 75.0, 1, 125.0] | [0.0, 75.0, 1, 95.312] | 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 |
| control: invalid seed above population | None | None | Passed |
SHA-256 / 7b8beed9f06bb4979a44ae398fa269e92a333789a97586e10a2fc666cec51a88
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]),
('regression: 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]),
('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)],
[('regression: 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]),
('regression: whole population seeded', (0.5, 0.2, 40, 40, 5, 2), [0.0, 40.0, 0, 26.053])],
[('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),
('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]),
('regression: 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: 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),
('regression: 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 |
| regression: overshoot-prone large beta single step | [0.0, 75.0, 1, 95.312] | [0.0, 75.0, 1, 95.312] | 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 |
| control: invalid seed above population | None | None | Passed |
SHA-256 / 2e1e47e0282579989fa8ef1ea7233739cba9a1d700e8bb9b09ab92a44e91e56c
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.683674+00:00.
Case digest / 1d6afa5dad4b96ad14ac6072de5e4af851276acc77d8f9088e1b465d6b0d4be0