# WetBulbTracker — Scientific Methodology

**A complete specification of every heat-stress metric, algorithm, physical
constant, and adjustment used by WetBulbTracker.com.**

Last updated: June 2026 · Engine version: sprint-2 (full Liljegren WBGT)

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 metres per second (m/s),
  pressure in hectopascals (hPa), shortwave irradiance in watts per square
  metre (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 (map clicks) | BigDataCloud | `reverse-geocode-client` |

**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
forecast and historical archive for 1 hour and 24 hours respectively. A failed
upstream request is retried once without cache before erroring.

**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.

Source: `src/lib/openmeteo.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 |

---

## 3. Metric overview

| Metric | App label | Method | Captures |
|---|---|---|---|
| Wet Bulb Globe Temperature | **Heat Stress** | Liljegren et al. (2008) | T, RH, wind, **sun** |
| Heat Index | Heat Index | NWS Rothfusz regression | T, RH (shade) |
| Apparent Temperature | Real Feel | BoM / Steadman | T, RH, wind |
| Wet-bulb temperature | Wet Bulb | Stull (2011) | T, RH (thermodynamic) |
| Dew point | Dew Pt | Magnus-Tetens | T, RH (absolute moisture) |

WBGT is the primary metric because it is the only one that incorporates solar
radiation and is the recognised 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 Z
```

Clearness 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·Ta
```

where `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 vapour | `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 | — |
| Globe albedo | `αg` | 0.05 | — |
| Wick emissivity | `εwick` | 0.95 | — |
| Wick albedo | `αwick` | 0.4 | — |
| Wick diameter | `Dwick` | 0.007 | m |
| Wick length | `Lwick` | 0.0254 | m |
| Surface albedo | `αsfc` | 0.45 | — |

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 vapour 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 vapour pressure `e` in hPa:

```
e      = RH·0.01 · (es·0.01)
εatm   = 0.575 · e^0.143
```

Water-vapour diffusivity in air:

```
D(T, Ps) = 2.471773765×10⁻⁵ · (T·0.00342105637)^2.334 / (Ps/101325)
```

Latent heat of vaporisation: `λ(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/Dwick
```

### 5.3 Black-globe energy balance

Direct-beam geometry term (0 when `fdir = 0` or `cos Z ≤ 0`):

```
directTerm = (0.5/cos Z − 1) · fdir
```

Radiative 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 = 0
```

bracket `[Ta − (100 − RH)/5 − 50, min(Ta + 70, 340)]`.

### 5.5 Adjustments and solver details

- **Wind height correction.** Inputs are 10 m winds; the model expects ~2 m.
  Converted with the power law `v₂ = v₁₀·(2/10)^0.2` and floored at 0.13 m/s, as
  in the reference (a still-air floor prevents division blow-up).
- **Humidity clamp.** RH is clamped to [1, 100] %.
- **Irradiance / geometry guards.** `S` is floored at 0; `fdir` is 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 and is reported to be more
accurate than handheld WBGT meters (Duke Nicholas Institute, 2024). Full
numerical validation against NWS WBGT output (target ±1.5 °C) is the sprint-2
deliverable. Physical sanity checks 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.

### 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·Ta
```

This 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.

---

## 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.686035
```

Note: 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 / "Real Feel" (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
"real feel". Water-vapour 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.00
```

When 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).

---

## 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 colour means the same thing across
metrics. Source: `src/lib/metrics/scales.ts`.

### 11.1 WBGT — "Heat Stress" (primary)

| Tier | From (°C) | From (°F) |
|---|---|---|
| Safe | < 18 | < 64.4 |
| Caution | 18 | 64.4 |
| Moderate | 23 | 73.4 |
| High | 28 | 82.4 |
| Extreme | 32 | 89.6 |
| Lethal | 35 | 95.0 |

### 11.2 Wet-bulb temperature

| Tier | From (°C) | From (°F) |
|---|---|---|
| Safe | < 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 Real Feel (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 Real Feel (which has cold bands) are excluded
from the unified advisory.

---

## 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`.

If fewer than five valid years are available, the panel hides itself.

---

## 13. Map visualisation (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 coarse global grid
(36 × 18 ≈ 648 points, CDN-cached hourly). Each sampled point is run through the
same metrics engine, then the grid is **bilinearly interpolated** per pixel and
coloured along the metric's continuous gradient; ocean / missing nodes fade to
transparent so the field flows seamlessly. This is a visualisation 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`.

---

## 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.45) and the assumption of a standing person in
  the open are fixed; true WBGT varies with ground cover and posture.
- 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.

---

## 15. References

1. 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.
2. Stull, R. (2011). *Wet-Bulb Temperature from Relative Humidity and Air
   Temperature.* Journal of Applied Meteorology and Climatology, 50(11),
   2267–2269.
3. Rothfusz, L. P. (1990). *The Heat Index Equation.* NWS Southern Region
   Technical Attachment SR/SSD 90-23.
4. Steadman, R. G. (1984). *A Universal Scale of Apparent Temperature.* Journal
   of Climate and Applied Meteorology, 23(12), 1674–1687. (BoM apparent
   temperature.)
5. Alduchov, O. A., & Eskridge, R. E. (1996). *Improved Magnus Form
   Approximation of Saturation Vapor Pressure.* Journal of Applied Meteorology,
   35(4), 601–609.
6. NOAA Global Monitoring Laboratory. *Solar Position Calculator equations*
   (general solar geometry).
7. Kong, Q., & Huber, M. *PyWBGT* — reference implementation of the Liljegren
   WBGT model. github.com/QINQINKONG/PyWBGT
8. Hersbach, H., et al. (2020). *The ERA5 global reanalysis.* Quarterly Journal
   of the Royal Meteorological Society, 146(730), 1999–2049.
9. Open-Meteo. *Open-Meteo Weather, Air-Quality, and Historical Reanalysis
   APIs.* open-meteo.com

---

*Generated for WetBulbTracker.com. The metrics engine is a pure, framework-free
module; every formula above is implemented in `src/lib/metrics/` and
`src/lib/solar.ts` and can be audited directly.*
