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.

Velocity deficit and power loss behind one turbine A chart with downstream distance from 2 to 20 rotor diameters on the horizontal axis. Two curves fall from left to right: the velocity deficit under the Jensen model with a thrust coefficient of 0.8 and a wake decay constant of 0.075, and the resulting power loss, which is the cube of the remaining velocity fraction. Marked points give 20 percent velocity and 49 percent power loss at 3 diameters, 14 and 36 percent at 5, 9.6 and 26 percent at 8, and 6.4 and 18 percent at 12. A shaded band marks the 7 to 10 diameter range used by conventional downwind spacing rules. Jensen deficit · Ct = 0.8 · k = 0.075 conventional 7–10 D 0% 10% 20% 30% 40% 50% 2 D 5 D 8 D 12 D 16 D 20 D downstream distance, rotor diameters 60% 45% 30% 20% velocity deficit power loss — the cube of the remaining speed A 10% velocity deficit is a 27% power deficit for as long as the wind blows from that direction.

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.

python
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.

python
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.

Circular spacing versus wind-aligned elliptical spacing Two plan views of the same buildable-area polygon, each holding 24 turbines. The left layout uses a circular 4 rotor-diameter minimum spacing and shows several turbines aligned along the west-south-westerly prevailing axis, marked with wake cones. The right layout uses an elliptical rule of 3.2 diameters crosswind and 9 diameters downwind, producing rows that run across the prevailing direction with wider gaps along it. Array efficiency is 0.902 for the circular layout and 0.928 for the elliptical one, a gain of 2.6 percentage points at the same turbine count. Same site, same 24 turbines, two spacing rules circular 4 D spacing array efficiency 0.902 elliptical 3.2 D × 9 D array efficiency 0.928 The gain comes from the rose, not from the geometry: on a site with a uniform wind rose the two rules produce the same efficiency, and the circular one is simpler. Direction weighting is what makes the difference.

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.

python
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.

Array efficiency against mean spacing, and why more is not always better A curve of array efficiency against mean turbine spacing in rotor diameters, rising from 0.861 at 3 diameters through 0.902 at 4, 0.925 at 5 and 0.949 at 7 to 0.968 at 10, flattening as it goes. A second series shows how many turbines fit in the same buildable area at each spacing: 34 at 3 diameters, 24 at 4, 17 at 5, 10 at 7 and 6 at 10. A third annotation gives the product — total net energy — which peaks near 4 diameters and falls away in both directions, making the point that efficiency alone is the wrong objective. Efficiency rises with spacing; turbine count falls faster 0.86 0.90 0.94 0.98 3 D 4 D 5 D 7 D 10 D 0.861 34 turbines 0.902 24 turbines 0.925 17 turbines 0.949 10 turbines 0.968 6 turbines mean spacing, rotor diameters net energy index 3 D 34 turbines × 0.861 29.3 4 D 24 turbines × 0.902 21.6 5 D 17 turbines × 0.925 15.7 7 D 10 turbines × 0.949 9.5 Efficiency is a means, not the objective: the layout that maximises net energy on this site is denser than the one that maximises efficiency, and both are constrained by what the buildable area holds.

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.