Metadata-Version: 2.4
Name: heatstress
Version: 0.1.0
Summary: Vectorised heat-stress indices: NWS Heat Index, Humidex, Apparent Temperature, wet bulb, WBGT (Liljegren), UTCI, wind chill, risk categories and heatwave detection.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/heatstress_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/heatstress_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/heatstress_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/heatstress_sdk/blob/main/CHANGELOG.md
Keywords: heat-stress,heat-index,wbgt,utci,humidex,apparent-temperature,wet-bulb,wind-chill,heatwave,excess-heat-factor,thermal-comfort,biometeorology,occupational-safety,public-health,climate-adaptation
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Provides-Extra: pandas
Requires-Dist: pandas>=1.3.0; extra == "pandas"
Provides-Extra: xarray
Requires-Dist: xarray>=0.19.0; extra == "xarray"
Requires-Dist: pandas>=1.3.0; extra == "xarray"
Provides-Extra: all
Requires-Dist: pandas>=1.3.0; extra == "all"
Requires-Dist: xarray>=0.19.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# heatstress

Vectorised heat-stress indices for public-health, climate-adaptation and
occupational-safety analysis: NWS Heat Index, Humidex, Apparent Temperature,
wet-bulb temperature, WBGT (full Liljegren et al. 2008 model, shade/indoor, ISO 7243,
BoM approximation), UTCI, Wind Chill, mean radiant temperature, solar position,
risk categories and heatwave detection, including the Excess Heat Factor.

* Pure numpy core; every function takes scalars, numpy arrays, pandas Series or
  xarray DataArrays and returns the same kind of object.
* Units are explicit: degC by default with a `units="C" | "F" | "K"` switch on
  temperature functions, wind in m/s, pressure in hPa, radiation in W/m2, humidity in %.
  Converters are in `heatstress.units`.
* Inputs outside a formula's validity range raise a `ValidityRangeWarning`; NaN propagates.
* Validated against published reference values and independent implementations
  (see [Validation](#validation)).
* A command-line tool for single values and CSV time series.

> **Disclaimer.** heatstress computes screening indices from weather data. It is
> not medical advice and is not a substitute for on-site measurement, an
> occupational health programme, clinical judgement or the official warnings of
> your national meteorological or public-health service.

## Installation

```bash
pip install heatstress            # numpy only
pip install "heatstress[pandas]"  # + pandas helpers (DataFrames, events_to_frame)
pip install "heatstress[xarray]"  # + xarray/pandas
```

Python 3.9 or newer.

## Quick start

```python
import heatstress as hs

hs.heat_index(35, 60)                     # 45.1 degC (NWS algorithm)
hs.heat_index(95, 50, units="F")          # 105.2 degF
hs.heat_index_category(45.1)              # 'danger'

hs.humidex(30, dew_point=15)              # 34.0
hs.apparent_temperature(30, 50, wind=2)   # 31.6 degC
hs.wet_bulb_psychrometric(25, 50)         # 17.9 degC
hs.utci(30, tmrt=50, wind=2, rh=50)       # 34.6 degC
hs.utci_category(34.6)                    # 'strong heat stress'

# Outdoor WBGT from standard weather data (Liljegren et al. 2008)
z = hs.solar_zenith("2026-07-15T10:00Z", lat=38.42, lon=27.14)
w = hs.wbgt_liljegren_components(t=33, rh=55, wind=1.5, solar=850, zenith=z, pressure=1008)
w.wbgt, w.globe_temperature, w.natural_wet_bulb    # 32.7, 49.7, 27.7 degC
hs.work_rest_allocation(w.wbgt, "moderate")   # ACGIH TLV work-rest guidance

# Heatwaves on a daily series
import pandas as pd
tmax = pd.Series(..., index=pd.date_range("1991-01-01", periods=..., freq="D"))
thr = hs.calendar_day_percentile(tmax, tmax.index, q=90, window=15, baseline=(1991, 2020))
events = hs.heatwave_events(tmax, thr, min_duration=3)
hs.events_to_frame(events)

ehf = hs.excess_heat_factor(tmean_daily)      # Nairn & Fawcett (2015)
hs.ehf_severity(ehf)                          # none / low-intensity / severe / extreme
```

## Indices

| Index | Function | Inputs | Source | Validity / notes |
|---|---|---|---|---|
| Heat Index | `heat_index` | T, RH | NWS/WPC algorithm: Steadman (1979) simple formula, Rothfusz (1990) regression and the two NWS adjustments | Regression error +/-1.3 degF; warning above 120 degF air temperature or 150 degF HI |
| Rothfusz regression only | `rothfusz_regression` | T, RH | Rothfusz (1990) NWS SR 90-23 | For comparison only |
| Humidex | `humidex` | T, RH or dew point | Masterton & Richardson (1979), Environment Canada | MSC vapour-pressure form |
| Apparent Temperature | `apparent_temperature` | T, RH, wind (10 m), optional net radiation | Steadman (1994), Australian Bureau of Meteorology | Shade and radiation forms |
| Wet bulb (Stull) | `wet_bulb_stull` | T, RH | Stull (2011) J. Appl. Meteor. Climatol. 50:2267 | RH 5-99 %, T -20 to 50 degC, 1013 hPa; error -1 to +0.65 K (larger in cold, dry air) |
| Wet bulb (psychrometric) | `wet_bulb_psychrometric` | T, RH, pressure | WMO No. 8 (2018) Annex 4.B psychrometer equation, Newton solver | Ventilated wet bulb, wick over water |
| WBGT outdoor | `wbgt_liljegren`, `wbgt_liljegren_components`, `globe_temperature_liljegren`, `natural_wet_bulb_liljegren` | T, RH, wind, global solar, zenith, pressure, optional direct fraction | Liljegren et al. (2008) J. Occup. Environ. Hyg. 5:645 | 50.8 mm globe; 2 m wind (other heights scaled with a power law); solar capped at 0.85 x top of atmosphere as in the reference code |
| WBGT shade / indoor | `wbgt_shade` | T, RH, pressure (wind for `method="liljegren"`) | Simplified `0.7 Tw + 0.3 Ta`, or Liljegren with no sun and `0.7 Tnwb + 0.3 Tg` | No solar load |
| WBGT from measurements | `wbgt_from_measurements` | Tnwb, Tg, (Ta) | ISO 7243:2017 | Indoor and outdoor formulas |
| WBGT approximation | `wbgt_bom` | T, RH | Australian Bureau of Meteorology | **Approximation**: assumes moderately high radiation and light wind; can be off by several degrees |
| UTCI | `utci` | T, Tmrt, wind (10 m), RH | Broede et al. (2012) Int. J. Biometeorol. 56:481, 6th-order polynomial, 210 coefficients from the reference program `UTCI_a002.f90` | Ta -50 to 50 degC, Tmrt-Ta -30 to 70 K, wind 0.5-17 m/s, vapour pressure <= 5 kPa; out-of-range values NaN by default; wind below 0.5 m/s raised to 0.5 |
| Wind Chill | `wind_chill` | T, wind (10 m) | NWS/MSC (2001), Osczevski & Bluestein (2005) | T <= 10 degC; MSC light-wind formula below 4.8 km/h |
| Feels like | `feels_like` | T, RH, wind | NWS convention | Wind chill / heat index / air temperature |
| Mean radiant temperature | `mean_radiant_temperature` | Tg, T, air speed | ISO 7726:1998 Annex B | Forced or natural convection; 150 mm globe, emissivity 0.95 by default |
| Solar position | `solar_position`, `solar_zenith` | time, lat, lon | NOAA solar calculator (Meeus 1991) | Geometric zenith, no refraction |
| Humidity helpers | `saturation_vapour_pressure`, `vapour_pressure`, `dew_point`, `relative_humidity_from_dew_point`, `relative_humidity_from_vapour_pressure` | T, RH / Td / e | Magnus (Alduchov & Eskridge 1996), WMO, Buck (1981), BoM, Hardy (1998) ITS-90 | Over liquid water |

### Risk categories

| Function | Scale |
|---|---|
| `heat_index_category` | NWS: caution 80-89 degF, extreme caution 90-102, danger 103-124, extreme danger >= 125 |
| `humidex_category` | Environment Canada: < 30, 30-39, 40-45, >= 46 |
| `utci_category` | 10 UTCI stress classes from extreme cold (< -40) to extreme heat stress (> 46) |
| `work_rest_allocation` | ACGIH TLV (acclimatized) / Action Limit (unacclimatized) WBGT work-rest table as printed in the OSHA Technical Manual Sec. III Ch. 4, for light / moderate / heavy / very heavy work, with a clothing adjustment value |
| `iso7243_reference_wbgt`, `iso7243_assessment` | ISO 7243:2017 Annex A reference WBGT `56.7 - 11.5 log10(M)` (acclimatized) and `59.9 - 14.1 log10(M)` (unacclimatized) |
| `ehf_severity` | Nairn & Fawcett (2015): low-intensity, severe (EHF >= EHF85), extreme (EHF >= 3 EHF85) |

### Heatwave detection

* `percentile_threshold` and `calendar_day_percentile` (centred moving window of
  calendar days, optional baseline years, 29 February merged with 28 February).
* `heatwave_mask`, `consecutive_runs` and `heatwave_events` find spells of at
  least `min_duration` consecutive days above a scalar or day-varying threshold and
  report start, end, duration, peak and exceedance statistics;
  `events_to_frame` converts them to a DataFrame.
* `excess_heat_factor`: `EHI_sig = T3 - T95`, `EHI_accl = T3 - T30prev`,
  `EHF = EHI_sig x max(1, EHI_accl)`, attributed to the first day of the three-day
  window; `T95` from a reference period (`t95=` or `reference=`).

## Command line

```console
$ heatstress hi 35 --rh 60
              heat_index: 45.1 degC
                category: danger

$ heatstress wbgt 33 --rh 55 --wind 1.5 --solar 850 --time 2026-07-15T10:00Z --lat 38.42 --lon 27.14
$ heatstress utci 30 --tmrt 55 --wind 2 --rh 50
$ heatstress windchill 0 --wind 15 --wind-units mph --units F
$ heatstress series weather.csv -o indices.csv --lat 38.42 --lon 27.14
```

Every value command accepts `--units C|F|K` and `--json`; wind accepts
`--wind-units ms|kmh|mph|kn`. `heatstress series` reads a CSV, recognises common
column names (`time`, `t`/`temperature`, `rh`, `td`, `wind`, `solar`/`ghi`,
`pressure`, `tmrt`, `tg`, `zenith`, `lat`, `lon`), and appends every index that
the columns allow (UTCI needs `tmrt` or a globe temperature; the Liljegren WBGT
needs wind, solar and either a zenith column or time plus coordinates). The same
logic is available in Python as `hs.compute_indices(df)`.

## Validation

All comparisons are in the test suite (`tests/test_reference_values.py`) unless noted.

| Check | Reference | Result |
|---|---|---|
| UTCI polynomial | 1000 official test conditions, Broede et al. UTCI-Test-Data (doi:10.5281/zenodo.5503968, CC BY 4.0), column `UTCI_polynomial` | max deviation 0.050 degC (= rounding of the published values) |
| UTCI vs UTCI-Fiala model | same data set, 921 cases with wind <= 17 m/s | RMS 1.17 K (Broede et al. report about 1.1 K) |
| UTCI vs independent code | thermofeel 2.3.0 `calculate_utci_polynomial` | max 1e-12 degC (scratch cross-check) |
| Heat index vs NWS chart | 135 cells of the NWS heat index chart | bare Rothfusz regression within 0.92 degF; full NWS algorithm within 0.92 degF where no adjustment applies and within 2.29 degF in the high-humidity cells where NWS adds its adjustment |
| Heat index vs independent code | thermofeel `calculate_heat_index_adjusted`, 5000 random cases T <= 43 degC | max 0.08 degC (scratch cross-check) |
| Liljegren WBGT, globe, natural wet bulb | thermofeel 2.3.0 `thermofeel.liljegren` (KNMI port of the reference C code), 3000 random cases | max 4e-13 degC; 8 frozen vectors in the tests |
| Wet bulb | Stull (2011) worked example 20 degC / 50 % -> 13.7 degC; psychrometric table 25 degC / 50 % -> 17.9 degC | exact to the published precision; Stull vs psychrometric within +/-1 K for 0-45 degC, RH 20-95 % |
| Wind chill | NWS wind chill chart (6 cells); thermofeel | within 0.6 degF; identical to thermofeel |
| Humidex | Environment Canada example 30 degC / Td 15 degC -> 34 | exact; thermofeel max 0.04 |
| Apparent temperature | thermofeel `calculate_apparent_temperature` | identical (scratch cross-check) |
| Mean radiant temperature | ISO 7726 formula; thermofeel | max 0.06 degC (thermofeel uses 273.15 K in place of ISO's 273) |
| Solar position | pvlib 0.16.1 NREL SPA, 4000 random times 1990-2060 and latitudes +/-80 deg | zenith max 0.017 deg, Earth-Sun distance max 7e-5 AU; 6 frozen cases in the tests |
| Excess Heat Factor | thermofeel `thermofeel.excess_heat` with pandas rolling windows | max 4e-11 |

## Units and conventions

* Temperatures: degC unless `units="F"` or `"K"` (applies to all temperature
  inputs and outputs of that call).
* Relative humidity in %, wind speed in m/s (10 m for Apparent Temperature,
  Wind Chill and UTCI; 2 m for Liljegren unless `wind_height` is given), pressure
  in hPa (station pressure), solar radiation in W/m2 (global horizontal).
* Results come back as Python floats for scalar input, numpy arrays for array
  input, and pandas Series / xarray DataArrays (same index or coordinates) when
  the first array argument is one.

## Citation

See `CITATION.cff`. Please also cite the original authors of each index you use.

## License

MIT (c) 2026 Yusuf Eminoğlu. The UTCI test data shipped in `tests/data` are by
Broede et al., CC BY 4.0.
