Learn R Programming

windfarmGA

A genetic algorithm to optimize the layout of wind farms.

Version 5.0.0 uses a combinatorial genome (n unique grid-cell IDs), adaptive operator rates, and a memetic neighbourhood search on elites. The design, the north-wind gold layout, and the sensitivity results are documented in inst/reports/memetic-layout-ga.md.

Installation

The latest version can be installed from GitHub with:

devtools::install_github("YsoSirius/windfarmGA")
install.packages("windfarmGA")

Description

The genetic algorithm is designed to optimize wind farms of any shape. It requires a predefined number of turbines, a uniform rotor radius and an average wind speed per wind direction. It can include a terrain effect model, which requires an elevation raster and a surface roughness raster. The elevation data is used to find mountains and valleys and to adjust the wind speeds accordingly by 'wind multipliers' and to determine the air densities at rotor heights. The surface roughness raster with an additional elevation roughness value is used to re-evaluate the surface roughness and to individually determine the wake-decay constant for each turbine.

To start an optimization use the function genetic_algorithm. A complete path — draw a site, pick an open IEA/NREL turbine, turn u/v or mast data into a rose, then optimize and plot — is in Realistic workflow.

Since version 1.1, hexagonal grid cells are possible, with their center points being possible locations for wind turbines. Furthermore, rasters can be included, which contain information on the Weibull parameters (shape k, scale a). Country GeoTIFFs from the Global Wind Atlas (~250 m) are a good source; see gwa_download_country() in experimental/climate_helpers.R.

Create an input Polygon

  • Input Polygon by source
library(sf)
dsn <- "Path to the Shapefile"
layer <- "Name of the Shapefile"
area <- sf::st_read(dsn = dsn, layer = layer)
plot(area, col = "blue")
  • Or create a random Polygon
library(sf)
area <- sf::st_as_sf(sf::st_sfc(
  sf::st_polygon(list(cbind(
    c(0, 0, 2000, 2000, 0),
    c(0, 2000, 2000, 0, 0)))),
  crs = 3035
))
plot(area, col = "blue", axes = TRUE)
  • Or draw a site on a map (GitHub clone; needs leaflet, mapedit >= 0.8, leafpm)
source("experimental/draw_shape.R")
area <- draw_shape()   # draw a polygon, then close the viewer
plot(area, axes = TRUE)

Create random Wind data

  • Exemplary input Wind data with uniform wind speed and single wind direction
wind_df <- data.frame(ws = c(12, 12), wd = c(0, 0), probab = c(25, 25))
windrosePlot <- plot_windrose(data = wind_df, spd = wind_df$ws,
                              dir = wind_df$wd, dirres=10, spdmax = 20)
  • Exemplary input Wind data with random wind speeds and random wind directions
wind_df <- data.frame(ws = sample(1:25, 10), wd = sample(1:260, 10))
windrosePlot <- plot_windrose(data = wind_df, spd = wind_df$ws,
                              dir = wind_df$wd)

Grid Spacing

Rectangular Grid Cells

Verify that the grid spacing is appropriate. Adapt the following input variables if necessary:

  • rotor: The rotor radius in meters.
  • fcr: The grid spacing factor, which should at least be 2, so that a single grid covers at least the whole rotor diameter.
  • prop: The proportionality factor used for grid calculation. It determines the minimum percentage that a grid cell must cover of the area.

Make sure that the Polygon is projected in meters.

Rotor <- 20
fcr <- 9
Grid <- grid_area(area, size = (Rotor * fcr), prop = 1, plot_grid = TRUE)
str(Grid)

Hexagonal Grid Cells

Rotor <- 20
fcr <- 9
HexGrid <- hexa_area(area, size = (Rotor * fcr), plot_grid = TRUE)
str(HexGrid)

Terrain Effect Model

If terrain is TRUE in genetic_algorithm, terrain effects are taken into account. A digital elevation model and a CORINE Land Cover raster are downloaded automatically. Prefer terrain = dem (a SpatRaster) to skip the download. Per-cell height, wind multiplier, z_0, k and air density are computed once; plot_result / random_search reuse the stored terrainModel instead of downloading again. (Download a CLC raster).

To use your own land cover raster, provide its path via ccl. Surface roughness values are taken from extdata/clc_legend.csv. To use custom values, add a numeric Rauhigkeit_z column for all land cover classes, save the file with ; as delimiter, and provide its path via ccl_roughness.

plot_result(..., terrain = TRUE) shows the full chain:

  • Elevation → wind-speed multiplier (DEM / mean DEM)
  • Elevation → air density (barometric; labels vs 1.225 kg/m³)
  • CORINE Land Cover → land-cover roughness z_0
  • Elevation roughness (terra::terrain) × CLC z_0 → modified z_0
  • Modified z_0 + hub height → wake decay k = 0.5 / \log(h/z_0)

The modified z_0 also scales hub-height wind via the log profile (not a separate plot_result panel).

Realistic workflow (site, turbine, wind)

Clone the GitHub repo so experimental/ is available (it is not in the CRAN tarball). Browse open reference turbines at the NREL Turbine Archive.

ERA5 / Copernicus (env COPERNICUS_CLIMATE_DATA) is useful as a directional rose over time (get_era5_wind()wind_from_era5()), but the grid is too coarse (~31 km) as a spatial wind field for siting. For mean speed per cell use Global Wind Atlas Weibull rasters (weibull = TRUE, weibull_src = list(k, a)). Hub-height correction uses reference_height (100 m for the ERA5 100 m u/v download) and rotor_height from the turbine. A met mast goes through wind_from_breeze() or wind_from_series().

library(windfarmGA)
library(sf)

source("experimental/draw_shape.R")
source("experimental/climate_helpers.R")
source("experimental/download_ERA5_historic.R")

## 1. Site: draw a polygon (or st_read a shapefile)
area <- draw_shape()
plot(area, axes = TRUE)

## 2. Turbine: IEA 3.4 MW, 130 m rotor, 110 m hub
nrel_curve_catalog()
curve <- nrel_fetch_curve("IEA_3.4MW_130")
plot_power_curve(curve)
ga_options(power_curve = curve)
rotor <- attr(curve, "rotor")           # radius in metres
hub <- attr(curve, "rotor_height")

## 3. Wind rose from ERA5 100 m u/v at the site (needs CDS key)
era5 <- get_era5_wind(area, "2021-01-01", "2025-12-31")
wind <- wind_from_era5(era5)
plot_windrose(wind, spd = "ws", dir = "wd")

##    Met mast instead of ERA5 (bReeze, or two numeric vectors):
##    library(bReeze)
##    data(winddata)
##    s40 <- set(height = 40, v.avg = winddata[, 2], dir.avg = winddata[, 14])
##    mast <- mast(timestamp(winddata[, 1]), s40)   # bReeze::mast
##    wind <- wind_from_breeze(mast)
##    wind <- wind_from_series(ws, wd)              # raw speed / direction

##    Spatial mean speed (GWA ~250 m); wind$ws is then ignored
gwa <- gwa_download_country("AUT", height = 100)  # ISO3 of the site country

## 4. Optimize
result <- genetic_algorithm(
  area = area,
  wind = wind,
  n = 12,
  rotor = rotor,
  rotor_height = hub,
  reference_height = 100,
  weibull = TRUE,
  weibull_src = gwa$weibull_src,
  terrain = TRUE,                   # DEM + CLC; needs elevatr
  fcr = 5,
  iteration = 40,
  plot = FALSE
)

## 5. Plot
print(result)
plot(result, area)
plot_result(result, area, terrain = TRUE)
plot_parkfitness(result)
plot_leaflet(result, area, which = 1)
explore_result(result, area)

source("experimental/rayshader.R")
plot_farm_3d_from_result(result, area, buffer = 5000, turbine_obj = "./experimental/wind_turbine_v1.obj")
source("experimental/plot_mapgl.R")
plot_mapgl_from_result(result, area, buffer = 8000, basemap = "satellite")


## 6. Optional: jitter turbines inside their cells (same physics as the GA)
##    Terrain is reused from result$terrainModel. Weibull rasters are not
##    stored — pass weibull_src again (that is enough, no weibull = TRUE).
refined <- random_search(
  result, area, runs = 20, best = 1, plot = FALSE,
  weibull_src = gwa$weibull_src
)
plot_random_search(refined, result, area, best = 1)

The screenshot above shows the optimized layout for a north-only wind rose. The best layout often sits in the first few upwind rows. In 2D, the arrangement looks cramped; however, wakes are in 3D. A turbine on lower ground can sit below the hub-height wake cone of a turbine further uphill and therefore is not counted as shadowed. These front rows are also at higher elevation, so the wind multiplier remains high while air density decreases less. The Leaflet map and the rayshader view (bottom right) show the same run in 2D and on the DEM.

Start an Optimization

An optimization can be initiated with the function genetic_algorithm. Search knobs that are not function arguments are session options (options(windfarmGA.*)); see Options.

  • without terrain effects
result <- genetic_algorithm(
  area = area, n = 12, rotor = 20, fcr = 9, iteration = 10,
  wind = wind_df, rotor_height = 100
)
  • with terrain effects
ccl <- "Source of the CCL raster (TIF)"
ccl_roughness <- "Source of the Adapted CCL legend (CSV)"

result <- genetic_algorithm(
  area = area, n = 12, rotor = 20, fcr = 9, iteration = 10,
  wind = wind_df, rotor_height = 100, terrain = TRUE,
  ccl = ccl, ccl_roughness = ccl_roughness
)
## Spatial mean speed from Global Wind Atlas (shape k, scale A).
## wind$ws is then ignored; wind$wd / wind$probab still weight directions.
source("experimental/climate_helpers.R")
gwa <- gwa_download_country("AUT", height = 100)
result_weibull <- genetic_algorithm(
  area = area, grid_method = "h", n = 12,
  fcr = 5, iteration = 10, wind = wind_df, rotor = 30,
  rotor_height = 100, weibull = TRUE, weibull_src = gwa$weibull_src
)
plot_windfarmGA(result = result_weibull, area = area)

Options

There are two layers. Arguments of genetic_algorithm() describe the site, the turbines and which GA pieces to use. Session options options(windfarmGA.*) control physics constants and the memetic search (inject, immigrants, seasons, neighbour local search). Set them before the run; they apply for the whole R session until you change them again.

ga_options()
ga_options(immigrants = 3, local_search_elites = 5, local_search_tries = 6)

The north-wind benchmark and why these search defaults exist are in inst/reports/memetic-layout-ga.md.

Arguments of genetic_algorithm()

Required: area, wind, n, rotor, rotor_height.

Site and turbines

ArgumentDefaultMeaning
areaArea as sf polygon, SpatialPolygons, or coordinate matrix. Must be projected (metres).
nNumber of turbines (fixed; every individual has exactly n unique cell IDs).
rotorRotor radius in metres.
fcr5Grid spacing factor. Cell size is fcr * rotor. Use at least 2 so a cell covers the rotor diameter.
grid_method"rectangular""h" / "hexagon" for hexagonal cells.
proportionality1Minimum fraction of a grid cell that must overlap the polygon (prop in grid_area).
crsEPSG:3035CRS if the polygon has none (numeric EPSG or PROJ string).
windWind data.frame (ws, wd, optional probab). See windata_format.
reference_heightrotor_heightHeight at which ws was measured.
rotor_heightHub height in metres.
surface_roughness0.3Roughness length z_0 in metres. Ignored per cell when terrain = TRUE.

Terrain and Weibull

ArgumentDefaultMeaning
terrainFALSETerrain model: elevation (via elevatr) plus Corine Land Cover for roughness and wake decay.
cclauto / packagePath to a CLC raster (.tif) if you do not want the download.
ccl_roughnessinst/extdataCSV legend with column Rauhigkeit_z (; separated).
weibullFALSEIf TRUE, mean speed at each turbine comes from Weibull rasters; ws in wind is ignored.
weibull_srcNULLlist(k, a) shape and scale rasters (e.g. GWA combined-Weibull-k / combined-Weibull-A).

GA loop

The loop is selection → set-crossover → swap-mutation → fitness.

ArgumentDefaultMeaning
iteration20Generation budget. Raise this for real searches (the gold-layout tests used 80–150).
mutation_rate2/nMutation probability per turbine (swap with an unused cell). Adaptive rates still keep this as the mutate target.
selection_mode"VAR""VAR": parent share follows fitness progress. "FIX": always 50 %.
elitismTRUEArchive the best layout unchanged; elites also spawn children and get local search.
n_elite3Base elite count. Grows in a long refine stall, drops by 1 during a disturbance pulse.
parallelFALSEParallel fitness (parallel + doParallel).
n_cluster2Worker count if parallel is TRUE (capped at cores − 1). On the demo site ~4 workers is fastest (~3× vs sequential); 6–8 is slower and can drop PSOCK connections on Windows.
verboseFALSEPrint a line per generation.
plotFALSEPlot the current best layout each generation.

Session options (options(windfarmGA.*))

Set with options(windfarmGA.foo = …). Defaults are assigned in .onLoad.

Physics (wake and power)

Fitness is E \times (\eta/100)^w. These options change how E and \eta are computed.

OptionDefaultMeaning
windfarmGA.Cp0.45Power coefficient.
windfarmGA.cT0.88Thrust coefficient (wake).
windfarmGA.k0.075Wake decay constant (without terrain).
windfarmGA.air_rh1.225Air density (kg/m³).
windfarmGA.wind_profile"log"Hub-height profile. "power" restores the old power law.
windfarmGA.cut_in0Cut-in wind speed (m/s). 0 = no cut-in.
windfarmGA.rated_wsInfRated wind speed; above this, power stays at rated.
windfarmGA.cut_outInfCut-out wind speed.
windfarmGA.power_curveNULLOptional data.frame(ws, power) in kW. Park energy is the sum of interpolated turbine kW (not Cp * v^3). On the rated plateau, wakes may not change power. Build a table with read_power_curve() or plot_power_curve(). ERA5 u/v: wind_from_uv().
windfarmGA.fitness_efficiency_weight1Preference weight w: fitness is E \times (\eta/100)^w. 0 = energy only; >1 punishes wake more. Not auto-tuned.
windfarmGA.max_angle20Max wake angle (degrees) when assigning downstream turbines.
windfarmGA.max_distance100000Max wake distance (m).

Population caps

OptionDefaultMeaning
windfarmGA.max_population300Upper bound on individuals after crossover.
windfarmGA.max_selection300Upper bound on selected parents.

Crossover, mutation, immigrants

OptionDefaultMeaning
windfarmGA.crossover_inject0.25Share of child cells taken from outside both parents, so the search is not stuck in the parental union. Starting value; adaptive seasons move it.
windfarmGA.spatial_crossover0.5Probability that crossover splits the park with a half-plane instead of a random set mix.
windfarmGA.min_swaps1Minimum cell swaps per mutated individual.
windfarmGA.immigrants3Random new layouts each generation (rarely visited cells preferred).

On the north-wind test, extra immigrants helped the coarse search but lost to stronger neighbour local search for the last percent.

Elitism and neighbour local search

OptionDefaultMeaning
windfarmGA.elite_children3Mutated copies of each elite (inject 0, structure kept). Extra copies if the max has been flat for 10 generations.
windfarmGA.elite_mix2Crossovers of an elite with a weaker layout (inject 0).
windfarmGA.local_search_elites5How many elites get a neighbour slide each generation. 0 turns local search off.
windfarmGA.local_search_tries6Hill-climb tries per elite: move one turbine to a free rook/hex neighbour. Only keep if fitness rises.

This is the memetic step. A random swap across the grid rarely fixes “one row too far inland”; a neighbour slide often does.

Adaptive rates (explore / refine / pulse)

Same three operators; only the rates change. Explore raises inject and mutation while the record is stalling (VAR selection starts near 55 %). Refine damps them toward inject 0.15, mutation 2/n, selection ~45 %. Pulse briefly raises them again so refine does not freeze.

OptionDefaultMeaning
windfarmGA.refine_min_gen18Earliest generation that may enter refine.
windfarmGA.refine_after12Stall generations (no new max) plus coverage ≥ 0.35 before refine.
windfarmGA.refine_hold25Generations spent in refine before a pulse.
windfarmGA.explore_pulse10Length of the disturbance pulse.
windfarmGA.stall_generations40Stop early only if this many generations in a row produce no new layout, no new cell and no new best. 0 disables early stop. A flat maximum while new sites are still tried is not a stop.

Other

OptionDefaultMeaning
windfarmGA.connectionstdin()Where interactive prompts read from (tests redirect this).

Plotting

A result from genetic_algorithm() has class windfarmGA: print(result) summarises the run, plot(result, area) is plot_windfarmGA(), and explore_result(result, area) opens a one-page Shiny viewer with a Leaflet map of the best layout and a plotly figure (fitness, rates, population; click a New max marker to jump to that generation). (Suggests: shiny, leaflet, plotly).

plot_windfarmGA() pages through the best layout, fitness and operator rates, the population census, and the cell heatmap. In an interactive session each page waits for Enter. Plotly hover is used only with ask = FALSE when the plotly package is installed.

print(result)
plot(result, area)
explore_result(result, area)
plot_windfarmGA(result, area)

plot_result(result, area)                                  # best layout
plot_parkfitness(result)                                   # fitness + operator rates
plot_population(result)                                    # individuals, elites, cells
plot_generation(result, area)                              # last generation (omit generation, or pass an index)
plot_cell_heatmap(result, area)                            # which cells were tried
plot_leaflet(result, area, which = 1)                      # best on a map
plot_leaflet(result, area, orderitems = FALSE, which = 1)  # last generation
plot_evolution(result)                                     # energy + efficiency
plot_development(result)                                   # when the max improved

Package data resulthex / resultrect and sp_polygon work the same way (plot_leaflet(resulthex, sp_polygon, which = 1)).

A full documentation of the genetic algorithm is given in my master thesis.

NOTE: The implementation has changed substantially since then. Many functions and arguments have been renamed or optimized, and the internal structure of the algorithm has been reworked. As a result, the current version differs significantly from the implementation described in the thesis and calculates and converges considerably faster.

Shiny Windfarm Optimization

I also made a Shiny App for the Genetic Algorithm. Unfortunately, as an optimization takes quite some time and the app is currently hosted by shinyapps.io under a public license, there is only 1 R-worker at hand. So only 1 optimization can be run at a time.

Copy Link

Version

Install

install.packages('windfarmGA')

Monthly Downloads

14

Version

5.0.0

License

MIT + file LICENSE

Maintainer

Sebastian Gatscha

Last Published

September 14th, 2026

Functions in windfarmGA (5.0.0)

permutations

Enumerate the Combinations or Permutations of the Elements of a Vector
plot_population

Plot population size, cells and efficiency
plot_parkfitness

Fitness and operator rates
plot_development

When the best layout improved
plot_generation

Plot all layouts of one generation
plot_leaflet

Plot a wind warm with leaflet
plot_power_curve

Manufacturer power curve
plot_windfarmGA

Plot the results of an optimization run
plot_windrose

Plot a Windrose
random_search

Randomize the output of the Genetic Algorithm
read_power_curve

Read a manufacturer or NREL/IEA power-curve table
plot_result

Plot the best results
plot_random_search

Plot the result of a randomized output.
plot_viewshed

Plot visibility
population_census

Population size and diversity per generation
random_search_single

Randomize the location of a single turbine
swap_mutation

Swap mutation of turbine layouts
sp_polygon

The rectangular POLYGON used to create resultrect & resulthex
trimton

Adjust the amount of turbines per windfarm
terrain_model

Get terrainhic rasters
turbine_influences

Find potentially influencing turbines
set_crossover

Set crossover of turbine layouts
selection

Selection Method
splitAt

Split matrices or numeric vectors at specific indices
resultrect

A resulting matrix of genetic_algorithm with 50 generations and a rectangular grid derived from sp_polygon
resulthex

A resulting matrix of genetic_algorithm with 10 iterations and a hexagonal grid derived from sp_polygon
wind_from_uv

Wind rose from u/v components
windata_format

Transform Winddata
wind_from_series

Wind rose from speed and direction series
wake_cones

Downwind wake search cones
windfarmGA-package

windfarmGA: Genetic Algorithm for Wind Farm Layout Optimization
as_windfarmGA

Mark a genetic_algorithm result
big_shape

A POLYGON with an area of ~70 km2
circle_intersection

Get area of intersecting circles
barometric_height

Calculates Air Density, Air Pressure and Temperature according to the Barometric Height Formula
explore_result

Shiny explorer for a GA result
crossover

Crossover Method
calculate_energy

Calculate Energy Outputs of Individuals
fitness

Evaluate the Individual Fitness values
generation_layouts

Layouts evaluated in one generation
ga_options

Get or set windfarmGA options
isSpatial

Transform to Simple Feature Polygons
get_grids

Map layouts to grid coordinates
hexa_area

Polygon to Hexagonal Grids
grid_area

Make a grid from a Simple Feature Polygon
genetic_algorithm

Run a Genetic Algorithm to optimize a wind farm layout
init_population

Create a random initial Population
mutation

Mutation Method
get_dist_angles

Calculate distances and angles of possibly influencing turbines
plot_evolution

Energy and efficiency over generations
package_installed

Is the package installed or not
plot_cloud

Per-generation fitness / efficiency / energy
plot_cell_heatmap

Heatmap of probed grid cells
multi_shape

A MULTIPOLYGON with 3 Polygons
hole_shape

A POLYGON with a hole