Source code for ard.layout.gridfarm

import numpy as np

import ard.layout.templates as templates


[docs] class GridFarmLayout(templates.LayoutTemplate): """ A simplified, uniform four-parameter parallelepiped grid farm layout class. This is a class to take a parameterized, structured grid farm defined by a gridded parallelepiped with spacing variables defined to 1) orient the farm with respect to North, 2) space the rows of turbines along this primary vector, 3) space the columns of turbines along the perpendicular, and 4) skew the positioning along a parallel to the primary (orientation) vector. The layout model is shown in a ASCII image below: |-------| <- streamwise spacing orient. x ----- x ----- x ----- x ----- x - angle / / / / / | <- spanwise spacing | x ----- x ----- x ----- x ----- x - (perpendicular v / / / / / w.r.t. primary) ------- x ----- x ----- x ----- x ----- x ----- primary vector ' / / / / / (rotated from ' x ----- x ----- x ----- x ----- x north CW by ' / / / / / orientation NORTH x ----- x ----- x ----- x ----- x angle) /| / | / | <- skew angle Options ------- modeling_options : dict a modeling options dictionary (inherited from `templates.LayoutTemplate`) N_turbines : int the number of turbines that should be in the farm layout (inherited from `templates.LayoutTemplate`) Inputs ------ angle_orientation : float orientation in degrees clockwise with respect to North of the primary axis of the wind farm layout spacing_primary : float spacing of turbine rows along the primary axis (rotated by `angle_orientation`) in nondimensional rotor diameters spacing_secondary : float spacing of turbine columns along the perpendicular to the primary axis (rotated by 90° with respect to the primary axis) in nondimensional rotor diameters angle_skew : float clockwise skew angle of turbine rows w.r.t. beyond the 90° clockwise perpendicular to the primary axis Outputs ------- x_turbines : np.ndarray a 1-D numpy array that represents that x (i.e. Easting) coordinate of the location of each of the turbines in the farm in meters (inherited from `templates.LayoutTemplate`) y_turbines : np.ndarray a 1-D numpy array that represents that y (i.e. Northing) coordinate of the location of each of the turbines in the farm in meters (inherited from `templates.LayoutTemplate`) spacing_effective_primary : float a measure of the spacing on a primary axis of a rectangular farm that would be equivalent to this one for the purposes of computing BOS costs measured in rotor diameters (inherited from `templates.LayoutTemplate`) spacing_effective_secondary : float a measure of the spacing on a secondary axis of a rectangular farm that would be equivalent to this one for the purposes of computing BOS costs measured in rotor diameters (inherited from `templates.LayoutTemplate`) """
[docs] def initialize(self): """Initialization of OM component.""" super().initialize()
[docs] def setup(self): """Setup of OM component.""" super().setup() # add four-parameter grid farm layout DVs self.add_input("spacing_primary", 7.0) self.add_input("spacing_secondary", 7.0) self.add_input("angle_orientation", 0.0, units="deg") self.add_input("angle_skew", 0.0, units="deg")
[docs] def setup_partials(self): """Derivative setup for OM component.""" # default complex step for the layout tools, since they're often algebraic self.declare_partials("*", "*", method="cs")
[docs] def compute(self, inputs, outputs): """Computation for the OM component.""" D_rotor = self.modeling_options["turbine"]["geometry"]["diameter_rotor"] lengthscale_spacing_streamwise = inputs["spacing_primary"] * D_rotor lengthscale_spacing_spanwise = inputs["spacing_secondary"] * D_rotor N_square = int(np.sqrt(self.N_turbines)) # floors count_y, count_x = np.meshgrid( np.arange(-((N_square - 1) / 2), ((N_square + 1) / 2)), np.arange(-((N_square - 1) / 2), ((N_square + 1) / 2)), ) if self.N_turbines == N_square**2: pass elif self.N_turbines <= N_square * (N_square + 1): # grid farm is a little bit above the last square... add a trailing # row. count_x = np.vstack([count_x, ((N_square + 1) / 2) * np.ones((N_square,))]) count_y = np.vstack( [count_y, np.arange(-((N_square - 1) / 2), ((N_square + 1) / 2))] ) count_x = count_x.flatten() count_y = count_y.flatten() else: # grid farm is nearly the next square... oversize and cut the last count_y, count_x = np.meshgrid( np.arange(-((N_square) / 2), ((N_square + 2) / 2)), np.arange(-((N_square) / 2), ((N_square + 2) / 2)), ) count_x = count_x.flatten()[: self.N_turbines] count_y = count_y.flatten()[: self.N_turbines] angle_skew = -np.pi / 180.0 * inputs["angle_skew"] Bmtx = np.array( [ [1.0, 0.0], [np.tan(float(angle_skew[0])), 1.0], ] ).squeeze() xi_positions = count_x * lengthscale_spacing_spanwise yi_positions = count_y * lengthscale_spacing_streamwise angle_orientation = np.pi / 180.0 * inputs["angle_orientation"] Amtx = np.array( [ [np.cos(angle_orientation), np.sin(angle_orientation)], [-np.sin(angle_orientation), np.cos(angle_orientation)], ] ).squeeze() xyp = Amtx @ (Bmtx @ np.vstack([xi_positions, yi_positions])) outputs["x_turbines"] = xyp[0, :].tolist() outputs["y_turbines"] = xyp[1, :].tolist() outputs["spacing_effective_secondary"] = inputs["spacing_primary"] outputs["spacing_effective_secondary"] = np.sqrt( inputs["spacing_secondary"] ** 2.0 / np.cos(angle_skew) ** 2.0 )
[docs] class GridFarmLanduse(templates.LanduseTemplate): """ Landuse class for four-parameter parallelepiped grid farm layout. This is a class to compute the landuse area of the parameterized, structured grid farm defined in `GridFarmLayout`. Options ------- modeling_options : dict a modeling options dictionary (inherited from `templates.LayoutTemplate`) N_turbines : int the number of turbines that should be in the farm layout (inherited from `templates.LayoutTemplate`) Inputs ------ distance_layback_diameters : float the number of diameters of layback desired for the landuse calculation (inherited from `templates.LayoutTemplate`) angle_orientation : float orientation in degrees clockwise with respect to North of the primary axis of the wind farm layout spacing_primary : float spacing of turbine rows along the primary axis (rotated by `angle_orientation`) in nondimensional rotor diameters spacing_secondary : float spacing of turbine columns along the perpendicular to the primary axis (rotated by 90° with respect to the primary axis) in nondimensional rotor diameters angle_skew : float clockwise skew angle of turbine rows w.r.t. beyond the 90° clockwise perpendicular to the primary axis Outputs ------- area_tight : float the area in square kilometers that the farm occupies based on the circumscribing geometry with a specified (default zero) layback buffer (inherited from `templates.LayoutTemplate`) area_aligned_parcel : float the area in square kilometers that the farm occupies based on the circumscribing rectangle that is aligned with the primary axis of the wind farm plus a specified (default zero) layback buffer area_compass_parcel : float the area in square kilometers that the farm occupies based on the circumscribing rectangle that is aligned with the compass rose plus a specified (default zero) layback buffer """
[docs] def setup(self): """Setup of OM component.""" super().setup() # add grid farm-specific inputs self.add_input("spacing_primary", 7.0) self.add_input("spacing_secondary", 7.0) self.add_input("angle_orientation", 0.0, units="deg") self.add_input("angle_skew", 0.0, units="deg") self.add_output( "area_aligned_parcel", 0.0, units="km**2", desc="area of the tightest rectangle around the farm (plus layback) that is aligned with the orientation vector", ) self.add_output( "area_compass_parcel", 0.0, units="km**2", desc="area of the tightest rectangular and compass-aligned land parcel that will fit the farm (plus layback)", )
[docs] def setup_partials(self): """Derivative setup for OM component.""" # default complex step for the layout tools, since they're often algebraic self.declare_partials("*", "*", method="cs")
[docs] def compute(self, inputs, outputs): """Computation for the OM component.""" D_rotor = self.modeling_options["turbine"]["geometry"]["diameter_rotor"] lengthscale_spacing_streamwise = inputs["spacing_primary"] * D_rotor lengthscale_spacing_spanwise = inputs["spacing_secondary"] * D_rotor lengthscale_layback = inputs["distance_layback_diameters"] * D_rotor N_square = int(np.sqrt(self.N_turbines)) # floors min_count_xf = -(N_square - 1) / 2 min_count_yf = -(N_square - 1) / 2 max_count_xf = (N_square - 1) / 2 max_count_yf = (N_square - 1) / 2 if self.N_turbines == N_square**2: pass elif self.N_turbines <= N_square * (N_square + 1): max_count_xf = (N_square + 1) / 2 else: min_count_xf = -(N_square) / 2 min_count_yf = -(N_square) / 2 max_count_xf = (N_square) / 2 max_count_yf = (N_square) / 2 # the side lengths of a parallelopiped oriented with the farm that encloses the farm with layback length_farm_xf = (max_count_xf - min_count_xf) * lengthscale_spacing_spanwise length_farm_yf = (max_count_yf - min_count_yf) * lengthscale_spacing_streamwise # the area of a parallelopiped oriented with the farm that encloses the farm with layback area_parallelopiped = (length_farm_xf + 2 * lengthscale_layback) * ( length_farm_yf + 2 * lengthscale_layback ) # the side lengths of a square oriented with the farm that encloses the farm with layback angle_skew = np.pi / 180.0 * inputs["angle_skew"] length_enclosing_farm_xf = length_farm_xf length_enclosing_farm_yf = ( max_count_yf * lengthscale_spacing_streamwise + np.abs(max_count_xf) * lengthscale_spacing_spanwise * np.abs(np.tan(angle_skew)) ) - ( min_count_yf * lengthscale_spacing_streamwise - np.abs(min_count_xf) * lengthscale_spacing_spanwise * np.abs(np.tan(angle_skew)) ) # the area of a square oriented with the farm that encloses the farm with layback area_enclosingsquare_farmoriented = ( length_enclosing_farm_xf + 2 * lengthscale_layback ) * (length_enclosing_farm_yf + 2 * lengthscale_layback) # the side lengths of a square oriented with the compass rose that encloses the farm with layback angle_orientation = np.pi / 180.0 * inputs["angle_orientation"] A_x = ( +max_count_yf * lengthscale_spacing_streamwise * np.sin(angle_orientation) + max_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation) - (max_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation)) * np.tan(angle_skew) ) A_y = ( +max_count_yf * lengthscale_spacing_streamwise * np.cos(angle_orientation) - max_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation) - (max_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation)) * np.tan(angle_skew) ) B_x = ( +min_count_yf * lengthscale_spacing_streamwise * np.sin(angle_orientation) + max_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation) - (max_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation)) * np.tan(angle_skew) ) B_y = ( +min_count_yf * lengthscale_spacing_streamwise * np.cos(angle_orientation) - max_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation) - (max_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation)) * np.tan(angle_skew) ) C_x = ( min_count_yf * lengthscale_spacing_streamwise * np.sin(angle_orientation) + min_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation) - (min_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation)) * np.tan(angle_skew) ) C_y = ( min_count_yf * lengthscale_spacing_streamwise * np.cos(angle_orientation) - min_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation) - (min_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation)) * np.tan(angle_skew) ) D_x = ( -min_count_yf * lengthscale_spacing_streamwise * np.sin(angle_orientation) + min_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation) - (min_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation)) * np.tan(angle_skew) ) D_y = ( -min_count_yf * lengthscale_spacing_streamwise * np.cos(angle_orientation) - min_count_xf * lengthscale_spacing_spanwise * np.sin(angle_orientation) - (min_count_xf * lengthscale_spacing_spanwise * np.cos(angle_orientation)) * np.tan(angle_skew) ) length_enclosing_farm_x = np.max([A_x, B_x, C_x, D_x]) - np.min( [A_x, B_x, C_x, D_x] ) length_enclosing_farm_y = np.max([A_y, B_y, C_y, D_y]) - np.min( [A_y, B_y, C_y, D_y] ) area_enclosingsquare_compass = ( length_enclosing_farm_x + 2 * lengthscale_layback ) * (length_enclosing_farm_y + 2 * lengthscale_layback) outputs["area_tight"] = area_parallelopiped / (1e3) ** 2 outputs["area_aligned_parcel"] = area_enclosingsquare_farmoriented / (1e3) ** 2 outputs["area_compass_parcel"] = area_enclosingsquare_compass / (1e3) ** 2