Wind energy — hub-height wind resource¶
Wind farm developers need long-term mean wind speed at hub height (typically 80–120 m above ground). ERA5 exposes 100 m wind components directly — enough to compute hub-height wind speed without extrapolation.
Domain context. A turbine's energy yield scales with the cube of wind speed:
$$ P = \tfrac{1}{2}\rho A C_p v^3 $$
where $\rho$ is air density (~1.225 kg/m³), $A$ is rotor swept area, $C_p$ is the power coefficient (max Betz limit 16/27 ≈ 0.593, real turbines reach 0.40–0.45), and $v$ is wind speed. The right metric for siting is the wind power density $WPD = \frac{1}{2}\rho \langle v^3 \rangle$ — note the cube before averaging.
Step 1 — pull a year of monthly 100 m wind components¶
Box: 1° around Cuxhaven, Germany (53°–54°N, 8°–9°E) — North Sea
coast, prime onshore wind territory. We need both u and v to
reconstruct the speed magnitude $|\mathbf{v}| = \sqrt{u^2 + v^2}$.
from pathlib import Path
from earthlens import EarthLens, AggregationConfig
OUT = Path("data/era5-wind-cuxhaven")
OUT.mkdir(parents=True, exist_ok=True)
earthlens = EarthLens(
data_source="ecmwf",
temporal_resolution="monthly",
start="2022-01-01",
end="2022-12-01",
variables={
"reanalysis-era5-single-levels-monthly-means": [
"100m-u-component-of-wind",
"100m-v-component-of-wind",
],
},
lat_lim=[53.0, 54.0],
lon_lim=[8.0, 9.0],
path=str(OUT),
)
earthlens.download(aggregate=AggregationConfig(freq="1MS", op="auto"))
2026-05-10 01:45:45.057 | INFO | earthlens.ecmwf.backend:download:536 - Download ECMWF reanalysis-era5-single-levels-monthly-means/100m-u-component-of-wind data for period 2022-01-01 00:00:00 till 2022-12-01 00:00:00
2026-05-10 01:45:45.756 | INFO | earthlens.ecmwf.backend:_api:724 - Requesting reanalysis-era5-single-levels-monthly-means from CDS; this may take several minutes
2026-05-10 01:45:46,310 INFO Request ID is 6ae625ac-75e9-43ae-bb73-4ffba92a81d2
2026-05-10 01:45:46,373 INFO status has been updated to accepted
2026-05-10 01:46:10,256 INFO status has been updated to running
2026-05-10 01:46:21,704 INFO status has been updated to successful
2026-05-10 01:46:22 | INFO | pyramids.base.config | Logging is configured.
2026-05-10 01:46:23.310 | INFO | earthlens.ecmwf.backend:download:536 - Download ECMWF reanalysis-era5-single-levels-monthly-means/100m-v-component-of-wind data for period 2022-01-01 00:00:00 till 2022-12-01 00:00:00
2026-05-10 01:46:23.311 | INFO | earthlens.ecmwf.backend:_api:724 - Requesting reanalysis-era5-single-levels-monthly-means from CDS; this may take several minutes
2026-05-10 01:46:23,525 INFO Request ID is 7f49d7a9-8073-473a-9c6b-bb7717a2bf95
2026-05-10 01:46:23 | INFO | ecmwf.datastores.legacy_client | Request ID is 7f49d7a9-8073-473a-9c6b-bb7717a2bf95
2026-05-10 01:46:23,608 INFO status has been updated to accepted
2026-05-10 01:46:23 | INFO | ecmwf.datastores.legacy_client | status has been updated to accepted
2026-05-10 01:46:56,695 INFO status has been updated to successful
2026-05-10 01:46:56 | INFO | ecmwf.datastores.legacy_client | status has been updated to successful
2026-05-10 01:46:56 | INFO | multiurl.base | Downloading https://object-store.os-api.cci2.ecmwf.int:443/cci2-prod-cache-2/2026-05-09/9aaf703c4472b2c344b37c54a297dc0e.nc
2026-05-10 01:46:57.672 | INFO | earthlens.ecmwf.backend:download:575 - ECMWF download summary: all 2 variables succeeded ([('reanalysis-era5-single-levels-monthly-means', '100m-u-component-of-wind'), ('reanalysis-era5-single-levels-monthly-means', '100m-v-component-of-wind')])
Step 2 — compute monthly mean wind speed¶
Wind components are state variables (instantaneous m/s); auto →
mean. The monthly GeoTIFFs carry the time-mean components, from
which we get the mean-of-magnitudes (different from the magnitude of
the mean — careful!).
import numpy as np
import pandas as pd
from pyramids.dataset import Dataset
agg = OUT / "aggregated"
u = np.stack([
Dataset.read_file(str(p)).read_array()
for p in sorted(agg.glob("100m_u_component_of_wind_1MS_*.tif"))
])
v = np.stack([
Dataset.read_file(str(p)).read_array()
for p in sorted(agg.glob("100m_v_component_of_wind_1MS_*.tif"))
])
speed = np.sqrt(u**2 + v**2) # (month, lat, lon) — but built from monthly-mean components
site_speed = np.nanmean(speed, axis=(1, 2))
rho = 1.225 # kg/m^3, sea level
wpd = 0.5 * rho * site_speed ** 3 # rough WPD; cubing the *mean* is conservative
months = pd.date_range("2022-01-01", periods=len(u), freq="MS")
df = pd.DataFrame(
{"|v|_100m [m/s]": site_speed.round(2), "WPD [W/m²]": wpd.round(1)},
index=months,
)
df
| |v|_100m [m/s] | WPD [W/m²] | |
|---|---|---|
| 2022-01-01 | 6.86 | 197.300003 |
| 2022-02-01 | 8.56 | 383.600006 |
| 2022-03-01 | 2.99 | 16.400000 |
| 2022-04-01 | 1.36 | 1.500000 |
| 2022-05-01 | 2.90 | 15.000000 |
| 2022-06-01 | 1.16 | 1.000000 |
| 2022-07-01 | 3.73 | 31.700001 |
| 2022-08-01 | 1.17 | 1.000000 |
| 2022-09-01 | 1.13 | 0.900000 |
| 2022-10-01 | 5.29 | 90.500000 |
| 2022-11-01 | 5.35 | 93.800003 |
| 2022-12-01 | 4.11 | 42.700001 |
Step 3 — plot seasonal cycle and wind rose¶
North Sea coastal sites peak in winter (storm season, Nov–Feb) and trough in summer. Mean wind direction is from the west / southwest.
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
axes[0].plot(months, site_speed, marker="o", color="tab:blue")
axes[0].set_ylabel("100 m wind speed [m/s]")
axes[0].set_title("Monthly mean — Cuxhaven 2022")
axes[0].grid(alpha=0.3)
u_mean = np.nanmean(u, axis=(1, 2))
v_mean = np.nanmean(v, axis=(1, 2))
ax2 = axes[1]
ax2.set_xlim(-15, 15)
ax2.set_ylim(-15, 15)
ax2.set_aspect("equal")
ax2.axhline(0, color="gray", lw=0.5)
ax2.axvline(0, color="gray", lw=0.5)
ax2.set_xlabel("u (eastward) [m/s]")
ax2.set_ylabel("v (northward) [m/s]")
ax2.set_title("Monthly mean wind vectors")
for um, vm, m in zip(u_mean, v_mean, months):
ax2.arrow(0, 0, um, vm, head_width=0.5, alpha=0.6)
ax2.text(um, vm, m.strftime("%b"), fontsize=8)
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Notes¶
- WPD from monthly means is conservative. Real WPD averages
$v^3$ over sub-daily samples, capturing the heavy tail of high
winds. Computing $\frac{1}{2}\rho \langle v\rangle^3$ from monthly
means underestimates by ~20–30% at typical sites. For accurate
bankable WPD, use hourly data (
temporal_resolution="daily"plus custom slicing) and cube before averaging. - Hub-height extrapolation. ERA5 also offers 10 m and 100 m components. For 80 m or 120 m hub heights, log-law extrapolation from 10 m and 100 m gives a stability-aware estimate.
- Air density correction. $\rho$ varies with site elevation and
temperature. Use
surface-pressureand2m-temperaturefor an ideal-gas correction in the WPD formula. - Power curves. Energy yield is the integral of the turbine's power curve weighted by the wind-speed PDF — Weibull-fit the hourly speeds for a proper AEP.