Wind Farm Layout & Wake Modeling
Layout is where a wind resource becomes a project, and it is the stage that consumes almost every other output in the solar and wind resource modeling workflows pipeline: the hub-height wind field, the wind rose, the terrain slope mask and the exclusion layers all arrive here and turn into turbine coordinates. The failure mode this page addresses is a layout that satisfies every geometric constraint and quietly loses a tenth of its energy to itself.
Wake losses are not a correction applied at the end. A turbine extracts momentum from the air, and everything downwind of it sees a slower, more turbulent flow for several rotor diameters. Because power scales with the cube of wind speed, a 10 percent velocity deficit is a 27 percent power deficit in the affected turbine for as long as the wind blows from that direction. A layout that ignores this does not fail — it produces an energy estimate that is 8 to 15 percent optimistic, which is larger than most of the uncertainties the yield report quotes.
The three constraints that decide a layout
Turbine positions are the solution to a constrained placement problem, and the constraints fall into three groups that behave very differently.
Hard geometry. Setbacks from dwellings, roads, property lines and infrastructure; the exclusion mask from environmental constraint and exclusion screening; and slope limits from the crane specification. These define where a turbine may stand at all, and they are non-negotiable, so they are applied first as a buildable-area mask.
Spacing rules. A minimum separation expressed in rotor diameters — commonly 3 to 5 D crosswind and 7 to 10 D downwind — is not a regulation but an engineering constraint standing in for the wake model. It is cheap to enforce and coarse: it treats every direction as equally important, which no site is.
Wake interaction. The actual physics, direction-weighted by the wind rose. This is what the spacing rule approximates, and modelling it explicitly is what lets a layout beat the rule: a site with a strongly prevailing direction can pack turbines much closer crosswind than a uniform 4 D rule allows, and must space them further apart along the prevailing axis.
The practical consequence is an ordering. Mask first, place with spacing rules second, then evaluate and refine against a wake model third. Optimising against the wake model from the start is computationally expensive and rarely changes the answer more than the mask already did.
Prerequisites and data requirements
The workflow assumes Python 3.11+ with geopandas>=0.14, shapely>=2.0, numpy, and optionally
floris for a full wake solve. Inputs are the buildable-area polygon, a turbine specification (rotor
diameter, hub height, thrust curve, power curve), the direction-binned wind rose from
building wind roses from met mast data,
and the hub-height wind speed field.
Everything must be in one projected metric frame. Rotor diameters become metres, spacing becomes a distance, and a layout computed in degrees is not merely wrong but wrong by a latitude-dependent factor in one axis only — which produces layouts that look correct and are systematically compressed east-west.
Core implementation: placing turbines under spacing constraints
The placement below is deliberately greedy rather than optimal. It sorts candidate positions by resource quality, accepts a position when it clears every constraint, and moves on. Greedy placement gets within a few percent of an optimised layout for a fraction of the effort, and — more importantly — it is explainable, which matters when a landowner asks why a turbine is where it is.
import geopandas as gpd
import numpy as np
from scipy.spatial import cKDTree
from shapely.geometry import Point
def place_turbines(
buildable: gpd.GeoSeries,
candidates: gpd.GeoDataFrame,
*,
rotor_diameter_m: float,
min_spacing_d: float = 4.0,
prevailing_deg: float | None = None,
downwind_spacing_d: float = 8.0,
max_turbines: int | None = None,
) -> gpd.GeoDataFrame:
"""Greedy placement: best resource first, subject to spacing and the buildable mask.
When a prevailing direction is given, spacing becomes elliptical — tighter across
the prevailing axis and wider along it — which is what the wake physics asks for.
"""
area = buildable.union_all()
inside = candidates[candidates.geometry.within(area)].copy()
inside = inside.sort_values("wind_speed_ms", ascending=False)
placed_xy: list[tuple[float, float]] = []
keep: list[int] = []
r_cross = min_spacing_d * rotor_diameter_m
r_down = downwind_spacing_d * rotor_diameter_m
for idx, row in inside.iterrows():
x, y = row.geometry.x, row.geometry.y
if placed_xy:
dx = np.array([x - px for px, _ in placed_xy])
dy = np.array([y - py for _, py in placed_xy])
if prevailing_deg is None:
too_close = np.hypot(dx, dy) < r_cross
else:
theta = np.radians(prevailing_deg)
# Rotate into wind-aligned coordinates: u along the wind, v across it.
u = dx * np.sin(theta) + dy * np.cos(theta)
v = dx * np.cos(theta) - dy * np.sin(theta)
too_close = ((u / r_down) ** 2 + (v / r_cross) ** 2) < 1.0
if too_close.any():
continue
placed_xy.append((x, y))
keep.append(idx)
if max_turbines and len(keep) >= max_turbines:
break
out = inside.loc[keep].copy()
out["turbine_id"] = [f"T{i + 1:03d}" for i in range(len(out))]
return out.set_geometry("geometry")
The elliptical spacing test is the part that earns its keep. A circular minimum separation wastes crosswind space at every site with a directional regime, and the ellipse costs one rotation and two divisions per comparison.
Wake modelling: the Jensen deficit and where it stops being enough
The Jensen (Park) model is the simplest wake model still worth using, and it is a good first
approximation for layout screening. It assumes the wake expands linearly behind the rotor and that
the velocity deficit is uniform across the wake at any distance. For a turbine with thrust
coefficient Ct at downstream distance x, the fractional deficit is
where k is the wake decay constant — about 0.075 onshore and 0.04 offshore — and D is the rotor
diameter. At 5 D behind a turbine with Ct = 0.8, that is a deficit of about 12 percent, or a 30
percent power loss for a turbine sitting directly in the wake.
import numpy as np
def jensen_deficit(
distance_m: np.ndarray,
*,
rotor_diameter_m: float,
thrust_coefficient: float = 0.8,
wake_decay_k: float = 0.075,
) -> np.ndarray:
"""Fractional velocity deficit behind a turbine under the Jensen model."""
x = np.maximum(distance_m, 1e-6)
numerator = 1.0 - np.sqrt(1.0 - thrust_coefficient)
expansion = (1.0 + 2.0 * wake_decay_k * x / rotor_diameter_m) ** 2
return numerator / expansion
def combine_deficits(deficits: np.ndarray) -> float:
"""Sum-of-squares superposition — the standard combination for multiple wakes."""
return float(np.sqrt(np.sum(np.square(deficits))))
Two properties of this model matter for how it is used. It has a top-hat profile, so a turbine is either fully in a wake or fully out of it — which makes it pessimistic for partial-wake geometries and means small position changes can produce discontinuous energy changes. And it takes no account of atmospheric stability, which in reality changes the wake recovery rate by a factor of two between a stable night and a convective afternoon.
Those limits define when to move to a Gaussian wake model or a full engineering solver such as FLORIS: when partial wakes dominate (tight crosswind spacing), when the site has strong stability structure (flat, inland, continental), or when the layout is being optimised rather than screened. For ranking candidate layouts, Jensen with sum-of-squares superposition is usually within one to two percentage points of the more expensive models, and it runs in milliseconds.
Direction weighting: the step that makes the number mean something
A wake deficit is a function of direction, and the annual energy loss is the deficit integrated over the wind rose. The consequence is that array loss is not a property of the layout alone — the same turbine positions on a site with a tight westerly regime and on a site with a uniform rose have different losses, and the tight regime is worse if the array is aligned with it and better if it is not.
The calculation is a loop over direction sectors, weighted by the frequency and the cube of the mean speed in each. Using frequency alone under-weights the strong sectors, which are precisely the ones where wake losses cost the most energy.
def array_efficiency(
positions: np.ndarray, # (n, 2) in metres
rose: list[dict], # [{'dir_deg': 270, 'freq': 0.21, 'mean_ms': 9.4}, ...]
*,
rotor_diameter_m: float,
thrust_coefficient: float = 0.8,
wake_decay_k: float = 0.075,
) -> float:
"""Energy-weighted array efficiency: 1.0 means no wake loss at all."""
gross = 0.0
net = 0.0
for sector in rose:
theta = np.radians(sector["dir_deg"])
# Unit vector pointing downwind.
wx, wy = np.sin(theta + np.pi), np.cos(theta + np.pi)
weight = sector["freq"] * sector["mean_ms"] ** 3
for i, (x, y) in enumerate(positions):
deficits = []
for j, (ox, oy) in enumerate(positions):
if i == j:
continue
dx, dy = x - ox, y - oy
downwind = dx * wx + dy * wy
if downwind <= 0:
continue # upwind of this turbine
cross = abs(dx * wy - dy * wx)
wake_radius = rotor_diameter_m / 2 + wake_decay_k * downwind
if cross > wake_radius:
continue # outside the wake cone
deficits.append(
jensen_deficit(
np.array([downwind]),
rotor_diameter_m=rotor_diameter_m,
thrust_coefficient=thrust_coefficient,
wake_decay_k=wake_decay_k,
)[0]
)
deficit = combine_deficits(np.array(deficits)) if deficits else 0.0
gross += weight
net += weight * (1.0 - deficit) ** 3 # power goes with the cube of speed
return net / gross if gross else 1.0
Error handling and edge cases
A layout with no feasible positions. Report it with the binding constraint, not as an empty frame. A site whose buildable area cannot hold two turbines at the specified spacing has a real answer — “this specification does not fit here” — and the useful output names whether it was the setback, the slope limit or the spacing rule that bound.
Candidate positions on the buildable-area boundary. A turbine centre inside the mask can still put a rotor tip or a crane pad outside it. Buffer the buildable area inward by the crane-pad radius before placement rather than checking the centre point, which is the same working-room argument that applies to routing exclusions.
Turbines just outside a wake cone. The Jensen top-hat makes this a cliff: a metre of movement changes a turbine from fully waked to unwaked. When a layout’s efficiency is sensitive at that level, the model is being used past its resolution — switch to a Gaussian profile rather than trusting the discontinuity.
Neighbouring projects. Wakes do not stop at a lease boundary. An adjacent operating wind farm upwind of the site is part of the flow, and omitting it produces an optimistic estimate that the neighbour’s operator will happily dispute. Include external turbines in the deficit calculation even though they are not in the layout.
Performance and scalability
The naive array-efficiency loop is O(sectors × n²), which for 16 sectors and 60 turbines is 57,600
pair evaluations — fast enough to sit inside an optimisation loop. At 300 turbines and 36 sectors it
is 3.2 million, which is not. Two optimisations recover most of it: skip pairs beyond a maximum wake
length (about 20 D, past which the deficit is under one percent) using a spatial index, and vectorise
the inner loop over turbines rather than looping in Python. Both are mechanical and neither changes
the result.
If the layout is being optimised rather than evaluated, the useful structure is to keep the mask and the candidate grid fixed and treat placement as a selection problem, so that each evaluation reuses the same precomputed pairwise geometry. Recomputing the constraint mask inside the optimisation loop is the most common reason a layout optimiser is slow.
Validation and audit trail
A bankable layout carries: the turbine specification and its thrust and power curves, the spacing rule applied, the buildable-area version and the exclusion layers behind it, the wind rose used for direction weighting, the wake model and its decay constant, the resulting array efficiency, and the external turbines included. Every one of those is a number an independent engineer will want to vary.
Three assertions belong in CI. Every turbine must lie inside the buildable area after the inward buffer, which catches a mask applied to centres rather than to pads. Every pairwise distance must clear the spacing rule, which catches an off-by-one in the greedy loop. And the array efficiency must lie between 0.75 and 1.0 — a value above 1.0 means the deficit was applied with the wrong sign, and a value below 0.75 means the layout is packed far past anything defensible.
Frequently asked questions
Is a 4 D by 8 D spacing rule good enough on its own?
For a first-pass layout, yes; for an energy estimate, no. The rule is a direction-blind approximation of the wake physics, and its whole value is that it needs no wind rose. Once the rose exists, the same turbine count can usually be placed with a lower array loss by tightening crosswind and widening along the prevailing axis — typically one to three percentage points of annual energy, which is larger than most layout optimisations recover by other means.
What array efficiency should a layout achieve?
Onshore projects at conventional spacing usually land between 0.88 and 0.94, and offshore projects lower because spacing is tighter relative to the resource. The number in isolation says little: an efficiency of 0.95 on a site with half the turbines that fit is a worse project than 0.90 with the full complement, since the objective is total net energy rather than per-turbine efficiency.
How much does the wake decay constant matter?
Enough to be recorded and not enough to agonise over at screening. Moving k from 0.075 to 0.05
deepens deficits and typically costs one to two points of array efficiency; the difference between
onshore and offshore values is larger than the uncertainty within either. Where it does matter is in
comparing a modelled layout against operating data, because a mismatched k will look like a layout
problem.
Should terrain effects be included in the wake model?
For anything but flat terrain, yes — but not through the wake model. Complex terrain changes the inflow, and the right place to represent that is the hub-height wind field from interpolating sparse met mast data with kriging, which already varies across the site. Feeding a per-turbine inflow speed into a simple wake model captures most of the terrain effect; using a flat-terrain wake model with a site-average wind speed captures none of it.
Can the same machinery lay out a solar project?
The masking and spacing parts, yes; the wake part has no analogue. Solar row spacing is a shading problem rather than a momentum one, and it trades ground coverage ratio against row-to-row shading loss in a way that is far more tractable — the geometry is deterministic given the sun position. The shared machinery is the buildable-area mask and the candidate grid.
How do neighbouring projects affect the estimate?
They reduce it, sometimes materially, and they are outside the developer’s control. An operating farm two kilometres upwind in the prevailing direction can cost one to three points of array efficiency on the near rows. Model them explicitly, record which external turbines were included, and expect the figure to be contested — wake interaction between adjacent projects is one of the more common sources of dispute in operating fleets.
Related
- Solar & Wind Resource Modeling Workflows — the parent pipeline
- Wind Speed & Direction Modeling — the hub-height field and rose this stage consumes
- Building Wind Roses from Met Mast Data with Python — the direction weighting behind array efficiency
- Automating Hillshade & Slope Analysis for Wind Turbine Siting — the slope mask that bounds placement
- Environmental Constraint & Exclusion Screening — the buildable area every layout starts from