# -*- 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