diff --git a/reV/bespoke/bespoke.py b/reV/bespoke/bespoke.py index 64a04c26a..5d5cbb878 100644 --- a/reV/bespoke/bespoke.py +++ b/reV/bespoke/bespoke.py @@ -222,6 +222,7 @@ def __init__( capital_cost_function, fixed_operating_cost_function, variable_operating_cost_function, + balance_of_system_cost_function, min_spacing="5x", wake_loss_multiplier=1, ga_kwargs=None, @@ -262,19 +263,33 @@ def __init__( return the objective to be minimized during layout optimization. Variables available are: - - n_turbines: the number of turbines - - system_capacity: wind plant capacity - - aep: annual energy production - - fixed_charge_rate: user input fixed_charge_rate if included - as part of the sam system config. - - self.wind_plant: the SAM wind plant object, through which - all SAM variables can be accessed - - capital_cost: plant capital cost as evaluated + - ``n_turbines``: the number of turbines + - ``system_capacity``: wind plant capacity + - ``aep``: annual energy production + - ``avg_sl_dist_to_center_m``: Average straight-line + distance to the supply curve point center from all + turbine locations (in m). Useful for computing plant + BOS costs. + - ``avg_sl_dist_to_medoid_m``: Average straight-line + distance to the medoid of all turbine locations + (in m). Useful for computing plant BOS costs. + - ``nn_conn_dist_m``: Total BOS connection distance + using nearest-neighbor connections. This variable is + only available for the + ``balance_of_system_cost_function`` equation. + - ``fixed_charge_rate``: user input fixed_charge_rate if + included as part of the sam system config. + - ``capital_cost``: plant capital cost as evaluated by `capital_cost_function` - - fixed_operating_cost: plant fixed annual operating cost as - evaluated by `fixed_operating_cost_function` - - variable_operating_cost: plant variable annual operating cost - as evaluated by `variable_operating_cost_function` + - ``fixed_operating_cost``: plant fixed annual operating + cost as evaluated by `fixed_operating_cost_function` + - ``variable_operating_cost``: plant variable annual + operating cost as evaluated by + `variable_operating_cost_function` + - ``balance_of_system_cost``: plant balance of system + cost as evaluated by `balance_of_system_cost_function` + - ``self.wind_plant``: the SAM wind plant object, + through which all SAM variables can be accessed capital_cost_function : str The plant capital cost function as a string, must return the total @@ -287,6 +302,16 @@ def __init__( variable_operating_cost_function : str The plant annual variable operating cost function as a string, must return the variable operating cost in $/kWh. Has access to the same + variables as the objective_function. You can set this to "0" + to effectively ignore variable operating costs. + balance_of_system_cost_function : str + The plant balance-of-system cost function as a string, must + return the variable operating cost in $. Has access to the + same variables as the objective_function. You can set this + to "0" to effectively ignore balance-of-system costs. + balance_of_system_cost_function : str + The plant balance-of-system cost function as a string, must + return the variable operating cost in $. Has access to the same variables as the objective_function. min_spacing : float | int | str Minimum spacing between turbines in meters. Can also be a string @@ -431,6 +456,7 @@ def __init__( self.variable_operating_cost_function = ( variable_operating_cost_function ) + self.balance_of_system_cost_function = balance_of_system_cost_function self.min_spacing = min_spacing self.wake_loss_multiplier = wake_loss_multiplier self.ga_kwargs = ga_kwargs or {} @@ -1026,6 +1052,7 @@ def plant_optimizer(self): self.capital_cost_function, self.fixed_operating_cost_function, self.variable_operating_cost_function, + self.balance_of_system_cost_function, self.include_mask, self.pixel_side_length, self.min_spacing, @@ -1046,11 +1073,13 @@ def recalc_lcoe(self): "multi-year mean AEP." ) - fcr = lcoe_kwargs["fixed_charge_rate"] - cap_cost = lcoe_kwargs["capital_cost"] - foc = lcoe_kwargs["fixed_operating_cost"] - voc = lcoe_kwargs["variable_operating_cost"] - aep = self.outputs["annual_energy-means"] + fcr = lcoe_kwargs['fixed_charge_rate'] + cc = lcoe_kwargs['capital_cost'] + foc = lcoe_kwargs['fixed_operating_cost'] + voc = lcoe_kwargs['variable_operating_cost'] + bos = lcoe_kwargs['balance_of_system_cost'] + aep = self.outputs['annual_energy-means'] + cap_cost = cc + bos my_mean_lcoe = lcoe_fcr(fcr, cap_cost, foc, aep, voc) @@ -1068,9 +1097,9 @@ def get_lcoe_kwargs(self): sam_sys_inputs, normalized to the original system_capacity, and updated based on the bespoke optimized system_capacity, includes fixed_charge_rate, system_capacity (kW), capital_cost ($), - fixed_operating_cos ($), variable_operating_cost ($/kWh) - Data source priority: outputs, plant_optimizer, - original_sam_sys_inputs, meta + fixed_operating_cos ($), variable_operating_cost ($/kWh), + balance_of_system_cost ($). Data source priority: outputs, + plant_optimizer, original_sam_sys_inputs, meta """ kwargs_list = [ @@ -1079,6 +1108,7 @@ def get_lcoe_kwargs(self): "capital_cost", "fixed_operating_cost", "variable_operating_cost", + "balance_of_system_cost", ] lcoe_kwargs = {} @@ -1153,19 +1183,15 @@ def check_dependencies(cls): raise ModuleNotFoundError(msg) @staticmethod - def _check_sys_inputs( - plant1, - plant2, - ignore=( - "wind_resource_model_choice", - "wind_resource_data", - "wind_turbine_powercurve_powerout", - "hourly", - "capital_cost", - "fixed_operating_cost", - "variable_operating_cost", - ), - ): + def _check_sys_inputs(plant1, plant2, + ignore=('wind_resource_model_choice', + 'wind_resource_data', + 'wind_turbine_powercurve_powerout', + 'hourly', + 'capital_cost', + 'fixed_operating_cost', + 'variable_operating_cost', + 'balance_of_system_cost')): """Check two reV-SAM models for matching system inputs. Parameters @@ -1294,6 +1320,11 @@ def run_plant_optimization(self): self._outputs["system_capacity"] = self.plant_optimizer.capacity self._meta["n_turbines"] = self.plant_optimizer.nturbs + self._meta["avg_sl_dist_to_center_m"] = \ + self.plant_optimizer.avg_sl_dist_to_center_m + self._meta["avg_sl_dist_to_medoid_m"] = \ + self.plant_optimizer.avg_sl_dist_to_medoid_m + self._meta["nn_conn_dist_m"] = self.plant_optimizer.nn_conn_dist_m self._meta["bespoke_aep"] = self.plant_optimizer.aep self._meta["bespoke_objective"] = self.plant_optimizer.objective self._meta["bespoke_capital_cost"] = self.plant_optimizer.capital_cost @@ -1303,6 +1334,9 @@ def run_plant_optimization(self): self._meta["bespoke_variable_operating_cost"] = ( self.plant_optimizer.variable_operating_cost ) + self._meta["bespoke_balance_of_system_cost"] = ( + self.plant_optimizer.balance_of_system_cost + ) self._meta["included_area"] = self.plant_optimizer.area self._meta["included_area_capacity_density"] = ( self.plant_optimizer.capacity_density @@ -1402,36 +1436,18 @@ def run(cls, *args, **kwargs): class BespokeWindPlants(BaseAggregation): """BespokeWindPlants""" - def __init__( - self, - excl_fpath, - res_fpath, - tm_dset, - objective_function, - capital_cost_function, - fixed_operating_cost_function, - variable_operating_cost_function, - project_points, - sam_files, - min_spacing="5x", - wake_loss_multiplier=1, - ga_kwargs=None, - output_request=("system_capacity", "cf_mean"), - ws_bins=(0.0, 20.0, 5.0), - wd_bins=(0.0, 360.0, 45.0), - excl_dict=None, - area_filter_kernel="queen", - min_area=None, - resolution=64, - excl_area=None, - data_layers=None, - pre_extract_inclusions=False, - prior_run=None, - gid_map=None, - bias_correct=None, - pre_load_data=False, - ): - r"""ReV bespoke analysis class. + def __init__(self, excl_fpath, res_fpath, tm_dset, objective_function, + capital_cost_function, fixed_operating_cost_function, + variable_operating_cost_function, + balance_of_system_cost_function, project_points, + sam_files, min_spacing='5x', wake_loss_multiplier=1, + ga_kwargs=None, output_request=('system_capacity', 'cf_mean'), + ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0), + excl_dict=None, area_filter_kernel='queen', min_area=None, + resolution=64, excl_area=None, data_layers=None, + pre_extract_inclusions=False, prior_run=None, gid_map=None, + bias_correct=None, pre_load_data=False): + """reV bespoke analysis class. Much like generation, ``reV`` bespoke analysis runs SAM simulations by piping in renewable energy resource data (usually @@ -1503,17 +1519,30 @@ def __init__( - ``n_turbines``: the number of turbines - ``system_capacity``: wind plant capacity - ``aep``: annual energy production + - ``avg_sl_dist_to_center_m``: Average straight-line + distance to the supply curve point center from all + turbine locations (in m). Useful for computing plant + BOS costs. + - ``avg_sl_dist_to_medoid_m``: Average straight-line + distance to the medoid of all turbine locations + (in m). Useful for computing plant BOS costs. + - ``nn_conn_dist_m``: Total BOS connection distance + using nearest-neighbor connections. This variable is + only available for the + ``balance_of_system_cost_function`` equation. - ``fixed_charge_rate``: user input fixed_charge_rate if included as part of the sam system config. - - ``self.wind_plant``: the SAM wind plant object, - through which all SAM variables can be accessed - ``capital_cost``: plant capital cost as evaluated by `capital_cost_function` - ``fixed_operating_cost``: plant fixed annual operating cost as evaluated by `fixed_operating_cost_function` - ``variable_operating_cost``: plant variable annual - operating cost, as evaluated by + operating cost as evaluated by `variable_operating_cost_function` + - ``balance_of_system_cost``: plant balance of system + cost as evaluated by `balance_of_system_cost_function` + - ``self.wind_plant``: the SAM wind plant object, + through which all SAM variables can be accessed capital_cost_function : str The plant capital cost function written out as a string. @@ -1530,6 +1559,13 @@ def __init__( out as a string. This expression must return the variable operating cost in $/kWh. This expression has access to the same variables as the `objective_function` argument above. + You can set this to "0" to effectively ignore variable + operating costs. + balance_of_system_cost_function : str + The plant balance-of-system cost function as a string, must + return the variable operating cost in $. Has access to the + same variables as the objective_function. You can set this + to "0" to effectively ignore balance-of-system costs. project_points : int | list | tuple | str | dict | pd.DataFrame | slice Input specifying which sites to process. A single integer representing the supply curve GID of a site may be specified @@ -1560,11 +1596,13 @@ def __init__( - ``capital_cost_multiplier`` - ``fixed_operating_cost_multiplier`` - ``variable_operating_cost_multiplier`` + - ``balance_of_system_cost_multiplier`` These particular inputs are treated as multipliers to be applied to the respective cost curves (`capital_cost_function`, `fixed_operating_cost_function`, - and `variable_operating_cost_function`) both during and + `variable_operating_cost_function`, and + `balance_of_system_cost_function`) both during and after the optimization. A DataFrame following the same guidelines as the CSV input (or a dictionary that can be used to initialize such a DataFrame) may be used for this @@ -1839,30 +1877,23 @@ def __init__( """ log_versions(logger) - logger.info("Initializing BespokeWindPlants...") - logger.info("Resource filepath: {}".format(res_fpath)) - logger.info("Exclusion filepath: {}".format(excl_fpath)) - logger.debug("Exclusion dict: {}".format(excl_dict)) - logger.info( - "Bespoke objective function: {}".format(objective_function) - ) - logger.info( - "Bespoke capital cost function: {}".format(capital_cost_function) - ) - logger.info( - "Bespoke fixed operating cost function: {}".format( - fixed_operating_cost_function - ) - ) - logger.info( - "Bespoke variable operating cost function: {}".format( - variable_operating_cost_function - ) - ) - logger.info( - "Bespoke wake loss multiplier: {}".format(wake_loss_multiplier) - ) - logger.info("Bespoke GA initialization kwargs: {}".format(ga_kwargs)) + logger.info('Initializing BespokeWindPlants...') + logger.info('Resource filepath: {}'.format(res_fpath)) + logger.info('Exclusion filepath: {}'.format(excl_fpath)) + logger.debug('Exclusion dict: {}'.format(excl_dict)) + logger.info('Bespoke objective function: {}' + .format(objective_function)) + logger.info('Bespoke capital cost function: {}' + .format(capital_cost_function)) + logger.info('Bespoke fixed operating cost function: {}' + .format(fixed_operating_cost_function)) + logger.info('Bespoke variable operating cost function: {}' + .format(variable_operating_cost_function)) + logger.info('Bespoke balance of system cost function: {}' + .format(balance_of_system_cost_function)) + logger.info('Bespoke wake loss multiplier: {}' + .format(wake_loss_multiplier)) + logger.info('Bespoke GA initialization kwargs: {}'.format(ga_kwargs)) logger.info( "Bespoke pre-extracting exclusions: {}".format( @@ -1897,6 +1928,7 @@ def __init__( self._cap_cost_fun = capital_cost_function self._foc_fun = fixed_operating_cost_function self._voc_fun = variable_operating_cost_function + self._bos_fun = balance_of_system_cost_function self._min_spacing = min_spacing self._wake_loss_multiplier = wake_loss_multiplier self._ga_kwargs = ga_kwargs or {} @@ -2363,37 +2395,21 @@ def save_outputs(self, out_fpath): # pylint: disable=arguments-renamed @classmethod - def run_serial( - cls, - excl_fpath, - res_fpath, - tm_dset, - sam_sys_inputs, - objective_function, - capital_cost_function, - fixed_operating_cost_function, - variable_operating_cost_function, - min_spacing="5x", - wake_loss_multiplier=1, - ga_kwargs=None, - output_request=("system_capacity", "cf_mean"), - ws_bins=(0.0, 20.0, 5.0), - wd_bins=(0.0, 360.0, 45.0), - excl_dict=None, - inclusion_mask=None, - area_filter_kernel="queen", - min_area=None, - resolution=64, - excl_area=0.0081, - data_layers=None, - gids=None, - exclusion_shape=None, - slice_lookup=None, - prior_meta=None, - gid_map=None, - bias_correct=None, - pre_loaded_data=None, - ): + def run_serial(cls, excl_fpath, res_fpath, tm_dset, + sam_sys_inputs, objective_function, + capital_cost_function, + fixed_operating_cost_function, + variable_operating_cost_function, + balance_of_system_cost_function, + min_spacing='5x', wake_loss_multiplier=1, ga_kwargs=None, + output_request=('system_capacity', 'cf_mean'), + ws_bins=(0.0, 20.0, 5.0), wd_bins=(0.0, 360.0, 45.0), + excl_dict=None, inclusion_mask=None, + area_filter_kernel='queen', min_area=None, + resolution=64, excl_area=0.0081, data_layers=None, + gids=None, exclusion_shape=None, slice_lookup=None, + prior_meta=None, gid_map=None, bias_correct=None, + pre_loaded_data=None): """ Standalone serial method to run bespoke optimization. See BespokeWindPlants docstring for parameter description. @@ -2446,6 +2462,7 @@ def run_serial( capital_cost_function, fixed_operating_cost_function, variable_operating_cost_function, + balance_of_system_cost_function, min_spacing=min_spacing, wake_loss_multiplier=wake_loss_multiplier, ga_kwargs=ga_kwargs, @@ -2523,39 +2540,37 @@ def run_parallel(self, max_workers=None): rs, cs = self.slice_lookup[gid] gid_incl_mask = self._inclusion_mask[rs, cs] - futures.append( - exe.submit( - self.run_serial, - self._excl_fpath, - self._res_fpath, - self._tm_dset, - self.sam_sys_inputs_with_site_data(gid), - self._obj_fun, - self._cap_cost_fun, - self._foc_fun, - self._voc_fun, - self._min_spacing, - wake_loss_multiplier=self._wake_loss_multiplier, - ga_kwargs=self._ga_kwargs, - output_request=self._output_request, - ws_bins=self._ws_bins, - wd_bins=self._wd_bins, - excl_dict=self._excl_dict, - inclusion_mask=gid_incl_mask, - area_filter_kernel=self._area_filter_kernel, - min_area=self._min_area, - resolution=self._resolution, - excl_area=self._excl_area, - data_layers=self._data_layers, - gids=gid, - exclusion_shape=self.shape, - slice_lookup=copy.deepcopy(self.slice_lookup), - prior_meta=self._get_prior_meta(gid), - gid_map=self._gid_map, - bias_correct=self._get_bc_for_gid(gid), - pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid), - ) - ) + futures.append(exe.submit( + self.run_serial, + self._excl_fpath, + self._res_fpath, + self._tm_dset, + self.sam_sys_inputs_with_site_data(gid), + self._obj_fun, + self._cap_cost_fun, + self._foc_fun, + self._voc_fun, + self._bos_fun, + self._min_spacing, + wake_loss_multiplier=self._wake_loss_multiplier, + ga_kwargs=self._ga_kwargs, + output_request=self._output_request, + ws_bins=self._ws_bins, + wd_bins=self._wd_bins, + excl_dict=self._excl_dict, + inclusion_mask=gid_incl_mask, + area_filter_kernel=self._area_filter_kernel, + min_area=self._min_area, + resolution=self._resolution, + excl_area=self._excl_area, + data_layers=self._data_layers, + gids=gid, + exclusion_shape=self.shape, + slice_lookup=copy.deepcopy(self.slice_lookup), + prior_meta=self._get_prior_meta(gid), + gid_map=self._gid_map, + bias_correct=self._get_bc_for_gid(gid), + pre_loaded_data=self._pre_loaded_data_for_sc_gid(gid))) # gather results for future in as_completed(futures): @@ -2617,35 +2632,34 @@ def run(self, out_fpath=None, max_workers=None): wlm = self._wake_loss_multiplier i_bc = self._get_bc_for_gid(gid) - si = self.run_serial( - self._excl_fpath, - self._res_fpath, - self._tm_dset, - sam_inputs, - self._obj_fun, - self._cap_cost_fun, - self._foc_fun, - self._voc_fun, - min_spacing=self._min_spacing, - wake_loss_multiplier=wlm, - ga_kwargs=self._ga_kwargs, - output_request=self._output_request, - ws_bins=self._ws_bins, - wd_bins=self._wd_bins, - excl_dict=self._excl_dict, - inclusion_mask=gid_incl_mask, - area_filter_kernel=afk, - min_area=self._min_area, - resolution=self._resolution, - excl_area=self._excl_area, - data_layers=self._data_layers, - slice_lookup=slice_lookup, - prior_meta=prior_meta, - gid_map=self._gid_map, - bias_correct=i_bc, - gids=gid, - pre_loaded_data=pre_loaded_data, - ) + si = self.run_serial(self._excl_fpath, + self._res_fpath, + self._tm_dset, + sam_inputs, + self._obj_fun, + self._cap_cost_fun, + self._foc_fun, + self._voc_fun, + self._bos_fun, + min_spacing=self._min_spacing, + wake_loss_multiplier=wlm, + ga_kwargs=self._ga_kwargs, + output_request=self._output_request, + ws_bins=self._ws_bins, + wd_bins=self._wd_bins, + excl_dict=self._excl_dict, + inclusion_mask=gid_incl_mask, + area_filter_kernel=afk, + min_area=self._min_area, + resolution=self._resolution, + excl_area=self._excl_area, + data_layers=self._data_layers, + slice_lookup=slice_lookup, + prior_meta=prior_meta, + gid_map=self._gid_map, + bias_correct=i_bc, + gids=gid, + pre_loaded_data=pre_loaded_data) self._outputs.update(si) else: self._outputs = self.run_parallel(max_workers=max_workers) diff --git a/reV/bespoke/cli_bespoke.py b/reV/bespoke/cli_bespoke.py index 7e4f65231..e80384e1e 100644 --- a/reV/bespoke/cli_bespoke.py +++ b/reV/bespoke/cli_bespoke.py @@ -54,6 +54,8 @@ def _log_bespoke_cli_inputs(config): .format(config.get("fixed_operating_cost_function"))) logger.info('Bespoke variable operating cost function: "{}"' .format(config.get("variable_operating_cost_function"))) + logger.info('Bespoke balance of system cost function: "{}"' + .format(config.get("balance_of_system_cost_function"))) logger.info('Bespoke wake loss multiplier: "{}"' .format(config.get("wake_loss_multiplier", 1))) logger.info('The following project points were specified: "{}"' diff --git a/reV/bespoke/place_turbines.py b/reV/bespoke/place_turbines.py index b2bd2ac83..adfc0f7e0 100644 --- a/reV/bespoke/place_turbines.py +++ b/reV/bespoke/place_turbines.py @@ -48,51 +48,74 @@ def __init__(self, wind_plant, objective_function, capital_cost_function, fixed_operating_cost_function, variable_operating_cost_function, + balance_of_system_cost_function, include_mask, pixel_side_length, min_spacing, wake_loss_multiplier=1): """ Parameters ---------- wind_plant : WindPowerPD - wind plant object to analyze wind plant performance. This object - should have everything in the plant defined, such that only the - turbine coordinates and plant capacity need to be defined during - the optimization. + wind plant object to analyze wind plant performance. This + object should have everything in the plant defined, such + that only the turbine coordinates and plant capacity need to + be defined during the optimization. objective_function : str - The objective function of the optimization as a string, should - return the objective to be minimized during layout optimization. - Variables available are: - - - n_turbines: the number of turbines - - system_capacity: wind plant capacity - - aep: annual energy production - - fixed_charge_rate: user input fixed_charge_rate if included - as part of the sam system config. - - capital_cost: plant capital cost as evaluated + The objective function of the optimization as a string, + should return the objective to be minimized during layout + optimization. Variables available are: + + - ``n_turbines``: the number of turbines + - ``system_capacity``: wind plant capacity + - ``aep``: annual energy production + - ``avg_sl_dist_to_center_m``: Average straight-line + distance to the supply curve point center from all + turbine locations (in m). Useful for computing plant + BOS costs. + - ``avg_sl_dist_to_medoid_m``: Average straight-line + distance to the medoid of all turbine locations + (in m). Useful for computing plant BOS costs. + - ``nn_conn_dist_m``: Total BOS connection distance + using nearest-neighbor connections. This variable is + only available for the + ``balance_of_system_cost_function`` equation. + - ``fixed_charge_rate``: user input fixed_charge_rate if + included as part of the sam system config. + - ``capital_cost``: plant capital cost as evaluated by `capital_cost_function` - - fixed_operating_cost: plant fixed annual operating cost as - evaluated by `fixed_operating_cost_function` - - variable_operating_cost: plant variable annual operating cost - as evaluated by `variable_operating_cost_function` - - self.wind_plant: the SAM wind plant object, through which - all SAM variables can be accessed - - cost: the annual cost of the wind plant (from cost_function) + - ``fixed_operating_cost``: plant fixed annual operating + cost as evaluated by `fixed_operating_cost_function` + - ``variable_operating_cost``: plant variable annual + operating cost as evaluated by + `variable_operating_cost_function` + - ``balance_of_system_cost``: plant balance of system + cost as evaluated by `balance_of_system_cost_function` + - ``self.wind_plant``: the SAM wind plant object, + through which all SAM variables can be accessed capital_cost_function : str - The plant capital cost function as a string, must return the total - capital cost in $. Has access to the same variables as the - objective_function. + The plant capital cost function as a string, must return the + total capital cost in $. Has access to the same variables as + the objective_function. fixed_operating_cost_function : str - The plant annual fixed operating cost function as a string, must - return the fixed operating cost in $/year. Has access to the same - variables as the objective_function. + The plant annual fixed operating cost function as a string, + must return the fixed operating cost in $/year. Has access + to the same variables as the objective_function. variable_operating_cost_function : str - The plant annual variable operating cost function as a string, must - return the variable operating cost in $/kWh. Has access to the same - variables as the objective_function. - exclusions : ExclusionMaskFromDict - The exclusions that define where turbines can be placed. Contains - exclusions.latitude, exclusions.longitude, and exclusions.mask + The plant annual variable operating cost function as a + string, must return the variable operating cost in $/kWh. + Has access to the same variables as the objective_function. + You can set this to "0" to effectively ignore variable + operating costs. + balance_of_system_cost_function : str + The plant balance-of-system cost function as a string, must + return the variable operating cost in $. Has access to the + same variables as the objective_function. You can set this + to "0" to effectively ignore balance-of-system costs. + include_mask : np.ndarray + Supply curve point 2D inclusion mask where included pixels + are set to 1 and excluded pixels are set to 0. + pixel_side_length : int + Side length (m) of a single pixel of the `include_mask`. min_spacing : float The minimum spacing between turbines (in meters). wake_loss_multiplier : float, optional @@ -110,6 +133,7 @@ def __init__(self, wind_plant, objective_function, self.fixed_operating_cost_function = fixed_operating_cost_function self.variable_operating_cost_function = \ variable_operating_cost_function + self.balance_of_system_cost_function = balance_of_system_cost_function self.objective_function = objective_function self.include_mask = include_mask @@ -128,12 +152,14 @@ def __init__(self, wind_plant, objective_function, self.packing_polygons = None self.optimized_design_variables = None self.safe_polygons = None + self._optimized_nn_conn_dist_m = None self.ILLEGAL = ('import ', 'os.', 'sys.', '.__', '__.', 'eval', 'exec') self._preflight(self.objective_function) self._preflight(self.capital_cost_function) self._preflight(self.fixed_operating_cost_function) self._preflight(self.variable_operating_cost_function) + self._preflight(self.balance_of_system_cost_function) def _preflight(self, eqn): """Run preflight checks on the equation string.""" @@ -147,7 +173,7 @@ def define_exclusions(self): """From the exclusions data, create a shapely MultiPolygon as self.safe_polygons that defines where turbines can be placed. """ - nx, ny = np.shape(self.include_mask) + ny, nx = np.shape(self.include_mask) self.safe_polygons = MultiPolygon() side_x = np.arange(nx + 1) * self.pixel_side_length side_y = np.arange(ny, -1, -1) * self.pixel_side_length @@ -210,6 +236,23 @@ def initialize_packing(self): self.x_locations = packing.turbine_x self.y_locations = packing.turbine_y + def _sc_center(self): + """Supply curve point center. """ + ny, nx = np.shape(self.include_mask) + cx = nx * self.pixel_side_length / 2 + cy = ny * self.pixel_side_length / 2 + return cx, cy + + def _avg_sl_dist_to_cent(self, x_locs, y_locs): + """Average straight-line distance to center from turb locations. """ + cx, cy = self._sc_center() + return np.hypot(x_locs - cx, y_locs - cy).mean() + + def _avg_sl_dist_to_med(self, x_locs, y_locs): + """Average straight-line distance to turbine medoid. """ + cx, cy = _turb_medoid(x_locs, y_locs) + return np.hypot(x_locs - cx, y_locs - cy).mean() + # pylint: disable=W0641,W0123 def optimization_objective(self, x): """The optimization objective used in the bespoke optimization @@ -217,8 +260,9 @@ def optimization_objective(self, x): x = [bool(y) for y in x] if len(x) > 0: n_turbines = np.sum(x) - self.wind_plant["wind_farm_xCoordinates"] = self.x_locations[x] - self.wind_plant["wind_farm_yCoordinates"] = self.y_locations[x] + x_locs, y_locs = self.x_locations[x], self.y_locations[x] + self.wind_plant["wind_farm_xCoordinates"] = x_locs + self.wind_plant["wind_farm_yCoordinates"] = y_locs system_capacity = n_turbines * self.turbine_capacity self.wind_plant["system_capacity"] = system_capacity @@ -226,8 +270,14 @@ def optimization_objective(self, x): self.wind_plant.assign_inputs() self.wind_plant.execute() aep = self._aep_after_scaled_wake_losses() + avg_sl_dist_to_center_m = self._avg_sl_dist_to_cent(x_locs, y_locs) + avg_sl_dist_to_medoid_m = self._avg_sl_dist_to_med(x_locs, y_locs) + if "nn_conn_dist_m" in self.balance_of_system_cost_function: + nn_conn_dist_m = _compute_nn_conn_dist(x_locs, y_locs) else: n_turbines = system_capacity = aep = 0 + avg_sl_dist_to_center_m = avg_sl_dist_to_medoid_m = 0 + nn_conn_dist_m = 0 fixed_charge_rate = self.fixed_charge_rate capital_cost = eval(self.capital_cost_function, @@ -236,13 +286,16 @@ def optimization_objective(self, x): globals(), locals()) variable_operating_cost = eval(self.variable_operating_cost_function, globals(), locals()) - + balance_of_system_cost = eval(self.balance_of_system_cost_function, + globals(), locals()) capital_cost *= self.wind_plant.sam_sys_inputs.get( 'capital_cost_multiplier', 1) fixed_operating_cost *= self.wind_plant.sam_sys_inputs.get( 'fixed_operating_cost_multiplier', 1) variable_operating_cost *= self.wind_plant.sam_sys_inputs.get( 'variable_operating_cost_multiplier', 1) + balance_of_system_cost *= self.wind_plant.sam_sys_inputs.get( + 'balance_of_system_cost_multiplier', 1) objective = eval(self.objective_function, globals(), locals()) @@ -353,6 +406,7 @@ def capital_cost_per_kw(self, capacity_mw): """ fixed_charge_rate = self.fixed_charge_rate + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m n_turbines = int(round(capacity_mw * 1e3 / self.turbine_capacity)) system_capacity = n_turbines * self.turbine_capacity mult = self.wind_plant.sam_sys_inputs.get( @@ -377,6 +431,28 @@ def turbine_y(self): """This is the final optimized turbine y locations (m)""" return self.y_locations[self.optimized_design_variables] + @property + @none_until_optimized + def avg_sl_dist_to_center_m(self): + """This is the final avg straight line distance to center (m)""" + return self._avg_sl_dist_to_cent(self.turbine_x, self.turbine_y) + + @property + @none_until_optimized + def avg_sl_dist_to_medoid_m(self): + """This is the final avg straight line distance to turb medoid (m)""" + return self._avg_sl_dist_to_med(self.turbine_x, self.turbine_y) + + @property + @none_until_optimized + def nn_conn_dist_m(self): + """This is the final avg straight line distance to turb medoid (m)""" + if self._optimized_nn_conn_dist_m is None: + self._optimized_nn_conn_dist_m = _compute_nn_conn_dist( + self.turbine_x, self.turbine_y + ) + return self._optimized_nn_conn_dist_m + @property @none_until_optimized def nturbs(self): @@ -476,6 +552,10 @@ def capital_cost(self): n_turbines = self.nturbs system_capacity = self.capacity aep = self.aep + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m + avg_sl_dist_to_medoid_m = self.avg_sl_dist_to_medoid_m + nn_conn_dist_m = self.nn_conn_dist_m + mult = self.wind_plant.sam_sys_inputs.get( 'capital_cost_multiplier', 1) return eval(self.capital_cost_function, globals(), locals()) * mult @@ -490,6 +570,10 @@ def fixed_operating_cost(self): n_turbines = self.nturbs system_capacity = self.capacity aep = self.aep + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m + avg_sl_dist_to_medoid_m = self.avg_sl_dist_to_medoid_m + nn_conn_dist_m = self.nn_conn_dist_m + mult = self.wind_plant.sam_sys_inputs.get( 'fixed_operating_cost_multiplier', 1) return eval(self.fixed_operating_cost_function, @@ -505,11 +589,32 @@ def variable_operating_cost(self): n_turbines = self.nturbs system_capacity = self.capacity aep = self.aep + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m + avg_sl_dist_to_medoid_m = self.avg_sl_dist_to_medoid_m + nn_conn_dist_m = self.nn_conn_dist_m + mult = self.wind_plant.sam_sys_inputs.get( 'variable_operating_cost_multiplier', 1) return eval(self.variable_operating_cost_function, globals(), locals()) * mult + @property + @none_until_optimized + def balance_of_system_cost(self): + """This is the balance of system cost of the optimized plant ($)""" + fixed_charge_rate = self.fixed_charge_rate + n_turbines = self.nturbs + system_capacity = self.capacity + aep = self.aep + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m + avg_sl_dist_to_medoid_m = self.avg_sl_dist_to_medoid_m + nn_conn_dist_m = self.nn_conn_dist_m + + mult = self.wind_plant.sam_sys_inputs.get( + 'balance_of_system_cost_multiplier', 1) + return eval(self.balance_of_system_cost_function, + globals(), locals()) * mult + # pylint: disable=W0641,W0123 @property @none_until_optimized @@ -522,4 +627,43 @@ def objective(self): capital_cost = self.capital_cost fixed_operating_cost = self.fixed_operating_cost variable_operating_cost = self.variable_operating_cost + balance_of_system_cost = self.balance_of_system_cost + avg_sl_dist_to_center_m = self.avg_sl_dist_to_center_m + avg_sl_dist_to_medoid_m = self.avg_sl_dist_to_medoid_m + nn_conn_dist_m = self.nn_conn_dist_m + return eval(self.objective_function, globals(), locals()) + + +def _turb_medoid(x_locs, y_locs): + """Turbine medoid. """ + return np.median(x_locs), np.median(y_locs) + + +def _compute_nn_conn_dist(x_coords, y_coords): + """Connect turbines using a greedy nearest-neighbor approach. """ + if len(x_coords) <= 1: + return 0 + + coordinates = np.c_[x_coords, y_coords] + allowed_conns = np.r_[coordinates.mean(axis=0)[None], coordinates] + + mask = np.zeros_like(allowed_conns) + mask[0] = 1 + left_to_connect = np.ma.array(allowed_conns, mask=mask) + + mask = np.ones_like(allowed_conns) + mask[0] = 0 + allowed_conns = np.ma.array(allowed_conns, mask=mask) + + total_dist = 0 + for __ in range(len(coordinates)): + dists = left_to_connect[:, :, None] - allowed_conns.T[None] + dists = np.hypot(dists[:, 0], dists[:, 1]) + min_dists = dists.min(axis=-1) + total_dist += min_dists.min() + next_connection = min_dists.argmin() + allowed_conns.mask[next_connection] = 0 + left_to_connect.mask[next_connection] = 1 + + return total_dist diff --git a/tests/test_bespoke.py b/tests/test_bespoke.py index 85ada355c..ddb83dbf1 100644 --- a/tests/test_bespoke.py +++ b/tests/test_bespoke.py @@ -18,7 +18,7 @@ from reV import TESTDATADIR from reV.bespoke.bespoke import BespokeSinglePlant, BespokeWindPlants -from reV.bespoke.place_turbines import PlaceTurbines +from reV.bespoke.place_turbines import PlaceTurbines, _compute_nn_conn_dist from reV.cli import main from reV.handlers.outputs import Outputs from reV.losses.power_curve import PowerCurveLossesMixin @@ -79,6 +79,7 @@ "* np.exp(-system_capacity / 1E5 * 0.1 + (1 - 0.1))" ) VOC_FUN = "3" +BOS_FUN = '0' OBJECTIVE_FUNCTION = ( "(0.0975 * capital_cost + fixed_operating_cost) " "/ aep + variable_operating_cost" @@ -101,19 +102,16 @@ def test_turbine_placement(gid=33): sam_sys_inputs["variable_operating_cost_multiplier"] = 5 TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - sam_sys_inputs, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + sam_sys_inputs, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + '10 * nn_conn_dist_m', + excl_dict=EXCL_DICT, + output_request=output_request, + ) place_optimizer = bsp.plant_optimizer assert place_optimizer.turbine_x is None @@ -125,6 +123,7 @@ def test_turbine_placement(gid=33): assert place_optimizer.capital_cost is None assert place_optimizer.fixed_operating_cost is None assert place_optimizer.variable_operating_cost is None + assert place_optimizer.balance_of_system_cost is None assert place_optimizer.objective is None place_optimizer.place_turbines(max_time=5) @@ -163,15 +162,18 @@ def test_turbine_placement(gid=33): capital_cost = eval(CAP_COST_FUN, globals(), locals()) fixed_operating_cost = eval(FOC_FUN, globals(), locals()) * 2 variable_operating_cost = eval(VOC_FUN, globals(), locals()) * 5 + balance_of_system_cost = 10 * _compute_nn_conn_dist( + place_optimizer.turbine_x, place_optimizer.turbine_y + ) # pylint: disable=W0123 assert place_optimizer.objective == eval( OBJECTIVE_FUNCTION, globals(), locals() ) assert place_optimizer.capital_cost == capital_cost assert place_optimizer.fixed_operating_cost == fixed_operating_cost - assert ( - place_optimizer.variable_operating_cost == variable_operating_cost - ) + assert (place_optimizer.variable_operating_cost + == variable_operating_cost) + assert place_optimizer.balance_of_system_cost == balance_of_system_cost bsp.close() @@ -194,19 +196,13 @@ def test_zero_area(gid=33): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + objective_function, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + ) optimizer = bsp.plant_optimizer optimizer.include_mask = np.zeros_like(optimizer.include_mask) @@ -244,32 +240,23 @@ def test_correct_turb_location(gid=33): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + objective_function, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + ) include_mask = np.zeros_like(bsp.include_mask) include_mask[1, -2] = 1 - pt = PlaceTurbines( - bsp.wind_plant_pd, - bsp.objective_function, - bsp.capital_cost_function, - bsp.fixed_operating_cost_function, - bsp.variable_operating_cost_function, - include_mask, - pixel_side_length=90, - min_spacing=45, - ) + pt = PlaceTurbines(bsp.wind_plant_pd, bsp.objective_function, + bsp.capital_cost_function, + bsp.fixed_operating_cost_function, + bsp.variable_operating_cost_function, + bsp.balance_of_system_cost_function, + include_mask, pixel_side_length=90, + min_spacing=45) pt.define_exclusions() pt.initialize_packing() @@ -286,6 +273,7 @@ def test_packing_algorithm(gid=33): cap_cost_fun = "" foc_fun = "" voc_fun = "" + bos_fun = "" objective_function = "" with tempfile.TemporaryDirectory() as td: res_fp = os.path.join(td, "ri_100_wtk_{}.h5") @@ -306,6 +294,7 @@ def test_packing_algorithm(gid=33): cap_cost_fun, foc_fun, voc_fun, + bos_fun, ga_kwargs={"max_time": 5}, excl_dict=EXCL_DICT, output_request=output_request, @@ -358,20 +347,14 @@ def test_single(gid=33): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request, + ) out = bsp.run_plant_optimization() out = bsp.run_wind_plant_ts() @@ -455,40 +438,28 @@ def test_extra_outputs(gid=33): TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) with pytest.raises(KeyError): - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + objective_function, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request, + ) sam_sys_inputs = copy.deepcopy(SAM_SYS_INPUTS) sam_sys_inputs["fixed_charge_rate"] = 0.0975 test_eos_cap = 200_000 - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - sam_sys_inputs, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - data_layers=copy.deepcopy(DATA_LAYERS), - eos_mult_baseline_cap_mw=test_eos_cap * 1e-3, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + sam_sys_inputs, + objective_function, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request, + data_layers=copy.deepcopy(DATA_LAYERS), + eos_mult_baseline_cap_mw=test_eos_cap * 1e-3 + ) out = bsp.run_plant_optimization() out = bsp.run_wind_plant_ts() @@ -514,21 +485,15 @@ def test_extra_outputs(gid=33): for layer in data_layers: assert "fpath" not in data_layers[layer] - bsp = BespokeSinglePlant( - gid, - excl_fp, - res_fp, - TM_DSET, - sam_sys_inputs, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - data_layers=data_layers, - ) + bsp = BespokeSinglePlant(gid, excl_fp, res_fp, TM_DSET, + sam_sys_inputs, + objective_function, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request, + data_layers=data_layers, + ) out = bsp.run_plant_optimization() out = bsp.run_wind_plant_ts() @@ -610,39 +575,23 @@ def test_bespoke(): # test no outputs with pytest.warns(UserWarning) as record: assert not os.path.exists(out_fpath_truth) - bsp = BespokeWindPlants( - excl_fp, - res_fp, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - fully_excluded_points, - SAM_CONFIGS, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + fully_excluded_points, + SAM_CONFIGS, ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request) test_fpath = bsp.run(max_workers=2, out_fpath=out_fpath_request) assert out_fpath_truth == test_fpath assert "points are excluded" in str(record[0].message) assert not os.path.exists(out_fpath_truth) - bsp = BespokeWindPlants( - excl_fp, - res_fp, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION, + CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, + points, SAM_CONFIGS, ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request) test_fpath = bsp.run(max_workers=2, out_fpath=out_fpath_request) assert out_fpath_truth == test_fpath assert os.path.exists(out_fpath_truth) @@ -685,22 +634,13 @@ def test_bespoke(): assert f[dset].shape[1] == len(meta) assert f[dset].any() # not all zeros - out_fpath_pre = os.path.join(td, "bespoke_out_pre.h5") - bsp = BespokeWindPlants( - excl_fp, - res_fp, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - pre_load_data=True, - ) + out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5') + bsp = BespokeWindPlants(excl_fp, res_fp, TM_DSET, OBJECTIVE_FUNCTION, + CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, + points, SAM_CONFIGS, ga_kwargs={'max_time': 1}, + excl_dict=EXCL_DICT, + output_request=output_request, + pre_load_data=True) bsp.run(max_workers=1, out_fpath=out_fpath_pre) with Resource(out_fpath_truth) as f1, Resource(out_fpath_pre) as f2: @@ -761,6 +701,7 @@ def test_consistent_eval_namespace(gid=33): cap_cost_fun = "2000" foc_fun = "0" voc_fun = "0" + bos_fun = "0" objective_function = ( "n_turbines + id(self.wind_plant) " "+ system_capacity + capital_cost + aep" @@ -784,6 +725,7 @@ def test_consistent_eval_namespace(gid=33): cap_cost_fun, foc_fun, voc_fun, + bos_fun, ga_kwargs={"max_time": 5}, excl_dict=EXCL_DICT, output_request=output_request, @@ -865,19 +807,16 @@ def test_wake_loss_multiplier(wlm): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + ) optimizer = bsp.plant_optimizer optimizer.define_exclusions() @@ -896,20 +835,16 @@ def test_wake_loss_multiplier(wlm): aep = optimizer._aep_after_scaled_wake_losses() bsp.close() - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - wake_loss_multiplier=wlm, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + wake_loss_multiplier=wlm) optimizer2 = bsp.plant_optimizer optimizer2.wind_plant["wind_farm_xCoordinates"] = optimizer.x_locations @@ -941,19 +876,16 @@ def test_bespoke_wind_plant_with_power_curve_losses(): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + ) optimizer = bsp.plant_optimizer optimizer.wind_plant["wind_farm_xCoordinates"] = [1000, -1000] @@ -963,6 +895,7 @@ def test_bespoke_wind_plant_with_power_curve_losses(): optimizer.wind_plant.assign_inputs() optimizer.wind_plant.execute() + # pylint: disable=W0612 aep = optimizer._aep_after_scaled_wake_losses() bsp.close() @@ -971,20 +904,15 @@ def test_bespoke_wind_plant_with_power_curve_losses(): "target_losses_percent": 10, "transformation": "exponential_stretching", } - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - sam_inputs, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) - + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + sam_inputs, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request) optimizer2 = bsp.plant_optimizer optimizer2.wind_plant["wind_farm_xCoordinates"] = [1000, -1000] optimizer2.wind_plant["wind_farm_yCoordinates"] = [1000, -1000] @@ -1024,6 +952,7 @@ def test_bespoke_run_with_power_curve_losses(): CAP_COST_FUN, FOC_FUN, VOC_FUN, + BOS_FUN, ga_kwargs={"max_time": 5}, excl_dict=EXCL_DICT, output_request=output_request, @@ -1048,6 +977,7 @@ def test_bespoke_run_with_power_curve_losses(): CAP_COST_FUN, FOC_FUN, VOC_FUN, + BOS_FUN, ga_kwargs={"max_time": 5}, excl_dict=EXCL_DICT, output_request=output_request, @@ -1079,20 +1009,13 @@ def test_bespoke_run_with_scheduled_losses(): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request) out = bsp.run_plant_optimization() out = bsp.run_wind_plant_ts() @@ -1119,20 +1042,16 @@ def test_bespoke_run_with_scheduled_losses(): sam_inputs["hourly"] = [0] * 8760 # only needed for testing output_request = ("system_capacity", "cf_mean", "cf_profile", "hourly") - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - sam_inputs, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + sam_inputs, + OBJECTIVE_FUNCTION, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request) out_losses = bsp.run_plant_optimization() out_losses = bsp.run_wind_plant_ts() @@ -1167,19 +1086,16 @@ def test_bespoke_aep_is_zero_if_no_turbines_placed(): res_fp = res_fp.format("*") TechMapping.run(excl_fp, RES.format(2012), dset=TM_DSET, max_workers=1) - bsp = BespokeSinglePlant( - 33, - excl_fp, - res_fp, - TM_DSET, - SAM_SYS_INPUTS, - objective_function, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeSinglePlant(33, excl_fp, res_fp, TM_DSET, + SAM_SYS_INPUTS, + objective_function, + CAP_COST_FUN, + FOC_FUN, + VOC_FUN, + BOS_FUN, + excl_dict=EXCL_DICT, + output_request=output_request, + ) optimizer = bsp.plant_optimizer optimizer.define_exclusions() @@ -1244,40 +1160,22 @@ def test_bespoke_prior_run(): assert not os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_all, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - sam_configs, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_all, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, sam_configs, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request) bsp.run(max_workers=1, out_fpath=out_fpath1) assert os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - sam_configs, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - prior_run=out_fpath1, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, sam_configs, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request, + prior_run=out_fpath1) bsp.run(max_workers=1, out_fpath=out_fpath2) assert os.path.exists(out_fpath2) @@ -1357,40 +1255,22 @@ def test_gid_map(): assert not os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, SAM_CONFIGS, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request) bsp.run(max_workers=1, out_fpath=out_fpath1) assert os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - gid_map=fp_gid_map, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, SAM_CONFIGS, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request, + gid_map=fp_gid_map) bsp.run(max_workers=1, out_fpath=out_fpath2) assert os.path.exists(out_fpath2) @@ -1417,23 +1297,13 @@ def test_gid_map(): assert not np.allclose(data1["ws_mean"], data2["ws_mean"], atol=0.2) assert np.allclose(ws.mean(), data2["ws_mean"], atol=0.01) - out_fpath_pre = os.path.join(td, "bespoke_out_pre.h5") - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - gid_map=fp_gid_map, - pre_load_data=True, - ) + out_fpath_pre = os.path.join(td, 'bespoke_out_pre.h5') + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, SAM_CONFIGS, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request, + gid_map=fp_gid_map, pre_load_data=True) bsp.run(max_workers=1, out_fpath=out_fpath_pre) with Resource(out_fpath2) as f1, Resource(out_fpath_pre) as f2: @@ -1486,40 +1356,22 @@ def test_bespoke_bias_correct(): assert not os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, SAM_CONFIGS, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request) bsp.run(max_workers=1, out_fpath=out_fpath1) assert os.path.exists(out_fpath1) assert not os.path.exists(out_fpath2) - bsp = BespokeWindPlants( - excl_fp, - res_fp_2013, - TM_DSET, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - SAM_CONFIGS, - ga_kwargs={"max_time": 1}, - excl_dict=EXCL_DICT, - output_request=output_request, - bias_correct=fp_bc, - ) + bsp = BespokeWindPlants(excl_fp, res_fp_2013, TM_DSET, + OBJECTIVE_FUNCTION, CAP_COST_FUN, + FOC_FUN, VOC_FUN, BOS_FUN, points, SAM_CONFIGS, + ga_kwargs={'max_time': 1}, excl_dict=EXCL_DICT, + output_request=output_request, + bias_correct=fp_bc) bsp.run(max_workers=1, out_fpath=out_fpath2) assert os.path.exists(out_fpath2) @@ -1581,6 +1433,7 @@ def test_cli(runner, clear_loggers): "capital_cost_function": CAP_COST_FUN, "fixed_operating_cost_function": FOC_FUN, "variable_operating_cost_function": VOC_FUN, + "balance_of_system_cost_function": "0", "project_points": [33, 35], "sam_files": SAM_CONFIGS, "min_spacing": "5x", @@ -1691,20 +1544,11 @@ def test_bespoke_5min_sample(): arr = np.random.choice(10, size=excl_file["latitude"].shape) excl_file.create_dataset(name=tm_dset, data=arr) - bsp = BespokeWindPlants( - excl_fp, - res_fp, - tm_dset, - OBJECTIVE_FUNCTION, - CAP_COST_FUN, - FOC_FUN, - VOC_FUN, - points, - sam_configs, - ga_kwargs={"max_time": 5}, - excl_dict=EXCL_DICT, - output_request=output_request, - ) + bsp = BespokeWindPlants(excl_fp, res_fp, tm_dset, OBJECTIVE_FUNCTION, + CAP_COST_FUN, FOC_FUN, VOC_FUN, BOS_FUN, + points, sam_configs, ga_kwargs={'max_time': 5}, + excl_dict=EXCL_DICT, + output_request=output_request) _ = bsp.run(max_workers=1, out_fpath=out_fpath) with Resource(out_fpath) as f: