{
"cells": [
{
"cell_type": "markdown",
"id": "0d85e158",
"metadata": {},
"source": [
"# Example: Optimize yaw with neighbor farm"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2f7f0a9d",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"Example: Optimize yaw with neighbor farm\n",
"\n",
"This example demonstrates how to optimize the yaw angles of a subset of turbines\n",
"in order to maximize the annual energy production (AEP) of a wind farm. In this\n",
"case, the wind farm is part of a larger collection of turbines, some of which are\n",
"part of a neighboring farm. The optimization is performed in two ways: first by\n",
"accounting for the wakes of the neighboring farm (while not including those turbines)\n",
"in the optimization as a target of yaw angle changes or including their power\n",
"in the objective function. In th second method the neighboring farms are removed\n",
"from FLORIS for the optimization. The AEP is then calculated for the optimized\n",
"yaw angles (accounting for and not accounting for the neighboring farm) and compared\n",
"to the baseline AEP.\n",
"\"\"\"\n",
"\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from floris import (\n",
" FlorisModel,\n",
" TimeSeries,\n",
" WindRose,\n",
")\n",
"from floris.optimization.yaw_optimization.yaw_optimizer_sr import YawOptimizationSR\n",
"\n",
"\n",
"# Load the wind rose from csv\n",
"wind_rose = WindRose.read_csv_long(\n",
" \"../inputs/wind_rose.csv\", wd_col=\"wd\", ws_col=\"ws\", freq_col=\"freq_val\", ti_col_or_value=0.06\n",
")\n",
"\n",
"# Load FLORIS\n",
"fmodel = FlorisModel(\"../inputs/gch.yaml\")\n",
"\n",
"# Specify a layout of turbines in which only the first 10 turbines are part\n",
"# of the farm to be optimized, while the others belong to a neighboring farm\n",
"X = (\n",
" np.array(\n",
" [\n",
" 0.0,\n",
" 756.0,\n",
" 1512.0,\n",
" 2268.0,\n",
" 3024.0,\n",
" 0.0,\n",
" 756.0,\n",
" 1512.0,\n",
" 2268.0,\n",
" 3024.0,\n",
" 0.0,\n",
" 756.0,\n",
" 1512.0,\n",
" 2268.0,\n",
" 3024.0,\n",
" 0.0,\n",
" 756.0,\n",
" 1512.0,\n",
" 2268.0,\n",
" 3024.0,\n",
" 4500.0,\n",
" 5264.0,\n",
" 6028.0,\n",
" 4878.0,\n",
" 0.0,\n",
" 756.0,\n",
" 1512.0,\n",
" 2268.0,\n",
" 3024.0,\n",
" ]\n",
" )\n",
" / 1.5\n",
")\n",
"Y = (\n",
" np.array(\n",
" [\n",
" 0.0,\n",
" 0.0,\n",
" 0.0,\n",
" 0.0,\n",
" 0.0,\n",
" 504.0,\n",
" 504.0,\n",
" 504.0,\n",
" 504.0,\n",
" 504.0,\n",
" 1008.0,\n",
" 1008.0,\n",
" 1008.0,\n",
" 1008.0,\n",
" 1008.0,\n",
" 1512.0,\n",
" 1512.0,\n",
" 1512.0,\n",
" 1512.0,\n",
" 1512.0,\n",
" 4500.0,\n",
" 4059.0,\n",
" 3618.0,\n",
" 5155.0,\n",
" -504.0,\n",
" -504.0,\n",
" -504.0,\n",
" -504.0,\n",
" -504.0,\n",
" ]\n",
" )\n",
" / 1.5\n",
")\n",
"\n",
"# Turbine weights: we want to only optimize for the first 10 turbines\n",
"turbine_weights = np.zeros(len(X), dtype=int)\n",
"turbine_weights[0:10] = 1.0\n",
"\n",
"# Now reinitialize FLORIS layout\n",
"fmodel.set(layout_x=X, layout_y=Y)\n",
"\n",
"# And visualize the floris layout\n",
"fig, ax = plt.subplots()\n",
"ax.plot(X[turbine_weights == 0], Y[turbine_weights == 0], \"ro\", label=\"Neighboring farms\")\n",
"ax.plot(X[turbine_weights == 1], Y[turbine_weights == 1], \"go\", label=\"Farm subset\")\n",
"ax.grid(True)\n",
"ax.set_xlabel(\"x coordinate (m)\")\n",
"ax.set_ylabel(\"y coordinate (m)\")\n",
"ax.legend()\n",
"\n",
"# Indicate turbine 0 in the plot above with an annotation arrow\n",
"ax.annotate(\n",
" \"Turbine 0\",\n",
" (X[0], Y[0]),\n",
" xytext=(X[0] + 100, Y[0] + 100),\n",
" arrowprops={'facecolor':\"black\", 'shrink':0.05},\n",
")\n",
"\n",
"\n",
"# Optimize the yaw angles. This could be done for every wind direction and wind speed\n",
"# but in practice it is much faster to optimize only for one speed and infer the rest\n",
"# using a rule of thumb\n",
"time_series = TimeSeries(\n",
" wind_directions=wind_rose.wind_directions, wind_speeds=8.0, turbulence_intensities=0.06\n",
")\n",
"fmodel.set(wind_data=time_series)\n",
"\n",
"# CASE 1: Optimize the yaw angles of the included farm while accounting for the\n",
"# wake effects of the neighboring farm by using turbine weights\n",
"\n",
"# It's important here to do two things:\n",
"# 1. Exclude the downstream turbines from the power optimization goal via\n",
"# turbine_weights\n",
"# 2. Prevent the optimizer from changing the yaw angles of the turbines in the\n",
"# neighboring farm by limiting the yaw angles min max both to 0\n",
"\n",
"# Set the yaw angles max min according to point(2) above\n",
"minimum_yaw_angle = np.zeros(\n",
" (\n",
" fmodel.n_findex,\n",
" fmodel.n_turbines,\n",
" )\n",
")\n",
"maximum_yaw_angle = np.zeros(\n",
" (\n",
" fmodel.n_findex,\n",
" fmodel.n_turbines,\n",
" )\n",
")\n",
"maximum_yaw_angle[:, :10] = 30.0\n",
"\n",
"\n",
"yaw_opt = YawOptimizationSR(\n",
" fmodel=fmodel,\n",
" minimum_yaw_angle=minimum_yaw_angle, # Allowable yaw angles lower bound\n",
" maximum_yaw_angle=maximum_yaw_angle, # Allowable yaw angles upper bound\n",
" Ny_passes=[5, 4],\n",
" exclude_downstream_turbines=True,\n",
" turbine_weights=turbine_weights,\n",
")\n",
"df_opt_with_neighbor = yaw_opt.optimize()\n",
"\n",
"# CASE 2: Repeat the optimization, this time ignoring the wakes of the neighboring farm\n",
"# by limiting the FLORIS model to only the turbines in the farm to be optimized\n",
"f_model_subset = fmodel.copy()\n",
"f_model_subset.set(\n",
" layout_x=X[:10],\n",
" layout_y=Y[:10],\n",
")\n",
"yaw_opt = YawOptimizationSR(\n",
" fmodel=f_model_subset,\n",
" minimum_yaw_angle=0, # Allowable yaw angles lower bound\n",
" maximum_yaw_angle=30, # Allowable yaw angles upper bound\n",
" Ny_passes=[5, 4],\n",
" exclude_downstream_turbines=True,\n",
")\n",
"df_opt_without_neighbor = yaw_opt.optimize()\n",
"\n",
"\n",
"# Calculate the AEP in the baseline case\n",
"# Use turbine weights again to only consider the first 10 turbines power\n",
"fmodel.set(wind_data=wind_rose)\n",
"fmodel.run()\n",
"farm_power_baseline = fmodel.get_farm_power(turbine_weights=turbine_weights)\n",
"aep_baseline = fmodel.get_farm_AEP(turbine_weights=turbine_weights)\n",
"\n",
"\n",
"# Now need to apply the optimal yaw angles to the wind rose to get the optimized AEP\n",
"# do this by applying a rule of thumb where the optimal yaw is applied between 6 and 12 m/s\n",
"# and ramped down to 0 above and below this range\n",
"\n",
"# Grab wind speeds and wind directions from the fmodel. Note that we do this because the\n",
"# yaw angles will need to be n_findex long, and accounting for the fact that some wind\n",
"# directions and wind speeds may not be present in the wind rose (0 frequency) and aren't\n",
"# included in the fmodel\n",
"wind_directions = fmodel.wind_directions\n",
"wind_speeds = fmodel.wind_speeds\n",
"n_findex = fmodel.n_findex\n",
"\n",
"yaw_angles_wind_rose_with_neighbor = np.zeros((n_findex, fmodel.n_turbines))\n",
"yaw_angles_wind_rose_without_neighbor = np.zeros((n_findex, fmodel.n_turbines))\n",
"for i in range(n_findex):\n",
" wind_speed = wind_speeds[i]\n",
" wind_direction = wind_directions[i]\n",
"\n",
" # Interpolate the optimal yaw angles for this wind direction from df_opt\n",
" id_opt_with_neighbor = df_opt_with_neighbor[\"wind_direction\"] == wind_direction\n",
" id_opt_without_neighbor = df_opt_without_neighbor[\"wind_direction\"] == wind_direction\n",
"\n",
" # Get the yaw angles for this wind direction\n",
" yaw_opt_full_with_neighbor = np.array(\n",
" df_opt_with_neighbor.loc[id_opt_with_neighbor, \"yaw_angles_opt\"]\n",
" )[0]\n",
" yaw_opt_full_without_neighbor = np.array(\n",
" df_opt_without_neighbor.loc[id_opt_without_neighbor, \"yaw_angles_opt\"]\n",
" )[0]\n",
"\n",
" # Extend the yaw angles from 10 turbine to n_turbine by filling with 0s\n",
" # in the case of the removed neighboring farms\n",
" yaw_opt_full_without_neighbor = np.concatenate(\n",
" (yaw_opt_full_without_neighbor, np.zeros(fmodel.n_turbines - 10))\n",
" )\n",
"\n",
" # Now decide what to do for different wind speeds\n",
" if (wind_speed < 4.0) | (wind_speed > 14.0):\n",
" yaw_opt_with_neighbor = np.zeros(fmodel.n_turbines) # do nothing for very low/high speeds\n",
" yaw_opt_without_neighbor = np.zeros(\n",
" fmodel.n_turbines\n",
" ) # do nothing for very low/high speeds\n",
" elif wind_speed < 6.0:\n",
" yaw_opt_with_neighbor = (\n",
" yaw_opt_full_with_neighbor * (6.0 - wind_speed) / 2.0\n",
" ) # Linear ramp up\n",
" yaw_opt_without_neighbor = (\n",
" yaw_opt_full_without_neighbor * (6.0 - wind_speed) / 2.0\n",
" ) # Linear ramp up\n",
" elif wind_speed > 12.0:\n",
" yaw_opt_with_neighbor = (\n",
" yaw_opt_full_with_neighbor * (14.0 - wind_speed) / 2.0\n",
" ) # Linear ramp down\n",
" yaw_opt_without_neighbor = (\n",
" yaw_opt_full_without_neighbor * (14.0 - wind_speed) / 2.0\n",
" ) # Linear ramp down\n",
" else:\n",
" yaw_opt_with_neighbor = (\n",
" yaw_opt_full_with_neighbor # Apply full offsets between 6.0 and 12.0 m/s\n",
" )\n",
" yaw_opt_without_neighbor = (\n",
" yaw_opt_full_without_neighbor # Apply full offsets between 6.0 and 12.0 m/s\n",
" )\n",
"\n",
" # Save to collective array\n",
" yaw_angles_wind_rose_with_neighbor[i, :] = yaw_opt_with_neighbor\n",
" yaw_angles_wind_rose_without_neighbor[i, :] = yaw_opt_without_neighbor\n",
"\n",
"\n",
"# Now apply the optimal yaw angles and get the AEP, first accounting for the neighboring farm\n",
"fmodel.set(yaw_angles=yaw_angles_wind_rose_with_neighbor)\n",
"fmodel.run()\n",
"aep_opt_with_neighbor = fmodel.get_farm_AEP(turbine_weights=turbine_weights)\n",
"aep_uplift_with_neighbor = 100.0 * (aep_opt_with_neighbor / aep_baseline - 1)\n",
"farm_power_opt_with_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights)\n",
"\n",
"# Repeat without accounting for neighboring farm\n",
"fmodel.set(yaw_angles=yaw_angles_wind_rose_without_neighbor)\n",
"fmodel.run()\n",
"aep_opt_without_neighbor = fmodel.get_farm_AEP(turbine_weights=turbine_weights)\n",
"aep_uplift_without_neighbor = 100.0 * (aep_opt_without_neighbor / aep_baseline - 1)\n",
"farm_power_opt_without_neighbor = fmodel.get_farm_power(turbine_weights=turbine_weights)\n",
"\n",
"print(\"Baseline AEP: {:.2f} GWh.\".format(aep_baseline / 1e9))\n",
"print(\n",
" \"Optimal AEP (Not accounting for neighboring farm): {:.2f} GWh.\".format(\n",
" aep_opt_without_neighbor / 1e9\n",
" )\n",
")\n",
"print(\n",
" \"Optimal AEP (Accounting for neighboring farm): {:.2f} GWh.\".format(aep_opt_with_neighbor / 1e9)\n",
")\n",
"\n",
"# Plot the optimal yaw angles for turbine 0 with and without accounting for the neighboring farm\n",
"yaw_angles_0_with_neighbor = np.vstack(df_opt_with_neighbor[\"yaw_angles_opt\"])[:, 0]\n",
"yaw_angles_0_without_neighbor = np.vstack(df_opt_without_neighbor[\"yaw_angles_opt\"])[:, 0]\n",
"\n",
"fig, ax = plt.subplots()\n",
"ax.plot(\n",
" df_opt_with_neighbor[\"wind_direction\"],\n",
" yaw_angles_0_with_neighbor,\n",
" label=\"Accounting for neighboring farm\",\n",
")\n",
"ax.plot(\n",
" df_opt_without_neighbor[\"wind_direction\"],\n",
" yaw_angles_0_without_neighbor,\n",
" label=\"Not accounting for neighboring farm\",\n",
")\n",
"ax.set_xlabel(\"Wind direction (deg)\")\n",
"ax.set_ylabel(\"Yaw angle (deg)\")\n",
"ax.legend()\n",
"ax.grid(True)\n",
"ax.set_title(\"Optimal yaw angles for turbine 0\")\n",
"\n",
"plt.show()\n",
"import warnings\n",
"warnings.filterwarnings('ignore')"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 5
}