WetBulbTracker Scientific Methodology
A complete specification of every heat-stress metric, algorithm, physical constant, and adjustment used by WetBulbTracker.com.
Last updated: October 2026 · Engine version: Liljegren WBGT (physics epoch 2026-07-alb020)
This document is intended for scientists, clinicians, safety officers, and engineers who need to audit or reproduce the numbers shown in the app. Every formula below is implemented verbatim in the open metrics engine; source-file references are given for each section so the code and this document can be cross-checked line by line.
1. Scope and conventions
- Internal units. Every quantity is computed and stored in SI / metric units: temperature in degrees Celsius (°C), wind in meters per second (m/s), pressure in hectopascals (hPa), shortwave irradiance in watts per square meter (W/m²). Imperial conversion happens only at the display layer, so the calculation engine has a single source of truth.
- Display conversion. °F = °C · 9/5 + 32; mph = (m/s) · 2.236936; km/h = (m/s) · 3.6. The default display unit is chosen from the location's country (°F for the United States, °C elsewhere) and can be overridden.
- Determinism. The metrics engine is a set of pure functions: given the same raw inputs it always returns the same outputs. It performs no I/O and has no dependency on the UI framework.
- Reference temperature for thermodynamics. Energy-balance solvers work in Kelvin (K = °C + 273.15) and pascals (Pa = hPa · 100) internally, converting back to °C for the public result.
Source: src/lib/units.ts, src/lib/metrics/index.ts.
2. Data sources and pipeline
| Purpose | Provider | Endpoint / dataset |
|---|---|---|
| Current & forecast weather | Open-Meteo | api.open-meteo.com/v1/forecast |
| Historical reanalysis | Open-Meteo (ERA5) | archive-api.open-meteo.com/v1/archive |
| Air quality (US AQI) | Open-Meteo | air-quality-api.open-meteo.com/v1/air-quality |
| Geocoding (search) | Open-Meteo | geocoding-api.open-meteo.com/v1/search |
| Reverse geocoding (place names) | BigDataCloud | reverse-geocode-client |
Place names come from BigDataCloud's free endpoint, api.bigdatacloud.net/data/reverse-geocode-client. When an API key is configured, they come from its keyed endpoint, api-bdc.net/data/reverse-geocode, instead.
Credit and license. Weather data by Open-Meteo.com (CC BY 4.0), modified. Open-Meteo's data comes to us under the Creative Commons Attribution 4.0 license. We change it: our heat values (WBGT, heat index, wet bulb and pavement temperature) are computed from it, and the city climate figures are summarized from its historical archive. Feels Like is Open-Meteo's own apparent temperature whenever it supplies one (section 8). The same credit sits in the site footer, in the corner of the map, on reports, in emailed reports, on the embeddable badge, and on the first line of the CSV downloads. The city climate figures draw on ERA5, the Copernicus Climate Change Service reanalysis in that archive, so this page and each city page, beside the figures, carry the Copernicus notice. So do the "this day in history" panel (section 12); any report or Studio document with dates more than 90 days back, which come from that archive (Older dates, below), beside its credit; a Pick a Time result for an hour more than 90 days back, in a line under it; and a CSV download holding any hour from that archive, on its second line. The notice reads: Contains modified Copernicus Climate Change Service information 2026. Neither the European Commission nor ECMWF is responsible for any use that may be made of the Copernicus information or data it contains. Its year is the year the figures beside it were put together: on a city page, the year its climate figures were computed; on a report, the year it was generated, as its own header gives it, and on a filed Studio document the year it was filed; and the year the panel, a Pick a Time result, an email or a CSV download is built. Here it is the year this section was written.
Raw weather fields requested (current conditions): temperature_2m, relative_humidity_2m, apparent_temperature, is_day, wind_speed_10m, surface_pressure, shortwave_radiation, dew_point_2m, uv_index, cloud_cover, precipitation. Wind is requested in m/s. Open-Meteo's underlying model is primarily ECMWF/national NWP blends at roughly 1–11 km resolution.
Freshness. Current conditions are cached for 15 minutes server-side (the map's live layer for 30 minutes, §13); the forecast and historical archive for 1 hour and 24 hours respectively. A failed upstream request is retried once without cache before erroring. Before a cached reading of current conditions more than 30 minutes old is served as current, the server asks the forecast service again without cache. It keeps the cached reading only if that request fails or takes more than 2.5 seconds, and the card always shows its reading's time. Until October 2026, the first request for a place after a quiet spell could be answered with the reading from the request before it, hours old. A page left open on screen asks again for its reading once that reading is 20 minutes old, at most once every 16 minutes and never while the page is hidden, so the reading and its time can change without a tap. A city page is made on its first visit after each update of the site, never while the site is built. The first visit after about 15 minutes is shown that page while a new one is made with a newer reading. If no reading can be had, the page made before stays up; a city page with no earlier page says its reading is briefly unavailable and tries again a minute later. The list of cities is built without its readings, and the first visit after an update starts the version with them.
Older dates. Report and Studio dates more than 90 days back come from the historical archive; newer dates come from the forecast service. If the archive refuses or cannot be reached, a range that needs it is not answered in part: the report says "Older dates are unavailable right now. Try again later, or choose dates within the last 90 days." That failure is never cached. A range spanning both is never answered with its older part alone either: if the newer part cannot be fetched, the report fails as a whole. The "this day in history" panel hides itself instead (§12). Pick a Time on the map reads the same archive for an hour more than 90 days back; if the archive refuses, the map shows the same sentence above current conditions for that place, and that answer is never cached either.
Solar instant. For "current conditions" the solar geometry (§4) is evaluated at the present UTC instant; the sun moves slowly enough that the ≤15-minute data lag is immaterial. For the forecast strip and map time scrubber, each hour is evaluated at its own true UTC instant.
Hour labels. Each hour is labeled with the place's own clock at that hour's UTC instant, read in the place's time zone, daylight saving included. The forecast service labels a whole answer in the one UTC offset in force when it answers, so its labels for hours on the far side of a clock change are an hour off; ours are read from the instant instead. On the autumn date one clock hour happens twice, and reports and the forecast show both; on the spring date one clock hour never happens, and picking it in Pick a Time shows current conditions instead. Until October 2026, Pick a Time and reports used the service's labels, so an hour picked in the other daylight saving season showed the hour next to it.
Source: src/lib/openmeteo.ts, src/lib/zoneclock.ts, src/app/api/conditions/route.ts.
2.1 Input variables
| Symbol | Meaning | Field | Unit |
|---|---|---|---|
Ta | Air (dry-bulb) temperature at 2 m | temperature_2m | °C |
RH | Relative humidity at 2 m | relative_humidity_2m | % |
v | Wind speed at 10 m | wind_speed_10m | m/s |
S | Global horizontal shortwave irradiance | shortwave_radiation | W/m² |
Ps | Surface pressure | surface_pressure | hPa |
Td | Dew point (native, when present) | dew_point_2m | °C |
AT* | Apparent temperature (native, when present) | apparent_temperature | °C |
2.2 When a variable is missing
The provider types every hourly variable as nullable, and a null is not a zero. It matters most for S: with no radiant load the Liljegren globe settles near air temperature, so a fabricated zero reads as a sunless afternoon and moves WBGT by roughly 4 °C. That is typically a whole band, in the under-warning direction.
So an absent S is replaced by the clear-sky upper bound at that hour's own solar geometry (Haurwitz, 1945: GHI = 1098·cos(z)·exp(−0.059/cos(z))), which is computable from latitude, longitude and time, none of which go missing when the irradiance does. The substitution errs upward: near-exact on a genuinely clear hour, up to about +2.6 °C on a genuinely overcast one. That direction is deliberate. Below the horizon the bound is exactly 0, so a substituted night and a reported night agree.
A reported value is never overridden, zero included: a provider reporting no sun in daylight is believed rather than second-guessed.
Substituted values are marked, and two things decline to act on them: the learned bias correction (§5.9), which is indexed by the provider's reported irradiance, and the accuracy corpus (§5.6), which refuses the sample outright. We will estimate in order to answer; we will not estimate in order to learn.
The reader is told, too. When the sun in a reading was substituted, the card says so under the headline number, the hourly strip marks the hour and footnotes it, and the CSV export's solar_radiation_source column reads clear-sky estimate rather than provider. The note describes the input, not the reading: the learned correction is skipped for an estimated sun, and near dawn and dusk that correction is positive, so the number as a whole can land either side of what a reported sun would have given.
Source: resolveSolar in src/lib/openmeteo.ts, clearSkyGHI in src/lib/solar.ts, and src/lib/solarnote.ts for what the reader is told.
3. Metric overview
| Metric | App label | Method | Captures |
|---|---|---|---|
| Wet Bulb Globe Temperature | Heat Stress | Liljegren et al. (2008) | T, RH, wind, sun / shade (§5.8) |
| Heat Index | Heat Index | NWS Rothfusz regression | T, RH (shade) |
| Apparent Temperature | Feels Like | BoM / Steadman | T, RH, wind |
| Wet-bulb temperature | Wet Bulb | Stull (2011) | T, RH (thermodynamic) |
| Dew point | Mugginess | Magnus-Tetens | T, RH (absolute moisture) |
| Air temperature (dry-bulb) | Air Temp | Direct reading | T only (cool→warm reference) |
WBGT is the primary metric because it is the only one that incorporates solar radiation and is the recognized standard for occupational, athletic, and military heat-stress management.
4. Solar geometry
The Liljegren WBGT model needs two solar quantities: the cosine of the solar zenith angle and the fraction of incoming shortwave that arrives as a direct beam. Source: src/lib/solar.ts.
4.1 Cosine of the solar zenith angle (NOAA equations)
Let doy be the day of year (Jan 1 = 1) and h the UTC time in fractional hours. Define the fractional-year angle γ (radians):
γ = (2π / 365) · (doy − 1 + (h − 12) / 24)Equation of time (minutes):
EqTime = 229.18 · ( 0.000075
+ 0.001868·cos γ − 0.032077·sin γ
− 0.014615·cos 2γ − 0.040849·sin 2γ )Solar declination δ (radians):
δ = 0.006918 − 0.399912·cos γ + 0.070257·sin γ
− 0.006758·cos 2γ + 0.000907·sin 2γ
− 0.002697·cos 3γ + 0.001480·sin 3γTrue solar time and hour angle (lng east-positive degrees):
TST = h·60 + EqTime + 4·lng (minutes)
HourAngle = (TST / 4 − 180) · π/180 (radians)Cosine of the zenith angle (φ = latitude):
cos Z = sin φ · sin δ + cos φ · cos δ · cos(HourAngle) (clamped to [−1, 1])cos Z > 0 means the sun is above the horizon.
4.2 Direct-beam fraction (Liljegren clearness index)
Top-of-atmosphere irradiance on a horizontal surface, including the Earth-Sun distance correction (S₀ = 1367 W/m²):
TOA = S₀ · (1 + 0.033·cos(2π(doy − 1)/365)) · cos ZClearness index and direct fraction:
Kt = min(1, S / TOA)
fdir = exp(3 − 1.34·Kt − 1.65/Kt) clamped to [0, 0.9]fdir = 0 whenever the sun is below the horizon (cos Z ≤ 0) or S ≤ 0.
5. Primary metric: WBGT (Liljegren et al., 2008)
WBGT combines three temperatures:
WBGT (outdoor, in sun) = 0.7·Tnwb + 0.2·Tg + 0.1·Ta
WBGT (shade / indoor) = 0.7·Tnwb + 0.3·Tawhere Tnwb is the natural wet-bulb temperature, Tg the black-globe temperature, and Ta the dry-bulb (air) temperature. The app uses the outdoor (in sun) form, solving Tnwb and Tg from two coupled radiative-convective energy balances by iteration.
This is a faithful port of the PyWBGT Cython implementation (github.com/QINQINKONG/PyWBGT), itself a port of Liljegren's original C code. Constants and equations are unchanged; the only deviation is the root-finder (robust bisection here versus Brent's method in the reference; both converge to the same root). Source: src/lib/metrics/liljegren.ts.
5.1 Physical constants
| Constant | Symbol | Value | Units |
|---|---|---|---|
| Molecular weight of dry air | MAIR | 28.97 | g/mol |
| Molecular weight of water vapor | MH2O | 18.015 | g/mol |
| Universal gas constant | RGAS | 8314.34 | J/(kmol·K) |
| Specific heat of air | CP | 1003.5 | J/(kg·K) |
| Stefan–Boltzmann constant | σ | 5.6696×10⁻⁸ | W/(m²·K⁴) |
| Globe diameter | Dglobe | 0.0508 | m |
| Globe emissivity | εg | 0.95 | dimensionless |
| Globe albedo | αg | 0.05 | dimensionless |
| Wick emissivity | εwick | 0.95 | dimensionless |
| Wick albedo | αwick | 0.4 | dimensionless |
| Wick diameter | Dwick | 0.007 | m |
| Wick length | Lwick | 0.0254 | m |
| Surface albedo | αsfc | 0.20 | dimensionless |
Derived: RATIO = CP·MAIR/MH2O, RAIR = RGAS/MAIR, Pr = CP / (CP + 1.25·RAIR) (Prandtl number).
5.2 Thermophysical helper functions
Air dynamic viscosity μ(T) [kg/(m·s)], with Ω = 1.2945 − T/1141.17647:
μ(T) = 2.6693×10⁻⁶ · √(28.97·T) / (13.082689 · Ω)Thermal conductivity: k(T) = (CP + 1.25·RAIR) · μ(T).
Saturation vapor pressure (Pa), Buck-type with a pressure-enhancement factor; separate branches over water (T > 273.15 K) and ice:
water: es = 611.21 · exp(17.502·(T−273.15)/(T−32.18)) · (1.0007 + 3.46×10⁻⁶·Ps_hPa)
ice: es = 611.15 · exp(22.452·(T−273.15)/(T− 0.60)) · (1.0003 + 4.18×10⁻⁶·Ps_hPa)Atmospheric emissivity, with vapor pressure e in hPa:
e = RH·0.01 · (es·0.01)
εatm = 0.575 · e^0.143Water-vapor diffusivity in air:
D(T, Ps) = 2.471773765×10⁻⁵ · (T·0.00342105637)^2.334 / (Ps/101325)Latent heat of vaporization: λ(T) = 1665134.5 + 2370·T.
Convective heat-transfer coefficients (ρ = Ps/(RAIR·T)):
Sphere (globe): Re = v·ρ·Dglobe/μ; Nu = 2 + 0.6·Re^0.5·Pr^0.3333; h = Nu·k/Dglobe
Cylinder (wick): Re = v·ρ·Dwick /μ; Nu = 0.281·Re^0.6·Pr^0.44; h = Nu·k/Dwick5.3 Black-globe energy balance
Direct-beam geometry term (0 when fdir = 0 or cos Z ≤ 0):
directTerm = (0.5/cos Z − 1) · fdirRadiative constant:
C0 = 0.5·(1 + εatm)·Ta⁴
+ (1 / (2·εg·σ)) · S·(1 − αg)·(1 + directTerm + αsfc)Globe temperature Tg (K) is the root of, with h the sphere coefficient evaluated at the film temperature ½(Ta + Tg):
C0 − (1/(εg·σ))·h·(Tg − Ta) − Tg⁴ = 0 bracket [Ta−50, Ta+90]5.4 Natural wet-bulb energy balance
eair = RH·0.01 · es(Ta)
directTerm = ( tan(arccos(min(1, cos Z)))/π + Dwick/(4·Lwick) ) · fdir
D4 = εwick·0.5·σ·Ta⁴·(εatm + 1)
+ (1 − αwick)·S·( (1 + Dwick/(4·Lwick))·(1 − fdir) + directTerm + αsfc )Natural wet-bulb temperature Tnwb (K) is the root of, with Tref = ½(Ta+Tw), Sc = μ(Tref)/(ρ·D(Tref)) the Schmidt number, h the cylinder coefficient at Tref, and Fatm = D4 − εwick·σ·Tw⁴:
Ta − (λ(Tref)/RATIO)·((es(Tw) − eair)/(Ps − es(Tw)))·(Pr/Sc)^0.56 + Fatm/h − Tw = 0bracket [Ta − (100 − RH)/5 − 50, min(Ta + 70, 340)].
5.5 Adjustments and solver details
- Wind height correction & low-wind floor. Inputs are 10 m winds; the model expects ~2 m. Converted with the power law
v₂ = v₁₀·(2/10)^0.2, then floored at 1.5 m/s. The Liljegren reference floors at 0.13 m/s, but near-calm air makes the globe and wet-wick energy balances hypersensitive and over-reads heat stress: checked against the NWS's own published WBGT across CONUS, below ~1 m/s we ran +5–8 °C high (a false "Extreme" on mild, sunny, dead-calm days) while NWS stayed put, with excellent agreement at a real breeze. The 1.5 m/s floor, a defensible minimum realistic ventilation at 2 m, removes those calm-air spikes. It does not keep us above NWS: below 50 W/m² our raw reading averages 1.64 °C lower than the reference (§5.9). The floor addresses the calm-sun over-read; the residual high bias at moderate wind and the night under-read are both handled by the conditions-based bias correction, not by this floor. - Humidity clamp. RH is clamped to [1, 100] %.
- Irradiance / geometry guards.
Sis floored at 0;fdiris forced to 0 when the sun is below the horizon. - Root finder. Bisection over the brackets above, tolerance 1×10⁻⁴ K, up to 80 iterations. If either balance fails to bracket a root (no sign change), the engine falls back to the simplified estimate (§5.7) rather than returning a bad number.
5.6 Validation status
The Liljegren method is the validated reference model for WBGT and the one this implementation follows (Liljegren et al., 2008, reference 1; ISO 7243).
This section previously claimed the method is "more accurate than handheld WBGT meters," attributed to "Duke Nicholas Institute, 2024," an attribution that appeared nowhere in §18 and pointed at no locatable paper. It has been removed rather than re-sourced. The claim also cuts against the published direction of error: Grundstein et al. (2025, GeoHealth) found smartphone-derived WBGT under-reporting on-site measurements. We model WBGT; we do not measure it, and we do not claim to beat an instrument.
Cross-validation against the National Weather Service. An automated suite (npm run validate:wbgt) compares this implementation against the NWS's published WBGT (api.weather.gov gridpoint forecasts) at eight CONUS cities for the next forecast hour. Representative run (June 9 2026, 23:00 UTC, a hot late-afternoon across the US):
| Station | NWS (°C) | Ours (°C) | Δ (°C) |
|---|---|---|---|
| Phoenix, AZ | 27.2 | 26.5 | −0.77 |
| Dallas, TX | 27.8 | 30.6 | +2.84 |
| Miami, FL | 26.7 | 27.7 | +1.07 |
| Atlanta, GA | 24.4 | 27.1 | +2.64 |
| St. Louis, MO | 30.0 | 30.8 | +0.75 |
| Washington, DC | 23.9 | 23.3 | −0.57 |
| New Orleans, LA | 27.8 | 28.3 | +0.50 |
| Kansas City, MO | 28.9 | 29.8 | +0.91 |
In this June 2026 eight-station snapshot of the raw model, mean |Δ| = 1.26 °C, with six of eight stations within ±1.1 °C. Treat this as a dated snapshot, not a standing guarantee: the figure moves day to day with conditions and station mix, and on some days it is well above that. It is also not the test of our ±1.5 °C target, which describes the corrected number we display and is judged on its held-out error, overall and after dark (§5.9). A live version of this comparison runs continuously at [/validation](/validation) (and the public /api/validation endpoint). That live value, not the number above, is the current source of truth. That endpoint's withinTarget field compares this raw one-hour mean with 1.5 °C as a diagnostic; it is not the test of the target, and the page does not show it. The field beside it, withinTargetBasis, says so in the response itself, so a reader of the JSON alone cannot take the flag for the target's verdict. Note the comparison conflates method and input differences: NWS drives its WBGT from the National Blend of Models while this app uses Open-Meteo's model blend, so part of each Δ (especially at hours with uncertain afternoon convection, e.g. the Dallas/Atlanta rows) is the two systems disagreeing about clouds and irradiance, not about the WBGT physics. The suite checks the raw model at that hour: it fails if any station is more than 3.5 °C off, or if at least four stations are in daylight and their mean |Δ| is above 1.5 °C. After dark, where the raw model reads low (§5.9), the mean is not graded. It can be re-run at any time.
Retained accuracy corpus. Beyond the live snapshot, a scheduled collector records each comparison into a growing, retained dataset: the full input feature vector, our WBGT (sun and shade), and the NWS reference. Because the inputs are stored (not just the final number), this corpus lets us measure systematic bias over time, correct it, and refine the methods, and it remains useful even as the models evolve. An aggregate, no-personal-data readout (total records and rolling mean bias / mean absolute error) is surfaced on the validation dashboard once enough has accrued. From October 2026, each comparison also keeps the NWS grid cell's own elevation, air temperature, dew point, relative humidity, sky cover, wind speed and wind direction for the same hour, as the NWS model gives them, so that we can later check how much of a difference between our estimate and the NWS model comes from different temperature, humidity, elevation, cloud or wind inputs. That check is partial: the NWS grid cell gives sky cover, the share of the sky covered by cloud, not the sunlight reaching the ground, so a difference in sunlight shows only through cloud. No calculation uses these values yet, and they change no reading. Each comparison also records the update time stamped on the NWS gridpoint response it was compared with, as the collector received it, so an audit can tell exactly which NWS grid it used (comparisons collected before this field was added have none).
Physical sanity checks also pass: dry desert heat (e.g. Phoenix) reads moderate WBGT, humid tropical nights (e.g. Miami) read elevated WBGT, and in direct sun Tnwb < WBGT < Tg, with Tg rising sharply under high irradiance and low wind.
Reference tiers, the monthly record and the forecast log (added September 2026). Each comparison in the corpus is now labeled with a reference tier as well as its source, so a figure built from the corpus can say what kind of number it was compared with. Tier A is an instrument WBGT at a physical site (A1 with the globe, the natural wet bulb and the dry bulb all instrumented; A2 with an instrumented globe and a computed wet-bulb term), tier B is a WBGT a network operator computes from inputs measured at its own site, tier C is a modeled WBGT from another system, and tier D is a WBGT derived from reanalysis. The National Weather Service's gridded WBGT, the only reference in use, is tier C. It is a model output, so the comparisons in this section show agreement between two models, not error against an instrument.
Two dated records are kept beside the corpus. The monthly accuracy record writes the figures for each closed calendar month (UTC) once, keeping each reference tier, engine version and shade convention apart, with a count of that month's comparisons archived under other engine versions and a flag for whether enough held-out comparisons exist to show a held-out figure. Its rows are never edited afterward. The series is public at /api/bias/monthly, and /validation shows it as a table once the first month is written. The forecast log files, every hour and for every reference location, the forecast inputs and our WBGT before any correction for a fixed set of lead times up to seven days ahead, beside the NWS gridded WBGT offered for the same hours. Once its hour has come, a forecast as it was served cannot be fetched again, so this log is the only record of what each forecast said when it was issued. The log refuses any forecast hour missing temperature, humidity or wind and, as the corpus does (§2.2), any hour whose sunlight value the provider did not report.
For WBGT forecasts, the peer-reviewed accuracy assessment to read is Clark, Konrad and Grundstein (2024, reference 28). It assessed the forecast from NOAA's Southeast Regional Climate Center and the Carolinas Integrated Sciences and Assessments, and a forecast modeled after National Weather Service methods, against WBGT measured on site in North Carolina over the summers of 2019 to 2021 (and against estimates from weather stations). It reports the first forecast within 0.6 °C of observations on average and less biased than the NWS-method forecast, whose biases were larger and colder, and it concludes that on-site measurements remain critical. Two limits on how that paper relates to this page. The NWS-method forecast it assessed was a reconstruction, not the operational NWS grid this section compares with, so it does not give the grid's error. And its reference is instruments (tier A) while ours is the NWS grid (tier C), so no figure in this section, in the forecast log or at /validation is comparable with its figures.
Source: src/lib/accuracysnapshot.ts, src/lib/forecastlog.ts.
5.7 Simplified fallback (only when geometry/pressure are unavailable)
When cos Z, fdir, or surface pressure are missing, WBGT uses a closed-form estimate: a Stull wet bulb (§7) for Tnwb and a parameterised globe term:
Tg ≈ Ta + min(25, 0.024·S / √max(0.5, v))
WBGT = 0.7·Tnwb_Stull + 0.2·Tg + 0.1·TaThis is directionally correct but not validated to the ±1.5 °C target; in normal operation (with full solar geometry) the Liljegren path is always used.
5.8 Sun vs shade (microclimate)
WBGT is acutely sensitive to direct solar load: the same air can be comfortable under a tree and dangerous on open pavement. To make that difference explicit, the conditions API returns a second WBGT computed in full shade alongside the in-sun value, and the app lets you toggle between them.
The shade value drops the direct beam and fixes the mean radiant temperature at air temperature, which is the common convention in the literature (e.g. CarbonPlan):
Tnwb = Liljegren's natural wet-bulb balance at S = 0, fdir = 0
Tmrt := Ta (so the globe reads air temperature, Tg = Ta)
WBGT_shade = 0.7·Tnwb + 0.3·TaThe second line is a choice, not a measurement, and it is where the honesty of this number lives. Running the full Liljegren globe balance at S = 0 and keeping its result is not shade: with no sun the globe radiates to a cold sky and settles below air temperature (34.3 °C globe at 35 °C air), which describes a clear night under open sky. Real shade replaces that cold sky with a canopy at or above air temperature. Pinning Tmrt to Ta is the simplest defensible stand-in for that canopy.
Treat the result as an idealised best case, and be careful how far you push that. It is a lower bound within the clear-sky open-shade convention family, not a physical floor. It ignores the diffuse and ground-reflected shortwave that real shade still receives, so ordinary shade reads higher. But shade with a hot radiant surface in view (shaded ground beside sunlit asphalt, or a sunlit wall) runs Tmrt above air temperature, and there the "floor" is a ceiling. We have no reference for that case, and no plan to get one.
The sun-minus-shade gap is surfaced directly on the card, rounded to a whole degree (e.g. "Full shade cuts heat stress by about 5°."). It is largest under high irradiance and light wind, and closes to nothing at night. It does not reach exactly zero there: the two numbers weight the globe differently by construction, so with S = 0 they differ by 0.2·(Ta − Tg), about 0.1–0.2 °C, with the shade figure marginally the higher of the two. The card floors the gap at zero rather than printing a benefit that runs the wrong way. The shade value is then re-classified against the same WBGT scale (§11.1), so the displayed tier, color, and advice follow the shade reading.
One number, three places. The accuracy corpus (§5.6) archives this same quantity per row, computed by the same function, and stamps the convention on the row so a corpus spanning a change of convention can tell its halves apart. Nothing grades it yet (there is no shade reference in the record), which is precisely why the archived value has to be the shown value: when a reference does arrive it must grade the number people actually read. The standalone calculator's shade row uses the same function as well, with standard sea-level pressure and, when no wind is entered, a light 1 m/s breeze. It applies no bias correction, because the correction depends on the hour's sunlight and the calculator does not ask for it, so for identical air the calculator matches the card's shade value before correction.
This is a microclimate estimate, not a substitute for measuring your specific spot: real shade also depends on ground albedo, sky-view fraction, nearby hot surfaces, and ventilation, which a point model cannot see.
5.9 Bias correction (data flywheel)
The retained corpus (§5.6) lets us not only measure systematic bias but correct it, and the corpus showed the bias is conditions-dependent, not constant. On the 30-day window read 2026-10-04 under physics epoch 2026-07-alb020 (17,958 compared samples) the raw global mean residual is −0.79 °C with a mean absolute error of 1.42 °C, and that single pair hides the structure that matters. By shortwave irradiance:
| Sun (W/m²) | n | Mean residual (ours − NWS) | Mean absolute Δ |
|---|---|---|---|
| < 50 (dark) | 10,118 | −1.64 °C | 1.73 °C |
| 50–300 | 2,511 | −0.31 °C | 1.10 °C |
| 300–600 | 2,544 | +0.74 °C | 1.08 °C |
| 600–850 | 2,468 | +0.51 °C | 0.92 °C |
| ≥ 850 | 317 | −0.18 °C | 0.75 °C |
The raw model runs slightly warm through the middle of the day and cool after dark, and the dark hours are both the largest share of the record and the least accurate: the one regime where the raw mean absolute error exceeds our ±1.5 °C target. Reading low is the direction worth stating plainly, so we state it. (The ≥ 850 W/m² row is small in this window because midday sun that strong gets rarer as autumn comes on, so treat it as the least certain row.)
A single global offset cannot fix that: the offset that lifted the night would push every daytime bin further off. So the correction is keyed on conditions, and it is what brings the dark hours inside the target. Day-blocked cross-validation on the same reading gives, for the displayed (corrected) number:
| n | Raw | Corrected | |
|---|---|---|---|
| Dark (< 50 W/m²) | 10,118 | 1.73 °C | 0.99 °C |
| Daylight (≥ 50 W/m²) | 7,840 | 1.02 °C | 0.85 °C |
| All | 17,958 | 1.42 °C | 0.93 °C |
What the ±1.5 °C target describes. The number we display: the corrected estimate, judged by its held-out error, the mean absolute difference from the NWS modeled WBGT on days the correction never trained on. It is judged twice, over all hours and after dark (the Dark row above), and it is met only when both are inside it, so a good daylight figure cannot hide a miss in the dark hours. On the 2026-10-04 reading the figure over all hours was 0.93 °C, inside the target. After dark it was 0.99 °C, also inside the target. The live verdict on /validation applies the same rule, and does not call the target met while the after-dark figure has too few held-out comparisons to be reported. The raw model's 1.42 °C over the same samples is the figure before correction, and we publish it as that without claiming it meets the target; after dark it does not. Two limits go with the target. It describes an average, not every reading, so one place at one hour can be further off. And while the correction is not applied (held back, as after a physics change, for about a minute after a failed build, or, outside the contiguous United States, wherever it would lower a reading), readers are shown the raw number, as they always are for shade in full daylight and for the calculator's full-sun row (both below).
The figures above are a dated read of a moving corpus. The live split is published on /validation, under "The correction in detail" beside the correction surface, and the raw segmentation at /api/bias/segments.
So the displayed WBGT is corrected by a small, learned correction surface over (solar irradiance × wind speed), the two variables that drive the residual. Both exist in the input for every point on Earth, but the surface is learned only at the U.S. reference locations, all in the contiguous United States, against the NWS modeled WBGT and on U.S. weather-model inputs, and nothing has checked it anywhere else. So outside the contiguous United States it is applied only where it raises a reading (the "Upward only" rule below). The correction is computed per point and bilinearly interpolated between anchor cells, so it changes smoothly with sun and wind (no banding on the map from the surface itself). The one exception is the edge of the contiguous United States: the land borders, the line through the Great Lakes, and the coast outline drawn offshore. In daytime sun the map can step there by up to about 1 °C, because the part of the correction that lowers a reading is held back on the outside. Strict guardrails keep it honest:
- Direction. Mean residual is ours − reference; a positive bias (we run warm) yields a negative correction, and vice-versa. On the 2026-10-04 reading that means subtracting about 0.74 °C at 300–600 W/m² and adding about 1.6 °C below 50 W/m². The exact cell values move with the corpus and are printed in the correction table on /validation.
- Shrinkage and clamping. Each (solar, wind) cell is shrunk toward its solar-trend by how much data supports it, so a thin, noisy cell borrows from the well-supported trend instead of swinging readings. Its magnitude is clamped to ±3 °C.
- Minimum data. No correction is applied until enough compared samples have accrued; below that threshold the correction is exactly zero.
- Checked on days it never trained on (self-gating). The surface is applied only if it lowers error under day-blocked cross-validation (trained on all other calendar days and tested on a held-out day) by a margin of at least 5 %. We block by day rather than by location because the reference locations share synoptic weather days, so a leave-one-location-out test leaves the held-out day's pattern in the training set through the other locations and reports an optimistic gain; blocking by day removes that temporal leakage and yields the honest, more-conservative number. The live held-out figures are shown on the /validation page; if the correction ever stops clearing the margin, it switches itself off. This tests new days at the same reference locations, not new places.
- Trained per physics epoch. The correction trains only on residuals gathered under the current raw-physics version. When the engine's physics changes (e.g. the §5.1 surface-albedo correction), residuals measured under the old physics are excluded, so a fixed bias can't be "corrected" twice. The correction simply falls back to the improved raw model until it re-accumulates.
- Upward only outside the contiguous United States. Since 2026-10-23, at a place outside the lower 48 states and DC (Alaska, Hawaii, Puerto Rico and every other country included), the correction is applied only where it raises a reading: the number shown is the raw model plus the correction when the correction is positive, and the raw model alone when it is negative. On the 2026-10-16 surface that keeps the part that adds (mostly after dark, about 1.1 to 1.7 °C, and in low sun near dawn and dusk) and leaves out the part that subtracts (up to about 1 °C in calm, partly sunny air). The reason is the direction of the risk. Every comparison behind the surface is in the contiguous United States, against a U.S. model, on U.S. weather-model inputs, so taking heat off a reading there would rest on nothing checked for that place and could under-warn, while an unchecked addition errs warm. The rule covers the in-sun and the shade figure and every surface in the next item, and the card and the city pages say so in one line when it held a correction back. Inside the contiguous United States nothing changed. The place test is an outline on coordinates (
src/lib/conus.ts). Its land and lake borders are OpenStreetMap's international boundary (© OpenStreetMap contributors, under the Open Database License, openstreetmap.org/copyright), simplified to within a kilometer of the full line and to well under that near every town, village and hamlet OpenStreetMap maps along it, and its coast is drawn a little offshore. A point right on a border can still land on the wrong side; if it lands outside, its reading can only be raised. The accuracy figures in this section and on /validation describe U.S. reference locations only, and we publish no accuracy figure for anywhere outside the contiguous United States until we have one for it. - Applied consistently. The same per-point correction is applied across all user-facing in-sun WBGT: the current reading, the forecast, the map, the city pages, the tiers the AI brief is written from, and heat alerts. So the corrected value (the product's core output) is the same everywhere. The AI brief joined that list on 2026-09-28: until then it named its current tier and its outlook from the uncorrected model, so after dark it could name a lower tier than the reading beside it. The one exception is the standalone calculator's full-sun row, left uncorrected on purpose: it works from air you type in, with no place or hour, and assumes a clear midday sun rather than reading one, so it shows the raw model's value, as the calculator's shade row does (§5.8). The shade figure takes the share of that same correction that the surface attributes to its beamless anchor: all of it when there is no sun, none of it in full daylight, and a smooth handover between. The reason for withholding it by day is unchanged: the correction surface has no daytime-shade reference (its solar=0 column is night-dominated), so applying it to daytime shade would add an unfounded offset. The reason for applying it after dark is that with no beam to remove, the in-sun and shade figures differ only by the globe convention of §5.8, a tenth or two of a degree, so correcting one and not the other would invent a difference of a degree or more rather than express the one the physics gives. This is a consistency rule, not a validated one: there is no shade reference at any hour, so we are not claiming the shade figure is more accurate, only that it does not disagree with the in-sun figure about the same air.
- No tail-chasing. It is applied only to that user-facing output. The collector and the validation suite always use the raw, uncorrected engine, so the flywheel keeps measuring the model's true bias rather than the bias of an already-corrected number.
Two things about how /validation reads. Its 30-day mean bias and mean absolute error, and the "Ours" column of its live comparison table, are the raw model, before correction, not the corrected figure readers are served while the correction is active. The mean bias there is ours minus the NWS gridded WBGT for the same hour, so a negative value means ours read lower, and the NWS figure is a model output too. The page's held-out error (mean absolute error) before and after correction is the test of whether the correction holds on days it never saw. The error reduction beside them is how much lower the after figure is than the before, as a share of the before, so it is printed without a sign; it is green only while the correction is applied. /api/bias/validate also reports a signed held-out bias, ours minus the NWS figure over the same held-out samples, before and after correction, overall and for the dark and daylight bins, and leaves each one empty until its row has enough held-out samples across enough separate days to be reported. After correction that bias sits near zero by construction, whether or not the correction holds on new days, because the correction is fitted to remove the average miss in each condition. So it is not a separate accuracy result, and it is not shown on the page. A figure away from zero means part of the average miss is left after correction, for example where a correction reaches its ±3 °C limit.
The conditions breakdown is public at /api/bias/segments, and the correction surface with its cross-validation at /api/bias/validate; when a correction is in effect it is also surfaced per reading in the conditions API (biasCorrection, °C). A fully learned model, a regression on the complete feature vector (adding humidity, cloud, time of day, season), is the natural next refinement (§17).
6. Heat Index (NWS Rothfusz regression)
The familiar US "feels like in the shade" number. Defined in °F; the engine converts in and out so the rest of the pipeline stays in °C. Source: src/lib/metrics/heatindex.ts.
A Steadman blend is always computed first (T in °F, R = RH %):
HI = 0.5·(T + 61 + (T − 68)·1.2 + R·0.094)If the average (HI + T)/2 ≥ 80 °F, the full Rothfusz regression replaces it:
HI = −42.379
+ 2.04901523·T + 10.14333127·R
− 0.22475541·T·R − 0.00683783·T²
− 0.05481717·R² + 0.00122874·T²·R
+ 0.00085282·T·R² − 0.00000199·T²·R²with the two standard corrections:
if R < 13 and 80 ≤ T ≤ 112: HI −= ((13 − R)/4)·√((17 − |T − 95|)/17)
if R > 85 and 80 ≤ T ≤ 87: HI += ((R − 85)/10)·((87 − T)/5)The result is converted back to °C.
7. Wet-bulb temperature (Stull, 2011)
A fast psychrometric (thermodynamic) wet-bulb approximation, valid near sea-level pressure for roughly T ∈ [−20, 50] °C and RH ∈ [5, 99] %, accurate to about ±0.3 °C. RH is clamped to [1, 100] %. Source: src/lib/metrics/wetbulb.ts.
Tw = T·arctan(0.151977·√(RH + 8.313659))
+ arctan(T + RH) − arctan(RH − 1.676331)
+ 0.00391838·RH^1.5·arctan(0.023101·RH)
− 4.686035Note: this thermodynamic wet bulb does not include radiation or wind, so in direct sun it slightly underestimates the natural wet bulb. The app surfaces it as its own "Wet Bulb" metric and uses it as the Tnwb term only in the simplified WBGT fallback (§5.7); the primary WBGT uses the full Liljegren natural wet-bulb solver.
8. Apparent Temperature / "Feels Like" (BoM, Steadman)
The Australian Bureau of Meteorology apparent-temperature formulation. Unlike Heat Index it includes the cooling effect of wind, making it the better everyday "feels like" number. Water-vapor pressure e in hPa, wind v in m/s. Source: src/lib/metrics/apparent.ts.
e = (RH/100) · 6.105 · exp(17.27·Ta / (237.7 + Ta))
AT = Ta + 0.33·e − 0.70·v − 4.00When Open-Meteo supplies a native apparent_temperature, that value is used in preference to this formula; otherwise the formula above is computed.
9. Dew point (Magnus-Tetens)
Coefficients from Alduchov & Eskridge (1996); accurate to about ±0.4 °C for 0 < T < 60 °C. RH clamped to [1, 100] %. a = 17.625, b = 243.04. Source: src/lib/metrics/dewpoint.ts.
γ = ln(RH/100) + (a·Ta)/(b + Ta)
Td = (b·γ) / (a − γ)The provider's native dew_point_2m is preferred when present.
10. Supporting context fields
These are displayed for context and are not inputs to the heat-stress calculations:
- UV Index (
uv_index): bucketed Low / Moderate / High / Very High / Extreme at 3 / 6 / 8 / 11. - Air Quality (
us_aqi): US AQI, bucketed Good / Moderate / Sensitive / Unhealthy / Very Bad / Hazardous at 50 / 100 / 150 / 200 / 300. - Cloud cover (
cloud_cover, %), surface pressure (surface_pressure, hPa), precipitation (precipitation, mm; shown in inches when imperial).
On the location card these fields sit with the air temperature, feels-like, humidity, dew point and wind in one Weather details row, below the paw check. It is closed by default, and each device remembers whether it was left open (in the browser's local storage, like the other display preferences). Closed, the row names every number it shows ("Air 85°F · Humidity 80% · Wind 6 mph"), so a bare air temperature never sits under the heat-stress reading looking like a second one. Two readings lead the closed row, in amber, when they apply: air quality above 100 (Sensitive or worse, decided on the unrounded index, as the tile's word is) and, on the live reading only, precipitation falling now. "Now" is said only of the present: a forecast hour or a picked past hour never gets the rain flag, and the open Precip tile names that hour's rain without it. Whether rain is falling uses the same rule as the AI brief: radar rain at the point confirms it anywhere; otherwise a precipitating weather code with a measurable amount counts, unless the radar sees the area around the point clear inside its coverage (the contiguous US), where radar overrules the model. A flagged line may wrap to a second line, so a flag is never cut off on a narrow phone. Source: src/lib/weatherdetails.ts.
11. Classification scales
All thresholds are stored in °C (engine units); °F equivalents are given here for convenience. Bands share a common green → yellow → orange → red → purple → black severity gradient so that color means the same thing across metrics. Source: src/lib/metrics/scales.ts.
11.1 WBGT: "Heat Stress" (primary)
| Tier | From (°C) | From (°F) |
|---|---|---|
| Low | < 18 | < 64.4 |
| Caution | 18 | 64.4 |
| Moderate | 23 | 73.4 |
| High | 28 | 82.4 |
| Extreme | 32 | 89.6 |
| Lethal | 35 | 95.0 |
The Work and General tabs of Activity guidance print these bands too. A callout gives the reading, its level and range ("High · 82.4 to under 89.6°F") and how far it sits below the next level, and a tap lists all six with that audience's guidance for each and the reading's band marked Now. The level, the range, the Now mark and the distance all come from the unrounded reading; only the printed figures round, so a reading that prints as 82.4°F can still be Moderate when it is a hair under the High line, exactly as the card's hero says. The callout prints the reading as its page's own reading does: one decimal on the location card and the calculator, whole degrees on the city, audience and /paws pages. When a reader's profile makes their own level stricter than the reading's (the "For your situation" block above the callout), the distance line says they are already at that level instead, and the list tags it "For you". Sports shows its state association's zones in the same form, and Tactical keeps its flag text. Source: src/lib/guidancebands.ts.
11.2 Wet-bulb temperature
| Tier | From (°C) | From (°F) |
|---|---|---|
| Low | < 25 | < 77.0 |
| Uncomfortable | 25 | 77.0 |
| Stressful | 28 | 82.4 |
| Dangerous | 31 | 87.8 |
| Severe | 33 | 91.4 |
| Unsurvivable | 35 | 95.0 |
The ~35 °C wet-bulb survivability limit is the temperature above which a healthy person can no longer shed metabolic heat by sweating, even at rest in shade.
11.3 Heat Index
| Tier | From (°C) | From (°F) |
|---|---|---|
| Comfortable | < 27 | < 80.6 |
| Caution | 27 | 80.6 |
| Extreme Caution | 32 | 89.6 |
| Danger | 39 | 102.2 |
| Extreme Danger | 51 | 123.8 |
11.4 Feels Like (apparent temperature)
| Tier | From (°C) | From (°F) |
|---|---|---|
| Cold | < 10 | < 50.0 |
| Cool | 10 | 50.0 |
| Warm | 20 | 68.0 |
| Hot | 27 | 80.6 |
| Very Hot | 32 | 89.6 |
| Dangerous | 39 | 102.2 |
11.5 Dew point (comfort)
| Tier | From (°C) | From (°F) |
|---|---|---|
| Dry | < 10 | < 50.0 |
| Comfortable | 10 | 50.0 |
| Slightly Humid | 13 | 55.4 |
| Humid | 16 | 60.8 |
| Very Humid | 18 | 64.4 |
| Oppressive | 21 | 69.8 |
| Miserable | 24 | 75.2 |
11.6 Unified advisory
The card shows one persistent heat-danger advisory: the most severe guidance across the three monotonic heat-stress metrics (WBGT, Heat Index, Wet Bulb), independent of which metric the user is viewing. Severity is ranked by band index and mapped to the WBGT advisory ladder:
| Level | Advice |
|---|---|
| Caution | Light precautions for intense activity. Keep water handy. |
| Moderate | Reduce exertion and hydrate frequently. Take regular shade breaks. |
| High | Limit outdoor exertion. Drink water every 15 minutes and rest in shade. |
| Extreme | Dangerous. Outdoor work and sport are not recommended. |
| Lethal | Survival limit. The body cannot cool itself. Stay indoors and cool. |
Dew point (a comfort scale) and Feels Like (which has cold bands) are excluded from the unified advisory.
11.7 Tactical heat categories (TB MED 507)
The Tactical guidance names the US Army's heat category, the "flag", from the reading itself. Source: Department of the Army, TB MED 507 (12 April 2022, reference 29), Table 3-2, "Fluid replacement and work-rest guidelines for training in warm and hot environments". Source: src/lib/tbmed507.ts.
| Heat category | WBGT (°F) | WBGT (°C) | Easy work | Moderate work | Heavy work | Very heavy work |
|---|---|---|---|---|---|---|
| Below category 1 | < 78 | < 25.6 | no cycle set | no cycle set | no cycle set | no cycle set |
| 1 (white) | 78 to under 82 | 25.6 to under 27.8 | no limit, ½ qt | no limit, ¾ qt | 40/20, ¾ qt | 20/40, 1 qt |
| 2 (green) | 82 to under 85 | 27.8 to under 29.4 | no limit, ½ qt | no limit, ¾ qt | 30/30, 1 qt | 15/45, 1 qt |
| 3 (yellow) | 85 to under 88 | 29.4 to under 31.1 | no limit, ¾ qt | no limit, ¾ qt | 30/30, 1 qt | 10/50, 1 qt |
| 4 (red) | 88 to under 90 | 31.1 to under 32.2 | no limit, ¾ qt | 50/10, ¾ qt | 20/40, 1 qt | 10/50, 1 qt |
| 5 (black) | 90 and up | 32.2 and up | no limit, 1 qt | 20/40, 1 qt | 15/45, 1 qt | 10/50, 1 qt |
Each cell is the work-rest cycle (minutes of work / minutes of rest in each hour; "no limit" is no limit to work per hour, for up to 4 continuous hours) and the water intake in quarts per hour. The table assumes an average-sized, heat-acclimatized Service member in the OCP uniform. Its cautions: hourly fluid should not exceed 1½ qt, and daily fluid 12 qt. Heavy protective clothing (CBRN, JSLIST) adds 10 °F to the WBGT for easy work and 20 °F for moderate and heavy work. Above 90 °F, TB MED 507 says training may require extreme modifications or temporary suspension.
How the category is chosen. From the unrounded WBGT, converted to °F and compared with TB MED 507's own lines, never from our tier. Each category runs from its line to under the next: the table prints 78 to 81.9 and then 82 to 84.9, so 81.95 °F is category 1, and "> 90" is read as 90 °F and up. The °C column is the same lines converted, to a tenth; display rounds, the actual number decides. Three of those tenths round down (85 °F is 29.44 °C, 88 °F is 31.11 °C, 90 °F is 32.22 °C), so a °C range alone could exclude the reading it labels. Wherever the guidance prints a category's range in °C, TB MED's own °F lines follow it, as in "27.8 to under 29.4 °C (82 to under 85 °F)", and the °F line is the one that decides.
Why not from the tier. The tiers in §11.1 and TB MED 507's categories draw their lines in different places. High alone (82.4 to under 89.6 °F) spans green, yellow and red; Moderate runs from below category 1 into category 2; Extreme starts 0.4 °F below black. A flag named by tier is wrong for most readings in it: until 2026-10-16 every High reading was labeled "Red flag". So the tactical text attached to a tier names no flag, and wherever the guidance names one (the card, the /tactical page and its shared links, reports and Studio documents) it is the reading's own. Past our Lethal line (35 °C, 95 °F) the black flag is still named, but TB MED's "easy work, no limit" cell is not printed there: the instruction is to halt exertion, as for every other audience.
12. "This day in history"
Places today's heat in climatological context using ERA5 reanalysis. Source: src/app/api/history/route.ts, src/lib/openmeteo.ts.
- Variable. Daily maximum apparent temperature (
apparent_temperature_max), a humidity-aware, precomputed daily aggregate, chosen so decades of context come from a single cheap request rather than recomputing hourly WBGT. - Window. A ±3-day window around today's calendar day, across the most recent 35 complete years.
- Per-year peak. For each year, the maximum daily feels-like high within the window. Window days are grouped by climatological season-year so a window that straddles 1 January stays in one group (correct for Southern-Hemisphere summers).
- Outputs. Percentile rank of today's forecast feels-like high among the per-year peaks; the climatological normal (mean of the peaks); the all-time record (value and year); and the full per-year series for the sparkline.
- Today's value. Today's forecast
apparent_temperature_max. - Location. The requested point rounded to 0.1° (about 11 km north-south), used for both today's value and the archive, so the whole panel describes one point and one cached answer serves a neighborhood.
If fewer than five valid years are available, the panel hides itself. It also hides itself, with no empty box and no error text, when the archive does not answer: it is served from Open-Meteo's free archive host, which can refuse or be unreachable. That empty answer is kept for at most a minute, so the panel returns soon after the archive does.
13. Map visualization (interpolation)
The heat map is a continuous field built from point samples. For a viewport the app samples a grid of points (current conditions, or a forecast hour from the time scrubber); at low zoom it uses an always-on 10°-aligned global grid (37 × 17 = 629 points). Sample nodes snap to a fixed absolute lat/lng lattice with a discrete halving step ladder (10° → 5° → … → ~0.01°), so node positions are stable across pans and zooming refines onto an aligned subgrid. All layers sample at a shared 15-minute time bucket: solar geometry is evaluated at the same instant for every zoom level, so the field is consistent across views and rolls over together. Each sampled point is run through the same metrics engine, then the grid is bilinearly interpolated per pixel and colored along the metric's continuous gradient; ocean / missing nodes fade to transparent so the field flows seamlessly. This is a visualization step only and does not alter the per-point metric values. Source: src/components/HeatMap.tsx, src/app/api/grid/route.ts, src/app/api/global/route.ts.
One answer per half hour. For the live layer the server asks the forecast service once for the half hour a bucket falls in (on the UTC clock, :00 to :30 or :30 to :00), gets both of its 15-minute values in that one request, and keeps the answer for 30 minutes, shared by every tile, view and visitor that asks for the same points. Each bucket is still drawn from its own 15-minute value, with the same correction and interpolation, so the colors match what each bucket's own request would show, unless a new model run landed during the half hour. In that case the second bucket is drawn from the older run: the forecast service's models update hourly at best, and 30 minutes is also the age past which the server asks again for a reading shown as current. Until 2026-10-24 every 15-minute bucket asked for its points again, so a map left open asked four times an hour instead of twice. The forecast hours have always worked this way, with one three-day answer kept for an hour. Source: src/lib/openmeteo.ts (LIVE_WINDOW_MS).
Test sites. On any address other than wetbulbtracker.com and www.wetbulbtracker.com (preview and deployment addresses, a local run), in headless Chrome anywhere, and for requests that do not come from a web browser (scripts and command-line tools), the map's data routes ask the forecast service nothing. The heat layer is drawn as a black and white hatch marked TEST, city labels say "Test" instead of a number, and the map says "Test heat layer: no live data on test sites". The website and the store apps, which load it, get the real layer in a normal browser. Since 2026-10-24. Source: src/lib/maptest.ts, src/lib/testtile.ts.
A daily budget for the map. The forecast service bills every point in a request, so the map's data routes (heat tiles, the regional and world grids, the city labels) count the calls they pay for, by the service's own weighting, in one shared count per UTC day. A call the server's cache answers is free and is not counted, and an answer the cache hands out again in its first seconds is counted once. Past a daily line (120,000 calls by default) the map stops asking until 00:00 UTC. A heat tile is then served from the last copy the server drew, if the data behind it is no more than three hours old (for a forecast hour, counted from when the forecast was made), or not at all; it is never drawn on a coarser grid. City labels are left off, and the map says "Heat layer paused until 7 PM. Any colors shown are up to 3 hours old." (the time in the reader's own zone). Readings for a place (the card, widgets, watch, alerts and reports) do not count toward the line and are never paused, and neither are the hourly accuracy collector and the forecast log, so the bias correction keeps learning. If the shared count cannot be reached, each server counts on its own and the map keeps working. Since 2026-10-24. Source: src/lib/mapbudget.ts, src/lib/openmeteoweight.ts, src/lib/pausedtiles.ts.
Colors follow the level. The gradient blends between the legend's tier colors, but it is anchored so that every value is drawn nearer its own tier's legend color than any other: each tier's pure color sits inside its band, and each tier boundary falls exactly halfway between the two colors either side of it (a value exactly on a boundary is drawn in the upper tier). For Heat Stress the pure colors sit at about 15.5, 20.5, 25.5, 30.5, 33.5 and 36.5 °C. So a place the card calls High is drawn nearest High's red, never nearest Extreme's purple, and the same holds for every metric and for the pavement layer's paw tiers. Until 2026-10-16 each color sat at its band's lower edge, which drew the upper half of every band nearer the next tier's color. Source: src/lib/heatcolor.ts, src/app/api/tiles/[metric]/[z]/[x]/[y]/route.ts.
Which hour "+3h" is. A forecast hour on the map is the hour under way plus the offset: at 8:30, "+3h" is the 11 AM hour. The field, the city labels, the night shading, the card, the pin and the scrubber's clock all use that one rule, the same one the hourly forecast and the widgets count from. Until 2026-10-16 the card, the pin and the clock counted from the next whole hour instead, so they named the hour after the one the field drew. The field is sampled at the top of the hour in UTC, so where the local clock sits on a half hour (India, central Australia, Newfoundland) it is 30 minutes off the hour the card names: in the first half of each local hour it is drawn from the half hour before that hour, and in the second half from the half hour inside it. Where the clock sits on a quarter hour (Nepal) it is 15 or 45 minutes off in the same way. A map left on a forecast hour keeps the rule as the clock moves: at the top of each hour "+3h" names the next hour, and the field, the labels, the card and the pin all move to it together. Until the card's reading for the new hour arrives, the pin shows no number rather than the old hour's. Source: src/lib/forecasthour.ts.
13.1 Map background and credits
The map's background (land, water, roads and place names) comes from CARTO's basemap tiles, drawn from OpenStreetMap data. If CARTO stops serving our map, or we switch providers, the background comes from OpenFreeMap instead: vector tiles in the OpenMapTiles schema, built from OpenStreetMap data, with fonts and icons from the same service. The heat field, the city readings, the rain and night layers and the pin are ours, and none of them change with the background. The credit in the corner of the map always names the providers of the background on screen.
Credits for the CARTO background: map data © OpenStreetMap contributors, under the Open Database License (openstreetmap.org/copyright); tiles and design © CARTO (carto.com/attribution).
Credits for the OpenFreeMap background:
- Map data © OpenStreetMap contributors, under the Open Database License (openstreetmap.org/copyright).
- Tiles © OpenMapTiles (openmaptiles.org), served by OpenFreeMap (openfreemap.org).
- Map style. Our light map is modified from OpenFreeMap's Positron (openmaptiles/positron-gl-style), and our dark map from OpenFreeMap's Dark, which is OpenMapTiles' Dark Matter (openmaptiles/dark-matter-gl-style), both by way of OpenFreeMap's styles (hyperknot/openfreemap-styles). Their design is licensed by MapTiler and the OpenMapTiles contributors under CC BY 4.0, and is derived from CartoDB Basemaps, designed by Stamen and Paul Norman for CartoDB Inc. and licensed under CC BY 3.0. The style code is under the BSD 3-Clause License (© 2015 CartoDB Inc.; © 2024 MapTiler.com and OpenMapTiles contributors), and OpenFreeMap's changes to it under the MIT License (© 2023 Zsolt Ero). Our copies are modified: their layers are reordered so every name sits above the heat layer, their road, water, building and label colors are changed toward CARTO's look, the light map's road names get a white halo, the dark map's motorway route numbers start at zoom 11, and images the icon set lacks are dropped. The full license texts, with the list of our changes, are at /licenses/basemap-styles.txt.
- Fonts: Noto Sans, under the SIL Open Font License 1.1.
14. Limitations and assumptions
- Inputs are NWP model output, not in-situ observations; local microclimate (pavement, shade, buildings) is not captured.
- The Liljegren surface albedo (0.20, representative of vegetated/urban CONUS) and the assumption of a standing person in the open are fixed; true WBGT varies with ground cover and posture.
- The bias correction is learned only in the contiguous United States. Outside it the correction is applied only where it raises a reading (§5.9), and no accuracy figure is published for those places.
- Shortwave radiation is global horizontal; the direct/diffuse split is inferred from a clearness-index parameterisation, not measured.
- Stull wet bulb assumes near-sea-level pressure.
- Historical context uses daily apparent-temperature maxima (not WBGT) for tractability, and ERA5 has a coarser grid than the live forecast.
- The simplified WBGT fallback (§5.7) is not validated and is used only when solar geometry or pressure are unavailable.
- The AI brief and the AI outlooks are written by a language model from the levels on the page, not computed. For the card, city, planning and paw briefs, since 2026-10-16 the model is given each day's highest level with its place in the order (Low, Caution, Moderate, High, Extreme, Lethal) and its direction from the day before, plus the place's date and season. Every one of these except the paw brief is also given the rest of today hour by hour (never the hours already over) and each later day's hours at Caution or above. The paw brief gets only the daily air lines, so it never names an hour when the ground turns walkable. A draft of one of these briefs that calls a higher day a break, or calls a window ideal while it overlaps any hour at Caution or above, is written again, and a second failure shows no brief.
- The report outlook is written from the report's own figures: the dates and their season, the daylight hours in each level, the daylight hours at or above High, and the levels of the peak, the low and the coolest daylight hour. It is not told the place's name or its coordinates, so it calls the place "this location", and it is given no per-day lines. Since 2026-10-20 it is told to say nothing those figures do not state, to name no policy, standard or organization, and to say for each audience only when the higher-risk and lower-risk times fall, never how to work or train through them. The summary in an emailed report has its own prompt, written from the report's peak, low and coolest daylight hour and each day's highest level, and it is told to give no advice at all.
- The card, city, planning, paws and report prompts tell the model to give no medical advice and to name no symptoms; everyday precautions such as shade, water and rest breaks are allowed on the card, city and planning briefs. Since 2026-10-20 every AI text, the emailed summary included, is also checked by a fixed word screen before it shows. The screen stops text that calls a time, a day or a level safe, promises an outcome, gives a web address, an email address or a phone number, or names one of a fixed list of symptoms, heat illnesses and kinds of medical help, and a draft the model cut off or refused. On the site a stopped draft is written once more, told what kind of problem it had, and a second failure shows no brief; an emailed summary is not written again, and the email goes out without it. A word screen does not read meaning, so the text can still be wrong: the numbers and colors are the record. So we also keep a private copy of each AI text, with the place name we gave the model and any contact details the screen finds replaced, to review what the model writes (the Privacy Policy, §6, says for how long), and every brief has a "Report this brief" link that emails us its reference code.
15. Personalization ("Your risk")
The objective WBGT is universal and is never altered by personalization. To make it actionable for an individual, the app computes a separate personal-risk readout shown alongside the objective number. Source: src/lib/personalize.ts.
The person optionally provides a few heat-risk factors. Each maps to a small °C-equivalent "personal heat load" added to the objective WBGT to form an effective value, which is then classified on the same WBGT scale (§11.1), so the "Your risk" tier and color mean the same thing as everywhere else.
| Factor | Setting → load (°C-equivalent) |
|---|---|
| Age band | child +1.0 · adult +0 · 65+ +1.5 |
| Heat acclimatization | yes +0 · unsure +0.5 · no +1.5 |
| Activity / exertion | resting +0 · light +1.5 · heavy +3.0 |
| Health sensitivities | +1.5 for one, +0.5 each additional, capped at +2.5 |
The total load is clamped to +6 °C.
The personal tiers the site displays all use this arithmetic, with two details: the "Your risk" readout (on the card and the city pages), the "For your situation" step in Activity guidance, and the tier a personalized heat alert names. The effective value is classified unrounded: a number may be rounded for display, but the level comes from the unrounded value. And a personal tier never reads Lethal unless the objective WBGT is itself Lethal: Lethal is a physical survival limit, and a personal load raises the person's risk without changing the reading, so an effective value at or above 35 °C reads Extreme instead.
The forecast cards are not personalized. The Outlook, At a glance (with its graph), the 7-day forecast and Your Week show the forecast's own levels whether or not a profile is set, the same tiers as the card's objective reading. With a profile set, the Outlook, At a glance and the 7-day forecast (which holds Your Week) each carry a line of fine print saying their levels aren't adjusted for it. The personal read is "Your risk" on the card, and for the forecast, a heat alert set For my personal risk.
The AI brief is not personalized either. It is written from the place's conditions alone, and nothing from the profile is sent to write it: not the answers, the personal load, the personal tier or the factors behind it. With a profile set, the brief on the card and on the /sports, /work and /tactical pages carries a line of fine print saying it isn't adjusted for the profile.
A heat alert with For my personal risk turned on fires on the same effective value, and the cap never holds it back: an alert set at Lethal fires once the effective value reaches 35 °C, and names the tier "Your risk" shows for that hour, which is Extreme unless the objective WBGT is itself Lethal. When a later forecast for the same day carries the effective value to 35 °C or more while the objective WBGT stays under it, so the tier the alert names is still Extreme, the follow-up alert says the forecast got hotter and the tier is unchanged, instead of naming a new tier. When the named tier does rise, including to Lethal once the objective WBGT itself reaches 35 °C after an alert that said Extreme, the follow-up alert names the new tier. Heat on a different day is not a follow-up: it is that day's first alert, and names its tier. Neither is heat in a part of a day that no alert covered. For this a day has three parts: the early hours (before 6 AM), the daytime, and the late evening (from 9 PM), and an alert about the daytime also covers the late evening after it.
A personalized alert follows the profile as it is now, not as it was when the alert was set. When the profile changes in a signed-in browser, that browser sends the new personal load to each alert set For my personal risk: the one number the alert stores, never the answers behind it. A higher load starts the alert over, so its next notification is a first alert for the new load; a lower load keeps what each device has already been told. A load of zero (resting, for example, or a reset to the general reading) keeps the alert at a 0.1 °C margin instead of zero, so it stays marked as personalized and keeps following the profile, whichever signed-in browser changes it next. The margin only moves an alert toward warning: it fires at, or names, a tier above the one "Your risk" shows only when that reading is within 0.1 °C under a tier line. Withdrawing consent for the profile returns every personalized alert to the objective WBGT, with no margin: they read the objective WBGT until they are set up again.
On the scientific basis (and its honest limits). There is no single published table that converts "age" or "a heart condition" into a WBGT offset. What the literature does establish is the underlying physiology and the practice of adjusting heat-stress limits for these factors. We translate that established guidance into one intuitive °C-equivalent readout. The loads are our own planning estimates, not values taken from any one standard. Every load is zero or positive, so a setting can raise the personal read or leave it level, and never lowers it below the objective WBGT:
- Exertion. Heat-stress standards do not use a single WBGT threshold. They lower the safe limit (or shorten the work/rest ratio) as metabolic heat production rises. ISO 7243 sets WBGT limits by metabolic rate, and the U.S. ACGIH/NIOSH action limits drop by roughly 4–6 °C WBGT from rest to heavy work [10][11][12]. Our resting → light → heavy loads (0 / +1.5 / +3.0) are smaller than that drop: the heavy load is roughly half to three-quarters of it, so for hard, sustained work the load is, if anything, on the low side.
- Heat acclimatization. Acclimatized individuals tolerate substantially higher heat strain; NIOSH and ACGIH publish separate, lower limits for the unacclimatized, and acclimatization is among the strongest modifiable protective adaptations [11][12][13]. Hence the +1.5 °C for "not acclimatized."
- Age. Older adults have blunted thermoregulation (reduced skin blood flow and sweating) [14]; children have a higher surface-area-to-mass ratio and lower sweat capacity [15]. Both are recognized heat-vulnerable groups [11].
- Health sensitivities. Cardiovascular and respiratory disease, pregnancy, and many common medications (e.g. diuretics, anticholinergics, some psychiatric drugs) impair heat dissipation or fluid balance and raise heat-illness risk [11][16][17]. We apply one capped load (+1.5 °C for one sensitivity, +0.5 °C for each further one, at most +2.5 °C) and never infer or display any specific condition.
This "objective hazard × personal vulnerability" framing mirrors public-health tools such as the U.S. CDC/NWS HeatRisk index [18]. Our specific weights are planning estimates, not clinical thresholds, and we expect to revise them as the literature informs them. Read them as estimates, not as margins added for safety: the heavy-exertion load is smaller than the occupational range above.
This is guidance, not medical advice or a diagnosis. Inputs are optional and self-reported, stored on the device and synced to the account only when signed in, and can be cleared at any time.
16. Pavement / paw-safety surface temperature
Source: `src/lib/pavement.ts`.
Air temperature badly understates how hot the ground gets in the sun. A dark, dry surface in full sun runs roughly 40–60 °F (22–33 °C) hotter than the air, and a contact burn becomes possible within about a minute once the surface passes ~125 °F / 52 °C. That is the gap a dog owner (or a barefoot child) never thinks to check: the air feels fine, but the pavement is dangerous. The app already has the three inputs a surface energy balance needs (shortwave solar, air temperature, and wind), so it estimates the surface temperature and turns it into a plain paw-safety verdict. This is an estimate to help you check before you walk, not a measurement. Read the limitations in §16.5, especially the after-dark one, before relying on the number.
16.1 Surface energy balance
A horizontal opaque surface in quasi-steady state balances absorbed sunlight against three losses (net longwave radiation to the sky, convection to the air, and conduction/storage into the pavement mass):
α·S = ε·σ·(Ts⁴ − Tsky⁴) + h_conv·(Ts − Ta) + h_sub·(Ts − Ta)solved for the surface temperature Ts by bisection (the balance is monotonic in Ts). S is the weather model's global horizontal shortwave irradiance (bounded as in §2.2 when the provider omits it), Tsky = Ta·ε_sky^0.25, and h_conv = 5.7 + 3.8·u is the standard McAdams (1954) combined convection coefficient [22] with u a near-surface wind (≈ half the 10 m wind). This is the same radiative–convective balance the app solves for the WBGT black globe (§5.3), specialised to flat ground; the closed form is essentially the Solaimanian & Kennedy (1993) maximum-pavement-temperature model [21], which is the basis of the Superpave high-temperature binder grade.
The one departure from a full treatment is collapsing substrate conduction and storage into a single lumped conductance h_sub (calibrated below) rather than solving the transient 1-D heat-conduction equation. On its own this is quasi-steady: it returns the temperature the current sun, air, and wind would sustain, which snaps to air temperature at night and so cannot see the heat still stored in the pavement after sunset.
Thermal memory (transient model). For the live reading we therefore integrate the lumped-capacitance form of the same balance over the recent ~day of hourly weather:
C·dTs/dt = α·S − ε·σ·(Ts⁴ − Tsky⁴) − (h_conv + h_sub)·(Ts − Ta)Now the surface has heat capacity C = ρ·c_p·D, with D the diurnal damping depth √(2·(k/ρc_p)/ω), ω = 2π/86400 s⁻¹. For asphalt (k≈1.2, ρ≈2300, c_p≈900), D≈0.13 m and C≈2.6×10⁵ J/m²·K, a derived thermophysical value, not a fitted knob. At light wind it gives a cooling time constant τ = C/h_total ≈ 3 h, so the surface lags the sun (peaking an hour or two after solar noon) and stays materially hotter than the air for hours after sunset, which matches field evidence that asphalt is far warmer late at night than at dawn. We report the warmer of the steady-state equilibrium and this transient integration: a single lumped capacity necessarily damps the mid-day peak, so the steady state (tuned to reproduce Berens' field readings) wins by day, while the transient term wins in the evening. The result keeps the Berens-calibrated afternoon number and an honest post-sunset one, and never reads cooler than either. When the recent history is unavailable (e.g. a scrubbed forecast hour) the reading falls back to the quasi-steady value.
16.2 Constants
| Quantity | Value | Notes / source |
|---|---|---|
| Asphalt absorptivity α | 0.90 (albedo 0.10) | Aged asphalt; new/dark asphalt is darker (~0.05, α≈0.95) and runs hotter than modeled [23] |
| Concrete absorptivity α | 0.70 (albedo 0.30) | Typical weathered gray concrete [23] |
| Surface emissivity ε | 0.93 | Within the 0.85–0.95 range for both surfaces [23] |
| Clear-sky emissivity ε_sky | 0.82 | Warm/humid end of the ~0.70–0.85 clear-sky range; a clear-sky, worst-case-daytime assumption |
| Substrate conductance h_sub | 8.5 W/m²·K | Lumped; calibrated in §16.3 |
Asphalt is the hot default (roads, lots, many paths); concrete sidewalks run cooler because they reflect more sunlight, so both are shown.
16.3 Calibration, and honesty about it
h_sub is set so the model reproduces the classic field readings of Berens (JAMA, 1970) [19], a Phoenix case series that recorded asphalt reaching ≈125 °F at 77 °F air, ≈135 °F at 86 °F, and ≈143 °F at 87 °F, explicitly "in direct sunlight and in the absence of wind." With h_sub ≈ 8.5 W/m²·K the model matches those readings to within a few °F at full sun.
We are deliberate about what that does and does not mean. Berens' numbers are informal spot-readings, not a controlled calibration curve: a 1 °F air rise (86→87) jumps the asphalt reading 8 °F, which only happens across different surfaces and days. So we treat them as illustrative field observations, not a lookup formula, and we do not claim the model is "validated" against them. The widely-copied "95 °F → 149 °F" row cannot be confirmed in Berens' original and is not used as an anchor. The general effect and its upper end are corroborated by peer-reviewed measurement: Harrington et al. (1995) recorded asphalt peaking near 68 °C / 154 °F [24]. Because the model is driven by the hour's own solar value rather than assuming "sunny," a mild but cloudy morning correctly reads cool while a merely-warm but sunny afternoon reads hot.
16.4 Paw-burn risk tiers
Contact injury follows a continuous time–temperature relationship (Moritz & Henriques, 1947) [20], not a hard cutoff, so the tiers are graded and never imply "below 125 °F is safe":
| Tier | Surface temperature | Meaning |
|---|---|---|
| Comfortable | < 43 °C / 109.4 °F | Cool enough for a normal walk |
| Warm: check first | 43 to under 52 °C / 109.4 to under 125.6 °F | At/above skin pain onset (~43–44 °C), so it can already be uncomfortable |
| Too hot for paws | 52 to under 60 °C / 125.6 to under 140 °F | A full-thickness burn takes ≈60 s at ~52 °C |
| Dangerous | ≥ 60 °C / 140 °F | Burns in seconds |
The engine compares in °C; the °F figures are exact conversions, the same numbers the map legend prints. The tier comes from the unrounded surface temperature, on the paw card, the map pin and a shared paw link alike; only the printed number is rounded (to a tenth, then to a whole degree), so 51.96 °C shows as 52 °C (126 °F) and is still "Warm: check first". A shared link carries the surface to the hundredth, cut down rather than rounded up, so it never lands in a higher tier than the app showed. Until 2026-10-16 the live reading's surface was rounded to 0.1 °C before it was graded, which put a surface within 0.05 °C below a boundary one tier up.
On the map's pavement layer the field is the daytime estimate colored by these tiers (§13), and the pin over the chosen place reports that place's asphalt surface, labeled "Asphalt" and colored by its tier: the same number and tier as the paw card. Where the paw card shows the after-dark hand-test warning instead of a number, the pin says "After dark: feel the surface" and shows no number. Until 2026-10-16 the pin kept the heat-stress (WBGT) number on this layer, unlabeled, in heat-stress colors that read as paw tiers.
These thresholds come from human-skin studies. Dog paw pads are thicker and more keratinized, so the figures are a conservative safety floor, not a measured pad value; puppies and thin or injured pads are more vulnerable.
16.5 Limitations (read these)
- After dark. The live reading uses the thermal-memory model (§16.1), so it keeps the day's stored heat and reads an honest evening/night number rather than snapping to air temperature. It is still a reduced-order (single lumped-capacity) approximation of a distributed system, so treat the nighttime value as indicative, not exact, and always finish with the hand test. A scrubbed forecast hour has no recent history to integrate, so it falls back to the quasi-steady value and shows the hand-test warning after dark instead.
- Thermal-lag timing. The transient model peaks an hour or two after solar noon and lingers into the evening, but a single lumped capacity cannot perfectly resolve the fast surface skin and the slow deep mass at once, so the exact peak timing and the evening decay rate are approximate.
- Single-point, dry, clear-sky, open surface. The estimate assumes dry, exposed, aged asphalt (or gray concrete) under a clear sky. Real pavement varies within meters with shade, color, age, wetness and material: a shaded or wet path runs far cooler, fresh black asphalt in full sun hotter. It is not a reading of the specific patch under your dog.
- The hand test is the cross-check. A widely-shared vet/shelter rule of thumb (durations vary; many guides say 5–7 s, and the AKC suggests ~10 s [25][26]): press the back of your hand to the pavement; if you can't hold it there comfortably, it's too hot for paws. It's a sensory heuristic, not a measurement. Pair it with the obvious moves: walk early or late, and use grass and shade.
17. Where this is headed (roadmap)
Accuracy here is a continuous commitment, not a fixed claim. Three directions, all powered by the retained corpus (§5.6):
- A fully learned bias-correction model. The conditions-based correction is already live (§5.9): a (solar × wind) surface, cross-validated to cut held-out error 34.5 % across 31 day-blocked folds on the corpus read 2026-10-04. The staged rollout was global offset → segmented surface → a learned model, and that last step is the open one: a regression (gradient-boosted trees or a small network) on the complete feature vector stored with each sample (adding humidity, cloud cover, time of day, and season) to squeeze the residual further. Every candidate stays guarded by shrinkage, magnitude clamps, and held-out cross-validation, so a richer model ships only if it provably generalizes to days it never trained on.
- Multi-source ground truth. The NWS reference is itself a model, so agreement measures method+input agreement, not absolute truth. The corpus schema carries a
ref_sourcefield so additional references slot in and are weighted by reliability: physical sensors (mesonets, handheld WBGT meters, a small owned network) > ERA5 reanalysis (global coverage, extends the comparison outside the US) > forecast models. Because raw inputs are retained, history can be re-graded as better truth arrives.
- Microclimate refinement. Sun vs. shade (§5.8) is the only layer built so far that moves from the model's open-surface, ~km-scale grid toward the meter-scale reality. Others could follow: surface type and albedo (e.g. hot asphalt vs. grass, which feed the globe energy balance directly), the urban heat island, sky-view factor in urban canyons, tree canopy and wind sheltering, each derivable from land-cover and terrain data. The comparison corpus cannot calibrate them: its reference is the NWS's modeled WBGT on a grid of about 2.5 km, which does not resolve them either. A layer like this would need a measured, on-site reference before it changed any number we show.
Full detail and guardrails are in the project roadmap (docs/roadmap.md).
18. References
- Liljegren, J. C., Carhart, R. A., Lawday, P., Tschopp, S., & Sharp, R. (2008). Modeling the Wet Bulb Globe Temperature Using Standard Meteorological Measurements. Journal of Occupational and Environmental Hygiene, 5(10), 645–655.
- Stull, R. (2011). Wet-Bulb Temperature from Relative Humidity and Air Temperature. Journal of Applied Meteorology and Climatology, 50(11), 2267–2269.
- Rothfusz, L. P. (1990). The Heat Index Equation. NWS Southern Region Technical Attachment SR/SSD 90-23.
- Steadman, R. G. (1984). A Universal Scale of Apparent Temperature. Journal of Climate and Applied Meteorology, 23(12), 1674–1687. (BoM apparent temperature.)
- Alduchov, O. A., & Eskridge, R. E. (1996). Improved Magnus Form Approximation of Saturation Vapor Pressure. Journal of Applied Meteorology, 35(4), 601–609.
- NOAA Global Monitoring Laboratory. Solar Position Calculator equations (general solar geometry). Haurwitz, B. (1945). Insolation in relation to cloudiness and cloud density. Journal of Meteorology, 2(3), 154-166. (Clear-sky global horizontal irradiance, used as the upper bound in §2.2.)
- Kong, Q., & Huber, M. PyWBGT, a reference implementation of the Liljegren WBGT model. github.com/QINQINKONG/PyWBGT
- Hersbach, H., et al. (2020). The ERA5 global reanalysis. Quarterly Journal of the Royal Meteorological Society, 146(730), 1999–2049.
- Open-Meteo. Open-Meteo Weather, Air-Quality, and Historical Reanalysis APIs. open-meteo.com. Data under CC BY 4.0.
- International Organization for Standardization (2017). ISO 7243:2017, Ergonomics of the thermal environment: Assessment of heat stress using the WBGT (wet bulb globe temperature) index.
- National Institute for Occupational Safety and Health (2016). Criteria for a Recommended Standard: Occupational Exposure to Heat and Hot Environments. DHHS (NIOSH) Publication No. 2016-106.
- American Conference of Governmental Industrial Hygienists (ACGIH). Heat Stress and Strain: TLV® Physical Agents documentation (WBGT screening limits by workload and acclimatization state).
- Périard, J. D., Racinais, S., & Sawka, M. N. (2015). Adaptations and mechanisms of human heat acclimation. Scandinavian Journal of Medicine & Science in Sports, 25(S1), 20–38.
- Kenney, W. L., & Munce, T. A. (2003). Invited Review: Aging and human temperature regulation. Journal of Applied Physiology, 95(6), 2598–2603.
- Falk, B., & Dotan, R. (2008). Children's thermoregulation during exercise in the heat: a revisit. Applied Physiology, Nutrition, and Metabolism, 33(2), 420–427.
- Westaway, K., Frank, O., Husband, A., et al. (2015). Medicines can affect thermoregulation and accentuate the risk of dehydration and heat-related illness during hot weather. Journal of Clinical Pharmacy and Therapeutics, 40(4), 363–367.
- Ravanelli, N., Casasola, W., English, T., Edwards, K. M., & Jay, O. (2019). Heat stress and fetal risk: environmental limits for exercise and passive heat stress during pregnancy: a systematic review. British Journal of Sports Medicine, 53(13), 799–805.
- U.S. Centers for Disease Control and Prevention & National Weather Service. HeatRisk, an index combining heat intensity with population vulnerability. cdc.gov / weather.gov.
- Berens, J. J. (1970). Thermal Contact Burns From Streets and Highways. JAMA, 214(11), 2025–2027. (Origin of the classic air-vs-asphalt field readings; Phoenix case series, direct sun, no wind.)
- Moritz, A. R., & Henriques, F. C. (1947). Studies of Thermal Injury: II. The Relative Importance of Time and Surface Temperature in the Causation of Cutaneous Burns. American Journal of Pathology, 23(5), 695–720.
- Solaimanian, M., & Kennedy, T. W. (1993). Predicting Maximum Pavement Surface Temperature Using Maximum Air Temperature and Hourly Solar Radiation. Transportation Research Record, 1417, 1–11.
- McAdams, W. H. (1954). Heat Transmission (3rd ed.). McGraw-Hill. (Combined surface convection coefficient h = 5.7 + 3.8·u.)
- Lawrence Berkeley National Laboratory, Heat Island Group. Cool Pavements: solar-reflectance and emissivity values for asphalt and concrete. heatisland.lbl.gov.
- Harrington, A. W., et al. (1995). Pavement Temperature and Burns: Streets of Fire. Annals of Emergency Medicine, 26(5), 563–568. (And the review "Streets of Fire revisited: contact burns," Burns & Trauma, 2019.)
- American Kennel Club. How to Protect Dog Paws From Hot Pavement (J. Klein, Chief Veterinary Officer). akc.org.
- FOUR PAWS USA. Hot Asphalt: A Danger to your Dog's Paws. fourpawsusa.org.
- Grundstein, A., Clark, J., et al. (2025). Evaluation of smartphone applications for estimating wet bulb globe temperature. GeoHealth. doi:10.1029/2025GH001347. (Finds app-derived WBGT under-reporting on-site measurements. Cited in §5.6 for the direction of error, not as a validation of this implementation.)
- Clark, J., Konrad, C. E., & Grundstein, A. (2024). The Development and Accuracy Assessment of Wet Bulb Globe Temperature Forecasts. Weather and Forecasting, 39(2), 403–419. doi:10.1175/WAF-D-23-0076.1. (An assessment of WBGT forecasts in North Carolina, referenced to on-site instruments; cited in §5.6 beside the forecast log, not as a check of this implementation.)
- Department of the Army (2022). TB MED 507: Heat Stress Control and Heat Casualty Management. Technical Bulletin, 12 April 2022 (supersedes TB MED 507/AFPAM 48-152 of 7 March 2003). Table 3-2, fluid replacement and work-rest guidelines by heat category, is reproduced in §11.7.