FA-65146 / Epidemic compartment models / Open access
Epidemic growth rate, doubling time and R: slope estimator · case 01
Growth rate depends only on the first and last counts and on how many days were positive.
ROOT CAUSE
An endpoint difference is divided by the number of points rather than fitting the regression.
VERIFIED REPAIR
Restore the slope estimator rule: `r = sxy / sxx`.
Unsuccessful approach: Endpoint slope over the day span still ignores interior observations.
Case contract
r is the least-squares slope of ln(count) against day index over days with positive counts (original day indices kept); doubling time ln2/r only when r>0 else None; latent time Tl=latent_fraction*Tg, infectious time Ti=Tg-Tl, R=(1+r*Tl)*(1+r*Ti); return [r rounded 6, doubling rounded 4, R rounded 6]; None with fewer than two positive days.
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(counts, generation_mean, latent_fraction):
pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
if len(pts) < 2:
return None
n = len(pts)
mx = sum(d for d, _ in pts) / n
my = sum(y for _, y in pts) / n
sxx = sum((d - mx) ** 2 for d, _ in pts)
sxy = sum((d - mx) * (y - my) for d, y in pts)
r = (pts[-1][1] - pts[0][1]) / (n - 1)
doubling = round(math.log(2) / r, 4) if r > 0 else None
tl = latent_fraction * generation_mean
ti = generation_mean - tl
rep = (1 + r * tl) * (1 + r * ti)
return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
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: noisy growth | [0.383764, 1.8062, 3.802471] | [0.372323, 1.8617, 3.693365] | Failed |
| regression: with reporting gap zero | [0.677013, 1.0238, 9.187189] | [0.437065, 1.5859, 5.341617] | Failed |
| regression: declining | [-0.240795, None, 0.231641] | [-0.246645, None, 0.217822] | Failed |
| control: flat | [0.0, None, 1.0] | [0.0, None, 1.0] | Passed |
| control: too few positives | None | None | Passed |
| control: exact doubling per day | [0.693147, 1.0, 4.160461] | [0.693147, 1.0, 4.160461] | Passed |
| regression: leading zeros | [0.828302, 0.8368, 14.86647] | [0.796555, 0.8702, 14.037592] | Failed |
SHA-256 / 9b8db7c46cc8a6ee34f4b40d6df1d99fff922d3aa6152563d4f3f0733b7cd6a4
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(counts, generation_mean, latent_fraction):
pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
if len(pts) < 2:
return None
n = len(pts)
mx = sum(d for d, _ in pts) / n
my = sum(y for _, y in pts) / n
sxx = sum((d - mx) ** 2 for d, _ in pts)
sxy = sum((d - mx) * (y - my) for d, y in pts)
r = (pts[-1][1] - pts[0][1]) / (pts[-1][0] - pts[0][0])
doubling = round(math.log(2) / r, 4) if r > 0 else None
tl = latent_fraction * generation_mean
ti = generation_mean - tl
rep = (1 + r * tl) * (1 + r * ti)
return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
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: noisy growth | [0.383764, 1.8062, 3.802471] | [0.372323, 1.8617, 3.693365] | Failed |
| regression: with reporting gap zero | [0.451342, 1.5357, 5.541434] | [0.437065, 1.5859, 5.341617] | Failed |
| regression: declining | [-0.240795, None, 0.231641] | [-0.246645, None, 0.217822] | Failed |
| control: flat | [0.0, None, 1.0] | [0.0, None, 1.0] | Passed |
| control: too few positives | None | None | Passed |
| control: exact doubling per day | [0.693147, 1.0, 4.160461] | [0.693147, 1.0, 4.160461] | Passed |
| regression: leading zeros | [0.828302, 0.8368, 14.86647] | [0.796555, 0.8702, 14.037592] | Failed |
SHA-256 / f7dcdeb63f63a29e8d2e69b6811ac9098421b979469b323687b3313b9661be5f
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(counts, generation_mean, latent_fraction):
pts = [(d, math.log(c)) for d, c in enumerate(counts) if c > 0]
if len(pts) < 2:
return None
n = len(pts)
mx = sum(d for d, _ in pts) / n
my = sum(y for _, y in pts) / n
sxx = sum((d - mx) ** 2 for d, _ in pts)
sxy = sum((d - mx) * (y - my) for d, y in pts)
r = sxy / sxx
doubling = round(math.log(2) / r, 4) if r > 0 else None
tl = latent_fraction * generation_mean
ti = generation_mean - tl
rep = (1 + r * tl) * (1 + r * ti)
return [round(r, 6), doubling, round(rep, 6)]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('regression: with reporting gap zero', ([2, 4, 0, 9, 13, 0, 30], 6.0, 0.5), [0.437065, 1.5859, 5.341617]),
('regression: declining', ([50, 41, 30, 26, 18, 15], 4.0, 0.3), [-0.246645, None, 0.217822]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592])],
[('regression: noisy growth', ([3, 5, 6, 10, 14, 19, 30], 5.0, 0.4), [0.372323, 1.8617, 3.693365]),
('control: flat', ([7, 7, 7, 7], 5.0, 0.5), [0.0, None, 1.0]),
('control: too few positives', ([0, 0, 4, 0], 5.0, 0.5), None),
('control: exact doubling per day', ([1, 2, 4, 8, 16], 3.0, 0.5), [0.693147, 1.0, 4.160461]),
('regression: leading zeros', ([0, 0, 1, 3, 5, 12], 7.0, 0.6), [0.796555, 0.8702, 14.037592]),
('regression: no latency', ([10, 13, 17, 22, 28], 5.0, 0.0), [0.258533, 2.6811, 2.292666]),
('control: two points', ([5, 9], 4.5, 0.25), [0.587787, 1.1792, 4.956834])]]
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: noisy growth | [0.372323, 1.8617, 3.693365] | [0.372323, 1.8617, 3.693365] | Passed |
| regression: with reporting gap zero | [0.437065, 1.5859, 5.341617] | [0.437065, 1.5859, 5.341617] | Passed |
| regression: declining | [-0.246645, None, 0.217822] | [-0.246645, None, 0.217822] | Passed |
| control: flat | [0.0, None, 1.0] | [0.0, None, 1.0] | Passed |
| control: too few positives | None | None | Passed |
| control: exact doubling per day | [0.693147, 1.0, 4.160461] | [0.693147, 1.0, 4.160461] | Passed |
| regression: leading zeros | [0.796555, 0.8702, 14.037592] | [0.796555, 0.8702, 14.037592] | Passed |
SHA-256 / 3fb9a4ce15d63c4c94c23e9a57afc36add7bc056b0466ce211c43b86c607e6fa
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:31.352929+00:00.
Case digest / 3967f486273c90c93e0697cfb5b4504502c839ec13e121cad51e3456c398e9f3