FA-70841 / Weather index computation / Open access
Heat index regression: regression gate average · case 01
Near 80 degF the reported heat index jumps between the simple and regression formulas at the wrong temperatures.
ROOT CAUSE
The switch to the Rothfusz regression tests the simple estimate alone instead of the mean of the simple estimate and the air temperature.
VERIFIED REPAIR
Gate on (simple + t_f) / 2 < 80 before choosing the regression.
Unsuccessful approach: Gating on the air temperature alone still disagrees with the averaged gate for humid 79 degF air and dry 81 degF air.
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 - 68.0) * 1.2 + rh * 0.094)
if simple < 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], ['79F at 85% RH', [79, 85], 80.6], ['79F at 100% RH', [79, 100], 83.8]], [['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], ['79.5F at 100% RH', [79.5, 100], 85.5], ['81.5F at 0% RH', [81.5, 0], 77.7]], [['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], ['80F at 13% RH', [80, 13], 78.3], ['81.5F at 12% RH', [81.5, 12], 79.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], ['79F at 85% RH', [79, 85], 80.6], ['79.5F at 86% RH', [79.5, 86], 83.8]], [['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], ['80F at 5% RH', [80, 5], 77.9], ['81.5F at 0% RH', [81.5, 0], 77.7]]]
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 |
| 79F at 85% RH | 82.6 | 80.6 | Failed |
| 79F at 100% RH | 83.8 | 83.8 | Passed |
SHA-256 / 568e1aaaf5b32530f820845f543daaf0e57c4dfb678b5aca91351f797542834a
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.094)
if t_f < 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], ['79F at 85% RH', [79, 85], 80.6], ['79F at 100% RH', [79, 100], 83.8]], [['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], ['79.5F at 100% RH', [79.5, 100], 85.5], ['81.5F at 0% RH', [81.5, 0], 77.7]], [['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], ['80F at 13% RH', [80, 13], 78.3], ['81.5F at 12% RH', [81.5, 12], 79.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], ['79F at 85% RH', [79, 85], 80.6], ['79.5F at 86% RH', [79.5, 86], 83.8]], [['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], ['80F at 5% RH', [80, 5], 77.9], ['81.5F at 0% RH', [81.5, 0], 77.7]]]
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 |
| 79F at 85% RH | 80.6 | 80.6 | Passed |
| 79F at 100% RH | 81.3 | 83.8 | Failed |
SHA-256 / c2a4607b35a1b29a0e3f61ddd554d8505f99ba9c134a232e2a7a2c628caf8fc5
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], ['79F at 85% RH', [79, 85], 80.6], ['79F at 100% RH', [79, 100], 83.8]], [['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], ['79.5F at 100% RH', [79.5, 100], 85.5], ['81.5F at 0% RH', [81.5, 0], 77.7]], [['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], ['80F at 13% RH', [80, 13], 78.3], ['81.5F at 12% RH', [81.5, 12], 79.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], ['79F at 85% RH', [79, 85], 80.6], ['79.5F at 86% RH', [79.5, 86], 83.8]], [['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], ['80F at 5% RH', [80, 5], 77.9], ['81.5F at 0% RH', [81.5, 0], 77.7]]]
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 |
| 79F at 85% RH | 80.6 | 80.6 | Passed |
| 79F at 100% RH | 83.8 | 83.8 | Passed |
SHA-256 / 63e46ea2eadd7fe1e35e8a748428bc6535b51c96308e7cf0db2562613ca63d7c
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.369373+00:00.
Case digest / b2a306ce7d556e15ca2ceadbccc723ca8484ce0a477015854e3a31fa72172376