\documentclass[nojss]{jss} %\documentclass{jss} %\VignetteIndexEntry{wflo} %\VignetteKeyword{wflo, wind farm layout optimization} %\VignettePackage{wflo} %% recommended packages \usepackage{thumbpdf,lmodern} %% author added packages \usepackage{amssymb} \usepackage[utf8]{inputenc} \usepackage{graphicx} %% new custom commands \newcommand{\class}[1]{`\code{#1}'} \newcommand{\fct}[1]{\code{#1()}} \SweaveOpts{engine = R, eps = FALSE, keep.source = TRUE} <>= options(prompt = "R> ", continue = "+ ", width = 70, useFancyQuotes = FALSE) library("MASS") @ %% -- Article metainformation (author, title, ...) ----------------------------- \author{Carsten Croonenbroeck\\Rostock University \And David Hennecke\\Rostock University} \Plainauthor{Carsten Croonenbroeck, David Hennecke} \title{wflo: A New Standard for Wind Farm Layout Optimization in \proglang{R}} \Plaintitle{wflo: A New Standard for Wind Farm Layout Optimization in R} \Shorttitle{wflo: A New Standard for Wind Farm Layout Optimization in \proglang{R}} \Abstract{ We provide a new and unified standard for the field of wind farm layout optimization (WFLO). The problem of optimally arranging turbines within a given area (e.g. irregularly shaped land owned by the potential wind farm operator) has been researched for quite some time. Dependent on the wind direction, a single turbine A may or may not be located within the wake of another turbine B, leading to a (potentially) unnecessary loss of electricity produced at A. Arranging the farm in a way that rules out potential wake losses is a difficult problem because of the interdependencies of the turbines. A comprehensive methodology to solve this problem is yet to be found. Our \proglang{R} package providing both, a high-quality data set as well as useful functionality, will enable researchers to focus on their actual methodology contributions. The package also serves as a benchmark to empirically compare the outcomes of contributed solutions. } \Keywords{WFLO, wind farm layout optimization, NP-hard, package, data set, benchmark, \proglang{R}} \Plainkeywords{WFLO, wind farm layout optimization, NP-hard, package, data set, benchmark, R} \Address{ Carsten Croonenbroeck\\ Rostock University\\ Environmental Science\\ Justus-von-Liebig-Weg 7\\ 18059 Rostock, Germany\\ Tel.: +49 381 498 3267\\ Fax: +49 381 498 3262\\ E-mail: \email{carsten.croonenbroeck@uni-rostock.de}\\ \vspace{5mm}\\ David Hennecke\\ Rostock University\\ Geodesy and Geoinformatics\\ Justus-von-Liebig-Weg 6\\ 18059 Rostock, Germany\\ Tel.: +49 381 498 3204\\ E-mail: \email{david.hennecke@uni-rostock.de}\\ } \begin{document} \SweaveOpts{concordance = TRUE} \section[Introduction]{Introduction}\label{section:Introduction} Wind farm layout optimization (WFLO), i.e. the question of how to optimally arrange a set of wind turbines inside a wind farm, is a problem that has been analyzed for quite some time. In mathematics, this problem is typically seen as a constraint optimization (i.e. maximization or minimization) task. However, while the objective space encompasses a typically rather simple surface in $\mathbb{R}^3$, ``feature space'' is a high-dimensional, mostly non-continuous (hence, non-differentiable), multimodal space of ex ante unknown complexity. Furthermore, as several points in that space are sought-after and these points possess a strongly interdependent behavior due to wake effects in the wind farm, the WFLO problem is considered to be $\mathcal{NP-}$hard (see \citealp{Garey1979} for a definition).\\ Approaches to solving this problem can not only be classified a) according to the wake model used, but also b) according to the class of optimization approaches (gradient-based approaches and gradient-free algorithms), or c) according to the target function class. Most approaches to WFLO incorporate a physical model for inner-farm wake effects. The model by \cite{Jensen1983} is most popular, as it provides a quite accurate approximation of measured wakes in actual farms, despite the model's simplicity. See \cite{Shakoor2016} and the references therein for a comprehensive overview of WFLO literature employing the Jensen model. However, some contributions employ other wake models. For instance, \cite{Kirchner2018} use a Gaussian wake model.\\ Most optimizers employ gradient-free methods, as these methods (e.g. metaheuristic algorithms such as genetic algorithms (GA) or simulated annealing) perform fairly well at exploring the feature space, given that this space is not of too high dimension. For example, \cite{Chen2013}, \cite{Park2019}, and \cite{Yang2019} use these methods. Higher-dimensional spaces however are the domain of gradient-based approaches, as \cite{Thomas2018} point out.\\ Many researchers use wind farm efficiency or annual energy production (AEP) as their target function, e.g. \cite{Park2019}. However, a more realistic approach considers what wind farm operators are really after: Earning money. Therefore, as \cite{Gualtieri2019} points out, AEP maximization is not an immediately meaningful goal. Instead, researchers recently tend to use economically driven target functions. \cite{Wu2020} use a profit-driven function, which also enables them to flexibly incorporate cost or revenue influencing drivers of the optimization problem, e.g. inner-farm wiring. As an alternative, \cite{Yang2019} discuss wake effect uniformity instead of AEP or profit maximization as an alternative target.\\ \cite{Antonini2020} point out the complex terrain aspects of the WFLO problem, an issue that \cite{Thomas2018} try to relax. However, as this is just a means of modifying the problem at hand to ease its handling, it even emphasizes the necessity for a unified setting and goal in order to focus on the methodology to solve that goal. Thus, researchers even start WFLO competitions to have participants compete on a unified setting. \cite{Baker2019} show the outcome of such a competition. Their competition does not only provide a unified target and unified data, it also serves as a benchmark that every participant can use to evaluate his/her own methodology idea/contribution.\\ Using \verb"R" by \cite{R}, package \textbf{windfarmGA} provides a genetic algorithm implementation to deal with WFLO. The methodology can be explored at \url{https://windfarmga.shinyapps.io/windga_shiny/} as well. However, as this is an approach at solving the problem, it still does not fully describe the problem itself, nor does it provide a data set or a benchmark. At this point, package \textbf{wflo} (see \url{https://CRAN.R-project.org/package=wflo}) comes into place: It provides high-quality and highly accurate data, which can be used for both, developing a methodology as well as benchmarking it. It does not, however, provide an actual optimization solution. \textbf{wflo} is merely a sandbox for solution researchers. The package also provides a set of modular functions around the WFLO problem, providing an economically driven target function implementing Jensen's multiple and partial wake effects model, plug-in functions for cost and revenue and a set of parameters. In this article, we discuss the components of package \textbf{wflo} and its intended usage in great detail. Section \ref{section:Data and Functions} presents the data and sheds light on the functions and their utility. Section \ref{section:Use Cases} provides usage examples and section \ref{section:Conclusion} draws a brief conclusion. \section{Data and Functions}\label{section:Data and Functions} \subsection{Data Description}\label{subsection:Data Description} Package \textbf{wflo} deals with a high-quality data set available in a list object \verb"FarmData" after the package is installed and loaded within \verb"R". The data stem from DWD (Deutscher Wetterdienst, German Meteorological Service) and NASA (National Aeronautics and Space Administration, SRTM data set, Shuttle Radar Topography Mission) and have been preprocessed, i.e. the meteorologic measurement station data are spatially interpolated using Geographic Information System (GIS) software\footnote{We use the inverse distance weighted (IDW) method, see \url{https://desktop.arcgis.com/en/arcmap/10.3/tools/3d-analyst-toolbox/how-idw-works.htm} for documentation.} and the wind direction data are temporally averaged by transforming polar coordinates to cartesian coordinates and then computing the vector averages.\\ The package comes with a subset of the entire data set. Due to CRAN\footnote{Comprehensive R Archive Network, the worldwide repository network for R packages.} package size limitations, it is not possible to enclose the entire data set within the package. However, the full data set can be downloaded conveniently using the built-in function \verb"AcquireData()". As a parameter, the function accepts the directory to where the file will be saved. For example, the user may choose to use the current working directory obtained via \verb"getwd()" as a target directory. After file \verb"FarmData.RData" is downloaded, it is loaded automatically. While the reduced, built-in data set is accessible via list object \verb"FarmData", once the full data set is available, it replaces the built-in data set by being embedded into user writeable environment \verb"e", easily accessible via \verb"e$FarmData". Therefore, one object does not mask the other. All subsequent functions check for the full data set being present in environment \verb"e" accordingly. On load, the package checks whether the file is present in the current working directory and if so, loads the file automatically. Otherwise, \textbf{wflo} automatically falls back to basic mode, using the built-in data set. The functionality described in this article works perfectly with the built-in data set. Downloading the full file is only necessary for cross-checks or advanced tests of a self-implemented optimizer. WFLO research is perfectly possible with the built-in data set.\\ List object \verb"FarmData" (or \verb"e$FarmData") has seven slots. Each of them contains a matrix that has 4,400 $\times$ 3,250 elements in full mode and 25 $\times$ 25 elements in basic mode (built-in data set). The subset in basic mode is ``carved'' out of the entire data set at position [2000:2024, 2000:2024], so it is nested in the full data set. The 4,400 $\times$ 3,250 elements in full mode cover the entire area of Germany. They represent a raster data set at a rather accurate resolution of 200 $\times$ 200 meters. Slot 1 of the list object, named \verb"AdjustedYield", contains adjusted potential AEP (``yield'') for German onshore sites. AEP is computed based on FGW technical guidelines (F\"ordergesellschaft Windenergie und andere Dezentrale Energien, support organization for wind energy and other types of decentralized energy). Those technical guidelines are mandatory to use in Germany, are stipulated in the German Renewable Energy Act (\citealp{EEG2017}), and are obtainable at \url{https://wind-fgw.de/shop/technical-guidelines/?lang=en}. The guidelines themselves are based on IEC 61400-12-1, the international standard on ``power performance measurements of electricity producing wind turbines'' by the International Electrotechnical Commission, IEC. According to \cite{EEG2017}, AEP computed that way must be multiplied by a correction factor. That factor is based on a specific location's quality in order to compensate for several market regulations in Germany. Users can interpret the contained values immediately as AEP in ``megawatt hours per year'' (MWh/a), however we decide to name that variable \textit{adjusted} yield, as researchers computing AEP on their own may obtain slightly different values. Figure \ref{graph:Adj} visualizes the data.\\ Slot 2, named \verb"WindSpeed", contains temporally averaged wind speed in meters per second (m/s), while slot 3, named \verb"WindDirection", contains temporally averaged wind direction in degrees (azimuth). Figure \ref{graph:WD_WS} presents both in a joint image. Slot 4, named \verb"SDDirection", contains the standard deviations of wind directions after temporally averaging. So far, standard deviations are not used anywhere in the package. However, they may provide important insight into the volatility of wind directions at each point and may thus be relevant for, e.g., computing confidence bands. Slot 5 (\verb"Elevation") holds the terrain elevation in meters, slot 6 (\verb"Slope") provides terrain slope in degrees, and slot 7 (\verb"SlopeDirection") gives the direction of hillside, in degrees as well. Figure \ref{graph:Slots} presents a joint image plot for the data in the seven slots (built-in data set).\\ The second data object in the \verb"e" environment installed by \textbf{wflo} is \verb"e$FarmVars". This list object contains a set of variables required for the functions of \textbf{wflo} and, among other things, controls the ``data window'' of consideration if the full data set is present. \verb"$StartPoint", \verb"$EndPoint" and \verb"$Width" control which area from the data set is used as the current wind farm area. In general, \verb"$Width" describes width (and height, as the area is assumed to be a square) of the area used, in raster points. As the raster resolution is 200 m, the default value of \verb"$Width" = 25 results in a square of 5 $\times$ 5 km. For convenience, \verb"e$FarmVars" also contains the farm size in meters in its variable \verb"$MeterWidth", which is computed as \begin{equation} \footnotesize\verb"e$FarmVars$MeterWidth <- e$FarmVars$Width * 200". \end{equation} In full mode (downloaded data set), \verb"$StartPoint" = 2000, while in basic mode (built-in data set), \verb"$StartPoint" = 1. In both cases, \verb"$EndPoint" is computed based on \verb"$StartPoint" and \verb"$Width" following \begin{equation} \footnotesize\verb"e$FarmVars$EndPoint <- e$FarmVars$StartPoint + e$FarmVars$Width - 1", \end{equation} which results in \verb"e$FarmVars$EndPoint" = 2024 in full mode and \verb"e$FarmVars$EndPoint" = 24 in basic mode. Furthermore, \verb"e$FarmVars" contains \verb"$MeterMinDist", which holds the minimum distance between any two turbines in the wind farm. Its default value is 500 meters. As internally, the wind farm is considered to be a unit square (automatically and irrespective of the current \verb"$StartPoint", \verb"$Width", \verb"$EndPoint", and \verb"$MeterWidth"), \verb"e$FarmVars" also contains a variable \verb"$MinDist" for the minimum distance in unit space as \begin{equation} \footnotesize\verb"e$FarmVars$MinDist <- e$FarmVars$MeterMinDist / e$FarmVars$MeterWidth", \end{equation} which consequently defaults to 500 / 5000 = 0.1, i.e. one tenth of the farm square's height or width. \verb"e$FarmVars" also contains variables \verb"$z", \verb"$z0", and \verb"$r0", which contain the turbines' hub height in meters (defaults to \verb"e$FarmVars$z" = 100), the terrain's roughness length required for Jensen's wake model (defaults to \verb"e$FarmVars$z0" = 0.1), and the turbines' rotor radius in meters (defaults to \verb"e$FarmVars$r0" = 45). Users should adjust these values according to their turbine specifications or use the default values for benchmarking. Variable \verb"e$FarmVars$Partial" is a boolean which controls whether \verb"Profit()" will compute the partial Jensen wake model (see below). Furthermore, \verb"e$FarmVars" contains the variables \verb"$UnitCost" and \verb"$Price". \verb"$UnitCost" is meant to carry a single turbine's yearly cost. If, for example, total turbine installation costs are 2 mio. EUR and the projected life span of the turbine is 20 years, then the yearly cost is roughly 100,000 EUR, which is also the default value for this item. Additional cost components such as wiring, (non-linear) amortization, maintenance, opportunity cost, or others can be modeled using an alternative cost function which can be plugged in easily (see below). Similarly, \verb"$Price" contains the sale price per megawatt hour. This defaults to an empirically reasonable average of 100 EUR. However, if revenue is to be modeled in a more sophisticated fashion than using ``constant price times sale volume'', the necessary adjustments are easily performed (once again, see below). Finally, \verb"e$FarmVars" contains a vector object \verb"$BenchmarkSolution", which holds 40 values (20 pairs of x and y coordinates) representing a setup of 20 turbines within the standard data set terrain. It has been obtained using optimizer \verb"genoud()" of package \textbf{rgenoud} by \cite{Mebane2011} and provides a standard of comparison for future optimizers or can be used as starting points for further exploring the feature space.\\ Changing, say, \verb"$r0" or \verb"$z", but more immediately, \verb"$StartPoint" and \verb"$EndPoint", turns the problem into an entirely new setup. For instance, assuming the full data set is present, selecting <>= e$FarmVars$StartPoint <- 1800 e$FarmVars$Width <- 100 e$FarmVars$EndPoint <- e$FarmVars$StartPoint + e$FarmVars$Width - 1 e$FarmVars$MeterWidth <- 200 * e$FarmVars$Width e$FarmVars$MinDist <- e$FarmVars$MeterMinDist / e$FarmVars$MeterWidth @ provides an entirely new (and larger) testing ground. A huge multitude of optimization areas is accessible that way, even allowing to immediately select costal regions, mountain regions, or areas known to be especially apt or unsuitable to wind power plants. \subsection{Functions}\label{subsection:Functions} The central function to \textbf{wflo} is \verb"Profit()". It takes a set of points in the unit square (turbine locations) and based on the adjusted AEP in the farm specified via the \verb"e$FarmVars" settings object, checks whether the layout is valid (i.e. minimum distances are met for all turbines), computes multiple Jensen wake penalties (and, by default, also the partial wake penalty as originally described by \citealp{Frandsen1992} and, more recently, by \citealp{Abdulrahman2017}, taking only partial wake cones coverage of rotor discs into account), generates the total marketable power production of the specified layout, takes cost and sale price into consideration and finally computes the farm's economic profit. Since most numeric optimizers by default operate as a minimizer, \verb"Profit()" returns the negative profit, i.e. a negative number for positive actual profit values and a positive number if cost is greater than revenue (negative actual profit).\\ \verb"Profit()" calls \verb"Cost()", which is a stub function only returning the \verb"$UnitCost" value from the \verb"FarmVars" object. For more sophisticated cost components, users should replace the \verb"Cost()" function by their own cost model. This can be done by embedding another function \verb"Cost()" in the \verb"e" environment. \verb"Profit()" will then operate on it immediately. Note that \verb"Cost()" must be a function that is provided with \verb"x" and \verb"y" (the turbine coordinates in the unit square) and both, \verb"x" and \verb"y", must be $\in \mathbb{R}^n$, i.e. both variables are vectors of length \verb"n" (the number of turbines). An example for such a workflow is given in Section \ref{section:Use Cases}. Similarly, \verb"Yield()" is a stub looking up the adjusted AEP based on a given turbine location. If yield or revenue should follow a more sophisticated model, replacing \verb"Yield()" by that model would be the way to go. Again, a plug-in yield function must be embedded in \verb"e", and \verb"Profit()" will use it accordingly. In contrast to \verb"Cost()", \verb"Yield()" must be a function provided with \verb"x", \verb"y", and \verb"AEP". Here, \verb"x" and \verb"y" must be $\in \mathbb{R}$, i.e. single-valued variables for one turbine at a time. \verb"AEP" will carry the AEP values for the wind farm. The user function is not required to evaluate this, the object is just provided for convenience. Note, however, that according to the calling convention, the function must have a prototype declaration that expects that object.\\ \verb"PlotResult()" is a convenience function that takes an optimizer result, i.e. an object as returned by \verb"optim()", visualizes the adjusted yield ``landscape'' in the current wind farm setting, superimposes a contour plot and a vector field representing the wind directions and then draws the provided points (turbines). For example, a usual optimization run can be performed in the first place via <>= library(wflo) NumTurbines <- 4 set.seed(2763) Result <- optim(par = runif(NumTurbines * 2), fn = Profit, method = "L-BFGS-B", lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 10)) Result @ Afterward, a call to \verb"PlotResult(Result)" will produce the image in Figure \ref{graph:PlotResult}. Lighter shades of blue indicate larger values of AEP, darker ones indicate low AEP values. The actual AEP values are also indicated via the contour lines. The area selected represents a rather complex AEP ``terrain'', a challenging task to optimizers. The light arrows indicate the main wind directions, while the grey arrows add/subtract half the standard deviation to/from the main direction to indicate the direction volatility. The gold dots, finally, show the turbines' arrangement. Figure \ref{graph:Big_setup} shows the alternative testing ground presented in Section \ref{subsection:Data Description}, together with a (not necessarily optimal) solution of 20 turbines (gold dots).\\ Another ``post-optimization'' result inspection functionality is provided by \verb"ShowWakePenalizers()". A call to \verb"ShowWakePenalizers(Result)", where again \verb"Result" is the optimizer's output (list object containing at least a ``\verb"$par"'' slot), draws a reduced field (neglecting terrain and contour, yet imposing wind directions) and in gold, draws all points (turbines) that are in the wake of other turbines (called ``sufferers'' in the plot's legend) while in grey, all points are drawn causing wake effects on other points (called ``causers''). The Jensen cones imposed on the sufferers are drawn in red. Figure \ref{graph:ShowWake} shows an example. Using \verb"ShowWakePenalizers()", it can be easily inspected whether the optimizer puts emphasis on avoiding wake effects or, vice versa, whether an optimizer finds an optimal arrangement \textit{although} it means that some points cause wake effects on others.\\ Function \verb"ProfitContributors()" computes the profit contribution of each point (turbine) as a part of an optimization result. That way, the setup location and wake influence of each point can be analyzed in greater detail. Calling \verb"ProfitContributors(Result)" returns a matrix with two columns and as many rows as points present in the provided list object. The first column represents turbine IDs, the second column contains the respective profit contributions of each point. If the solution provided is valid in terms of the minimum distance criterion, the sum of values in the second column will be identical to the (absolute value of) the returned value of \verb"Profit()". \begin{figure}[t!] \centering \includegraphics[width=12cm]{Adj}\\ \caption{Adjusted yield (AEP) in Germany.} \label{graph:Adj} \end{figure} \begin{figure}[t!] \centering \includegraphics[width=13cm]{WD_WS}\\ \caption{Wind speed and wind direction in Germany.} \label{graph:WD_WS} \end{figure} \begin{figure}[t!] \centering <>= Oldpar <- par(no.readonly = TRUE) par(mfrow = c(4, 2), mar = c(0, 0, 0, 0), mai = c(0.2, 0.2, 0.2, 0.2)) MyNames <- c("AEP", "Wind speed", "Wind direction", "SD of wind direction", "Elevation", "Slope", "Hillside") for (i in 1:7) { X <- FarmData[[i]][1:25, 1:25] image(X, main = MyNames[i], xaxt = "n", yaxt = "n", bty = "n", col = rgb(0, 0, seq(from = 0, to = 1, length.out = e$FarmVars$Width * e$FarmVars$Width))) } par(Oldpar) @ \caption{Image plots for the seven slots in FarmData. Lighter shades of blue indicate larger values, darker ones indicate low values.} \label{graph:Slots} \end{figure} \begin{figure}[t!] \centering \includegraphics[width=13cm]{Fig4}\\ \caption{PlotResult() showing the benchmark area, four turbines, L-BFGS-B optimizer.} \label{graph:PlotResult} \end{figure} \begin{figure}[t!] \centering \includegraphics[width=13cm]{Big_setup}\\ \caption{Alternative testing ground, together with a first solution.} \label{graph:Big_setup} \end{figure} \begin{figure}[t!] \centering <>= ShowWakePenalizers(Result) @ \caption{ShowWakePenalizers() showing the wake structure in the benchmark area, four turbines, L-BFGS-B optimizer.} \label{graph:ShowWake} \end{figure} \section{Use Cases}\label{section:Use Cases} It is the foremost intention of \textbf{wflo} to be compatible to the \verb"R" optimization quasi-standard, i.e. optimizers are minimizers, they expect start values and the function to optimize as their parameters and that function is to be provided with nothing but a vector object of values. The optimizer returns a list which contains at least \verb"$par" and \verb"$value". With \textbf{wflo}, researchers in WFLO can focus on developing and providing a standard-compliant optimizer and use it to measure its performance on the wind farm setting provided by the package. An actual \textbf{wflo} workflow of course implies that the user utilizes a self-developed plug-in optimization function. Since none is available here, we merely show a set of sample workflows based on pre-existing optimizers. As can be seen, a \textbf{wflo} use case is as simple as just minimizing \verb"Profit()" using an optimizer. Post-optimization analysis can be performed using \verb"PlotResult()", \verb"ShowWakePenalizers()", \verb"ProfitContributors()", and all arbitrary analysis procedures for optimizers.\\ In \verb"R", most optimizers observe the quasi-standard described above. The only additional requirement for \textbf{wflo} is that the optimizer respects box constraints. However, for optimizers that do not, a crude wrapper is presented below. As a first use case, package \textbf{nloptr} by \cite{NLopt} provides a controlled random search optimizer (CRS). It can be utilized like this: <>= library(nloptr) set.seed(1357) Result <- crs2lm(x0 = runif(NumTurbines * 2), fn = Profit, lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), maxeval = 1000) Result @ Many WFLO researchers use genetic algorithms to optimize their wind farms. In \verb"R", a sophisticated GA implementation is provided via package \textbf{rgenoud}: <>= library(rgenoud) set.seed(1357) Dom = cbind(rep(0, 2 * NumTurbines), rep(1, 2 * NumTurbines)) Result <- genoud(fn = Profit, nvars = 2 * NumTurbines, starting.values = runif(NumTurbines * 2), Domains = Dom, boundary.enforcement = 2, print.level = 0, max.generations = 10) Result @ Finally, particle swarm optimization (PSO) is another metaheuristic frequently used in WFLO. Package \textbf{pso} by \cite{pso} provides a straight-forward implementation: <>= library(pso) set.seed(1357) Result <- psoptim(par = runif(NumTurbines * 2), fn = Profit, lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 100)) Result @ For optimizers that do not provide box-constraint compliance, e.g. the SANN (simulated annealing) optimizer that is part of \verb"optim()", a simple wrapper can look like this: <>= lower <- rep(0, NumTurbines * 2) upper <- rep(1, NumTurbines * 2) Wrapper <- function(X) { xSel <- seq(from = 1, to = length(X) - 1, by = 2) x <- X[xSel] y <- X[xSel + 1] if (any(x < lower) | any(x > upper) | any(y < lower) | any(y > upper)) { return(sum(rep(e$FarmVars$UnitCost, length(x)))) } return(Profit(X)) } set.seed(1357) Result <- optim(par = runif(NumTurbines * 2), fn = Wrapper, method = "SANN", control = list(maxit = 1000)) Result @ Returning the sum of \verb"$UnitCost" if any point violates the constraints may seem crude. More sophisticated ways to deal with that are up to the researcher. For now, this simple wrapper gets the job done.\\ Replacing \verb"Cost()" and \verb"Yield()" functions by user defined code is straightforward and enables the researcher to incorporate more sophisticated models for these components, still using the integrated \textbf{wlfo} workflow. For example, <>= e$Cost <- function(x, y) #x, y \in R^n { retVal <- rep(e$FarmVars$UnitCost, min(length(x), length(y))) retVal[x > 0.5] <- retVal[x > 0.5] * 2 return(retVal) } @ embeds a cost function that overly punishes turbine locations on the right (or eastern) half of the unit square. There is no fundamental reason to this as it is just a crude and simple proof-of-concept example. In practice, users may want to setup a function that instead encompasses, e.g., (possibly non-linear) maintenance or opportunity cost. As to be expected, an optimizer run <>= set.seed(1357) Result <- psoptim(par = runif(NumTurbines * 2), fn = Profit, lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 100)) Result rm(Cost, envir = e) @ returns locations on the left (or western) half of the wind farm. Note the last code line to remove the superimposed cost function. A more realistic idea for a cost based model could be that in many cases, turbine locations should a) keep their minimum distances, b) possess as little wake as possible, and c) still be as close to one another as possible to at the same time minimize wiring cost. A cost function that obtains that may be <>= e$Cost <- function(x, y) { n <- min(length(x), length(y)) retVal <- rep(e$FarmVars$UnitCost, n) DistMat <- matrix(ncol = n, nrow = n) for (i in 1:n) { for (j in 1:n) { DistMat[i, j] <- sqrt((x[i] - x[j]) ^ 2 + (y[i] - y[j]) ^ 2) } } SumDist <- as.numeric() for (i in 1:n) SumDist[i] <- sum(DistMat[i, ]) retVal <- retVal * SumDist return(retVal) } rm(Cost, envir = e) @ Similarly to \verb"Cost()", users can incorporate an alternative yield function that provides yield for each turbine individually. \verb"Profit()" will automatically impose Jensen wake penalties on the result. For instance, <>= e$Yield <- function(x, y, AEP) #x, y \in R { return(x + y) } @ is again a crude example. The function does not even evaluate AEP and provides a strong tendency of the turbine layout result to locate turbines in the topright corner, where both, x and y, take their largest values, however still keeping their minimum distance and still subject to wake penalties. A run <>= set.seed(1357) Result <- psoptim(par = runif(NumTurbines * 2), fn = Profit, lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 100)) Result rm(Yield, envir = e) @ returns a result as expected in that sense. Note, again, the last code line to remove the superimposed yield function.\\ The \verb"value"s returned by optimizers operating on the standard cost and yield functions through \verb"Profit()" can be interpreted as negative profits in currency (Euros) immediately. As pso, CRS, and genoud each return a good result of \verb"-2445920" (pso setup shown in Figure \ref{graph:PSO-Fig}), the setups found by these optimizers generate 2.4 mio. Euros of profit per year. The setup found via SANN generates 2.3 mio. Euros, a simple L-BFGS-B approach only 2.1 mio. Euros. Optimizers should compete in terms of the returned \verb"value"s, higher (absolute) profits are better.\\ Using function \verb"ProfitContributors()", the influence of each turbine on the total profit can be analyzed using the following code chunk, while using the second column values as labels for \verb"PlotResult()" results in Figure \ref{graph:TurbsContribs}: <>= NumTurbines <- 4 set.seed(1235) Result <- optim(par = runif(NumTurbines * 2), fn = Profit, method = "L-BFGS-B", lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 10)) Contribs <- ProfitContributors(Result) Contribs PlotResult(Result, DoLabels = TRUE, Labels = Contribs[, 2]) @ We emphasize that increasing the number of turbines to a more reasonable number increases feature space dimension, and with it, the necessity for increased iterations, population sizes, generations, or whatever tuning parameter necessary for the optimizer to more profoundly explore the problem space. Gradient-based optimizers might come in handy, then. So far, for a 20 turbines setting, \verb"e$FarmVars$BenchmarkSolution" provides an orientation (the plots are shows in Figures \ref{graph:20TurbsPlot} and \ref{graph:20TurbsWake}, respectively): <>= Result <- list(par = e$FarmVars$BenchmarkSolution) Result$value <- Profit(Result$par) Result PlotResult(Result) ShowWakePenalizers(Result) @ \begin{figure}[t!] \centering <>= set.seed(1357) Result <- psoptim(par = runif(NumTurbines * 2), fn = Profit, lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 100)) PlotResult(Result) @ \caption{PlotResult() showing the pso optimizer result, four turbines.} \label{graph:PSO-Fig} \end{figure} \begin{figure}[t!] \centering <>= NumTurbines <- 4 set.seed(1235) Result <- optim(par = runif(NumTurbines * 2), fn = Profit, method = "L-BFGS-B", lower = rep(0, NumTurbines * 2), upper = rep(1, NumTurbines * 2), control = list(maxit = 10)) MyLabels <- ProfitContributors(Result) PlotResult(Result, DoLabels = TRUE, Labels = MyLabels[, 2]) @ \caption{PlotResult() showing an L-BFGS-B result (four turbines), imposing the values returned by ProfitContributors() as labels.} \label{graph:TurbsContribs} \end{figure} \begin{figure}[t!] \centering <>= Result <- list(par = e$FarmVars$BenchmarkSolution) PlotResult(Result) @ \caption{PlotResult() showing the 20 turbines benchmark setup result.} \label{graph:20TurbsPlot} \end{figure} \begin{figure}[t!] \centering <>= Result <- list(par = e$FarmVars$BenchmarkSolution) ShowWakePenalizers(Result) @ \caption{ShowWakePenalizers() showing the 20 turbines benchmark wake structure result.} \label{graph:20TurbsWake} \end{figure} \section{Conclusion}\label{section:Conclusion} \textbf{wflo} is an approach to provide a level playing field for researchers in WFLO: It makes the necessary data available at high quality and contains a useful set of functions that takes the necessity to implement a wake model and a specific problem function away from the researcher, enabling her/him to entirely focus on the actual job, which is the optimizer itself. Furthermore, a unified benchmark is provided.\\ A number of turbines to place of around four is a good start to obtain first experience with \textbf{wflo}. Things become however really challenging if, say, 20 turbines are to be placed. Every WFLO researcher is invited to try her/his approach with \textbf{wflo} to get to see how competitive the contributed approach really is. \clearpage \bibliography{refs} \end{document}