Source code for floris.optimization.yaw_optimization.yaw_optimizer_sr


import copy
import warnings
from time import perf_counter as timerpc

import numpy as np
import pandas as pd

from floris.logging_manager import LoggingManager

# from .yaw_optimizer_scipy import YawOptimizationScipy
from .yaw_optimization_base import YawOptimization


[docs] class YawOptimizationSR(YawOptimization, LoggingManager): def __init__( self, fmodel, minimum_yaw_angle=0.0, maximum_yaw_angle=25.0, yaw_angles_baseline=None, x0=None, Ny_passes=[5, 4], # Optimization options turbine_weights=None, exclude_downstream_turbines=True, verify_convergence=False, ): """ Instantiate YawOptimizationSR object with a FlorisModel object and assign parameter values. """ # Initialize base class super().__init__( fmodel=fmodel, minimum_yaw_angle=minimum_yaw_angle, maximum_yaw_angle=maximum_yaw_angle, yaw_angles_baseline=yaw_angles_baseline, x0=x0, turbine_weights=turbine_weights, calc_baseline_power=True, exclude_downstream_turbines=exclude_downstream_turbines, verify_convergence=verify_convergence, ) # Start a timer for FLORIS computations self.time_spent_in_floris = 0 # Confirm that Ny_passes are integers and odd/even for Nii, Ny in enumerate(Ny_passes): if not isinstance(Ny, int): raise ValueError("Ny_passes must contain exclusively integers") if Ny < 2: raise ValueError("Each entry in Ny_passes must have a value of at least 2.") if (Nii > 0) & ((Ny + 1) % 2 == 0): raise ValueError( "The second and further entries of Ny_passes must be even numbers. " "This is to ensure the same yaw angles are not evaluated twice between passes." ) # # Set baseline and optimization settings # if reduce_ngrid: # for ti in range(self.nturbs): # # Force number of grid points to 2 # self.fmodel.core.farm.turbines[ti].ngrid = 2 # self.fmodel.core.farm.turbines[ti].initialize_turbine() # print("Reducing ngrid. Unsure if this functionality works!") # Save optimization choices to self self.Ny_passes = Ny_passes # For each wind direction, determine the order of turbines self._get_turbine_orders() def _get_turbine_orders(self): layout_x = self.fmodel.layout_x layout_y = self.fmodel.layout_y turbines_ordered_array = [] for wd in self.fmodel_subset.core.flow_field.wind_directions: layout_x_rot = ( np.cos((wd - 270.0) * np.pi / 180.0) * layout_x - np.sin((wd - 270.0) * np.pi / 180.0) * layout_y ) turbines_ordered = np.argsort(layout_x_rot) turbines_ordered_array.append(turbines_ordered) self.turbines_ordered_array_subset = np.vstack(turbines_ordered_array) def _calc_powers_with_memory(self, yaw_angles_subset, use_memory=True): # Define current optimal solutions and floris wind directions locally yaw_angles_opt_subset = self._yaw_angles_opt_subset farm_power_opt_subset = self._farm_power_opt_subset wd_array_subset = self.fmodel_subset.core.flow_field.wind_directions ws_array_subset = self.fmodel_subset.core.flow_field.wind_speeds ti_array_subset = self.fmodel_subset.core.flow_field.turbulence_intensities turbine_weights_subset = self._turbine_weights_subset # Reformat yaw_angles_subset, if necessary eval_multiple_passes = (len(np.shape(yaw_angles_subset)) == 3) if eval_multiple_passes: # Four-dimensional; format everything into three-dimensional Ny = yaw_angles_subset.shape[0] # Number of passes yaw_angles_subset = np.vstack( [yaw_angles_subset[iii, :, :] for iii in range(Ny)] ) yaw_angles_opt_subset = np.tile(yaw_angles_opt_subset, (Ny, 1)) farm_power_opt_subset = np.tile(farm_power_opt_subset, (Ny)) wd_array_subset = np.tile(wd_array_subset, Ny) ws_array_subset = np.tile(ws_array_subset, Ny) ti_array_subset = np.tile(ti_array_subset, Ny) turbine_weights_subset = np.tile(turbine_weights_subset, (Ny, 1)) # Initialize empty matrix for floris farm power outputs farm_powers = np.zeros((yaw_angles_subset.shape[0])) # Find indices of yaw angles that we previously already evaluated, and # prevent redoing the same calculations if use_memory: idx = (np.abs(yaw_angles_opt_subset - yaw_angles_subset) < 0.01).all(axis=1) farm_powers[idx] = farm_power_opt_subset[idx] if self.print_progress: self.logger.info( "Skipping {:d}/{:d} calculations: already in memory.".format( np.sum(idx), len(idx)) ) else: idx = np.zeros(yaw_angles_subset.shape[0], dtype=bool) if not np.all(idx): # Now calculate farm powers for conditions we haven't yet evaluated previously start_time = timerpc() if (hasattr(self.fmodel.core.flow_field, 'heterogeneous_inflow_config') and self.fmodel.core.flow_field.heterogeneous_inflow_config is not None): het_sm_orig = np.array( self.fmodel.core.flow_field.heterogeneous_inflow_config['speed_multipliers'] ) het_sm = np.tile(het_sm_orig, (Ny, 1))[~idx, :] else: het_sm = None farm_powers[~idx] = self._calculate_farm_power( wd_array=wd_array_subset[~idx], ws_array=ws_array_subset[~idx], ti_array=ti_array_subset[~idx], turbine_weights=turbine_weights_subset[~idx, :], yaw_angles=yaw_angles_subset[~idx, :], heterogeneous_speed_multipliers=het_sm ) self.time_spent_in_floris += (timerpc() - start_time) # Finally format solutions back to original format, if necessary if eval_multiple_passes: farm_powers = np.reshape( farm_powers, ( Ny, self.fmodel_subset.core.flow_field.n_findex ) ) return farm_powers def _generate_evaluation_grid(self, pass_depth, turbine_depth): """ Calculate the yaw angles for every iteration in the SR algorithm, for turbine, for every wind direction, for every wind speed, for every TI. Basically, this should yield a grid of yaw angle sets to evaluate the wind farm AEP with 'Ny' times. Then, for each ambient condition set, """ # Initialize yaw angles to evaluate, 'Ny' times the wind rose Ny = self.Ny_passes[pass_depth] evaluation_grid = np.tile(self._yaw_angles_opt_subset, (Ny, 1, 1)) # Get a list of the turbines in order of x and sort front to back for iw in range(self._n_findex_subset): turbid = self.turbines_ordered_array_subset[iw, turbine_depth] # Turbine to manipulate # # Check if this turbine needs to be optimized. If not, continue # if not self._turbs_to_opt_subset[iw, 0, turbid]: # continue # # Remove turbines that need not be optimized # turbines_ordered = [ti for ti in turbines_ordered if ti in self.turbs_to_opt] # Grab yaw bounds from self yaw_lb = self._yaw_lbs[iw, turbid] yaw_ub = self._yaw_ubs[iw, turbid] # Saturate to allowable yaw limits yaw_lb = np.clip( yaw_lb, self.minimum_yaw_angle[iw, turbid], self.maximum_yaw_angle[iw, turbid] ) yaw_ub = np.clip( yaw_ub, self.minimum_yaw_angle[iw, turbid], self.maximum_yaw_angle[iw, turbid] ) if pass_depth == 0: # Evaluate all possible coordinates yaw_angles_subset = np.linspace(yaw_lb, yaw_ub, Ny) else: # Remove middle point: was evaluated in previous iteration c = int(Ny / 2) # Central point (to remove) ids = [*list(range(0, c)), *list(range(c + 1, Ny + 1))] yaw_angles_subset = np.linspace(yaw_lb, yaw_ub, Ny + 1)[ids] evaluation_grid[:, iw, turbid] = yaw_angles_subset self._yaw_evaluation_grid = evaluation_grid return evaluation_grid def _process_evaluation_grid(self): # Evaluate the farm AEPs for the grid of possible yaw angles evaluation_grid = self._yaw_evaluation_grid farm_powers = self._calc_powers_with_memory(evaluation_grid) return farm_powers
[docs] def optimize(self, print_progress=True): """ Find the yaw angles that maximize the power production for every wind direction, wind speed and turbulence intensity. """ self.print_progress = print_progress # For each pass, from front to back ii = 0 for Nii in range(len(self.Ny_passes)): # Disturb yaw angles for one turbine at a time, from front to back for turbine_depth in range(self.nturbs): p = 100.0 * ii / (len(self.Ny_passes) * self.nturbs) ii += 1 if self.print_progress: print( f"[Serial Refine] Processing pass={Nii}, " f"turbine_depth={turbine_depth} ({p:.1f}%)" ) # Create grid to evaluate yaw angles for one turbine == turbine_depth evaluation_grid = self._generate_evaluation_grid( pass_depth=Nii, turbine_depth=turbine_depth ) # Evaluate grid of yaw angles, get farm powers and find optimal solutions farm_powers = self._process_evaluation_grid() # If farm powers contains any nans, then issue a warning if np.any(np.isnan(farm_powers)): err_msg = ( "NaNs found in farm powers during SerialRefine " "optimization routine. Proceeding to maximize over yaw " "settings that produce valid powers." ) self.logger.warning(err_msg, stack_info=True) # Find optimal solutions in new evaluation grid args_opt = np.expand_dims(np.nanargmax(farm_powers, axis=0), axis=0) farm_powers_opt_new = np.squeeze( np.take_along_axis(farm_powers, args_opt, axis=0), axis=0, ) yaw_angles_opt_new = np.squeeze( np.take_along_axis( evaluation_grid, np.expand_dims(args_opt, axis=2), axis=0 ), axis=0 ) farm_powers_opt_prev = self._farm_power_opt_subset yaw_angles_opt_prev = self._yaw_angles_opt_subset # Now update optimal farm powers if better than previous ids_better = (farm_powers_opt_new > farm_powers_opt_prev) farm_power_opt = farm_powers_opt_prev farm_power_opt[ids_better] = farm_powers_opt_new[ids_better] # Now update optimal yaw angles if better than previous turbs_sorted = self.turbines_ordered_array_subset turbids = turbs_sorted[np.where(ids_better)[0], turbine_depth] ids = (*np.where(ids_better), turbids) yaw_angles_opt = yaw_angles_opt_prev yaw_angles_opt[ids] = yaw_angles_opt_new[ids] # Update bounds for next iteration to close proximity of optimal solution dx = ( evaluation_grid[1, :, :] - evaluation_grid[0, :, :] )[ids] self._yaw_lbs[ids] = np.clip( yaw_angles_opt[ids] - 0.50 * dx, self._minimum_yaw_angle_subset[ids], self._maximum_yaw_angle_subset[ids] ) self._yaw_ubs[ids] = np.clip( yaw_angles_opt[ids] + 0.50 * dx, self._minimum_yaw_angle_subset[ids], self._maximum_yaw_angle_subset[ids] ) # Save results to self self._farm_power_opt_subset = farm_power_opt self._yaw_angles_opt_subset = yaw_angles_opt # Finalize optimization, i.e., retrieve full solutions df_opt = self._finalize() return df_opt