Source code for reV.econ.economies_of_scale

# -*- coding: utf-8 -*-
"""
reV module for calculating economies of scale where larger power plants will
have reduced capital cost.
"""

import copy
import logging
import re

# pylint: disable=unused-import
import numpy as np
import pandas as pd
from rex.utilities.utilities import check_eval_str

from reV.econ.utilities import lcoe_fcr
from reV.utilities import SupplyCurveField

logger = logging.getLogger(__name__)


[docs] class EconomiesOfScale: """Class to calculate economies of scale where power plant capital cost is reduced for larger power plants. Units ----- capacity_factor : unitless capacity : kW annual_energy_production : kWh fixed_charge_rate : unitless fixed_operating_cost : $ (per year) variable_operating_cost : $/kWh lcoe : $/MWh """ def __init__(self, data, cap_eqn=None, fixed_eqn=None, var_eqn=None): """ Parameters ---------- data : dict | pd.DataFrame Namespace of econ data to use to calculate economies of scale. Keys in dict or column labels in dataframe should match the Independent variables in the eqn input. Should also include variables required to calculate LCOE. cap_eqn : str, optional LCOE scaling equation to implement "economies of scale". Equation must be in python string format and return a scalar value to multiply the capital cost by. Independent variables in the equation should match the keys in the data input arg. This equation may use numpy functions with the package prefix "np". If ``None``, no economies of scale are applied to the capital cost. By default, ``None``. fixed_eqn : str, optional LCOE scaling equation to implement "economies of scale". Equation must be in python string format and return a scalar value to multiply the fixed operating cost by. Independent variables in the equation should match the keys in the data input arg. This equation may use numpy functions with the package prefix "np". If ``None``, no economies of scale are applied to the fixed operating cost. By default, ``None``. var_eqn : str, optional LCOE scaling equation to implement "economies of scale". Equation must be in python string format and return a scalar value to multiply the variable operating cost by. Independent variables in the equation should match the keys in the data input arg. This equation may use numpy functions with the package prefix "np". If ``None``, no economies of scale are applied to the variable operating cost. By default, ``None``. """ self._data = data self._cap_eqn = cap_eqn self._fixed_eqn = fixed_eqn self._var_eqn = var_eqn self._vars = None self._preflight() def _preflight(self): """Run checks to validate EconomiesOfScale equation and input data.""" for eq in self._all_equations: if eq is not None: check_eval_str(str(eq)) if isinstance(self._data, pd.DataFrame): self._data = { k: self._data[k].values.flatten() for k in self._data.columns } if not isinstance(self._data, dict): e = ( "Cannot evaluate EconomiesOfScale with data input of type: " "{}".format(type(self._data)) ) logger.error(e) raise TypeError(e) missing = [name for name in self.vars if name not in self._data] if any(missing): e = ( "Cannot evaluate EconomiesOfScale, missing data for variables" ": {} for equation: {}".format(missing, self._cap_eqn) ) logger.error(e) raise KeyError(e) @property def _all_equations(self): """gen: All EOS equations""" yield from (self._cap_eqn, self._fixed_eqn, self._var_eqn)
[docs] @staticmethod def is_num(s): """Check if a string is a number""" try: float(s) except ValueError: return False else: return True
[docs] @staticmethod def is_method(s): """Check if a string is a numpy/pandas or python builtin method""" return bool(s.startswith(("np.", "pd.")) or s in dir(__builtins__))
@property def vars(self): """Get a list of variable names that the EconomiesOfScale equation uses as input. Returns ------- vars : list List of strings representing variable names that were parsed from the equation string. This will return an empty list if the equation has no variables. """ if self._vars is not None: return self._vars self._vars = [] for eq in self._all_equations: if eq is None: continue delimiters = (">", "<", ">=", "<=", "==", ",", "*", "/", "+", "-", " ", "(", ")", "[", "]") regex_pattern = "|".join(map(re.escape, delimiters)) for sub_str in re.split(regex_pattern, str(eq)): is_valid_var_name = (sub_str and not self.is_num(sub_str) and not self.is_method(sub_str)) if is_valid_var_name: self._vars.append(sub_str) self._vars = sorted(set(self._vars)) return self._vars def _evaluate(self, eqn): """Evaluate the EconomiesOfScale equation with Independent variables parsed into a kwargs dictionary input. Parameters ---------- eqn : str LCOE scaling equation to implement "economies of scale". Equation must be in python string format and return a scalar multiplier. Independent variables in the equation should match the keys in the data input arg. This equation may use numpy functions with the package prefix "np". If ``None``, this function returns ``1``. Returns ------- out : float | np.ndarray Evaluated output of the EconomiesOfScale equation. """ if eqn is None: return 1 kwargs = {k: self._data[k] for k in self.vars} # pylint: disable=eval-used return eval(str(eqn), globals(), kwargs) @staticmethod def _get_prioritized_keys(input_dict, key_list): """Get data from an input dictionary based on an ordered (prioritized) list of retrieval keys. If no keys are found in the input_dict, an error will be raised. Parameters ---------- input_dict : dict Dictionary of data key_list : list | tuple Ordered (prioritized) list of retrieval keys. Returns ------- out : object Data retrieved from input_dict using the first key in key_list found in the input_dict. """ out = None for key in key_list: if key in input_dict: out = input_dict[key] break if out is None: e = ( "Could not find requested key list ({}) in the input " "dictionary keys: {}".format(key_list, list(input_dict.keys())) ) logger.error(e) raise KeyError(e) return out @property def capital_cost_scalar(self): """Evaluated output of the EconomiesOfScale capital cost equation. Should be numeric scalars to apply directly to the capital cost. Returns ------- out : float | np.ndarray Evaluated output of the EconomiesOfScale equation. Should be numeric scalars to apply directly to the capital cost. """ return self._evaluate(self._cap_eqn) @property def fixed_operating_cost_scalar(self): """Evaluated output of the EconomiesOfScale fixed operating cost equation. Should be numeric scalars to apply directly to the fixed operating cost. Returns ------- out : float | np.ndarray Evaluated output of the EconomiesOfScale equation. Should be numeric scalars to apply directly to the fixed operating cost. """ return self._evaluate(self._fixed_eqn) @property def variable_operating_cost_scalar(self): """Evaluated output of the EconomiesOfScale equation variable operating cost. Should be numeric scalars to apply directly to the variable operating cost. Returns ------- out : float | np.ndarray Evaluated output of the EconomiesOfScale equation. Should be numeric scalars to apply directly to the variable operating cost. """ return self._evaluate(self._var_eqn) def _cost_from_cap(self, col_name): """Get full cost value from cost per mw in data. Parameters ---------- col_name : str Name of column containing the cost per mw value. Returns ------- float | None Cost value if it was found in data, ``None`` otherwise. """ cap = self._data.get(SupplyCurveField.CAPACITY_AC_MW) if cap is None: return None cost_per_mw = self._data.get(col_name) if cost_per_mw is None: return None return cap * cost_per_mw @property def raw_capital_cost(self): """Unscaled (raw) capital cost found in the data input arg. Returns ------- out : float | np.ndarray Unscaled (raw) capital_cost ($) found in the data input arg. """ raw_capital_cost_from_cap = self._cost_from_cap( SupplyCurveField.COST_SITE_CC_USD_PER_AC_MW ) if raw_capital_cost_from_cap is not None: return raw_capital_cost_from_cap key_list = ["capital_cost", "mean_capital_cost"] return self._get_prioritized_keys(self._data, key_list) @property def scaled_capital_cost(self): """Capital cost found in the data input arg scaled by the evaluated EconomiesOfScale input equation. Returns ------- out : float | np.ndarray Capital cost ($) found in the data input arg scaled by the evaluated EconomiesOfScale equation. """ cc = copy.deepcopy(self.raw_capital_cost) cc *= self.capital_cost_scalar return cc @property def fcr(self): """Fixed charge rate from input data arg Returns ------- out : float | np.ndarray Fixed charge rate from input data arg """ fcr = self._data.get(SupplyCurveField.FIXED_CHARGE_RATE) if fcr is not None and fcr > 0: return fcr key_list = ["fixed_charge_rate", "mean_fixed_charge_rate", "fcr", "mean_fcr"] return self._get_prioritized_keys(self._data, key_list) @property def raw_fixed_operating_cost(self): """Unscaled (raw) fixed operating cost from input data arg Returns ------- out : float | np.ndarray Unscaled (raw) fixed operating cost ($/year) from input data arg """ foc_from_cap = self._cost_from_cap( SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW ) if foc_from_cap is not None: return foc_from_cap key_list = ["fixed_operating_cost", "mean_fixed_operating_cost", "foc", "mean_foc"] return self._get_prioritized_keys(self._data, key_list) @property def scaled_fixed_operating_cost(self): """Fixed operating cost found in the data input arg scaled by the evaluated EconomiesOfScale input equation. Returns ------- out : float | np.ndarray Fixed operating cost ($/year) found in the data input arg scaled by the evaluated EconomiesOfScale equation. """ foc = copy.deepcopy(self.raw_fixed_operating_cost) foc *= self.fixed_operating_cost_scalar return foc @property def raw_variable_operating_cost(self): """Unscaled (raw) variable operating cost from input data arg Returns ------- out : float | np.ndarray Unscaled (raw) variable operating cost ($/kWh) from input data arg """ voc_mwh = self._data.get(SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MWH) if voc_mwh is not None: return voc_mwh / 1000 # convert to $/kWh key_list = ["variable_operating_cost", "mean_variable_operating_cost", "voc", "mean_voc"] return self._get_prioritized_keys(self._data, key_list) @property def scaled_variable_operating_cost(self): """Variable operating cost found in the data input arg scaled by the evaluated EconomiesOfScale input equation. Returns ------- out : float | np.ndarray Variable operating cost ($/kWh) found in the data input arg scaled by the evaluated EconomiesOfScale equation. """ voc = copy.deepcopy(self.raw_variable_operating_cost) voc *= self.variable_operating_cost_scalar return voc @property def aep(self): """Annual energy production (kWh) back-calculated from the raw LCOE: AEP = (fcr * raw_cap_cost + raw_foc) / (raw_lcoe - raw_voc) Returns ------- out : float | np.ndarray """ num = self.fcr * self.raw_capital_cost + self.raw_fixed_operating_cost denom = self.raw_lcoe - (self.raw_variable_operating_cost * 1000) return num / denom * 1000 # convert MWh to KWh @property def raw_lcoe(self): """Raw LCOE ($/MWh) taken from the input data Returns ------- lcoe : float | np.ndarray """ key_list = [SupplyCurveField.RAW_LCOE, SupplyCurveField.MEAN_LCOE] return copy.deepcopy(self._get_prioritized_keys(self._data, key_list)) @property def scaled_lcoe(self): """LCOE ($/MWh) calculated with the scaled costs based on the EconomiesOfScale input equation. LCOE = (FCR * scaled_capital_cost + scaled_FOC) / AEP + scaled_VOC Returns ------- lcoe : float | np.ndarray LCOE calculated with the scaled costs based on the EconomiesOfScale input equation. """ return lcoe_fcr(self.fcr, self.scaled_capital_cost, self.scaled_fixed_operating_cost, self.aep, self.scaled_variable_operating_cost)