# -*- coding: utf-8 -*-
"""
SAM Wind Balance of System Cost Model
"""
from copy import deepcopy
import numpy as np
from PySAM.PySSC import ssc_sim_from_dict
from reV.utilities.exceptions import SAMInputError
[docs]class WindBos:
"""Wind Balance of System Cost Model."""
MODULE = 'windbos'
# keys for the windbos input data dictionary.
# Some keys may not be found explicitly in the SAM input.
KEYS = ('tech_model',
'financial_model',
'machine_rating',
'rotor_diameter',
'hub_height',
'number_of_turbines',
'interconnect_voltage',
'distance_to_interconnect',
'site_terrain',
'turbine_layout',
'soil_condition',
'construction_time',
'om_building_size',
'quantity_test_met_towers',
'quantity_permanent_met_towers',
'weather_delay_days',
'crane_breakdowns',
'access_road_entrances',
'turbine_capital_cost',
'turbine_cost_per_kw',
'tower_top_mass',
'delivery_assist_required',
'pad_mount_transformer_required',
'new_switchyard_required',
'rock_trenching_required',
'mv_thermal_backfill',
'mv_overhead_collector',
'performance_bond',
'contingency',
'warranty_management',
'sales_and_use_tax',
'overhead',
'profit_margin',
'development_fee',
'turbine_transportation')
def __init__(self, inputs):
"""
Parameters
----------
inputs : dict
SAM key value pair inputs.
"""
self._turbine_capital_cost = 0.0
self._datadict = {}
self._inputs = inputs
self._special = {'tech_model': 'windbos',
'financial_model': 'none',
'machine_rating': self.machine_rating,
'hub_height': self.hub_height,
'rotor_diameter': self.rotor_diameter,
'number_of_turbines': self.number_of_turbines,
'turbine_capital_cost': self.turbine_capital_cost,
}
self._parse_inputs()
self._out = ssc_sim_from_dict(self._datadict)
def _parse_inputs(self):
"""Parse SAM inputs into a windbos input dict and perform any
required special operations."""
for k in self.KEYS:
if k in self._special:
self._datadict[k] = self._special[k]
elif k not in self._inputs:
raise SAMInputError('Windbos requires input key: "{}"'
.format(k))
else:
self._datadict[k] = self._inputs[k]
@property
def machine_rating(self):
"""Single turbine machine rating either from input or power curve."""
if 'machine_rating' in self._inputs:
return self._inputs['machine_rating']
else:
return np.max(self._inputs['wind_turbine_powercurve_powerout'])
@property
def hub_height(self):
"""Turbine hub height."""
if 'wind_turbine_hub_ht' in self._inputs:
return self._inputs['wind_turbine_hub_ht']
else:
return self._inputs['hub_height']
@property
def rotor_diameter(self):
"""Turbine rotor diameter."""
if 'wind_turbine_rotor_diameter' in self._inputs:
return self._inputs['wind_turbine_rotor_diameter']
else:
return self._inputs['rotor_diameter']
@property
def number_of_turbines(self):
"""Number of turbines either based on input or system (farm) capacity
and machine rating"""
if 'number_of_turbines' in self._inputs:
return self._inputs['number_of_turbines']
else:
return self._inputs['system_capacity'] / self.machine_rating
@property
def turbine_capital_cost(self):
"""Returns zero (no turbine capital cost for WindBOS input,
and assigns any input turbine_capital_cost to an attr"""
if 'turbine_capital_cost' in self._inputs:
self._turbine_capital_cost = self._inputs['turbine_capital_cost']
else:
self._turbine_capital_cost = 0.0
return 0.0
@property
def bos_cost(self):
"""Get the balance of system cost ($)."""
return self._out['project_total_budgeted_cost']
@property
def turbine_cost(self):
"""Get the turbine cost ($)."""
tcost = ((self._inputs['turbine_cost_per_kw']
* self.machine_rating
* self.number_of_turbines)
+ (self._turbine_capital_cost
* self.number_of_turbines))
return tcost
@property
def sales_tax_mult(self):
"""Get a sales tax multiplier (frac of the total installed cost)."""
basis = self._inputs.get('sales_tax_basis', 0) / 100
tax = self._datadict.get('sales_and_use_tax', 0) / 100
return basis * tax
@property
def sales_tax_cost(self):
"""Get the cost of sales tax ($)."""
return (self.bos_cost + self.turbine_cost) * self.sales_tax_mult
@property
def total_installed_cost(self):
"""Get the total installed cost ($) (bos + turbine)."""
return self.bos_cost + self.turbine_cost + self.sales_tax_cost
@property
def output(self):
"""Get a dictionary containing the cost breakdown."""
output = {'total_installed_cost': self.total_installed_cost,
'turbine_cost': self.turbine_cost,
'sales_tax_cost': self.sales_tax_cost,
'bos_cost': self.bos_cost}
return output
# pylint: disable-msg=W0613
[docs] @classmethod
def reV_run(cls, points_control, site_df,
output_request=('total_installed_cost',), **kwargs):
"""Execute SAM SingleOwner simulations based on reV points control.
Parameters
----------
points_control : config.PointsControl
PointsControl instance containing project points site and SAM
config info.
site_df : pd.DataFrame
Dataframe of site-specific input variables. Row index corresponds
to site number/gid (via df.loc not df.iloc), column labels are the
variable keys that will be passed forward as SAM parameters.
output_request : list | tuple | str
Output(s) to retrieve from SAM.
kwargs : dict
Not used but maintained for polymorphic calls with other
SAM econ reV_run() methods (lcoe and single owner).
Breaks pylint error W0613: unused argument.
Returns
-------
out : dict
Nested dictionaries where the top level key is the site index,
the second level key is the variable name, second level value is
the output variable value.
"""
out = {}
for site in points_control.sites:
# get SAM inputs from project_points based on the current site
_, inputs = points_control.project_points[site]
# ensure that site-specific data is not persisted to other sites
site_inputs = deepcopy(inputs)
site_inputs.update(dict(site_df.loc[site, :]))
wb = cls(site_inputs)
out[site] = {k: v for k, v in wb.output.items()
if k in output_request}
return out