Source: github.com/brunolimaam/windforecast
Noise Assessment: Hohn, Schleswig-Holstein
A noise immission forecast for the same five turbines at Hohn: octave-band sound propagation under ISO 9613-2 and the Interimsverfahren, checked against the TA Lärm night limit at the nearest houses.
This notebook is the companion to the yield assessment in analysis.ipynb. The story and the turbines are the same: five Nordex N100/2500 turbines on the decommissioned Hohn airfield. The question is different: how loud will the wind farm be at the nearest houses?
In Germany, a wind turbine taller than 50 m is approved under the Federal Immission Control Act (BImSchG), and the approval requires a noise immission forecast (Schallimmissionsprognose). The forecast must show that the wind farm stays within the TA Lärm limits at every relevant receiver. Night (22:00–06:00) limits are 15 dB lower than the day limits:
| Area type | Day | Night |
|---|---|---|
| Purely residential (reines Wohngebiet) | 50 dB(A) | 35 dB(A) |
| General residential (allgemeines Wohngebiet) | 55 dB(A) | 40 dB(A) |
| Village / mixed area, outskirts (Dorf-/Mischgebiet, Außenbereich) | 60 dB(A) | 45 dB(A) |
Sound propagation is calculated in octave bands with the general method of ISO 9613-2, as modified for wind turbines by the Interimsverfahren (NALS 2015), which the LAI recommends. ISO 9613-2 was validated for sources up to about 30 m above ground. For a source at 100 m hub height, the Interimsverfahren replaces the ground attenuation with a constant −3 dB and applies no meteorological correction. The acoustics are computed with the open-source phonometry library, which implements ISO 9613-1 atmospheric absorption and the ISO 9613-2 general method.
The notebook works through four steps:
- Source: the octave-band sound power level of the N100/2500, emitted at hub height.
- Receivers: the nearest dwellings around the airfield, each assigned the limit for its area type.
- Propagation: for each turbine–receiver pair, the attenuation terms (geometric divergence, atmospheric absorption, ground) in each octave band. The results are then A-weighted and summed over the five turbines.
- Assessment: the rating level at its upper 90 % confidence limit, as the LAI requires, compared with the TA Lärm night limit. The step ends with a noise map of the area.
If a receiver exceeds its limit, the usual fix is to run the nearest turbines in a sound-reduced mode at night. That mode costs energy, which is where this notebook feeds back into the yield assessment.
# --- Site layout (figure from analysis.ipynb) -----------------------------
from io import BytesIO
from PIL import Image as PILImage
from IPython.display import Image, display
from windforecast.paths import FIGURES
# downscale before embedding, so the notebook stays small
img = PILImage.open(FIGURES / "site_map_hohn.png").convert("RGB")
img.thumbnail((1800, 1800))
buf = BytesIO()
img.save(buf, format="JPEG", quality=85)
display(Image(data=buf.getvalue(), width=700))
import phonometry as pn
# --- Five turbines, L_WA = 106 dB(A) each: propagation map (upper 90 % limit) ---
import numpy as np
import matplotlib.pyplot as plt
import contextily as cx
from pyproj import Transformer
from phonometry.environmental.outdoor_propagation import (
geometric_divergence,
atmospheric_absorption,
)
from windforecast import config
cfg = config.load()
L_WA = 106.0 # dB(A) per turbine, max of N100/2500 data sheet
f_oct = np.array([63, 125, 250, 500, 1000, 2000, 4000], dtype=float)
L_WA_oct = L_WA + np.array([-20.3, -11.9, -7.7, -5.5, -6.0, -8.0, -12.0]) # LAI reference spectrum, dB(A)
h_s = cfg["turbine"]["hub_height_m"] # m, source at hub height
h_r = 5.0 # m, receiver height
A_GR = -3.0 # dB, Interimsverfahren ground term
T, RH = 10.0, 70.0 # °C, %: Interimsverfahren atmosphere
# uncertainty, LAI-Hinweise (2016) Ziffer 3
# manufacturer declaration (3 a): no sigma_R / sigma_P, only the model uncertainty.
# with a type measurement instead: sigma_R = 0.5 (3 b), sigma_P = 1.2 (3 c)
sigma_R, sigma_P, sigma_Prog = 0.0, 0.0, 1.0 # dB
sigma_tot = np.sqrt(sigma_R**2 + sigma_P**2 + sigma_Prog**2)
dL = 1.28 * sigma_tot # dB, upper 90 % confidence limit
# turbine positions in ETRS89 / UTM 32N (metres)
to_utm = Transformer.from_crs("EPSG:4326", "EPSG:25832", always_xy=True)
turbines = cfg["site"]["turbines"]
tx, ty = to_utm.transform([t["longitude"] for t in turbines],
[t["latitude"] for t in turbines])
tx, ty = np.asarray(tx), np.asarray(ty)
# receiver grid: every 10 m, 2.5 km beyond the outermost turbines
pad, step = 2500.0, 10.0
xs = np.arange(tx.min() - pad, tx.max() + pad + step, step)
ys = np.arange(ty.min() - pad, ty.max() + pad + step, step)
X, Y = np.meshgrid(xs, ys)
alpha = atmospheric_absorption(1.0, frequencies=f_oct, temperature=T,
relative_humidity=RH) # dB/m per band
div = np.vectorize(geometric_divergence)
# level per turbine and octave band, energetic sum over all of them
energy = np.zeros_like(X)
for x0, y0 in zip(tx, ty):
d = np.hypot(np.hypot(X - x0, Y - y0), h_s - h_r) # m, slant distance
a_div = div(d)
for L_band, a in zip(L_WA_oct, alpha):
energy += 10 ** ((L_band - (a_div + a * d + A_GR)) / 10)
L_AT = 10 * np.log10(energy) # expected level
L_o = L_AT + dL # upper 90 % confidence limit, compared with the limit
# --- map ------------------------------------------------------------------
fig, ax = plt.subplots(figsize=(11, 8))
ax.set_xlim(xs[0], xs[-1])
ax.set_ylim(ys[0], ys[-1])
cx.add_basemap(ax, source=cx.providers.Esri.WorldImagery, crs="EPSG:25832", zoom=14)
levels = np.arange(30, 56, 1)
cf = ax.contourf(X, Y, L_o, levels=levels, cmap="inferno_r", alpha=0.45, extend="both")
cs = ax.contour(X, Y, L_o, levels=[35, 40, 45], colors="white", linewidths=1.4)
ax.clabel(cs, fmt="%d dB(A)", fontsize=9)
ax.plot(tx, ty, ls="none", marker="*", color="red", markersize=18, markeredgecolor="white")
for x0, y0, t in zip(tx, ty, turbines):
ax.annotate(t["name"], (x0, y0), xytext=(8, 6), textcoords="offset points",
color="white", fontsize=10, fontweight="bold")
fig.colorbar(cf, ax=ax, shrink=0.75, label="$L_o$ at receiver [dB(A)]")
ax.set_title(f"{len(turbines)} turbines, $L_{{WA}}$ = {L_WA:.0f} dB(A) each, LAI reference spectrum, "
f"upper 90 % limit (+{dL:.1f} dB)\n"
f"$h_s$ = {h_s} m, $h_r$ = {h_r:.0f} m (Interimsverfahren)")
ax.set_xticks([])
ax.set_yticks([])
fig.tight_layout()
plt.show()
# --- Export to QGIS: level rasters, isophones, turbines (EPSG:25832) ------
import rasterio
from rasterio.transform import from_origin
import geopandas as gpd
from shapely.geometry import LineString, Point
from windforecast.paths import PROCESSED
out = PROCESSED / "noise"
out.mkdir(parents=True, exist_ok=True)
# rasters: expected level and upper 90 % confidence limit ("Noise")
# GeoTIFF rows run north -> south, so flip the grid
transform = from_origin(xs[0] - step / 2, ys[-1] + step / 2, step, step)
for name, grid in [("noise_LAT.tif", L_AT), ("Noise.tif", L_o)]:
with rasterio.open(out / name, "w", driver="GTiff",
height=grid.shape[0], width=grid.shape[1], count=1,
dtype="float32", crs="EPSG:25832", transform=transform) as dst:
dst.write(np.flipud(grid).astype("float32"), 1)
# isophones of L_o, contoured here on a hidden figure (independent of the map)
iso_levels = [45, 50, 55] # dB(A)
fig_iso, ax_iso = plt.subplots()
cs_iso = ax_iso.contour(X, Y, L_o, levels=iso_levels)
plt.close(fig_iso)
iso = gpd.GeoDataFrame(
[{"level_dBA": lvl, "geometry": LineString(seg)}
for lvl, segs in zip(cs_iso.levels, cs_iso.allsegs) for seg in segs if len(seg) > 1],
crs="EPSG:25832",
)
iso.to_file(out / "noise.gpkg", layer="isophones")
turb = gpd.GeoDataFrame(
{"name": [t["name"] for t in turbines], "L_WA_dBA": L_WA, "hub_m": h_s},
geometry=[Point(x, y) for x, y in zip(tx, ty)],
crs="EPSG:25832",
)
turb.to_file(out / "noise.gpkg", layer="Turbines", overwrite='yes')
Noise map¶
The receivers were identified with the Flächennutzungsplan of the Gemeinde Hohn, in force since 17.07.2003, which I accessed through the Bauleitplanung layers of the Kreis Rendsburg-Eckernförde in the state geoportal (Digitaler Atlas Nord). No Bebauungsplan covers the area. I selected five receivers, the houses closest to the turbines, and all of them lie in an area designated Fläche für die Landwirtschaft. They are therefore in the Außenbereich (§ 35 BauGB). The Außenbereich is assessed like a Dorf- or Mischgebiet, so the TA Lärm limits (No. 6.1) at these receivers are 60 dB(A) by day and 45 dB(A) at night. Night is the binding case for wind turbines.
The map shows the night-time noise level of the five turbines at the upper 90 % confidence limit ($L_o$), together with five receivers which lie inside the 45 dB(A) isophone, meaning that the night limit is exceeded for them.
This is bad news for the project, because measures are needed to reduce the noise at these receivers. They range from sound-reduced operating modes of the turbines closest to them to removing a turbine from the layout. I couldn't find public information on sound-reduced modes for the Nordex N100/2500, so this step remains open.
The map below was made in QGIS. It combines the noise grid and isophones exported by this notebook with the receivers, the F-Plan layers and Esri satellite imagery.
from io import BytesIO
from PIL import Image as PILImage
from IPython.display import Image, display
from windforecast.paths import FIGURES
# downscale before embedding, so the notebook stays small
img = PILImage.open(FIGURES / "hohn_park_layout.png").convert("RGB")
img.thumbnail((1800, 1800))
buf = BytesIO()
img.save(buf, format="JPEG", quality=85)
display(Image(data=buf.getvalue(), width=900))
Comparison with commercial software: WindPRO DECIBEL¶
In practice, noise forecasts for wind farms are done with commercial software. The most common in Germany is WindPRO (EMD International), whose noise module is DECIBEL. It implements the same chain as this notebook: ISO 9613-2 with the German Interimsverfahren, octave-band calculation, the LAI confidence margin, receivers with their area type and limit, and noise maps with isophones. Producing a map like the one above takes minutes in WindPRO, because the terrain, the basemaps and the turbine data are already built in.
Another advantage is the turbine catalogue. It contains the manufacturers' noise data for each turbine type, including the sound-reduced operating modes, with sound power level and octave spectrum for each mode. In DECIBEL, each turbine can be switched to a different mode and the receiver levels are recalculated immediately. This is exactly the step that remains open here: finding a combination of night-time modes that brings all five receivers below 45 dB(A), and seeing what it costs in energy yield (together with the PARK module).
This notebook rebuilds the same workflow with open tools (phonometry for the acoustics, QGIS for the spatial part) to show each step of the method explicitly. The missing piece is the data rather than the method: without the Nordex mode table, the noise conflict at Hohn can be identified here but not resolved.