Chris Parmer — home

Wind farm wakes

PyWake's Bastankhah–Porté-Agel model over Horns Rev 1, 80 turbines

Example from a field guide to quiver · shared helpers

Wind farm wakes — PyWake's Bastankhah–Porté-Agel model over Horns Rev 1, 80 turbines

Python Code

"""Wind farm wakes — PyWake's Bastankhah–Porté-Agel model over Horns Rev 1, 80 turbines."""
import warnings
from pathlib import Path

from _shared import save, themed_layout, TEXT, MUTED, VIOLET, PINK

import numpy as np

warnings.filterwarnings("ignore")
from py_wake.examples.data.hornsrev1 import Hornsrev1Site, V80, wt_x, wt_y
from py_wake.flow_map import XYGrid
from py_wake.literature.gaussian_models import Bastankhah_PorteAgel_2014

CHART_NUM = 11

WD = 282.0   # wind from WNW, so the wakes trail down-right across the rows
WS = 8.0     # m/s


def generate():
    print("running PyWake on Horns Rev 1 …")
    x = np.array(wt_x) - min(wt_x)
    y = np.array(wt_y) - min(wt_y)
    site, turbine = Hornsrev1Site(), V80()
    wfm = Bastankhah_PorteAgel_2014(site, turbine, k=0.0324555)
    sim = wfm(x, y, wd=WD, ws=WS)
    power_mw = np.asarray(sim.Power).squeeze() / 1e6
    print(f"  {len(x)} turbines · farm output {power_mw.sum():.1f} MW")

    # Effective wind speed on a dense grid, straight from the model
    gx = np.arange(-500, x.max() + 3000, 60.0)
    gy = np.arange(-400, y.max() + 400, 60.0)
    fm = sim.flow_map(XYGrid(x=gx, y=gy))
    ws_eff = np.asarray(fm.WS_eff).squeeze()          # shape (len(gy), len(gx))
    print(f"  flow map {ws_eff.shape} · slowest {ws_eff.min():.1f} m/s")

    # The wind-speed field as a smooth color underlay — the wakes read as
    # plumes, the way wake-model flow maps are usually drawn
    field = {
        "type": "contour",
        "x": (gx / 1000).tolist(),   # km, purely to keep the JSON small
        "y": (gy / 1000).tolist(),
        "z": ws_eff.tolist(),
        "contours": {"coloring": "heatmap", "showlines": False},
        "line": {"width": 0},
        "colorscale": [[0.0, PINK], [0.4, VIOLET], [1.0, "#eef6fa"]],
        # floor the color range in the far-wake regime: the handful of
        # near-rotor cells would otherwise wash out the streak contrast
        "zmin": 5.0,
        "zmax": WS,
        "colorbar": {"title": {"text": "m/s"}, "thickness": 12, "len": 0.7},
        "hovertemplate": "%{z:.1f} m/s<extra></extra>",
        "showlegend": False,
    }

    # Direction and speed on top: a sparser quiver, one arrow per 4th cell
    # met "from" direction → the flow vector points the opposite way
    theta = np.deg2rad(270 - WD)
    cosp, sinp = np.cos(theta), np.sin(theta)
    GX, GY = np.meshgrid(gx, gy)
    sub = np.zeros_like(GX, dtype=bool)
    sub[2::4, 2::4] = True
    arrows = {
        "type": "quiver",
        "x": (GX[sub] / 1000).tolist(),
        "y": (GY[sub] / 1000).tolist(),
        "u": (ws_eff[sub] * cosp).tolist(),
        "v": (ws_eff[sub] * sinp).tolist(),
        # the rc draws the quiver layer beneath contour fills; an overlaying
        # axis pair lifts the arrows above the field
        "xaxis": "x2",
        "yaxis": "y2",
        "arrowref": "paper",
        "lengthmode": "scaled",
        "lengthfactor": 1.5,
        # colorscale with no color array → arrows color by |(u,v)| = wind speed;
        # a darker ramp than the field underlay so the arrows stay legible
        "marker": {
            "colorscale": [[0.0, "#c22d76"], [0.4, "#5233c9"], [1.0, "#55616e"]],
            "cmin": 5.0,
            "cmax": WS,
            "showscale": False,
            "line": {"width": 1.4},
        },
        "customdata": [f"{s:.1f} m/s" for s in ws_eff[sub]],
        "hovertemplate": "%{customdata}<extra></extra>",
        "showlegend": False,
    }

    # Each turbine drawn as its rotor: a line perpendicular to the wind,
    # exaggerated ~2.5x rotor scale so the machines read at farm scale
    half = 1.25 * 80.0
    rx, ry = [], []
    for xt, yt in zip(x, y):
        rx += [(xt - half * -sinp) / 1000, (xt + half * -sinp) / 1000, None]
        ry += [(yt - half * cosp) / 1000, (yt + half * cosp) / 1000, None]
    rotors = {
        "type": "scatter",
        "x": rx,
        "y": ry,
        "mode": "lines",
        "line": {"color": TEXT, "width": 3},
        "hoverinfo": "skip",
        "showlegend": False,
    }
    # Invisible markers at the hubs carry the per-turbine power on hover
    hubs = {
        "type": "scatter",
        "x": (x / 1000).tolist(),
        "y": (y / 1000).tolist(),
        "mode": "markers",
        "marker": {"size": 14, "opacity": 0},
        "customdata": [
            f"turbine {i + 1} · {p:.2f} MW" for i, p in enumerate(power_mw)
        ],
        "hovertemplate": "%{customdata}<extra></extra>",
        "showlegend": False,
    }

    layout = themed_layout(
        xaxis={"visible": False},
        yaxis={"visible": False, "scaleanchor": "x"},
        xaxis2={"overlaying": "x", "matches": "x", "visible": False},
        yaxis2={"overlaying": "y", "matches": "y", "visible": False},
        annotations=[
            {"x": -0.45, "y": (y.max() + 650) / 1000, "text": "wind 8 m/s ↘",
             "showarrow": False, "font": {"color": MUTED, "size": 12},
             "xanchor": "left"},
            {"x": (x.max() + 2900) / 1000, "y": (y.max() + 650) / 1000,
             "text": f"80 × Vestas V80 · {power_mw.sum():.0f} MW",
             "showarrow": False, "font": {"color": MUTED, "size": 12},
             "xanchor": "right"},
        ],
        margin={"t": 30, "b": 30, "l": 30, "r": 30},
    )
    save(CHART_NUM, {"data": [field, arrows, rotors, hubs], "layout": layout})

Made with Plotly