heatstress 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. heatstress-0.1.0/CHANGELOG.md +42 -0
  2. heatstress-0.1.0/CITATION.cff +20 -0
  3. heatstress-0.1.0/LICENSE +21 -0
  4. heatstress-0.1.0/MANIFEST.in +9 -0
  5. heatstress-0.1.0/PKG-INFO +213 -0
  6. heatstress-0.1.0/README.md +169 -0
  7. heatstress-0.1.0/pyproject.toml +135 -0
  8. heatstress-0.1.0/setup.cfg +4 -0
  9. heatstress-0.1.0/src/heatstress/__init__.py +120 -0
  10. heatstress-0.1.0/src/heatstress/__main__.py +5 -0
  11. heatstress-0.1.0/src/heatstress/_utils.py +164 -0
  12. heatstress-0.1.0/src/heatstress/apparent.py +61 -0
  13. heatstress-0.1.0/src/heatstress/cli.py +297 -0
  14. heatstress-0.1.0/src/heatstress/heat_index.py +111 -0
  15. heatstress-0.1.0/src/heatstress/heatwave.py +297 -0
  16. heatstress-0.1.0/src/heatstress/humidex.py +78 -0
  17. heatstress-0.1.0/src/heatstress/humidity.py +145 -0
  18. heatstress-0.1.0/src/heatstress/mrt.py +75 -0
  19. heatstress-0.1.0/src/heatstress/py.typed +0 -0
  20. heatstress-0.1.0/src/heatstress/risk.py +235 -0
  21. heatstress-0.1.0/src/heatstress/series.py +199 -0
  22. heatstress-0.1.0/src/heatstress/solar.py +164 -0
  23. heatstress-0.1.0/src/heatstress/units.py +142 -0
  24. heatstress-0.1.0/src/heatstress/utci.py +336 -0
  25. heatstress-0.1.0/src/heatstress/wbgt.py +589 -0
  26. heatstress-0.1.0/src/heatstress/wetbulb.py +114 -0
  27. heatstress-0.1.0/src/heatstress/windchill.py +94 -0
  28. heatstress-0.1.0/src/heatstress.egg-info/PKG-INFO +213 -0
  29. heatstress-0.1.0/src/heatstress.egg-info/SOURCES.txt +37 -0
  30. heatstress-0.1.0/src/heatstress.egg-info/dependency_links.txt +1 -0
  31. heatstress-0.1.0/src/heatstress.egg-info/entry_points.txt +2 -0
  32. heatstress-0.1.0/src/heatstress.egg-info/requires.txt +18 -0
  33. heatstress-0.1.0/src/heatstress.egg-info/top_level.txt +1 -0
  34. heatstress-0.1.0/tests/data/utci_test_data.tsv +1006 -0
  35. heatstress-0.1.0/tests/test_cli_series.py +219 -0
  36. heatstress-0.1.0/tests/test_heatwave_solar.py +136 -0
  37. heatstress-0.1.0/tests/test_indices.py +308 -0
  38. heatstress-0.1.0/tests/test_reference_values.py +246 -0
  39. heatstress-0.1.0/tests/test_vectorisation.py +139 -0
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-11-09
8
+ ### Added
9
+ - NWS Heat Index (`heat_index`) following the WPC algorithm: Steadman low-range
10
+ formula, Rothfusz regression and both NWS adjustments; `rothfusz_regression` for
11
+ comparison.
12
+ - Humidex (`humidex`, from RH or dew point) and Apparent Temperature
13
+ (`apparent_temperature`, Steadman/BoM with optional radiation term).
14
+ - Wet-bulb temperature: Stull (2011) formula and a WMO psychrometric Newton solver
15
+ with pressure dependence.
16
+ - WBGT: full Liljegren et al. (2008) outdoor model with globe, natural wet-bulb and
17
+ psychrometric wet-bulb components, direct-beam fraction estimate and wind-height
18
+ scaling; shade/indoor estimate; ISO 7243 formulas for measured values; the
19
+ Australian BoM approximation (labelled as such).
20
+ - UTCI operational polynomial (Broede et al. 2012) with the 210 coefficients of the
21
+ reference program, validity masking and stress categories.
22
+ - Wind Chill (NWS/MSC 2001) with the MSC light-wind formula, and an NWS-style
23
+ `feels_like`.
24
+ - Mean radiant temperature from globe temperature (ISO 7726, forced/natural
25
+ convection).
26
+ - NOAA solar position (zenith, elevation, azimuth, declination, equation of time,
27
+ Earth-Sun distance) with time-zone-aware input handling.
28
+ - Humidity helpers: saturation vapour pressure (Magnus, WMO, Buck, BoM, Hardy
29
+ ITS-90), vapour pressure, dew point and relative humidity conversions.
30
+ - Risk categories: NWS heat index bands, Humidex bands, UTCI stress classes, ACGIH
31
+ TLV / Action Limit WBGT work-rest allocation (OSHA Technical Manual), ISO 7243:2017
32
+ reference WBGT.
33
+ - Heatwave detection: fixed and calendar-day percentile thresholds, spells of
34
+ consecutive exceedance days with event statistics, Excess Heat Factor (Nairn &
35
+ Fawcett 2015) with severity classes.
36
+ - `compute_indices` for dicts and pandas DataFrames, and the `heatstress` CLI
37
+ (`hi`, `humidex`, `at`, `wetbulb`, `wbgt`, `utci`, `windchill`, `series`).
38
+ - Scalars, numpy arrays, pandas Series and xarray DataArrays are accepted and
39
+ returned in kind; `ValidityRangeWarning` for out-of-range inputs.
40
+ - Validation tests against the official UTCI test data set, the NWS heat index and
41
+ wind chill charts, published worked examples, an independent Liljegren
42
+ implementation and the NREL SPA solar position algorithm.
@@ -0,0 +1,20 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ authors:
4
+ - family-names: "Eminoğlu"
5
+ given-names: "Yusuf"
6
+ email: "yusufeminoglu@gmail.com"
7
+ orcid: "https://orcid.org/0009-0005-6000-2934"
8
+ title: "heatstress"
9
+ version: "0.1.0"
10
+ date-released: "2026-11-09"
11
+ license: "MIT"
12
+ repository-code: "https://github.com/YusufEminoglu/heatstress_sdk"
13
+ url: "https://pypi.org/project/heatstress/"
14
+ abstract: "Vectorised heat-stress indices for public-health, climate-adaptation and occupational-safety analysis: NWS Heat Index, Humidex, Apparent Temperature, wet-bulb temperature, WBGT (Liljegren et al. 2008), UTCI, wind chill, risk categories and heatwave detection including the Excess Heat Factor."
15
+ keywords:
16
+ - heat stress
17
+ - WBGT
18
+ - UTCI
19
+ - heat index
20
+ - heatwave
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yusuf Eminoğlu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CITATION.cff
5
+ include pyproject.toml
6
+ include src/heatstress/py.typed
7
+ recursive-include src/heatstress *.py
8
+ recursive-include tests *.py
9
+ recursive-include tests/data *.tsv *.csv
@@ -0,0 +1,213 @@
1
+ Metadata-Version: 2.4
2
+ Name: heatstress
3
+ Version: 0.1.0
4
+ Summary: Vectorised heat-stress indices: NWS Heat Index, Humidex, Apparent Temperature, wet bulb, WBGT (Liljegren), UTCI, wind chill, risk categories and heatwave detection.
5
+ Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/YusufEminoglu/heatstress_sdk
8
+ Project-URL: Repository, https://github.com/YusufEminoglu/heatstress_sdk
9
+ Project-URL: Issues, https://github.com/YusufEminoglu/heatstress_sdk/issues
10
+ Project-URL: Changelog, https://github.com/YusufEminoglu/heatstress_sdk/blob/main/CHANGELOG.md
11
+ 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
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
21
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
22
+ Classifier: Intended Audience :: Science/Research
23
+ Classifier: Intended Audience :: Developers
24
+ Classifier: Intended Audience :: Healthcare Industry
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.20.0
30
+ Provides-Extra: pandas
31
+ Requires-Dist: pandas>=1.3.0; extra == "pandas"
32
+ Provides-Extra: xarray
33
+ Requires-Dist: xarray>=0.19.0; extra == "xarray"
34
+ Requires-Dist: pandas>=1.3.0; extra == "xarray"
35
+ Provides-Extra: all
36
+ Requires-Dist: pandas>=1.3.0; extra == "all"
37
+ Requires-Dist: xarray>=0.19.0; extra == "all"
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
40
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
41
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
42
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # heatstress
46
+
47
+ Vectorised heat-stress indices for public-health, climate-adaptation and
48
+ occupational-safety analysis: NWS Heat Index, Humidex, Apparent Temperature,
49
+ wet-bulb temperature, WBGT (full Liljegren et al. 2008 model, shade/indoor, ISO 7243,
50
+ BoM approximation), UTCI, Wind Chill, mean radiant temperature, solar position,
51
+ risk categories and heatwave detection, including the Excess Heat Factor.
52
+
53
+ * Pure numpy core; every function takes scalars, numpy arrays, pandas Series or
54
+ xarray DataArrays and returns the same kind of object.
55
+ * Units are explicit: degC by default with a `units="C" | "F" | "K"` switch on
56
+ temperature functions, wind in m/s, pressure in hPa, radiation in W/m2, humidity in %.
57
+ Converters are in `heatstress.units`.
58
+ * Inputs outside a formula's validity range raise a `ValidityRangeWarning`; NaN propagates.
59
+ * Validated against published reference values and independent implementations
60
+ (see [Validation](#validation)).
61
+ * A command-line tool for single values and CSV time series.
62
+
63
+ > **Disclaimer.** heatstress computes screening indices from weather data. It is
64
+ > not medical advice and is not a substitute for on-site measurement, an
65
+ > occupational health programme, clinical judgement or the official warnings of
66
+ > your national meteorological or public-health service.
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ pip install heatstress # numpy only
72
+ pip install "heatstress[pandas]" # + pandas helpers (DataFrames, events_to_frame)
73
+ pip install "heatstress[xarray]" # + xarray/pandas
74
+ ```
75
+
76
+ Python 3.9 or newer.
77
+
78
+ ## Quick start
79
+
80
+ ```python
81
+ import heatstress as hs
82
+
83
+ hs.heat_index(35, 60) # 45.1 degC (NWS algorithm)
84
+ hs.heat_index(95, 50, units="F") # 105.2 degF
85
+ hs.heat_index_category(45.1) # 'danger'
86
+
87
+ hs.humidex(30, dew_point=15) # 34.0
88
+ hs.apparent_temperature(30, 50, wind=2) # 31.6 degC
89
+ hs.wet_bulb_psychrometric(25, 50) # 17.9 degC
90
+ hs.utci(30, tmrt=50, wind=2, rh=50) # 34.6 degC
91
+ hs.utci_category(34.6) # 'strong heat stress'
92
+
93
+ # Outdoor WBGT from standard weather data (Liljegren et al. 2008)
94
+ z = hs.solar_zenith("2026-07-15T10:00Z", lat=38.42, lon=27.14)
95
+ w = hs.wbgt_liljegren_components(t=33, rh=55, wind=1.5, solar=850, zenith=z, pressure=1008)
96
+ w.wbgt, w.globe_temperature, w.natural_wet_bulb # 32.7, 49.7, 27.7 degC
97
+ hs.work_rest_allocation(w.wbgt, "moderate") # ACGIH TLV work-rest guidance
98
+
99
+ # Heatwaves on a daily series
100
+ import pandas as pd
101
+ tmax = pd.Series(..., index=pd.date_range("1991-01-01", periods=..., freq="D"))
102
+ thr = hs.calendar_day_percentile(tmax, tmax.index, q=90, window=15, baseline=(1991, 2020))
103
+ events = hs.heatwave_events(tmax, thr, min_duration=3)
104
+ hs.events_to_frame(events)
105
+
106
+ ehf = hs.excess_heat_factor(tmean_daily) # Nairn & Fawcett (2015)
107
+ hs.ehf_severity(ehf) # none / low-intensity / severe / extreme
108
+ ```
109
+
110
+ ## Indices
111
+
112
+ | Index | Function | Inputs | Source | Validity / notes |
113
+ |---|---|---|---|---|
114
+ | 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 |
115
+ | Rothfusz regression only | `rothfusz_regression` | T, RH | Rothfusz (1990) NWS SR 90-23 | For comparison only |
116
+ | Humidex | `humidex` | T, RH or dew point | Masterton & Richardson (1979), Environment Canada | MSC vapour-pressure form |
117
+ | Apparent Temperature | `apparent_temperature` | T, RH, wind (10 m), optional net radiation | Steadman (1994), Australian Bureau of Meteorology | Shade and radiation forms |
118
+ | 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) |
119
+ | 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 |
120
+ | 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 |
121
+ | 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 |
122
+ | WBGT from measurements | `wbgt_from_measurements` | Tnwb, Tg, (Ta) | ISO 7243:2017 | Indoor and outdoor formulas |
123
+ | WBGT approximation | `wbgt_bom` | T, RH | Australian Bureau of Meteorology | **Approximation**: assumes moderately high radiation and light wind; can be off by several degrees |
124
+ | 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 |
125
+ | 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 |
126
+ | Feels like | `feels_like` | T, RH, wind | NWS convention | Wind chill / heat index / air temperature |
127
+ | 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 |
128
+ | Solar position | `solar_position`, `solar_zenith` | time, lat, lon | NOAA solar calculator (Meeus 1991) | Geometric zenith, no refraction |
129
+ | 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 |
130
+
131
+ ### Risk categories
132
+
133
+ | Function | Scale |
134
+ |---|---|
135
+ | `heat_index_category` | NWS: caution 80-89 degF, extreme caution 90-102, danger 103-124, extreme danger >= 125 |
136
+ | `humidex_category` | Environment Canada: < 30, 30-39, 40-45, >= 46 |
137
+ | `utci_category` | 10 UTCI stress classes from extreme cold (< -40) to extreme heat stress (> 46) |
138
+ | `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 |
139
+ | `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) |
140
+ | `ehf_severity` | Nairn & Fawcett (2015): low-intensity, severe (EHF >= EHF85), extreme (EHF >= 3 EHF85) |
141
+
142
+ ### Heatwave detection
143
+
144
+ * `percentile_threshold` and `calendar_day_percentile` (centred moving window of
145
+ calendar days, optional baseline years, 29 February merged with 28 February).
146
+ * `heatwave_mask`, `consecutive_runs` and `heatwave_events` find spells of at
147
+ least `min_duration` consecutive days above a scalar or day-varying threshold and
148
+ report start, end, duration, peak and exceedance statistics;
149
+ `events_to_frame` converts them to a DataFrame.
150
+ * `excess_heat_factor`: `EHI_sig = T3 - T95`, `EHI_accl = T3 - T30prev`,
151
+ `EHF = EHI_sig x max(1, EHI_accl)`, attributed to the first day of the three-day
152
+ window; `T95` from a reference period (`t95=` or `reference=`).
153
+
154
+ ## Command line
155
+
156
+ ```console
157
+ $ heatstress hi 35 --rh 60
158
+ heat_index: 45.1 degC
159
+ category: danger
160
+
161
+ $ heatstress wbgt 33 --rh 55 --wind 1.5 --solar 850 --time 2026-07-15T10:00Z --lat 38.42 --lon 27.14
162
+ $ heatstress utci 30 --tmrt 55 --wind 2 --rh 50
163
+ $ heatstress windchill 0 --wind 15 --wind-units mph --units F
164
+ $ heatstress series weather.csv -o indices.csv --lat 38.42 --lon 27.14
165
+ ```
166
+
167
+ Every value command accepts `--units C|F|K` and `--json`; wind accepts
168
+ `--wind-units ms|kmh|mph|kn`. `heatstress series` reads a CSV, recognises common
169
+ column names (`time`, `t`/`temperature`, `rh`, `td`, `wind`, `solar`/`ghi`,
170
+ `pressure`, `tmrt`, `tg`, `zenith`, `lat`, `lon`), and appends every index that
171
+ the columns allow (UTCI needs `tmrt` or a globe temperature; the Liljegren WBGT
172
+ needs wind, solar and either a zenith column or time plus coordinates). The same
173
+ logic is available in Python as `hs.compute_indices(df)`.
174
+
175
+ ## Validation
176
+
177
+ All comparisons are in the test suite (`tests/test_reference_values.py`) unless noted.
178
+
179
+ | Check | Reference | Result |
180
+ |---|---|---|
181
+ | 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) |
182
+ | 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) |
183
+ | UTCI vs independent code | thermofeel 2.3.0 `calculate_utci_polynomial` | max 1e-12 degC (scratch cross-check) |
184
+ | 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 |
185
+ | Heat index vs independent code | thermofeel `calculate_heat_index_adjusted`, 5000 random cases T <= 43 degC | max 0.08 degC (scratch cross-check) |
186
+ | 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 |
187
+ | 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 % |
188
+ | Wind chill | NWS wind chill chart (6 cells); thermofeel | within 0.6 degF; identical to thermofeel |
189
+ | Humidex | Environment Canada example 30 degC / Td 15 degC -> 34 | exact; thermofeel max 0.04 |
190
+ | Apparent temperature | thermofeel `calculate_apparent_temperature` | identical (scratch cross-check) |
191
+ | Mean radiant temperature | ISO 7726 formula; thermofeel | max 0.06 degC (thermofeel uses 273.15 K in place of ISO's 273) |
192
+ | 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 |
193
+ | Excess Heat Factor | thermofeel `thermofeel.excess_heat` with pandas rolling windows | max 4e-11 |
194
+
195
+ ## Units and conventions
196
+
197
+ * Temperatures: degC unless `units="F"` or `"K"` (applies to all temperature
198
+ inputs and outputs of that call).
199
+ * Relative humidity in %, wind speed in m/s (10 m for Apparent Temperature,
200
+ Wind Chill and UTCI; 2 m for Liljegren unless `wind_height` is given), pressure
201
+ in hPa (station pressure), solar radiation in W/m2 (global horizontal).
202
+ * Results come back as Python floats for scalar input, numpy arrays for array
203
+ input, and pandas Series / xarray DataArrays (same index or coordinates) when
204
+ the first array argument is one.
205
+
206
+ ## Citation
207
+
208
+ See `CITATION.cff`. Please also cite the original authors of each index you use.
209
+
210
+ ## License
211
+
212
+ MIT (c) 2026 Yusuf Eminoğlu. The UTCI test data shipped in `tests/data` are by
213
+ Broede et al., CC BY 4.0.
@@ -0,0 +1,169 @@
1
+ # heatstress
2
+
3
+ Vectorised heat-stress indices for public-health, climate-adaptation and
4
+ occupational-safety analysis: NWS Heat Index, Humidex, Apparent Temperature,
5
+ wet-bulb temperature, WBGT (full Liljegren et al. 2008 model, shade/indoor, ISO 7243,
6
+ BoM approximation), UTCI, Wind Chill, mean radiant temperature, solar position,
7
+ risk categories and heatwave detection, including the Excess Heat Factor.
8
+
9
+ * Pure numpy core; every function takes scalars, numpy arrays, pandas Series or
10
+ xarray DataArrays and returns the same kind of object.
11
+ * Units are explicit: degC by default with a `units="C" | "F" | "K"` switch on
12
+ temperature functions, wind in m/s, pressure in hPa, radiation in W/m2, humidity in %.
13
+ Converters are in `heatstress.units`.
14
+ * Inputs outside a formula's validity range raise a `ValidityRangeWarning`; NaN propagates.
15
+ * Validated against published reference values and independent implementations
16
+ (see [Validation](#validation)).
17
+ * A command-line tool for single values and CSV time series.
18
+
19
+ > **Disclaimer.** heatstress computes screening indices from weather data. It is
20
+ > not medical advice and is not a substitute for on-site measurement, an
21
+ > occupational health programme, clinical judgement or the official warnings of
22
+ > your national meteorological or public-health service.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ pip install heatstress # numpy only
28
+ pip install "heatstress[pandas]" # + pandas helpers (DataFrames, events_to_frame)
29
+ pip install "heatstress[xarray]" # + xarray/pandas
30
+ ```
31
+
32
+ Python 3.9 or newer.
33
+
34
+ ## Quick start
35
+
36
+ ```python
37
+ import heatstress as hs
38
+
39
+ hs.heat_index(35, 60) # 45.1 degC (NWS algorithm)
40
+ hs.heat_index(95, 50, units="F") # 105.2 degF
41
+ hs.heat_index_category(45.1) # 'danger'
42
+
43
+ hs.humidex(30, dew_point=15) # 34.0
44
+ hs.apparent_temperature(30, 50, wind=2) # 31.6 degC
45
+ hs.wet_bulb_psychrometric(25, 50) # 17.9 degC
46
+ hs.utci(30, tmrt=50, wind=2, rh=50) # 34.6 degC
47
+ hs.utci_category(34.6) # 'strong heat stress'
48
+
49
+ # Outdoor WBGT from standard weather data (Liljegren et al. 2008)
50
+ z = hs.solar_zenith("2026-07-15T10:00Z", lat=38.42, lon=27.14)
51
+ w = hs.wbgt_liljegren_components(t=33, rh=55, wind=1.5, solar=850, zenith=z, pressure=1008)
52
+ w.wbgt, w.globe_temperature, w.natural_wet_bulb # 32.7, 49.7, 27.7 degC
53
+ hs.work_rest_allocation(w.wbgt, "moderate") # ACGIH TLV work-rest guidance
54
+
55
+ # Heatwaves on a daily series
56
+ import pandas as pd
57
+ tmax = pd.Series(..., index=pd.date_range("1991-01-01", periods=..., freq="D"))
58
+ thr = hs.calendar_day_percentile(tmax, tmax.index, q=90, window=15, baseline=(1991, 2020))
59
+ events = hs.heatwave_events(tmax, thr, min_duration=3)
60
+ hs.events_to_frame(events)
61
+
62
+ ehf = hs.excess_heat_factor(tmean_daily) # Nairn & Fawcett (2015)
63
+ hs.ehf_severity(ehf) # none / low-intensity / severe / extreme
64
+ ```
65
+
66
+ ## Indices
67
+
68
+ | Index | Function | Inputs | Source | Validity / notes |
69
+ |---|---|---|---|---|
70
+ | 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 |
71
+ | Rothfusz regression only | `rothfusz_regression` | T, RH | Rothfusz (1990) NWS SR 90-23 | For comparison only |
72
+ | Humidex | `humidex` | T, RH or dew point | Masterton & Richardson (1979), Environment Canada | MSC vapour-pressure form |
73
+ | Apparent Temperature | `apparent_temperature` | T, RH, wind (10 m), optional net radiation | Steadman (1994), Australian Bureau of Meteorology | Shade and radiation forms |
74
+ | 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) |
75
+ | 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 |
76
+ | 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 |
77
+ | 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 |
78
+ | WBGT from measurements | `wbgt_from_measurements` | Tnwb, Tg, (Ta) | ISO 7243:2017 | Indoor and outdoor formulas |
79
+ | WBGT approximation | `wbgt_bom` | T, RH | Australian Bureau of Meteorology | **Approximation**: assumes moderately high radiation and light wind; can be off by several degrees |
80
+ | 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 |
81
+ | 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 |
82
+ | Feels like | `feels_like` | T, RH, wind | NWS convention | Wind chill / heat index / air temperature |
83
+ | 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 |
84
+ | Solar position | `solar_position`, `solar_zenith` | time, lat, lon | NOAA solar calculator (Meeus 1991) | Geometric zenith, no refraction |
85
+ | 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 |
86
+
87
+ ### Risk categories
88
+
89
+ | Function | Scale |
90
+ |---|---|
91
+ | `heat_index_category` | NWS: caution 80-89 degF, extreme caution 90-102, danger 103-124, extreme danger >= 125 |
92
+ | `humidex_category` | Environment Canada: < 30, 30-39, 40-45, >= 46 |
93
+ | `utci_category` | 10 UTCI stress classes from extreme cold (< -40) to extreme heat stress (> 46) |
94
+ | `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 |
95
+ | `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) |
96
+ | `ehf_severity` | Nairn & Fawcett (2015): low-intensity, severe (EHF >= EHF85), extreme (EHF >= 3 EHF85) |
97
+
98
+ ### Heatwave detection
99
+
100
+ * `percentile_threshold` and `calendar_day_percentile` (centred moving window of
101
+ calendar days, optional baseline years, 29 February merged with 28 February).
102
+ * `heatwave_mask`, `consecutive_runs` and `heatwave_events` find spells of at
103
+ least `min_duration` consecutive days above a scalar or day-varying threshold and
104
+ report start, end, duration, peak and exceedance statistics;
105
+ `events_to_frame` converts them to a DataFrame.
106
+ * `excess_heat_factor`: `EHI_sig = T3 - T95`, `EHI_accl = T3 - T30prev`,
107
+ `EHF = EHI_sig x max(1, EHI_accl)`, attributed to the first day of the three-day
108
+ window; `T95` from a reference period (`t95=` or `reference=`).
109
+
110
+ ## Command line
111
+
112
+ ```console
113
+ $ heatstress hi 35 --rh 60
114
+ heat_index: 45.1 degC
115
+ category: danger
116
+
117
+ $ heatstress wbgt 33 --rh 55 --wind 1.5 --solar 850 --time 2026-07-15T10:00Z --lat 38.42 --lon 27.14
118
+ $ heatstress utci 30 --tmrt 55 --wind 2 --rh 50
119
+ $ heatstress windchill 0 --wind 15 --wind-units mph --units F
120
+ $ heatstress series weather.csv -o indices.csv --lat 38.42 --lon 27.14
121
+ ```
122
+
123
+ Every value command accepts `--units C|F|K` and `--json`; wind accepts
124
+ `--wind-units ms|kmh|mph|kn`. `heatstress series` reads a CSV, recognises common
125
+ column names (`time`, `t`/`temperature`, `rh`, `td`, `wind`, `solar`/`ghi`,
126
+ `pressure`, `tmrt`, `tg`, `zenith`, `lat`, `lon`), and appends every index that
127
+ the columns allow (UTCI needs `tmrt` or a globe temperature; the Liljegren WBGT
128
+ needs wind, solar and either a zenith column or time plus coordinates). The same
129
+ logic is available in Python as `hs.compute_indices(df)`.
130
+
131
+ ## Validation
132
+
133
+ All comparisons are in the test suite (`tests/test_reference_values.py`) unless noted.
134
+
135
+ | Check | Reference | Result |
136
+ |---|---|---|
137
+ | 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) |
138
+ | 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) |
139
+ | UTCI vs independent code | thermofeel 2.3.0 `calculate_utci_polynomial` | max 1e-12 degC (scratch cross-check) |
140
+ | 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 |
141
+ | Heat index vs independent code | thermofeel `calculate_heat_index_adjusted`, 5000 random cases T <= 43 degC | max 0.08 degC (scratch cross-check) |
142
+ | 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 |
143
+ | 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 % |
144
+ | Wind chill | NWS wind chill chart (6 cells); thermofeel | within 0.6 degF; identical to thermofeel |
145
+ | Humidex | Environment Canada example 30 degC / Td 15 degC -> 34 | exact; thermofeel max 0.04 |
146
+ | Apparent temperature | thermofeel `calculate_apparent_temperature` | identical (scratch cross-check) |
147
+ | Mean radiant temperature | ISO 7726 formula; thermofeel | max 0.06 degC (thermofeel uses 273.15 K in place of ISO's 273) |
148
+ | 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 |
149
+ | Excess Heat Factor | thermofeel `thermofeel.excess_heat` with pandas rolling windows | max 4e-11 |
150
+
151
+ ## Units and conventions
152
+
153
+ * Temperatures: degC unless `units="F"` or `"K"` (applies to all temperature
154
+ inputs and outputs of that call).
155
+ * Relative humidity in %, wind speed in m/s (10 m for Apparent Temperature,
156
+ Wind Chill and UTCI; 2 m for Liljegren unless `wind_height` is given), pressure
157
+ in hPa (station pressure), solar radiation in W/m2 (global horizontal).
158
+ * Results come back as Python floats for scalar input, numpy arrays for array
159
+ input, and pandas Series / xarray DataArrays (same index or coordinates) when
160
+ the first array argument is one.
161
+
162
+ ## Citation
163
+
164
+ See `CITATION.cff`. Please also cite the original authors of each index you use.
165
+
166
+ ## License
167
+
168
+ MIT (c) 2026 Yusuf Eminoğlu. The UTCI test data shipped in `tests/data` are by
169
+ Broede et al., CC BY 4.0.
@@ -0,0 +1,135 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "heatstress"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Yusuf Eminoğlu", email = "yusufeminoglu@gmail.com" }
10
+ ]
11
+ description = "Vectorised heat-stress indices: NWS Heat Index, Humidex, Apparent Temperature, wet bulb, WBGT (Liljegren), UTCI, wind chill, risk categories and heatwave detection."
12
+ keywords = [
13
+ "heat-stress",
14
+ "heat-index",
15
+ "wbgt",
16
+ "utci",
17
+ "humidex",
18
+ "apparent-temperature",
19
+ "wet-bulb",
20
+ "wind-chill",
21
+ "heatwave",
22
+ "excess-heat-factor",
23
+ "thermal-comfort",
24
+ "biometeorology",
25
+ "occupational-safety",
26
+ "public-health",
27
+ "climate-adaptation",
28
+ ]
29
+ readme = "README.md"
30
+ license = "MIT"
31
+ license-files = ["LICENSE"]
32
+ requires-python = ">=3.9"
33
+ classifiers = [
34
+ "Development Status :: 4 - Beta",
35
+ "Programming Language :: Python :: 3",
36
+ "Programming Language :: Python :: 3.9",
37
+ "Programming Language :: Python :: 3.10",
38
+ "Programming Language :: Python :: 3.11",
39
+ "Programming Language :: Python :: 3.12",
40
+ "Programming Language :: Python :: 3.13",
41
+ "Operating System :: OS Independent",
42
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
43
+ "Topic :: Scientific/Engineering :: Medical Science Apps.",
44
+ "Intended Audience :: Science/Research",
45
+ "Intended Audience :: Developers",
46
+ "Intended Audience :: Healthcare Industry",
47
+ "Typing :: Typed",
48
+ ]
49
+ dependencies = [
50
+ "numpy>=1.20.0",
51
+ ]
52
+
53
+ [project.optional-dependencies]
54
+ pandas = [
55
+ "pandas>=1.3.0",
56
+ ]
57
+ xarray = [
58
+ "xarray>=0.19.0",
59
+ "pandas>=1.3.0",
60
+ ]
61
+ all = [
62
+ "pandas>=1.3.0",
63
+ "xarray>=0.19.0",
64
+ ]
65
+ dev = [
66
+ "pytest>=7.0.0",
67
+ "pytest-cov>=4.0.0",
68
+ "ruff>=0.1.0",
69
+ "mypy>=1.0.0",
70
+ ]
71
+
72
+ [project.scripts]
73
+ heatstress = "heatstress.cli:main"
74
+
75
+ [project.urls]
76
+ Homepage = "https://github.com/YusufEminoglu/heatstress_sdk"
77
+ Repository = "https://github.com/YusufEminoglu/heatstress_sdk"
78
+ Issues = "https://github.com/YusufEminoglu/heatstress_sdk/issues"
79
+ Changelog = "https://github.com/YusufEminoglu/heatstress_sdk/blob/main/CHANGELOG.md"
80
+
81
+ [tool.setuptools]
82
+ package-dir = {"" = "src"}
83
+
84
+ [tool.setuptools.packages.find]
85
+ where = ["src"]
86
+
87
+ [tool.setuptools.package-data]
88
+ heatstress = ["py.typed"]
89
+
90
+ [tool.ruff]
91
+ line-length = 100
92
+ target-version = "py39"
93
+ exclude = [
94
+ "scratch",
95
+ ".venv",
96
+ "build",
97
+ "dist",
98
+ ]
99
+
100
+ [tool.ruff.lint]
101
+ select = ["E", "F", "W", "I", "B", "C4"]
102
+ ignore = ["E501"]
103
+
104
+ [tool.mypy]
105
+ files = ["src"]
106
+ warn_unused_ignores = true
107
+
108
+ [[tool.mypy.overrides]]
109
+ module = [
110
+ "pandas.*",
111
+ "xarray.*",
112
+ ]
113
+ ignore_missing_imports = true
114
+
115
+ [tool.pytest.ini_options]
116
+ testpaths = ["tests"]
117
+ # Lets `pytest` run straight from a clone, without an editable install first.
118
+ pythonpath = ["src"]
119
+ filterwarnings = [
120
+ "ignore::DeprecationWarning",
121
+ "ignore::PendingDeprecationWarning",
122
+ "ignore::FutureWarning",
123
+ ]
124
+
125
+ [tool.coverage.run]
126
+ source = ["heatstress"]
127
+ branch = true
128
+
129
+ [tool.coverage.report]
130
+ show_missing = true
131
+ fail_under = 85
132
+ exclude_lines = [
133
+ "pragma: no cover",
134
+ "if __name__ == .__main__.:",
135
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+