FA-70861 / Weather index computation / Open access
Heat index regression: simple estimate base offset · case 01
Mild-temperature heat index values are tens of degrees too high.
ROOT CAUSE
The simple Steadman estimate offsets temperature from the freezing point (32) instead of 68 degF.
VERIFIED REPAIR
Use 1.2*(T - 68) in the simple estimate.
Unsuccessful approach: Scaling humidity by 0.94 instead of 0.094 leaves the simple branch wrong for all humid inputs.
Case contract
Input dry-bulb temperature in degF and relative humidity in percent. Compute the simple Steadman estimate 0.5*(T+61+1.2*(T-68)+0.094*RH); if the mean of that estimate and T is below 80 return it, otherwise use the Rothfusz regression with the dry adjustment (RH<13, 80<=T<=112) or the humid adjustment (RH>85, 80<=T<=87). Round to 0.1 degF.
Why this case matters
Heat advisories are issued from this value; a wrong branch changes public warnings.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(t_f, rh):
simple = 0.5 * (t_f + 61.0 + (t_f - 32.0) * 1.2 + rh * 0.094)
if (simple + t_f) / 2 < 80:
return round(simple, 1)
hi = (-42.379 + 2.04901523 * t_f + 10.14333127 * rh - 0.22475541 * t_f * rh
- 0.00683783 * t_f * t_f - 0.05481717 * rh * rh + 0.00122874 * t_f * t_f * rh
+ 0.00085282 * t_f * rh * rh - 0.00000199 * t_f * t_f * rh * rh)
if rh < 13 and 80 <= t_f <= 112:
hi -= ((13 - rh) / 4) * math.sqrt((17 - abs(t_f - 95)) / 17)
elif rh > 85 and 80 <= t_f <= 87:
hi += ((rh - 85) / 10) * ((87 - t_f) / 5)
return round(hi, 1)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['70F at 0% RH', [70, 0], 66.7], ['70F at 5% RH', [70, 5], 66.9], ['70F at 10% RH', [70, 10], 67.2], ['70F at 12% RH', [70, 12], 67.3], ['70F at 13% RH', [70, 13], 67.3], ['70F at 40% RH', [70, 40], 68.6], ['70F at 60% RH', [70, 60], 69.5], ['70F at 85% RH', [70, 85], 70.7]], [['70F at 12% RH', [70, 12], 67.3], ['70F at 85% RH', [70, 85], 70.7], ['70F at 86% RH', [70, 86], 70.7], ['70F at 90% RH', [70, 90], 70.9], ['70F at 95% RH', [70, 95], 71.2], ['70F at 100% RH', [70, 100], 71.4], ['76F at 0% RH', [76, 0], 73.3], ['76F at 5% RH', [76, 5], 73.5]], [['70F at 60% RH', [70, 60], 69.5], ['76F at 5% RH', [76, 5], 73.5], ['76F at 10% RH', [76, 10], 73.8], ['76F at 12% RH', [76, 12], 73.9], ['76F at 13% RH', [76, 13], 73.9], ['76F at 40% RH', [76, 40], 75.2], ['76F at 60% RH', [76, 60], 76.1], ['76F at 85% RH', [76, 85], 77.3]], [['70F at 90% RH', [70, 90], 70.9], ['76F at 60% RH', [76, 60], 76.1], ['76F at 90% RH', [76, 90], 77.5], ['76F at 95% RH', [76, 95], 77.8], ['76F at 100% RH', [76, 100], 78.0], ['79F at 0% RH', [79, 0], 76.6], ['79F at 5% RH', [79, 5], 76.8], ['79F at 10% RH', [79, 10], 77.1]], [['76F at 0% RH', [76, 0], 73.3], ['76F at 100% RH', [76, 100], 78.0], ['79F at 13% RH', [79, 13], 77.2], ['79F at 40% RH', [79, 40], 78.5], ['79F at 60% RH', [79, 60], 79.4], ['79F at 85% RH', [79, 85], 80.6], ['79F at 86% RH', [79, 86], 80.6], ['79F at 90% RH', [79, 90], 80.8]]]
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 |
|---|---|---|---|
| 70F at 0% RH | 88.3 | 66.7 | Failed |
| 70F at 5% RH | 88.5 | 66.9 | Failed |
| 70F at 10% RH | 88.8 | 67.2 | Failed |
| 70F at 12% RH | 88.9 | 67.3 | Failed |
| 70F at 13% RH | 88.9 | 67.3 | Failed |
| 70F at 40% RH | 77.0 | 68.6 | Failed |
| 70F at 60% RH | 75.9 | 69.5 | Failed |
| 70F at 85% RH | 69.0 | 70.7 | Failed |
SHA-256 / b11b43c568c8d9b11cffb758f2befe1e55664b12c959715a1a763da2cd1360be
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(t_f, rh):
simple = 0.5 * (t_f + 61.0 + (t_f - 68.0) * 1.2 + rh * 0.94)
if (simple + t_f) / 2 < 80:
return round(simple, 1)
hi = (-42.379 + 2.04901523 * t_f + 10.14333127 * rh - 0.22475541 * t_f * rh
- 0.00683783 * t_f * t_f - 0.05481717 * rh * rh + 0.00122874 * t_f * t_f * rh
+ 0.00085282 * t_f * rh * rh - 0.00000199 * t_f * t_f * rh * rh)
if rh < 13 and 80 <= t_f <= 112:
hi -= ((13 - rh) / 4) * math.sqrt((17 - abs(t_f - 95)) / 17)
elif rh > 85 and 80 <= t_f <= 87:
hi += ((rh - 85) / 10) * ((87 - t_f) / 5)
return round(hi, 1)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['70F at 0% RH', [70, 0], 66.7], ['70F at 5% RH', [70, 5], 66.9], ['70F at 10% RH', [70, 10], 67.2], ['70F at 12% RH', [70, 12], 67.3], ['70F at 13% RH', [70, 13], 67.3], ['70F at 40% RH', [70, 40], 68.6], ['70F at 60% RH', [70, 60], 69.5], ['70F at 85% RH', [70, 85], 70.7]], [['70F at 12% RH', [70, 12], 67.3], ['70F at 85% RH', [70, 85], 70.7], ['70F at 86% RH', [70, 86], 70.7], ['70F at 90% RH', [70, 90], 70.9], ['70F at 95% RH', [70, 95], 71.2], ['70F at 100% RH', [70, 100], 71.4], ['76F at 0% RH', [76, 0], 73.3], ['76F at 5% RH', [76, 5], 73.5]], [['70F at 60% RH', [70, 60], 69.5], ['76F at 5% RH', [76, 5], 73.5], ['76F at 10% RH', [76, 10], 73.8], ['76F at 12% RH', [76, 12], 73.9], ['76F at 13% RH', [76, 13], 73.9], ['76F at 40% RH', [76, 40], 75.2], ['76F at 60% RH', [76, 60], 76.1], ['76F at 85% RH', [76, 85], 77.3]], [['70F at 90% RH', [70, 90], 70.9], ['76F at 60% RH', [76, 60], 76.1], ['76F at 90% RH', [76, 90], 77.5], ['76F at 95% RH', [76, 95], 77.8], ['76F at 100% RH', [76, 100], 78.0], ['79F at 0% RH', [79, 0], 76.6], ['79F at 5% RH', [79, 5], 76.8], ['79F at 10% RH', [79, 10], 77.1]], [['76F at 0% RH', [76, 0], 73.3], ['76F at 100% RH', [76, 100], 78.0], ['79F at 13% RH', [79, 13], 77.2], ['79F at 40% RH', [79, 40], 78.5], ['79F at 60% RH', [79, 60], 79.4], ['79F at 85% RH', [79, 85], 80.6], ['79F at 86% RH', [79, 86], 80.6], ['79F at 90% RH', [79, 90], 80.8]]]
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 |
|---|---|---|---|
| 70F at 0% RH | 66.7 | 66.7 | Passed |
| 70F at 5% RH | 69.0 | 66.9 | Failed |
| 70F at 10% RH | 71.4 | 67.2 | Failed |
| 70F at 12% RH | 72.3 | 67.3 | Failed |
| 70F at 13% RH | 72.8 | 67.3 | Failed |
| 70F at 40% RH | 85.5 | 68.6 | Failed |
| 70F at 60% RH | 75.9 | 69.5 | Failed |
| 70F at 85% RH | 69.0 | 70.7 | Failed |
SHA-256 / d985faa7b1c0cd32a5f05c60c0c73a86f6233eb7697ebf668e2c7741ad7944de
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(t_f, rh):
simple = 0.5 * (t_f + 61.0 + (t_f - 68.0) * 1.2 + rh * 0.094)
if (simple + t_f) / 2 < 80:
return round(simple, 1)
hi = (-42.379 + 2.04901523 * t_f + 10.14333127 * rh - 0.22475541 * t_f * rh
- 0.00683783 * t_f * t_f - 0.05481717 * rh * rh + 0.00122874 * t_f * t_f * rh
+ 0.00085282 * t_f * rh * rh - 0.00000199 * t_f * t_f * rh * rh)
if rh < 13 and 80 <= t_f <= 112:
hi -= ((13 - rh) / 4) * math.sqrt((17 - abs(t_f - 95)) / 17)
elif rh > 85 and 80 <= t_f <= 87:
hi += ((rh - 85) / 10) * ((87 - t_f) / 5)
return round(hi, 1)
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['70F at 0% RH', [70, 0], 66.7], ['70F at 5% RH', [70, 5], 66.9], ['70F at 10% RH', [70, 10], 67.2], ['70F at 12% RH', [70, 12], 67.3], ['70F at 13% RH', [70, 13], 67.3], ['70F at 40% RH', [70, 40], 68.6], ['70F at 60% RH', [70, 60], 69.5], ['70F at 85% RH', [70, 85], 70.7]], [['70F at 12% RH', [70, 12], 67.3], ['70F at 85% RH', [70, 85], 70.7], ['70F at 86% RH', [70, 86], 70.7], ['70F at 90% RH', [70, 90], 70.9], ['70F at 95% RH', [70, 95], 71.2], ['70F at 100% RH', [70, 100], 71.4], ['76F at 0% RH', [76, 0], 73.3], ['76F at 5% RH', [76, 5], 73.5]], [['70F at 60% RH', [70, 60], 69.5], ['76F at 5% RH', [76, 5], 73.5], ['76F at 10% RH', [76, 10], 73.8], ['76F at 12% RH', [76, 12], 73.9], ['76F at 13% RH', [76, 13], 73.9], ['76F at 40% RH', [76, 40], 75.2], ['76F at 60% RH', [76, 60], 76.1], ['76F at 85% RH', [76, 85], 77.3]], [['70F at 90% RH', [70, 90], 70.9], ['76F at 60% RH', [76, 60], 76.1], ['76F at 90% RH', [76, 90], 77.5], ['76F at 95% RH', [76, 95], 77.8], ['76F at 100% RH', [76, 100], 78.0], ['79F at 0% RH', [79, 0], 76.6], ['79F at 5% RH', [79, 5], 76.8], ['79F at 10% RH', [79, 10], 77.1]], [['76F at 0% RH', [76, 0], 73.3], ['76F at 100% RH', [76, 100], 78.0], ['79F at 13% RH', [79, 13], 77.2], ['79F at 40% RH', [79, 40], 78.5], ['79F at 60% RH', [79, 60], 79.4], ['79F at 85% RH', [79, 85], 80.6], ['79F at 86% RH', [79, 86], 80.6], ['79F at 90% RH', [79, 90], 80.8]]]
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 |
|---|---|---|---|
| 70F at 0% RH | 66.7 | 66.7 | Passed |
| 70F at 5% RH | 66.9 | 66.9 | Passed |
| 70F at 10% RH | 67.2 | 67.2 | Passed |
| 70F at 12% RH | 67.3 | 67.3 | Passed |
| 70F at 13% RH | 67.3 | 67.3 | Passed |
| 70F at 40% RH | 68.6 | 68.6 | Passed |
| 70F at 60% RH | 69.5 | 69.5 | Passed |
| 70F at 85% RH | 70.7 | 70.7 | Passed |
SHA-256 / 4e765227afb9adc33e3c08a2aaf671312d60947ce9d24a734b36b21b2f394aee
Verification & scope
Stipulated deterministic teaching model of an operational weather index; coefficients and thresholds are fixed by the contract and no claim of standards conformance is made. 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:48:24.559816+00:00.
Case digest / d325545fea2562faa02a350dd261b05ee58a6d79fd62d94a9158ff0efc36034f