FAILURE MAP
← Case archive

FA-64856 / Epidemic compartment models / Open access

SIR Euler integrator with daily sampling: day indexing · case 01

Peak days are reported one day early and the run stops one day short in its own numbering.

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

The day loop is zero-based although day 0 is reserved for the seed state.

VERIFIED REPAIR

Restore the day indexing rule: `range(1, days + 1)`.

Unsuccessful approach: Starting at day 1 but stopping at days-1 still simulates one day too few.

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(days):
        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 fixtureActualExpectedOutcome
regression: mild outbreak in 1000[85.915, 303.71, 38, 810.961][85.915, 303.71, 39, 810.961]Failed
regression: fast outbreak daily steps[2.433, 249.994, 9, 497.014][2.433, 249.994, 10, 497.014]Failed
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, 0, 95.312][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 populationNoneNonePassed

SHA-256 / 9d0eaf7d0694611ce939ab9f11e0df4566dc3a817504aec0bc46055d293fc7ce

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):
        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 fixtureActualExpectedOutcome
regression: mild outbreak in 1000[88.758, 303.71, 39, 800.155][85.915, 303.71, 39, 810.961]Failed
regression: fast outbreak daily steps[2.436, 249.994, 10, 496.876][2.433, 249.994, 10, 497.014]Failed
regression: subcritical seed decays[984.083, 10.0, 0, 14.986][984.011, 10.0, 0, 15.167]Failed
regression: overshoot-prone large beta single step[0.0, 75.0, 1, 90.625][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 populationNoneNonePassed

SHA-256 / f333de954911180167ebd938b173b5d74ff737b0530ef6ac27cc404c1fbe3b61

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 fixtureActualExpectedOutcome
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 populationNoneNonePassed

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.691005+00:00.

Case digest / 693fc7c2fd5d51471aa8c8f70f5a69d8a509766e07978969bf4fb13cf1f45f3b