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.
Setup¶
All imports live in one cell near the top. earthlens provides the unified
EarthLens entry point plus AggregationConfig for the time aggregation;
pyramids reads the downloaded GeoTIFFs, and numpy / pandas / matplotlib
do the analysis and plotting.
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pyramids.dataset import Dataset
from earthlens import AggregationConfig, EarthLens
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}$.
First create the output directory the GeoTIFFs will be written to.
OUT = Path("data/era5-wind-cuxhaven")
OUT.mkdir(parents=True, exist_ok=True)
Build the ECMWF request — a year of monthly 100 m u and v wind
components over the Cuxhaven box, written into OUT.
earthlens = EarthLens(
data_source="ecmwf",
cadence="monthly",
start="2022-01-01",
end="2022-12-01",
dataset="reanalysis-era5-single-levels-monthly-means",
variables=[
"100m-u-component-of-wind",
"100m-v-component-of-wind",
],
aoi=[8.0, 53.0, 9.0, 54.0],
path=str(OUT),
)
Run the download, chaining the monthly aggregation into the same call. Wind
components are state variables, so op="auto" resolves to a time-mean.
earthlens.download(aggregate=AggregationConfig(freq="1MS", op="auto"))
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!).
Read the aggregated components¶
Stack the per-month aggregated GeoTIFFs for each component into (month, lat, lon) arrays.
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"))
]
)
Wind speed and power density¶
Combine the components into a speed magnitude, take the spatial mean over the box, and turn it into a rough wind power density (cubing the mean is conservative).
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
Assemble the per-month speed and WPD into a table indexed by month.
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
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.
Compute the monthly mean u / v for the vector plot, then draw the seasonal
speed cycle alongside the monthly mean wind vectors in a single figure.
u_mean = np.nanmean(u, axis=(1, 2))
v_mean = np.nanmean(v, axis=(1, 2))
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)
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.