From 1699b3f2996c358bff9ddda296d7f590f0d79b67 Mon Sep 17 00:00:00 2001 From: Bart Doekemeijer Date: Wed, 9 Mar 2022 12:25:58 -0700 Subject: [PATCH 1/2] Revise verify_convergence to support multiple wind speeds --- .../yaw_optimization/yaw_optimization_base.py | 116 ++++++++++++------ 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py index 1d72c4bd2..bf6ab9c01 100644 --- a/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py +++ b/floris/tools/optimization/yaw_optimization/yaw_optimization_base.py @@ -14,6 +14,7 @@ import copy +from time import perf_counter as timerpc import numpy as np import pandas as pd @@ -522,7 +523,7 @@ def _verify_solutions_for_convergence( self, farm_power_opt_subset, yaw_angles_opt_subset, - min_yaw_offset=0.10, + min_yaw_offset=0.01, min_power_gain_for_yaw=0.02, verbose=True, ): @@ -560,12 +561,16 @@ def _verify_solutions_for_convergence( print("Verifying convergence of the found optimal yaw angles.") + # Start timer + start_time = timerpc() + # Define variables locally yaw_angles_opt_subset = np.array(yaw_angles_opt_subset, copy=True) yaw_angles_baseline_subset = self._yaw_angles_baseline_subset farm_power_baseline_subset = self._farm_power_baseline_subset + turbs_to_opt_subset = self._turbs_to_opt_subset - # Round small nonzero yaw angles + # Round small nonzero yaw angles to zero ydiff = np.abs(yaw_angles_opt_subset - yaw_angles_baseline_subset) ids = np.where((ydiff < min_yaw_offset) & (ydiff > 0.0)) if len(ids[0]) > 0: @@ -576,8 +581,8 @@ def _verify_solutions_for_convergence( ydiff[ids] = 0.0 # Turbines to test whether their angles sufficiently improve farm power - ids = np.where((self._turbs_to_opt_subset) & (ydiff > min_yaw_offset)) - + ids = np.where((turbs_to_opt_subset) & (ydiff > min_yaw_offset)) + # Define situations that need to be calculated and find farm power. # Each situation basically contains the exact same conditions as the # baseline conditions and optimal yaw angles, besides for a single @@ -585,70 +590,101 @@ def _verify_solutions_for_convergence( # typically 0.0 deg). This way, we investigate whether the yaw offset # of that turbine really adds significant uplift to the farm power # production. - wd_array = self.fi_subset.floris.flow_field.wind_directions[ids[0]] - # ws_array = self.fi_subset.floris.flow_field.wind_speeds[ids[1]] - turbine_weights = self._turbine_weights_subset[ids[0]] - yaw_angles = yaw_angles_opt_subset[ids[0], :] - for wdii, ti in enumerate(ids[2]): - yaw_angles[wdii, 0, ti] = \ - yaw_angles_baseline_subset[ids[0][wdii], 0, ti] + + # For each turbine in the farm, reset its values to baseline. Thus, + # we copy the atmospheric conditions n_turbs times and for each + # copy of atmospheric conditions, we reset that turbine's yaw angle + # to its baseline value for all conditions. + n_turbs = len(self.fi.layout_x) + sp = (n_turbs, 1, 1) # Tile shape for matrix expansion + wd_array_nominal = self.fi_subset.floris.flow_field.wind_directions + n_wind_directions = len(wd_array_nominal) + yaw_angles_verify = np.tile(yaw_angles_opt_subset, sp) + yaw_angles_bl_verify = np.tile(yaw_angles_baseline_subset, sp) + turbine_id_array = np.zeros(np.shape(yaw_angles_verify)[0], dtype=int) + for ti in range(n_turbs): + ids = ti * n_wind_directions + np.arange(n_wind_directions) + yaw_angles_verify[ids, :, ti] = yaw_angles_bl_verify[ids, :, ti] + turbine_id_array[ids] = ti # Now evaluate all situations + farm_power_baseline_verify = np.tile(farm_power_baseline_subset, (n_turbs, 1)) farm_power = self._calculate_farm_power( - yaw_angles=yaw_angles, - wd_array=wd_array, - turbine_weights=turbine_weights + yaw_angles=yaw_angles_verify, + wd_array=np.tile(wd_array_nominal, n_turbs), + turbine_weights=np.tile(self._turbs_to_opt_subset, sp) ) # Calculate power uplift for optimal solutions - uplift_o = 100.0 * ( - farm_power_opt_subset[ids[0]].flatten() / - farm_power_baseline_subset[ids[0]].flatten() - ) - 100.0 - + uplift_o = 100 * ( + np.tile(farm_power_opt_subset, (n_turbs, 1)) / + farm_power_baseline_verify - 1.0 + ) + # Calculate power uplift for all cases we evaluated - uplift_n = 100.0 * ( - farm_power.flatten() / - farm_power_baseline_subset[ids[0]].flatten() - ) - 100.0 + uplift_n = 100.0 * (farm_power / farm_power_baseline_verify - 1.0) # Check difference in uplift, where each row represents a different # situation (i.e., where one turbine was set to its baseline yaw angle # instead of its optimal yaw angle). dp = uplift_o - uplift_n - ids_to_simplify = np.where(dp < min_power_gain_for_yaw)[0] - ids = (ids[0][ids_to_simplify], 0, ids[2][ids_to_simplify]) - yaw_angles_opt_subset[ids] = yaw_angles_baseline_subset[ids] - n = len(ids_to_simplify) + ids_to_simplify = np.where(dp < min_power_gain_for_yaw) + ids_to_simplify = ( + np.remainder(ids_to_simplify[0], n_wind_directions), # Wind direction identifier + ids_to_simplify[1], # Wind speed identifier + turbine_id_array[ids_to_simplify[0]], # Turbine identifier + ) + + # Overwrite yaw angles that insufficiently increased farm power with baseline values + yaw_angles_opt_subset[ids_to_simplify] = ( + yaw_angles_baseline_subset[ids_to_simplify] + ) + + n = len(ids_to_simplify[0]) if n > 0: # Yaw angles notably changed: recalculate farm powers - farm_power_opt_subset_new = self._calculate_farm_power( - yaw_angles_opt_subset) + farm_power_opt_subset_new = ( + self._calculate_farm_power(yaw_angles_opt_subset) + ) if verbose: # Calculate old uplift for all conditions - uplift_o = 100.0 * ( - farm_power_opt_subset.flatten() / - farm_power_baseline_subset.flatten() + dP_old = 100.0 * ( + farm_power_opt_subset / + farm_power_baseline_subset ) - 100.0 # Calculate new uplift for all conditions - uplift_n = 100.0 * ( - farm_power_opt_subset_new.flatten() / - farm_power_baseline_subset.flatten() + dP_new = 100.0 * ( + farm_power_opt_subset_new / + farm_power_baseline_subset ) - 100.0 # Calculate differences in power uplift - dp = uplift_o - uplift_n - id_max_loss = np.argmax(dp) + diff_uplift = dP_old - dP_new + ids_max_loss = np.where(np.nanmax(diff_uplift) == diff_uplift) + jj = (ids_max_loss[0][0], ids_max_loss[1][0]) + ws_array_nominal = self.fi_subset.floris.flow_field.wind_speeds print( "Nullified the optimal yaw offset for {:d}".format(n) + - " turbines over all wind directions. The maximum change " + - "in power uplift: from {:.3f}% to {:.3f}%.".format( - uplift_o[id_max_loss], uplift_n[id_max_loss] + " conditions and turbines." + ) + print( + "Simplifying the yaw angles for these conditions lead " + + "to a maximum change in wake-steering power uplift from " + + "{:.5f}% to {:.5f}% at ".format(dP_old[jj], dP_new[jj]) + + " WD = {:.1f} deg and WS = {:.1f} m/s.".format( + wd_array_nominal[jj[0]], ws_array_nominal[jj[1]], ) ) + t = timerpc() - start_time + print( + "Time spent to verify the convergence of the optimal " + + "yaw angles: {:.3f} s.".format(t) + ) + + # Return optimal solutions to the user farm_power_opt_subset = farm_power_opt_subset_new return yaw_angles_opt_subset, farm_power_opt_subset From e2b58df9b8ad0dd800595deae08e64dabb93a202 Mon Sep 17 00:00:00 2001 From: Bart Doekemeijer Date: Wed, 9 Mar 2022 13:57:55 -0700 Subject: [PATCH 2/2] Expand description for verify_convergence option in example case. Also fix ylims for power plots --- examples/09_opt_yaw_multiple_ws.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/examples/09_opt_yaw_multiple_ws.py b/examples/09_opt_yaw_multiple_ws.py index e13f06e41..aa464ffb5 100644 --- a/examples/09_opt_yaw_multiple_ws.py +++ b/examples/09_opt_yaw_multiple_ws.py @@ -38,7 +38,17 @@ ) # Initialize optimizer object and run optimization using the Serial-Refine method -yaw_opt = YawOptimizationSR(fi) +# Now, we enable the verify_convergence option. This function is useful to prevent +# yaw misalignment that increases the wind farm power production by a negligible +# amount. For example, at high wind speeds (e.g., 16 m/s), a turbine might yaw +# by a substantial amount to increase the power production by less than 1 W. This +# is typically the result of numerical inprecision of the power coefficient curve, +# which slightly differs for different above-rated wind speeds. The option +# verify_convergence therefore refines and validates the yaw angle choices +# but has no effect on the predicted power uplift from wake steering. +# Hence, it should mostly be used when actually synthesizing a practicable +# wind farm controller. +yaw_opt = YawOptimizationSR(fi, verify_convergence=True) df_opt = yaw_opt.optimize() print("Optimization results:") @@ -49,7 +59,13 @@ df_opt['t%d' % t] = df_opt.yaw_angles_opt.apply(lambda x: x[t]) # Show the results: optimal yaw angles -fig, axarr = plt.subplots(nrows=4, ncols=4, sharex=True, sharey=True, figsize=(10, 8)) +fig, axarr = plt.subplots( + nrows=4, + ncols=4, + sharex=True, + sharey=True, + figsize=(10, 8) +) jj = 0 for ii, ws in enumerate(fi.floris.flow_field.wind_speeds): xi = np.remainder(ii, 4) @@ -73,7 +89,13 @@ plt.tight_layout() # Show the results: baseline and optimized farm power -fig, axarr = plt.subplots(nrows=4, ncols=4, sharex=True, figsize=(10, 8)) +fig, axarr = plt.subplots( + nrows=4, + ncols=4, + sharex=True, + sharey=True, + figsize=(10, 8) +) jj = 0 for ii, ws in enumerate(fi.floris.flow_field.wind_speeds): xi = np.remainder(ii, 4) @@ -87,6 +109,7 @@ ax.plot(wd, power_baseline / 1e6, color='k', label='Baseline') ax.plot(wd, power_opt / 1e6, color='r', label='Optimized') ax.set_title("Wind speed = {:.1f} m/s".format(ws), size=10) + ax.set_ylim([0.0, 16.0]) if ((ii == 0) & (jj == 0)): ax.legend() ax.grid(True)