Source code for python.financial_functions
import numpy as np
import pandas as pd
import decorators
import datetime
from scipy import optimize
import settings
import utility_functions as utilfunc
import agent_mutation
import pyarrow as pa
import pyarrow.parquet as pq
import PySAM
import PySAM.Battwatts as battery
import PySAM.BatteryTools as batt_tools
import PySAM.Utilityrate5 as utility
import PySAM.Cashloan as cashloan
#==============================================================================
# Load logger
logger = utilfunc.get_logger()
#==============================================================================
#%%
[docs]def calc_system_performance(kw, pv, utilityrate, loan, batt, costs, agent, rate_switch_table, en_batt=True, batt_simple_dispatch=0):
"""
Executes Battwatts, Utilityrate5, and Cashloan PySAM modules with system sizes (kw) as input
Parameters
----------
kw: Capacity (in kW)
pv: Dictionary with generation_hourly and consumption_hourly
utilityrate: PySAM Utilityrate5 module
loan: PySAM Cashloan module
batt: PySAM Battwatts module
costs: Dictionary with system costs
agent: pd.Series with agent attirbutes
rate_switch_table: pd.DataFrame with details on how rates will switch with DG/storage adoption
en_batt: Enable battery
batt_simple_dispatch: batt.Battery.batt_simple_dispatch
- batt_simple_dispatch = 0 (peak shaving look ahead)
- batt_simple_dispatch = 1 (peak shaving look behind)
Returns
-------
-loan.Outputs.npv: the negative net present value of system + storage to be optimized for system sizing
"""
inv_eff = 0.96 # default SAM inverter efficiency for PV
gen_hourly = pv['generation_hourly']
load_hourly = pv['consumption_hourly'] # same field as 'load_kwh_per_customer_in_bin_initial' when summed
dc = [(i * kw) * 1000 for i in gen_hourly] # W
ac = [i * inv_eff for i in dc] # W
gen = [i / 1000 for i in ac] # W to kW
# Set up battery, with system generation conditional on the battery generation being included
if en_batt:
batt.Battery.dc = dc
batt.Battery.ac = ac
batt.Battery.batt_simple_enable = 1
batt.Battery.batt_simple_chemistry = 1 # default value is 1: li ion for residential
batt.Battery.batt_simple_dispatch = batt_simple_dispatch
batt.Battery.batt_simple_meter_position = 0 # default value
batt.Battery.inverter_efficiency = 100 # recommended by Darice for dc-connected
batt.Battery.load = load_hourly
# PV to Battery ratio (kW) - From Ashreeta, 02/08/2020
pv_to_batt_ratio = 1.31372
batt_capacity_to_power_ratio = 2 # hours of operation
desired_size = kw / pv_to_batt_ratio # Default SAM value for residential systems is 10
desired_power = desired_size / batt_capacity_to_power_ratio
batt_inputs = {
'batt_chem': batt.Battery.batt_simple_chemistry,
'batt_Qfull': 2.5, # default SAM value
'batt_Vnom_default': 3.6, # default SAM value
'batt_ac_or_dc': 0, # dc-connected
'desired_power': desired_power,
'desired_capacity': desired_size,
'desired_voltage': 500,
'size_by_ac_not_dc': 0, # dc-connected
'inverter_eff': batt.Battery.inverter_efficiency
# 'batt_dc_dc_efficiency': (optional)
}
# Default values for lead acid batteries
if batt.Battery.batt_simple_chemistry == 0:
batt_inputs['LeadAcid_q10'] = 93.2
batt_inputs['LeadAcid_q20'] = 100
batt_inputs['LeadAcid_qn'] = 58.12
# batt_inputs['LeadAcid_tn']: (optional)
batt_outputs = batt_tools.size_li_ion_battery(batt_inputs)
computed_size = batt_outputs['batt_computed_bank_capacity']
computed_power = batt_outputs['batt_power_discharge_max_kwdc']
batt.Battery.batt_simple_kwh = computed_size
batt.Battery.batt_simple_kw = computed_power
batt.execute()
# apply storage rate switch if computed_size is nonzero
if computed_size > 0.:
agent, one_time_charge = agent_mutation.elec.apply_rate_switch(rate_switch_table, agent, computed_size, tech='storage')
else:
one_time_charge = 0.
# declare value for net billing sell rate
if agent.loc['compensation_style']=='none':
net_billing_sell_rate = 0.
else:
net_billing_sell_rate = agent.loc['wholesale_elec_price_dollars_per_kwh'] * agent.loc['elec_price_multiplier']
utilityrate = process_tariff(utilityrate, agent.loc['tariff_dict'], net_billing_sell_rate)
utilityrate.SystemOutput.gen = batt.Outputs.gen
loan.BatterySystem.en_batt = 1
loan.BatterySystem.batt_computed_bank_capacity = batt.Outputs.batt_bank_installed_capacity
loan.BatterySystem.batt_bank_replacement = batt.Outputs.batt_bank_replacement
# #loan.BatterySystem.battery_per_kWh = costs['batt_capex_per_kwh']
# # Battery capacity-based System Costs amount [$/kWcap]
# loan.SystemCosts.om_capacity1 = [costs['batt_om_per_kw']]
# # Battery production-based System Costs amount [$/MWh]
# # loan.SystemCosts.om_production1 = [costs['batt_om_per_kwh'] * 1000]
# loan.SystemCosts.om_production1 = [costs['batt_om_per_kwh'] * 1000.]
# specify number of O&M types (1 = PV+batt)
loan.SystemCosts.add_om_num_types = 1
# if PV system size nonzero, specify combined O&M costs; otherwise, specify standalone O&M costs
if kw > 0:
loan.BatterySystem.battery_per_kWh = costs['batt_capex_per_kwh_combined']
loan.SystemCosts.om_capacity = [costs['system_om_per_kw_combined'] + costs['system_variable_om_per_kw_combined']]
loan.SystemCosts.om_capacity1 = [costs['batt_om_per_kw_combined']]
loan.SystemCosts.om_production1 = [costs['batt_om_per_kwh_combined'] * 1000.]
loan.SystemCosts.om_replacement_cost1 = [0.]
system_costs = costs['system_capex_per_kw_combined'] * kw
# specify linear constant adder for PV+batt (combined) system
linear_constant = agent.loc['linear_constant_combined']
else:
loan.BatterySystem.battery_per_kWh = costs['batt_capex_per_kwh']
loan.SystemCosts.om_capacity = [costs['system_om_per_kw'] + costs['system_variable_om_per_kw']]
loan.SystemCosts.om_capacity1 = [costs['batt_om_per_kw']]
loan.SystemCosts.om_production1 = [costs['batt_om_per_kwh'] * 1000.]
loan.SystemCosts.om_replacement_cost1 = [0.]
system_costs = costs['system_capex_per_kw'] * kw
# specify linear constant adder for standalone battery system
linear_constant = agent.loc['linear_constant']
# Battery capacity for System Costs values [kW]
loan.SystemCosts.om_capacity1_nameplate = batt.Battery.batt_simple_kw
# Battery production for System Costs values [kWh]
loan.SystemCosts.om_production1_values = [batt.Battery.batt_simple_kwh] # should this be batt.Outputs.batt_bank_installed_capacity?
#batt_costs = (costs['batt_capex_per_kw']*batt.Battery.batt_simple_kw) + (costs['batt_capex_per_kwh'] * batt.Battery.batt_simple_kwh)
batt_costs = ((costs['batt_capex_per_kw_combined']*batt.Battery.batt_simple_kw) +
(costs['batt_capex_per_kwh_combined'] * batt.Battery.batt_simple_kwh))
value_of_resiliency = agent.loc['value_of_resiliency_usd']
else:
batt.Battery.batt_simple_enable = 0
loan.BatterySystem.en_batt = 0
computed_power = computed_size = 0
# apply solar rate switch if computed_size is nonzero
if kw > 0:
agent, one_time_charge = agent_mutation.elec.apply_rate_switch(rate_switch_table, agent, kw, tech='solar')
else:
one_time_charge = 0.
# declare value for net billing sell rate
if agent.loc['compensation_style']=='none':
net_billing_sell_rate = 0.
else:
net_billing_sell_rate = agent.loc['wholesale_elec_price_dollars_per_kwh'] * agent.loc['elec_price_multiplier']
utilityrate = process_tariff(utilityrate, agent.loc['tariff_dict'], net_billing_sell_rate)
utilityrate.SystemOutput.gen = gen
loan.BatterySystem.en_batt = 1
# specify number of O&M types (0 = PV only)
loan.SystemCosts.add_om_num_types = 0
# since battery system size is zero, specify standalone PV O&M costs
loan.SystemCosts.om_capacity = [costs['system_om_per_kw'] + costs['system_variable_om_per_kw']]
loan.SystemCosts.om_replacement_cost1 = [0.]
system_costs = costs['system_capex_per_kw'] * kw
batt_costs = 0.
# linear constant for standalone PV system is 0.
linear_constant = 0.
value_of_resiliency = 0.
# Execute utility rate module
utilityrate.Load.load = load_hourly
#utilityrate.ElectricityRates.ur_metering_option = ur_metering_option
utilityrate.execute()
loan = process_incentives(loan, kw, computed_power, computed_size, gen_hourly, agent)
# Specify final Cashloan parameters
loan.FinancialParameters.system_capacity = kw
# Add value_of_resiliency -- should only apply from year 1 onwards, not to year 0
annual_energy_value = ([utilityrate.Outputs.annual_energy_value[0]] +
[x + value_of_resiliency for i,x in enumerate(utilityrate.Outputs.annual_energy_value) if i!=0])
loan.SystemOutput.annual_energy_value = annual_energy_value
loan.SystemOutput.gen = utilityrate.SystemOutput.gen
loan.ThirdPartyOwnership.elec_cost_with_system = utilityrate.Outputs.elec_cost_with_system
loan.ThirdPartyOwnership.elec_cost_without_system = utilityrate.Outputs.elec_cost_without_system
# Calculate system costs
#system_costs = costs['system_capex_per_kw'] * kw
direct_costs = (system_costs + batt_costs) * costs['cap_cost_multiplier']
sales_tax = 0.
loan.SystemCosts.total_installed_cost = direct_costs + linear_constant + sales_tax + one_time_charge
# Execute financial module
loan.execute()
return -loan.Outputs.npv
[docs]def calc_system_size_and_performance(agent, sectors, rate_switch_table=None):
"""
Calculate the optimal system and battery size and generation profile, and resulting bill savings and financial metrics.
Parameters
----------
agent : 'pd.df'
individual agent object.
Returns
-------
agent: 'pd.df'
Adds several features to the agent dataframe:
- agent_id
- system_kw - system capacity selected by agent
- batt_kw - battery capacity selected by agent
- batt_kwh - battery energy capacity
- npv - net present value of system + storage
- cash_flow - array of annual cash flows from system adoption
- batt_dispatch_profile - array of hourly battery dispatch
- annual_energy_production_kwh - annual energy production (kwh) of system
- naep - normalized annual energy production (kwh/kW) of system
- capacity_factor - annual capacity factor
- first_year_elec_bill_with_system - first year electricity bill with adopted system ($/yr)
- first_year_elec_bill_savings - first year electricity bill savings with adopted system ($/yr)
- first_year_elec_bill_savings_frac - fraction of savings on electricity bill in first year of system adoption
- max_system_kw - maximum system size allowed as constrained by roof size or not exceeding annual consumption
- first_year_elec_bill_without_system - first year electricity bill without adopted system ($/yr)
- avg_elec_price_cents_per_kwh - first year electricity price (c/kwh)
- cbi - ndarray of capacity-based incentives applicable to agent
- ibi - ndarray of investment-based incentives applicable to agent
- pbi - ndarray of performance-based incentives applicable to agent
- cash_incentives - ndarray of cash-based incentives applicable to agent
- export_tariff_result - summary of structure of retail tariff applied to agent
"""
# Initialize new DB connection
model_settings = settings.init_model_settings()
con, cur = utilfunc.make_con(model_settings.pg_conn_string, model_settings.role)
# PV
pv = dict()
load_profile_df = agent_mutation.elec.get_and_apply_agent_load_profiles(con, agent)
pv['consumption_hourly'] = pd.Series(load_profile_df['consumption_hourly']).iloc[0]
del load_profile_df
# Using the scale offset factor of 1E6 for capacity factors
norm_scaled_pv_cf_profiles_df = agent_mutation.elec.get_and_apply_normalized_hourly_resource_solar(con, agent)
pv['generation_hourly'] = pd.Series(norm_scaled_pv_cf_profiles_df['solar_cf_profile'].iloc[0]) / 1e6
del norm_scaled_pv_cf_profiles_df
agent.loc['naep'] = float(np.sum(pv['generation_hourly']))
# Battwatts
if agent.loc['sector_abbr'] == 'res':
batt = battery.default("PVWattsBatteryResidential")
else:
batt = battery.default("PVWattsBatteryCommercial")
# Instantiate utilityrate5 model based on agent sector
if agent.loc['sector_abbr'] == 'res':
utilityrate = utility.default("PVWattsBatteryResidential")
else:
utilityrate = utility.default("PVWattsBatteryCommercial")
tariff_dict = agent.loc['tariff_dict']
######################################
###--------- UTILITYRATE5 ---------###
###--- SYSTEM LIFETIME SETTINGS ---###
######################################
# Inflation rate [%]
utilityrate.Lifetime.inflation_rate = agent.loc['inflation_rate'] * 100
# Number of years in analysis [years]
utilityrate.Lifetime.analysis_period = agent.loc['economic_lifetime_yrs']
# Lifetime hourly system outputs [0/1]; Options: 0=hourly first year,1=hourly lifetime
utilityrate.Lifetime.system_use_lifetime_output = 0
######################################
###--------- UTILITYRATE5 ---------###
###---- DEGRADATION/ESCALATION ----###
######################################
# Annual energy degradation [%]
utilityrate.SystemOutput.degradation = [agent.loc['pv_degradation_factor'] * 100] # convert decimal to %
# old method
# degradation = [agent.loc['pv_degradation_factor'] * 100] # convert decimal to %
# utilityrate.SystemOutput.degradation = degradation
# Annual electricity rate escalation [%/year]
utilityrate.ElectricityRates.rate_escalation = [agent.loc['elec_price_escalator'] * 100] # convert decimal to %
######################################
###--------- UTILITYRATE5 ---------###
###---- NET METERING SETTINGS -----###
######################################
# Dictionary to map dGen compensation styles to PySAM options
nem_options = {'net metering':0, 'net billing':2, 'buy all sell all':4, 'none':2}
# Metering options [0=net energy metering,1=net energy metering with $ credits,2=net billing,3=net billing with carryover to next month,4=buy all - sell all]
utilityrate.ElectricityRates.ur_metering_option = nem_options[agent.loc['compensation_style']]
# Year end sell rate [$/kWh]
utilityrate.ElectricityRates.ur_nm_yearend_sell_rate = agent.loc['wholesale_elec_price_dollars_per_kwh'] * agent.loc['elec_price_multiplier']
if agent.loc['compensation_style']=='none':
net_billing_sell_rate = 0.
else:
net_billing_sell_rate = agent.loc['wholesale_elec_price_dollars_per_kwh'] * agent.loc['elec_price_multiplier']
# ######################################
# ###--------- UTILITYRATE5 ---------###
# ###--- FIXED AND ANNUAL CHARGES ---###
# ######################################
# # Monthly fixed charge [$]
# utilityrate.ElectricityRates.ur_monthly_fixed_charge = tariff_dict['fixed_charge']
# # Annual minimum charge [$]
# utilityrate.ElectricityRates.ur_annual_min_charge = 0. # not currently tracked in URDB rate attribute downloads
# # Monthly minimum charge [$]
# utilityrate.ElectricityRates.ur_monthly_min_charge = 0. # not currently tracked in URDB rate attribute downloads
# ######################################
# ###--------- UTILITYRATE5 ---------###
# ###-------- DEMAND CHARGES --------###
# ######################################
# # Enable demand charge
# utilityrate.ElectricityRates.ur_dc_enable = (tariff_dict['d_flat_exists']) | (tariff_dict['d_tou_exists'])
# if utilityrate.ElectricityRates.ur_dc_enable:
# if tariff_dict['d_flat_exists']:
# # Reformat demand charge table from dGen format
# n_periods = len(tariff_dict['d_flat_levels'][0])
# n_tiers = len(tariff_dict['d_flat_levels'])
# ur_dc_flat_mat = []
# for period in range(n_periods):
# for tier in range(n_tiers):
# row = [period, tier+1, tariff_dict['d_flat_levels'][tier][period], tariff_dict['d_flat_prices'][tier][period]]
# ur_dc_flat_mat.append(row)
# # Demand rates (flat) table
# utilityrate.ElectricityRates.ur_dc_flat_mat = ur_dc_flat_mat
# if tariff_dict['d_tou_exists']:
# # Reformat demand charge table from dGen format
# n_periods = len(tariff_dict['d_tou_levels'][0])
# n_tiers = len(tariff_dict['d_tou_levels'])
# ur_dc_tou_mat = []
# for period in range(n_periods):
# for tier in range(n_tiers):
# row = [period+1, tier+1, tariff_dict['d_tou_levels'][tier][period], tariff_dict['d_tou_prices'][tier][period]]
# ur_dc_tou_mat.append(row)
# # Demand rates (TOU) table
# utilityrate.ElectricityRates.ur_dc_tou_mat = ur_dc_tou_mat
# # Reformat 12x24 tables - original are indexed to 0, PySAM needs index starting at 1
# d_wkday_12by24 = []
# for m in range(len(tariff_dict['d_wkday_12by24'])):
# row = [x+1 for x in tariff_dict['d_wkday_12by24'][m]]
# d_wkday_12by24.append(row)
# d_wkend_12by24 = []
# for m in range(len(tariff_dict['d_wkend_12by24'])):
# row = [x+1 for x in tariff_dict['d_wkend_12by24'][m]]
# d_wkend_12by24.append(row)
# # Demand charge weekday schedule
# utilityrate.ElectricityRates.ur_dc_sched_weekday = d_wkday_12by24
# # Demand charge weekend schedule
# utilityrate.ElectricityRates.ur_dc_sched_weekend = d_wkend_12by24
# ######################################
# ###--------- UTILITYRATE5 ---------###
# ###-------- ENERGY CHARGES --------###
# ######################################
# if tariff_dict['e_exists']:
# # Dictionary to map dGen max usage units to PySAM options
# max_usage_dict = {'kWh':0, 'kWh/kW':1, 'kWh daily':2, 'kWh/kW daily':3}
# # If max usage units are 'kWh daily', divide max usage by 30 -- rate download procedure converts daily to monthly
# modifier = 30. if tariff_dict['energy_rate_unit'] == 'kWh daily' else 1.
# # Reformat energy charge table from dGen format
# n_periods = len(tariff_dict['e_levels'][0])
# n_tiers = len(tariff_dict['e_levels'])
# ur_ec_tou_mat = []
# for period in range(n_periods):
# for tier in range(n_tiers):
# row = [period+1, tier+1, tariff_dict['e_levels'][tier][period]/modifier,
# max_usage_dict[tariff_dict['energy_rate_unit']], tariff_dict['e_prices'][tier][period], net_billing_sell_rate]
# ur_ec_tou_mat.append(row)
# # Energy rates table
# utilityrate.ElectricityRates.ur_ec_tou_mat = ur_ec_tou_mat
# # Reformat 12x24 tables - original are indexed to 0, PySAM needs index starting at 1
# e_wkday_12by24 = []
# for m in range(len(tariff_dict['e_wkday_12by24'])):
# row = [x+1 for x in tariff_dict['e_wkday_12by24'][m]]
# e_wkday_12by24.append(row)
# e_wkend_12by24 = []
# for m in range(len(tariff_dict['e_wkend_12by24'])):
# row = [x+1 for x in tariff_dict['e_wkend_12by24'][m]]
# e_wkend_12by24.append(row)
# # Energy charge weekday schedule
# utilityrate.ElectricityRates.ur_ec_sched_weekday = e_wkday_12by24
# # Energy charge weekend schedule
# utilityrate.ElectricityRates.ur_ec_sched_weekend = e_wkend_12by24
######################################
###--------- UTILITYRATE5 ---------###
###-------- BUY/SELL RATES --------###
######################################
# Enable time step sell rates [0/1]
utilityrate.ElectricityRates.ur_en_ts_sell_rate = 0
# Time step sell rates [0/1]
utilityrate.ElectricityRates.ur_ts_sell_rate = [0.]
# Set sell rate equal to buy rate [0/1]
utilityrate.ElectricityRates.ur_sell_eq_buy = 0
######################################
###--------- UTILITYRATE5 ---------###
###-------- MISC. SETTINGS --------###
######################################
# Use single monthly peak for TOU demand charge; options: 0=use TOU peak,1=use flat peak
utilityrate.ElectricityRates.TOU_demand_single_peak = 0 # ?
# Optionally enable/disable electricity_rate [years]
utilityrate.ElectricityRates.en_electricity_rates = 1
######################################
###--------- UTILITYRATE5 ---------###
###----- TARIFF RESTRUCTURING -----###
######################################
utilityrate = process_tariff(utilityrate, agent.loc['tariff_dict'], net_billing_sell_rate)
######################################
###----------- CASHLOAN -----------###
###----- FINANCIAL PARAMETERS -----###
######################################
# Initiate cashloan model and set market-specific variables
# Assume res agents do not evaluate depreciation at all
# Assume non-res agents only evaluate federal depreciation (not state)
if agent.loc['sector_abbr'] == 'res':
loan = cashloan.default("PVWattsBatteryResidential")
loan.FinancialParameters.market = 0
else:
loan = cashloan.default("PVWattsBatteryCommercial")
loan.FinancialParameters.market = 1
loan.FinancialParameters.analysis_period = agent.loc['economic_lifetime_yrs']
loan.FinancialParameters.debt_fraction = 100 - (agent.loc['down_payment_fraction'] * 100)
loan.FinancialParameters.federal_tax_rate = [(agent.loc['tax_rate'] * 100) * 0.7] # SAM default
loan.FinancialParameters.inflation_rate = agent.loc['inflation_rate'] * 100
loan.FinancialParameters.insurance_rate = 0
loan.FinancialParameters.loan_rate = agent.loc['loan_interest_rate'] * 100
loan.FinancialParameters.loan_term = agent.loc['loan_term_yrs']
loan.FinancialParameters.mortgage = 0 # default value - standard loan (no mortgage)
loan.FinancialParameters.prop_tax_assessed_decline = 5 # PySAM default
loan.FinancialParameters.prop_tax_cost_assessed_percent = 95 # PySAM default
loan.FinancialParameters.property_tax_rate = 0 # PySAM default
loan.FinancialParameters.real_discount_rate = agent.loc['real_discount_rate'] * 100
loan.FinancialParameters.salvage_percentage = 0
loan.FinancialParameters.state_tax_rate = [(agent.loc['tax_rate'] * 100) * 0.3] # SAM default
loan.FinancialParameters.system_heat_rate = 0
######################################
###----------- CASHLOAN -----------###
###--------- SYSTEM COSTS ---------###
######################################
# System costs that are input to loan.SystemCosts will depend on system configuration (PV, batt, PV+batt)
# and are therefore specified in calc_system_performance()
system_costs = dict()
system_costs['system_capex_per_kw'] = agent.loc['system_capex_per_kw']
system_costs['system_om_per_kw'] = agent.loc['system_om_per_kw']
system_costs['system_variable_om_per_kw'] = agent.loc['system_variable_om_per_kw']
system_costs['cap_cost_multiplier'] = agent.loc['cap_cost_multiplier']
system_costs['batt_capex_per_kw'] = agent.loc['batt_capex_per_kw']
system_costs['batt_capex_per_kwh'] = agent.loc['batt_capex_per_kwh']
system_costs['batt_om_per_kw'] = agent.loc['batt_om_per_kw']
system_costs['batt_om_per_kwh'] = agent.loc['batt_om_per_kwh']
system_costs['linear_constant'] = agent.loc['linear_constant']
# costs for PV+batt configuration are distinct from standalone techs
system_costs['system_capex_per_kw_combined'] = agent.loc['system_capex_per_kw_combined']
system_costs['system_om_per_kw_combined'] = agent.loc['system_om_per_kw']
system_costs['system_variable_om_per_kw_combined'] = agent.loc['system_variable_om_per_kw']
system_costs['batt_capex_per_kw_combined'] = agent.loc['batt_capex_per_kw_combined']
system_costs['batt_capex_per_kwh_combined'] = agent.loc['batt_capex_per_kwh_combined']
system_costs['batt_om_per_kw_combined'] = agent.loc['batt_om_per_kw_combined']
system_costs['batt_om_per_kwh_combined'] = agent.loc['batt_om_per_kwh_combined']
system_costs['linear_constant_combined'] = agent.loc['linear_constant_combined']
######################################
###----------- CASHLOAN -----------###
###---- DEPRECIATION PARAMETERS ---###
######################################
if agent.loc['sector_abbr'] == 'res':
loan.Depreciation.depr_fed_type = 0
loan.Depreciation.depr_sta_type = 0
else:
loan.Depreciation.depr_fed_type = 1
loan.Depreciation.depr_sta_type = 0
######################################
###----------- CASHLOAN -----------###
###----- TAX CREDIT INCENTIVES ----###
######################################
loan.TaxCreditIncentives.itc_fed_percent = agent.loc['itc_fraction_of_capex'] * 100
######################################
###----------- CASHLOAN -----------###
###-------- BATTERY SYSTEM --------###
######################################
loan.BatterySystem.batt_replacement_option = 2 # user schedule
batt_replacement_schedule = [0 for i in range(0, agent.loc['batt_lifetime_yrs'] - 1)] + [1]
loan.BatterySystem.batt_replacement_schedule = batt_replacement_schedule
######################################
###----------- CASHLOAN -----------###
###-------- SYSTEM OUTPUT ---------###
######################################
loan.SystemOutput.degradation = [agent.loc['pv_degradation_factor'] * 100]
######################################
###----------- CASHLOAN -----------###
###----------- LIFETIME -----------###
######################################
loan.Lifetime.system_use_lifetime_output = 0
######################################
###-------- SYSTEM SIZING ---------###
######################################
# From dGen - calc_system_size_and_financial_performance()
max_size_load = agent.loc['load_kwh_per_customer_in_bin'] / agent.loc['naep']
max_size_roof = agent.loc['developable_roof_sqft'] * agent.loc['pv_kw_per_sqft']
max_system_kw = min(max_size_load, max_size_roof)
# set tolerance for minimize_scalar based on max_system_kw value
tol = min(0.25 * max_system_kw, 0.5)
#tol = 0.25 * max_system_kw
# Calculate the PV system size that maximizes the agent's NPV, to a tolerance of 0.5 kW.
# Note that the optimization is technically minimizing negative NPV
# ! As is, because of the tolerance this function would not necessarily return a system size of 0 or max PV size if those are optimal
res_with_batt = optimize.minimize_scalar(calc_system_performance,
args = (pv, utilityrate, loan, batt, system_costs, agent, rate_switch_table, True, 0),
bounds = (0, max_system_kw),
method = 'bounded',
tol = tol)
# PySAM Module outputs with battery
batt_loan_outputs = loan.Outputs.export()
batt_util_outputs = utilityrate.Outputs.export()
batt_annual_energy_kwh = np.sum(utilityrate.SystemOutput.gen)
batt_kw = batt.Battery.batt_simple_kw
batt_kwh = batt.Battery.batt_simple_kwh
batt_dispatch_profile = batt.Outputs.batt_power
# Run without battery
res_no_batt = optimize.minimize_scalar(calc_system_performance,
args = (pv, utilityrate, loan, batt, system_costs, agent, rate_switch_table, False, 0),
bounds = (0, max_system_kw),
method = 'bounded',
tol = tol)
# PySAM Module outputs without battery
no_batt_loan_outputs = loan.Outputs.export()
no_batt_util_outputs = utilityrate.Outputs.export()
no_batt_annual_energy_kwh = np.sum(utilityrate.SystemOutput.gen)
# Retrieve NPVs of system with batt and system without batt
npv_w_batt = batt_loan_outputs['npv']
npv_no_batt = no_batt_loan_outputs['npv']
# Choose the system with the higher NPV
if npv_w_batt >= npv_no_batt:
system_kw = res_with_batt.x
annual_energy_production_kwh = batt_annual_energy_kwh
first_year_elec_bill_with_system = batt_util_outputs['elec_cost_with_system_year1']
first_year_elec_bill_without_system = batt_util_outputs['elec_cost_without_system_year1']
npv = npv_w_batt
payback = batt_loan_outputs['payback']
cash_flow = list(batt_loan_outputs['cf_payback_with_expenses']) # ?
cbi_total = batt_loan_outputs['cbi_total']
cbi_total_fed = batt_loan_outputs['cbi_total_fed']
cbi_total_oth = batt_loan_outputs['cbi_total_oth']
cbi_total_sta = batt_loan_outputs['cbi_total_sta']
cbi_total_uti = batt_loan_outputs['cbi_total_uti']
ibi_total = batt_loan_outputs['ibi_total']
ibi_total_fed = batt_loan_outputs['ibi_total_fed']
ibi_total_oth = batt_loan_outputs['ibi_total_oth']
ibi_total_sta = batt_loan_outputs['ibi_total_sta']
ibi_total_uti = batt_loan_outputs['ibi_total_uti']
cf_pbi_total = batt_loan_outputs['cf_pbi_total']
pbi_total_fed = batt_loan_outputs['cf_pbi_total_fed']
pbi_total_oth = batt_loan_outputs['cf_pbi_total_oth']
pbi_total_sta = batt_loan_outputs['cf_pbi_total_sta']
pbi_total_uti = batt_loan_outputs['cf_pbi_total_uti']
else:
system_kw = res_no_batt.x
annual_energy_production_kwh = no_batt_annual_energy_kwh
first_year_elec_bill_with_system = no_batt_util_outputs['elec_cost_with_system_year1']
first_year_elec_bill_without_system = no_batt_util_outputs['elec_cost_without_system_year1']
npv = npv_no_batt
payback = no_batt_loan_outputs['payback']
cash_flow = list(no_batt_loan_outputs['cf_payback_with_expenses'])
batt_kw = 0
batt_kwh = 0
batt_dispatch_profile = np.nan
cbi_total = no_batt_loan_outputs['cbi_total']
cbi_total_fed = no_batt_loan_outputs['cbi_total_fed']
cbi_total_oth = no_batt_loan_outputs['cbi_total_oth']
cbi_total_sta = no_batt_loan_outputs['cbi_total_sta']
cbi_total_uti = no_batt_loan_outputs['cbi_total_uti']
ibi_total = no_batt_loan_outputs['ibi_total']
ibi_total_fed = no_batt_loan_outputs['ibi_total_fed']
ibi_total_oth = no_batt_loan_outputs['ibi_total_oth']
ibi_total_sta = no_batt_loan_outputs['ibi_total_sta']
ibi_total_uti = no_batt_loan_outputs['ibi_total_uti']
cf_pbi_total = no_batt_loan_outputs['cf_pbi_total']
pbi_total_fed = no_batt_loan_outputs['cf_pbi_total_fed']
pbi_total_oth = no_batt_loan_outputs['cf_pbi_total_oth']
pbi_total_sta = no_batt_loan_outputs['cf_pbi_total_sta']
pbi_total_uti = no_batt_loan_outputs['cf_pbi_total_uti']
# change 0 value to 1 to avoid divide by zero errors
if first_year_elec_bill_without_system == 0:
first_year_elec_bill_without_system = 1.0
# Add outputs to agent df
naep = annual_energy_production_kwh / system_kw
first_year_elec_bill_savings = first_year_elec_bill_without_system - first_year_elec_bill_with_system
first_year_elec_bill_savings_frac = first_year_elec_bill_savings / first_year_elec_bill_without_system
avg_elec_price_cents_per_kwh = first_year_elec_bill_without_system / agent.loc['load_kwh_per_customer_in_bin']
agent.loc['system_kw'] = system_kw
agent.loc['npv'] = npv
agent.loc['payback_period'] = np.round(np.where(np.isnan(payback), 30.1, payback), 1).astype(float)
agent.loc['cash_flow'] = cash_flow
agent.loc['annual_energy_production_kwh'] = annual_energy_production_kwh
agent.loc['naep'] = naep
agent.loc['capacity_factor'] = agent.loc['naep'] / 8760
agent.loc['first_year_elec_bill_with_system'] = first_year_elec_bill_with_system
agent.loc['first_year_elec_bill_savings'] = first_year_elec_bill_savings
agent.loc['first_year_elec_bill_savings_frac'] = first_year_elec_bill_savings_frac
agent.loc['max_system_kw'] = max_system_kw
agent.loc['first_year_elec_bill_without_system'] = first_year_elec_bill_without_system
agent.loc['avg_elec_price_cents_per_kwh'] = avg_elec_price_cents_per_kwh
agent.loc['batt_kw'] = batt_kw
agent.loc['batt_kwh'] = batt_kwh
agent.loc['batt_dispatch_profile'] = batt_dispatch_profile
# Financial outputs (find out which ones to include):
agent.loc['cbi'] = np.array({'cbi_total': cbi_total,
'cbi_total_fed': cbi_total_fed,
'cbi_total_oth': cbi_total_oth,
'cbi_total_sta': cbi_total_sta,
'cbi_total_uti': cbi_total_uti
})
agent.loc['ibi'] = np.array({'ibi_total': ibi_total,
'ibi_total_fed': ibi_total_fed,
'ibi_total_oth': ibi_total_oth,
'ibi_total_sta': ibi_total_sta,
'ibi_total_uti': ibi_total_uti
})
agent.loc['pbi'] = np.array({'pbi_total': cf_pbi_total,
'pbi_total_fed': pbi_total_fed,
'pbi_total_oth': pbi_total_oth,
'pbi_total_sta': pbi_total_sta,
'pbi_total_uti': pbi_total_uti
})
agent.loc['cash_incentives'] = ''
agent.loc['export_tariff_results'] = ''
out_cols = ['agent_id',
'system_kw',
'batt_kw',
'batt_kwh',
'npv',
'payback_period',
'cash_flow',
'batt_dispatch_profile',
'annual_energy_production_kwh',
'naep',
'capacity_factor',
'first_year_elec_bill_with_system',
'first_year_elec_bill_savings',
'first_year_elec_bill_savings_frac',
'max_system_kw',
'first_year_elec_bill_without_system',
'avg_elec_price_cents_per_kwh',
'cbi',
'ibi',
'pbi',
'cash_incentives',
'export_tariff_results'
]
return agent[out_cols]
#%%
[docs]def process_tariff(utilityrate, tariff_dict, net_billing_sell_rate):
"""
Instantiate the utilityrate5 PySAM model and process the agent's rate json object to conform with PySAM input formatting.
Parameters
----------
agent : 'pd.Series'
Individual agent object.
Returns
-------
utilityrate: 'PySAM.Utilityrate5'
"""
######################################
###--------- UTILITYRATE5 ---------###
###--- FIXED AND ANNUAL CHARGES ---###
######################################
# Monthly fixed charge [$]
utilityrate.ElectricityRates.ur_monthly_fixed_charge = tariff_dict['fixed_charge']
# Annual minimum charge [$]
utilityrate.ElectricityRates.ur_annual_min_charge = 0. # not currently tracked in URDB rate attribute downloads
# Monthly minimum charge [$]
utilityrate.ElectricityRates.ur_monthly_min_charge = 0. # not currently tracked in URDB rate attribute downloads
######################################
###--------- UTILITYRATE5 ---------###
###-------- DEMAND CHARGES --------###
######################################
# Enable demand charge
utilityrate.ElectricityRates.ur_dc_enable = (tariff_dict['d_flat_exists']) | (tariff_dict['d_tou_exists'])
if utilityrate.ElectricityRates.ur_dc_enable:
if tariff_dict['d_flat_exists']:
# Reformat demand charge table from dGen format
n_periods = len(tariff_dict['d_flat_levels'][0])
n_tiers = len(tariff_dict['d_flat_levels'])
ur_dc_flat_mat = []
for period in range(n_periods):
for tier in range(n_tiers):
row = [period, tier+1, tariff_dict['d_flat_levels'][tier][period], tariff_dict['d_flat_prices'][tier][period]]
ur_dc_flat_mat.append(row)
# Demand rates (flat) table
utilityrate.ElectricityRates.ur_dc_flat_mat = ur_dc_flat_mat
if tariff_dict['d_tou_exists']:
# Reformat demand charge table from dGen format
n_periods = len(tariff_dict['d_tou_levels'][0])
n_tiers = len(tariff_dict['d_tou_levels'])
ur_dc_tou_mat = []
for period in range(n_periods):
for tier in range(n_tiers):
row = [period+1, tier+1, tariff_dict['d_tou_levels'][tier][period], tariff_dict['d_tou_prices'][tier][period]]
ur_dc_tou_mat.append(row)
# Demand rates (TOU) table
utilityrate.ElectricityRates.ur_dc_tou_mat = ur_dc_tou_mat
# Reformat 12x24 tables - original are indexed to 0, PySAM needs index starting at 1
d_wkday_12by24 = []
for m in range(len(tariff_dict['d_wkday_12by24'])):
row = [x+1 for x in tariff_dict['d_wkday_12by24'][m]]
d_wkday_12by24.append(row)
d_wkend_12by24 = []
for m in range(len(tariff_dict['d_wkend_12by24'])):
row = [x+1 for x in tariff_dict['d_wkend_12by24'][m]]
d_wkend_12by24.append(row)
# Demand charge weekday schedule
utilityrate.ElectricityRates.ur_dc_sched_weekday = d_wkday_12by24
# Demand charge weekend schedule
utilityrate.ElectricityRates.ur_dc_sched_weekend = d_wkend_12by24
######################################
###--------- UTILITYRATE5 ---------###
###-------- ENERGY CHARGES --------###
######################################
if tariff_dict['e_exists']:
# Dictionary to map dGen max usage units to PySAM options
max_usage_dict = {'kWh':0, 'kWh/kW':1, 'kWh daily':2, 'kWh/kW daily':3}
# If max usage units are 'kWh daily', divide max usage by 30 -- rate download procedure converts daily to monthly
modifier = 30. if tariff_dict['energy_rate_unit'] == 'kWh daily' else 1.
# Reformat energy charge table from dGen format
n_periods = len(tariff_dict['e_levels'][0])
n_tiers = len(tariff_dict['e_levels'])
ur_ec_tou_mat = []
for period in range(n_periods):
for tier in range(n_tiers):
row = [period+1, tier+1, tariff_dict['e_levels'][tier][period], max_usage_dict[tariff_dict['energy_rate_unit']],
tariff_dict['e_prices'][tier][period], net_billing_sell_rate]
ur_ec_tou_mat.append(row)
# Energy rates table
utilityrate.ElectricityRates.ur_ec_tou_mat = ur_ec_tou_mat
# Reformat 12x24 tables - original are indexed to 0, PySAM needs index starting at 1
e_wkday_12by24 = []
for m in range(len(tariff_dict['e_wkday_12by24'])):
row = [x+1 for x in tariff_dict['e_wkday_12by24'][m]]
e_wkday_12by24.append(row)
e_wkend_12by24 = []
for m in range(len(tariff_dict['e_wkend_12by24'])):
row = [x+1 for x in tariff_dict['e_wkend_12by24'][m]]
e_wkend_12by24.append(row)
# Energy charge weekday schedule
utilityrate.ElectricityRates.ur_ec_sched_weekday = e_wkday_12by24
# Energy charge weekend schedule
utilityrate.ElectricityRates.ur_ec_sched_weekend = e_wkend_12by24
return utilityrate
#%%
[docs]def process_incentives(loan, kw, batt_kw, batt_kwh, generation_hourly, agent):
######################################
###----------- CASHLOAN -----------###
###------ PAYMENT INCENTIVES ------###
######################################
# Read incentive dataframe from agent attributes
incentive_df = agent.loc['state_incentives']
# Check dtype of incentive_df - process incentives if pd.DataFrame, otherwise do not assign incentive values to cashloan
if isinstance(incentive_df, pd.DataFrame):
# Fill NaNs in incentive_df - assume max incentive duration of 5 years and max incentive value of $10,000
incentive_df = incentive_df.fillna(value={'incentive_duration_yrs' : 5, 'max_incentive_usd' : 10000})
# Filter for CBI's in incentive_df
cbi_df = (incentive_df.loc[pd.notnull(incentive_df['cbi_usd_p_w'])]
.sort_values(['cbi_usd_p_w'], axis=0, ascending=False)
.reset_index(drop=True)
)
# Process state capacity-based incentives (CBI)
#cbi_value = calculate_capacity_based_incentives(kw, batt_kw, batt_kwh, agent)
# For multiple CBIs that are applicable to the agent, cap at 2 and use PySAM's "state" and "other" option
if len(cbi_df) == 1:
loan.PaymentIncentives.cbi_sta_amount = cbi_df['cbi_usd_p_w'].iloc[0]
loan.PaymentIncentives.cbi_sta_deprbas_fed = 0
loan.PaymentIncentives.cbi_sta_deprbas_sta = 0
loan.PaymentIncentives.cbi_sta_maxvalue = cbi_df['max_incentive_usd'].iloc[0]
loan.PaymentIncentives.cbi_sta_tax_fed = 0
loan.PaymentIncentives.cbi_sta_tax_sta = 0
elif len(cbi_df) >= 2:
loan.PaymentIncentives.cbi_sta_amount = cbi_df['cbi_usd_p_w'].iloc[0]
loan.PaymentIncentives.cbi_sta_deprbas_fed = 0
loan.PaymentIncentives.cbi_sta_deprbas_sta = 0
loan.PaymentIncentives.cbi_sta_maxvalue = cbi_df['max_incentive_usd'].iloc[0]
loan.PaymentIncentives.cbi_sta_tax_fed = 1
loan.PaymentIncentives.cbi_sta_tax_sta = 1
loan.PaymentIncentives.cbi_oth_amount = cbi_df['cbi_usd_p_w'].iloc[1]
loan.PaymentIncentives.cbi_oth_deprbas_fed = 0
loan.PaymentIncentives.cbi_oth_deprbas_sta = 0
loan.PaymentIncentives.cbi_oth_maxvalue = cbi_df['max_incentive_usd'].iloc[1]
loan.PaymentIncentives.cbi_oth_tax_fed = 1
loan.PaymentIncentives.cbi_oth_tax_sta = 1
else:
pass
# Filter for PBI's in incentive_df
pbi_df = (incentive_df.loc[pd.notnull(incentive_df['pbi_usd_p_kwh'])]
.sort_values(['pbi_usd_p_kwh'], axis=0, ascending=False)
.reset_index(drop=True)
)
# Process state production-based incentives (CBI)
agent.loc['timesteps_per_year'] = 1
pv_kwh_by_year = np.array(list(map(lambda x: sum(x), np.split(np.array(generation_hourly), agent.loc['timesteps_per_year']))))
pv_kwh_by_year = np.concatenate([(pv_kwh_by_year - (pv_kwh_by_year * agent.loc['pv_degradation_factor'] * i)) for i in range(1, agent.loc['economic_lifetime_yrs']+1)])
kwh_by_timestep = kw * pv_kwh_by_year
#pbi_value = calculate_production_based_incentives(kw, kwh_by_timestep, agent)
# For multiple PBIs that are applicable to the agent, cap at 2 and use PySAM's "state" and "other" option
if len(pbi_df) == 1:
# Aamount input [$/kWh] requires sequence -- repeat pbi_usd_p_kwh using incentive_duration_yrs
loan.PaymentIncentives.pbi_sta_amount = [pbi_df['pbi_usd_p_kwh'].iloc[0]] * int(pbi_df['incentive_duration_yrs'].iloc[0])
loan.PaymentIncentives.pbi_sta_escal = 0.
loan.PaymentIncentives.pbi_sta_tax_fed = 1
loan.PaymentIncentives.pbi_sta_tax_sta = 1
loan.PaymentIncentives.pbi_sta_term = pbi_df['incentive_duration_yrs'].iloc[0]
elif len(pbi_df) >= 2:
# Aamount input [$/kWh] requires sequence -- repeat pbi_usd_p_kwh using incentive_duration_yrs
loan.PaymentIncentives.pbi_sta_amount = [pbi_df['pbi_usd_p_kwh'].iloc[0]] * int(pbi_df['incentive_duration_yrs'].iloc[0])
loan.PaymentIncentives.pbi_sta_escal = 0.
loan.PaymentIncentives.pbi_sta_tax_fed = 1
loan.PaymentIncentives.pbi_sta_tax_sta = 1
loan.PaymentIncentives.pbi_sta_term = pbi_df['incentive_duration_yrs'].iloc[0]
# Aamount input [$/kWh] requires sequence -- repeat pbi_usd_p_kwh using incentive_duration_yrs
loan.PaymentIncentives.pbi_oth_amount = [pbi_df['pbi_usd_p_kwh'].iloc[1]] * int(pbi_df['incentive_duration_yrs'].iloc[1])
loan.PaymentIncentives.pbi_oth_escal = 0.
loan.PaymentIncentives.pbi_oth_tax_fed = 1
loan.PaymentIncentives.pbi_oth_tax_sta = 1
loan.PaymentIncentives.pbi_oth_term = pbi_df['incentive_duration_yrs'].iloc[1]
else:
pass
# Filter for IBI's in incentive_df
ibi_df = (incentive_df.loc[pd.notnull(incentive_df['ibi_pct'])]
.sort_values(['ibi_pct'], axis=0, ascending=False)
.reset_index(drop=True)
)
# Process state investment-based incentives (CBI)
#ibi_value = calculate_investment_based_incentives(kw, batt_kw, batt_kwh, agent)
# For multiple IBIs that are applicable to the agent, cap at 2 and use PySAM's "state" and "other" option
# NOTE: this specifies IBI percentage, instead of IBI absolute amount
if len(ibi_df) == 1:
loan.PaymentIncentives.ibi_sta_percent = ibi_df['ibi_pct'].iloc[0]
loan.PaymentIncentives.ibi_sta_percent_deprbas_fed = 0
loan.PaymentIncentives.ibi_sta_percent_deprbas_sta = 0
loan.PaymentIncentives.ibi_sta_percent_maxvalue = ibi_df['max_incentive_usd'].iloc[0]
loan.PaymentIncentives.ibi_sta_percent_tax_fed = 1
loan.PaymentIncentives.ibi_sta_percent_tax_sta = 1
elif len(ibi_df) >= 2:
loan.PaymentIncentives.ibi_sta_percent = ibi_df['ibi_pct'].iloc[0]
loan.PaymentIncentives.ibi_sta_percent_deprbas_fed = 0
loan.PaymentIncentives.ibi_sta_percent_deprbas_sta = 0
loan.PaymentIncentives.ibi_sta_percent_maxvalue = ibi_df['max_incentive_usd'].iloc[0]
loan.PaymentIncentives.ibi_sta_percent_tax_fed = 1
loan.PaymentIncentives.ibi_sta_percent_tax_sta = 1
loan.PaymentIncentives.ibi_oth_percent = ibi_df['ibi_pct'].iloc[1]
loan.PaymentIncentives.ibi_oth_percent_deprbas_fed = 0
loan.PaymentIncentives.ibi_oth_percent_deprbas_sta = 0
loan.PaymentIncentives.ibi_oth_percent_maxvalue = ibi_df['max_incentive_usd'].iloc[1]
loan.PaymentIncentives.ibi_oth_percent_tax_fed = 1
loan.PaymentIncentives.ibi_oth_percent_tax_sta = 1
else:
pass
else:
pass
return loan
#%%
[docs]@decorators.fn_timer(logger = logger, tab_level = 2, prefix = '')
def calc_financial_performance(dataframe):
"""
Function to calculate the payback period and join it on the agent dataframe
Parameters
----------
dataframe : "pd.df"
Agent dataframe
Returns
-------
- dataframe: 'pd.df' - Agent dataframe with payback period joined on dataframe
"""
dataframe = dataframe.reset_index()
cfs = np.vstack(dataframe['cash_flow']).astype(np.float)
# calculate payback period
tech_lifetime = np.shape(cfs)[1] - 1
payback = calc_payback_vectorized(cfs, tech_lifetime)
# All agents (residential and non-residential use payback period)
dataframe['payback_period'] = payback
dataframe = dataframe.set_index('agent_id')
return dataframe
#%%
[docs]def calc_payback_vectorized(cfs, tech_lifetime):
"""
Calculate the payback period in years for a given cash flow. Payback is defined as the first year where cumulative cash flows are positive.
Cash flows that do not result in payback are given a period of 30.1
Parameters
----------
cfs : "numpy.ndarray"
Annual cash flows of investment, where 0th index refers to 0th year of investment
tech_lifetime : "numpy.ndarray"
Number of years to assume for technology lifetime
Returns
-------
pp_final : 'numpy.ndarray'
Payback period in years
"""
years = np.array([np.arange(0, tech_lifetime)] * cfs.shape[0])
cum_cfs = cfs.cumsum(axis = 1)
no_payback = np.logical_or(cum_cfs[:, -1] <= 0, np.all(cum_cfs <= 0, axis = 1))
instant_payback = np.all(cum_cfs > 0, axis = 1)
neg_to_pos_years = np.diff(np.sign(cum_cfs)) > 0
base_years = np.amax(np.where(neg_to_pos_years, years, -1), axis = 1)
# replace values of -1 with 30
base_years_fix = np.where(base_years == -1, tech_lifetime - 1, base_years)
base_year_mask = years == base_years_fix[:, np.newaxis]
# base year values
base_year_values = cum_cfs[:, :-1][base_year_mask]
next_year_values = cum_cfs[:, 1:][base_year_mask]
frac_years = base_year_values/(base_year_values - next_year_values)
pp_year = base_years_fix + frac_years
pp_precise = np.where(no_payback, 30.1, np.where(instant_payback, 0, pp_year))
# round to nearest 0.1 to join with max_market_share
pp_final = np.array(pp_precise).round(decimals =1)
return pp_final
#%%
#%%
# system_costs --> ssytem_cost look for error in later commits?
[docs]def check_incentive_constraints(incentive_data, incentive_value, system_cost):
# Reduce the incentive if is is more than the max allowable payment (by percent total costs)
if not pd.isnull(incentive_data['max_incentive_usd']):
incentive_value = min(incentive_value, incentive_data['max_incentive_usd'])
# Reduce the incentive if is is more than the max allowable payment (by percent of total installed costs)
if not pd.isnull(incentive_data['max_incentive_pct']):
incentive_value = min(incentive_value, system_cost * incentive_data['max_incentive_pct'])
# Set the incentive to zero if it is less than the minimum incentive
if not pd.isnull(incentive_data['min_incentive_usd']):
incentive_value *= int(incentive_value > incentive_data['min_incentive_usd'])
return incentive_value
# #%%
# def calculate_investment_based_incentives(pv, batt_kw, batt_kwh, agent):
# # Get State Incentives that have a valid Investment Based Incentive value (based on percent of total installed costs)
# ibi_list = agent.loc['state_incentives'].loc[pd.notnull(agent.loc['state_incentives']['ibi_pct'])]
# # Create a empty dataframe to store cumulative ibi's for each system configuration
# result = 0.
# # Loop through each incenctive and add it to the result df
# for row in ibi_list.to_dict('records'):
# if row['tech'] == 'solar':
# # Size filer calls a function to check for valid system size limitations - a boolean so if the size in invalid it will add zero's to the results df
# size_filter = check_minmax(pv, row['min_kw'], row['max_kw'])
# # Scale costs based on system size
# system_cost = (pv * agent.loc['system_capex_per_kw'])
# if row['tech'] == 'storage':
# # Size filer calls a function to check for valid system size limitations - a boolean so if the size in invalid it will add zero's to the results df
# size_filter = check_minmax(batt_kwh, row['min_kwh'], row['max_kwh'])
# size_filter = size_filter * check_minmax(batt_kw, row['min_kw'], row['max_kw'])
# # Calculate system costs
# system_costs = (batt_kw * agent.loc['batt_capex_per_kw']) + (batt_kwh * agent.loc['batt_capex_per_kwh'])
# # Total incentive
# incentive_value = (system_cost * row['ibi_pct']) * size_filter
# # Add the result to the cumulative total
# result += check_incentive_constraints(row, incentive_value, system_cost)
# return np.array(result)
#%%
# def calculate_capacity_based_incentives(pv, batt_kw, batt_kwh, agent):
# # Get State Incentives that have a valid Capacity Based Incentive value (based on $ per watt)
# cbi_list = agent.loc['state_incentives'].loc[pd.notnull(agent.loc['state_incentives']['cbi_usd_p_w']) | pd.notnull(agent.loc['state_incentives']['cbi_usd_p_wh'])]
# # Create a empty dataframe to store cumulative bi's for each system configuration
# result = 0.
# # Loop through each incenctive and add it to the result df
# for row in cbi_list.to_dict('records'):
# if row['tech'] == 'solar':
# # Size filer calls a function to check for valid system size limitations - a boolean so if the size in invalid it will add zero's to the results df
# size_filter = check_minmax(pv, row['min_kw'], row['max_kw'])
# # Calculate incentives
# incentive_value = (pv * (row['cbi_usd_p_w']*1000)) * size_filter
# # Calculate system costs
# system_cost = pv * agent.loc['system_capex_per_kw']
# if row['tech'] == 'storage' and not np.isnan(row['cbi_usd_p_wh']):
# # Size filer calls a function to check for valid system size limitations - a boolean so if the size in invalid it will add zero's to the results df
# size_filter = check_minmax(batt_kwh, row['min_kwh'], row['max_kwh'])
# size_filter = size_filter * check_minmax(batt_kw, row['min_kw'], row['max_kw'])
# # Calculate incentives
# incentive_value = (row['cbi_usd_p_wh'] * batt_kwh + row['cbi_usd_p_w'] * batt_kw) * 1000 * size_filter
# # Calculate system costs
# system_cost = (batt_kw * agent.loc['batt_capex_per_kw']) + (batt_kwh * agent.loc['batt_capex_per_kwh'])
# result += check_incentive_constraints(row, incentive_value, system_cost)
# return np.array(result)
# #%%
# def calculate_production_based_incentives(pv, kwh_by_timestep, agent):
# # Get State Incentives that have a valid Production Based Incentive value
# pbi_list = agent.loc['state_incentives'].loc[pd.notnull(agent.loc['state_incentives']['pbi_usd_p_kwh'])]
# # Create a empty dataframe to store cumulative pbi's for each system configuration (each system should have an array as long as the number of years times the number of timesteps per year)
# result = np.tile(np.array([0]*agent.loc['economic_lifetime_yrs']*agent.loc['timesteps_per_year']), (1,1))
# #Loop through incentives
# for row in pbi_list.to_dict('records'):
# #Build boolean array to express if system sizes are valid
# size_filter = check_minmax(pv, row['min_kw'], row['max_kw'])
# if row['tech'] == 'solar':
# # Assume flat rate timestep function for PBI
# default_expiration = datetime.date(agent.loc['year'] + agent.loc['economic_lifetime_yrs'], 1, 1)
# fn = {'function':eqn_flat_rate,
# 'row_params':['pbi_usd_p_kwh','incentive_duration_yrs','end_date'],
# 'default_params':[0, agent.loc['economic_lifetime_yrs'], default_expiration],
# 'additional_params':[agent.loc['year'], agent.loc['timesteps_per_year']]}
# # Vectorize the function
# f = np.vectorize(fn['function'](row, fn['row_params'], fn['default_params'], fn['additional_params']))
# # Apply the function to each row (containing an array of timestep values)
# incentive_value = kwh_by_timestep * f(list(range(0,len(kwh_by_timestep))))
# #Add the pbi the cumulative total
# result = result + list(incentive_value * size_filter)
# #Sum the incentive at each timestep by year for each system size
# result = [np.array([sum(x) for x in np.split(x,agent.loc['economic_lifetime_yrs'] )]) for x in result]
# return result
#%%
[docs]def check_minmax(value, min_, max_):
#Returns 1 if the value is within a valid system size limitation - works for single numbers and arrays (assumes valid is system size limitation are not known)
output = True
# output = value.apply(lambda x: True)
if isinstance(min_,float):
if not np.isnan(min_):
output = output * (value >= min_)
# output = output * value.apply(lambda x: x >= min_)
if isinstance(max_, float):
if not np.isnan(max_):
output = output * (value <= max_)
#output = output * value.apply(lambda x: x <= max_)
return output
#%%
[docs]def get_expiration(end_date, current_year, timesteps_per_year):
#Calculates the timestep at which the end date occurs based on pytoh datetime.date objects and a number of timesteps per year
return float(((end_date - datetime.date(current_year, 1, 1)).days / 365.0) * timesteps_per_year)
#%%
[docs]def eqn_builder(method,incentive_info, info_params, default_params,additional_data):
#Builds an equation to scale a series of timestep values
#method: 'linear_decay' linearly drop from the full price to zero at a given timestep (used for SREC's currently)
# 'flat_rate' used as a defualt to keep the consistent value until an endpoint at which point the value is always zero
#incentive_info: a row from the agent['state_incentives'] dataframe from which to draw info to customize and equation
#incentive params: an array containing the names of the params in agent['state_incentives'] to use in the equation
#default params: an array of default values for each incentive param. Entries must match the order of the incentive params.
#additional_data: Addtional data can be used to customize the equation
#Loop through params and grab the default value is the agent['state_incentives'] entry does not have a valid value for it
for i, r in enumerate(info_params):
try:
if np.isnan(incentive_info[r]):
incentive_info[r] = default_params[i]
except:
if incentive_info[r] is None:
incentive_info[r] = default_params[i]
pbi_usd_p_kwh = float(incentive_info[info_params[0]])
years = float(incentive_info[info_params[1]])
end_date = incentive_info[info_params[2]]
current_year = int(additional_data[0])
timesteps_per_year = float(additional_data[1])
#Get the timestep at which the incentive expires
try:
#Find expiration timestep by explict program end date
expiration = get_expiration(end_date, current_year, timesteps_per_year)
except:
#Assume the incetive applies for all years if there is an error in the previous step
expiration = years * timesteps_per_year
#Reduce the expiration if there is a cap on the number of years the incentive can be applied
expiration = min(years * timesteps_per_year, expiration)
if method =='linear_decay':
#Linear decline to zero at expiration
def function(ts):
if ts > expiration:
return 0.0
else:
if expiration - ts < 1:
fraction = expiration - ts
else:
fraction = 1
return fraction * (pbi_usd_p_kwh + ((-1 * (pbi_usd_p_kwh / expiration) * ts)))
return function
if method == 'flat_rate':
# Flat rate until expiration, and then zero
def function(ts):
if ts > expiration:
return 0.0
else:
if expiration - ts < 1:
fraction = expiration - ts
else:
fraction = 1
return fraction * pbi_usd_p_kwh
return function
#%%
[docs]def eqn_linear_decay_to_zero(incentive_info, info_params, default_params,additional_params):
return eqn_builder('linear_decay',incentive_info, info_params, default_params,additional_params)
#%%
[docs]def eqn_flat_rate(incentive_info, info_params, default_params,additional_params):
return eqn_builder('flat_rate', incentive_info, info_params, default_params,additional_params)