Skip to content

Commit d6f25e5

Browse files
sunt05claude
andauthored
feat: add wind speed height correction to read_epw (GH #149) (#1016)
* feat: add wind speed height correction to read_epw utility (GH #149) Add optional target_height and z0m parameters to read_epw() to enable logarithmic wind profile correction when EPW data must be extrapolated to different measurement heights. EPW files standardise wind speed at 10m, but SUEWS forcing can use any height. This enhancement allows users to: - Keep default behaviour (no correction) for backward compatibility - Apply log-law wind profile correction when target height differs from 10m - Emit warning about neutral atmosphere assumption when correcting Also add comprehensive documentation explaining EPW height assumptions and troubleshooting guide for common wind speed discrepancies. Fixes GH #149 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add input validation and floating-point comparison to read_epw - Add explicit validation for z0m and target_height (must be positive) - Use np.isclose() for floating-point comparison to avoid edge cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: remove duplicate icon files from site/brand/assets/icon Remove icon files that are duplicates of the disk-style icons. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0f49b61 commit d6f25e5

7 files changed

Lines changed: 240 additions & 43 deletions

File tree

docs/source/inputs/forcing-data.rst

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,115 @@ For model-level data or spatial grids, use the gridded dataset:
286286

287287
See :func:`~supy.util.gen_forcing_era5` API documentation for all options.
288288

289+
Using EPW Weather Files
290+
-----------------------
291+
292+
EPW (EnergyPlus Weather) files are a common format for building energy simulation, often derived from Typical Meteorological Year (TMY) data. SUEWS can read EPW data via the :func:`~supy.util.read_epw` utility function.
293+
294+
**Important: Measurement Height Assumptions**
295+
296+
EPW files follow standard meteorological station conventions with fixed measurement heights:
297+
298+
.. list-table::
299+
:header-rows: 1
300+
:widths: 30 30 40
301+
302+
* - Variable
303+
- EPW Height
304+
- SUEWS Forcing Variable
305+
* - Wind speed
306+
- 10 m agl
307+
- U
308+
* - Air temperature
309+
- 2 m agl
310+
- Tair
311+
* - Relative humidity
312+
- 2 m agl
313+
- RH
314+
315+
**Correct Configuration for EPW Data**
316+
317+
When using EPW data, set the forcing height to match the wind speed measurement:
318+
319+
.. code-block:: yaml
320+
321+
sites:
322+
- name: "MySite"
323+
properties:
324+
z: 10.0 # Must be 10 m to match EPW wind speed height
325+
326+
.. warning::
327+
328+
Using EPW data with a different forcing height (e.g., ``z: 50``) will cause incorrect wind profile calculations, as SUEWS assumes all forcing data originate from the specified height.
329+
330+
**Basic Workflow**
331+
332+
.. code-block:: python
333+
334+
import supy as sp
335+
import pandas as pd
336+
from pathlib import Path
337+
338+
# 1. Read EPW file (wind speed at 10 m by default)
339+
df_epw = sp.util.read_epw(Path("weather.epw"))
340+
341+
# 2. Extract and rename columns for SUEWS forcing
342+
df_forcing = pd.DataFrame({
343+
'U': df_epw['Wind Speed'],
344+
'Tair': df_epw['Dry Bulb Temperature'],
345+
'RH': df_epw['Relative Humidity'],
346+
'pres': df_epw['Atmospheric Station Pressure'] / 1000, # Pa to kPa
347+
'kdown': df_epw['Global Horizontal Radiation'],
348+
'ldown': df_epw['Horizontal Infrared Radiation Intensity'],
349+
'rain': df_epw['Liquid Precipitation Depth'],
350+
}, index=df_epw.index)
351+
352+
# 3. Fill required time columns
353+
df_forcing['iy'] = df_forcing.index.year
354+
df_forcing['id'] = df_forcing.index.dayofyear
355+
df_forcing['it'] = df_forcing.index.hour
356+
df_forcing['imin'] = df_forcing.index.minute
357+
358+
**Wind Speed Height Correction**
359+
360+
If you need EPW wind speed at a different height (e.g., to match flux tower measurements at 50 m), use the ``target_height`` parameter:
361+
362+
.. code-block:: python
363+
364+
# Read EPW and extrapolate wind speed from 10 m to 50 m
365+
df_epw = sp.util.read_epw(
366+
Path("weather.epw"),
367+
target_height=50.0, # Target height [m]
368+
z0m=0.5 # Urban roughness length [m]
369+
)
370+
371+
This applies a logarithmic wind profile correction assuming neutral atmospheric conditions.
372+
373+
.. note::
374+
375+
The log-law correction assumes neutral atmospheric stability. Under strongly stable or unstable conditions, actual wind profiles may differ significantly. For most applications using EPW data, setting ``z=10`` in your site configuration is the simpler and recommended approach.
376+
377+
**Comparison with ERA5**
378+
379+
Unlike EPW files with fixed heights, ERA5 forcing data from :func:`~supy.util.gen_forcing_era5` are extrapolated to a user-specified height (default 100 m) using Monin-Obukhov Similarity Theory.
380+
381+
.. list-table::
382+
:header-rows: 1
383+
:widths: 25 35 40
384+
385+
* - Data Source
386+
- Wind Speed Height
387+
- Recommended ``z`` Setting
388+
* - EPW files
389+
- Fixed at 10 m
390+
- ``z: 10``
391+
* - ERA5 (timeseries)
392+
- Extrapolated to ``hgt_agl_diag`` (default 100 m)
393+
- Match ``hgt_agl_diag`` value
394+
* - Flux tower
395+
- Tower-specific
396+
- Actual measurement height
397+
289398
Data Preparation Tips
290399
---------------------
291400

docs/source/troubleshooting.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,41 @@ Please check the following:
188188
A general rule of thumb is to use the ``load_SampleData`` to generate the initial model states from the sample data shipped by SuPy.
189189

190190

191+
Wind speed seems incorrect when using EPW data
192+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193+
194+
**Symptom**: Wind-related outputs (turbulent fluxes, roughness parameters, u*) appear unrealistic when using EPW weather files.
195+
196+
**Likely cause**: Mismatch between EPW wind speed measurement height (10 m) and configured forcing height (``z``).
197+
198+
EPW files contain wind speed measured at 10 m above ground level. If your site configuration uses a different forcing height (e.g., ``z: 50``), SUEWS will incorrectly assume the EPW wind data originated from 50 m, leading to erroneous friction velocity and flux calculations.
199+
200+
**Solution**: Ensure your site configuration sets ``z: 10`` when using EPW data:
201+
202+
.. code-block:: yaml
203+
204+
sites:
205+
- name: "MySite"
206+
properties:
207+
z: 10.0 # Match EPW standard wind measurement height
208+
209+
Alternatively, use the ``target_height`` parameter in :func:`~supy.util.read_epw` to extrapolate wind speed to your desired forcing height:
210+
211+
.. code-block:: python
212+
213+
import supy as sp
214+
from pathlib import Path
215+
216+
# Extrapolate EPW wind speed from 10 m to 50 m
217+
df_epw = sp.util.read_epw(
218+
Path("weather.epw"),
219+
target_height=50.0,
220+
z0m=0.5 # roughness length for urban areas
221+
)
222+
223+
See :ref:`met_forcing` for detailed guidance on EPW data usage.
224+
225+
191226
YAML Configuration Validation Errors
192227
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193228

-21.9 KB
Binary file not shown.
-21.6 KB
Binary file not shown.

site/brand/assets/icon/suews-icon-dark.svg

Lines changed: 0 additions & 19 deletions
This file was deleted.

site/brand/assets/icon/suews-icon-light.svg

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/supy/util/_tmy.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from pathlib import Path
23
from typing import Optional, Tuple, Union
34

@@ -6,6 +7,9 @@
67

78
from .._post import resample_output
89

10+
# Logger for this module
11+
logger = logging.getLogger(__name__)
12+
913

1014
#################################################################
1115
# generate TMY dataframe from supy results
@@ -313,19 +317,91 @@ def conv_0to24(df_TMY):
313317

314318

315319
# function to read in EPW file
316-
def read_epw(path_epw: Path) -> pd.DataFrame:
317-
"""Read in `epw` file as a DataFrame
320+
def read_epw(
321+
path_epw: Path,
322+
target_height: float = 10.0,
323+
z0m: float = 0.1,
324+
) -> pd.DataFrame:
325+
"""Read in EPW (EnergyPlus Weather) file as a DataFrame.
318326
319327
Parameters
320328
----------
321329
path_epw : Path
322-
path to `epw` file
330+
Path to EPW file.
331+
target_height : float, optional
332+
Target height for wind speed extrapolation [m]. EPW files contain
333+
wind speed at 10 m agl. If target_height differs from 10 m, the
334+
wind speed will be adjusted using a logarithmic wind profile.
335+
Default is 10.0 (no correction applied).
336+
z0m : float, optional
337+
Roughness length for momentum [m], used in wind profile correction.
338+
Typical values: 0.01 (open water), 0.1 (grassland), 0.5-2.0 (urban).
339+
Default is 0.1.
323340
324341
Returns
325342
-------
326-
df_tmy: pd.DataFrame
327-
TMY results of `epw` file
343+
df_tmy : pd.DataFrame
344+
DataFrame containing weather data with columns named according
345+
to EPW standard variable names.
346+
347+
Notes
348+
-----
349+
**Measurement Height Assumptions**
350+
351+
EPW files follow standard meteorological station conventions:
352+
353+
- **Wind Speed**: 10 m above ground level (agl)
354+
- **Temperature and Humidity**: 2 m agl (screen height)
355+
356+
When using EPW data with SUEWS, ensure the forcing height parameter
357+
``z`` in your site configuration matches these heights. For EPW data,
358+
set ``z = 10`` to match the wind speed measurement height.
359+
360+
**Wind Speed Height Correction**
361+
362+
If ``target_height != 10.0``, the wind speed is adjusted using the
363+
logarithmic wind profile (assuming neutral atmospheric conditions):
364+
365+
.. math::
366+
367+
U(z_2) = U(z_1) \\frac{\\ln((z_2 + z_0) / z_0)}{\\ln((z_1 + z_0) / z_0)}
368+
369+
where :math:`z_1 = 10` m (EPW height), :math:`z_2` is the target height,
370+
and :math:`z_0` is the roughness length.
371+
372+
.. warning::
373+
374+
The log-law profile assumes neutral atmospheric stability. Under
375+
strongly stable or unstable conditions, actual wind profiles may
376+
differ significantly from this approximation.
377+
378+
See Also
379+
--------
380+
gen_epw : Generate EPW file from SUEWS simulation output.
381+
supy.util.gen_forcing_era5 : Generate forcing from ERA5 (extrapolated
382+
to user-specified height).
383+
384+
Examples
385+
--------
386+
>>> import supy as sp
387+
>>> from pathlib import Path
388+
>>>
389+
>>> # Read EPW file without height correction (default)
390+
>>> df_epw = sp.util.read_epw(Path("weather.epw"))
391+
>>>
392+
>>> # Read EPW file and extrapolate wind speed to 50 m
393+
>>> df_epw = sp.util.read_epw(
394+
... Path("weather.epw"),
395+
... target_height=50.0,
396+
... z0m=0.5 # urban roughness length
397+
... )
328398
"""
399+
# Input validation
400+
if z0m <= 0:
401+
raise ValueError(f"z0m must be positive, got {z0m}")
402+
if target_height <= 0:
403+
raise ValueError(f"target_height must be positive, got {target_height}")
404+
329405
df_tmy = pd.read_csv(path_epw, skiprows=8, sep=",", header=None)
330406
df_tmy.columns = [x.strip() for x in header_EPW.split("\n")[1:-1]]
331407
df_tmy["DateTime"] = pd.to_datetime(
@@ -336,6 +412,21 @@ def read_epw(path_epw: Path) -> pd.DataFrame:
336412
+ pd.to_timedelta(df_tmy["Hour"], unit="h")
337413
)
338414
df_tmy = df_tmy.set_index("DateTime")
415+
416+
# Apply wind speed height correction if target height differs from EPW standard (10 m)
417+
epw_height = 10.0
418+
if not np.isclose(target_height, epw_height):
419+
logger.warning(
420+
f"Applying wind speed height correction from {epw_height}m to {target_height}m "
421+
f"using log-law profile (assumes neutral atmospheric conditions). "
422+
f"This approximation may be less accurate under strongly stable or unstable conditions."
423+
)
424+
# Log-law wind profile correction (neutral conditions)
425+
correction_factor = np.log((target_height + z0m) / z0m) / np.log(
426+
(epw_height + z0m) / z0m
427+
)
428+
df_tmy["Wind Speed"] = df_tmy["Wind Speed"] * correction_factor
429+
339430
return df_tmy
340431

341432

0 commit comments

Comments
 (0)