Skip to content

Commit

Permalink
Merge pull request #24 from grgmiller/costs
Browse files Browse the repository at this point in the history
Costs (closes #6 #7 #12)
  • Loading branch information
grgmiller authored Aug 11, 2021
2 parents b9ee901 + 16739a6 commit f410385
Show file tree
Hide file tree
Showing 7 changed files with 1,023 additions and 1,025 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
-------------------------------------------------------------------------------
Commmit 2021.08.11
-------------------------------------------------------------------------------
Fixes #4, #6, #7, #12

Updates to how costs are calculated in the model.
- Updates the `uniform_series_to_present_value` function in `financials.py` to use the formula for the present value of an annuity due (#12)
- In `generators.extensions.storage`, removes the PPA cost discount for hybrid energy storage dispatch (#6)
- Updates the `summary_report.ipynb` to show a plot of nodal costs, and display hybrid storage charging/discharging as part of storage, rather than the paired resource
- In summary_report, fixes the generator cost per MWh table. For hybrid resources, the congestion cost of the ES component nets out the congestion cost of the RE component, so there is no energy arbitrage cost associated with hybrids anymore. Energy arbitrage could be shown if the calculation is reconfigured, but it seems that this is not important.

-------------------------------------------------------------------------------
Commmit 2021.08.10
-------------------------------------------------------------------------------
Expand Down
12 changes: 5 additions & 7 deletions switch_model/financials.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,23 @@ def uniform_series_to_present_value(dr, t):
"""
Returns a coefficient to convert a uniform series of payments over t
periods to a present value in the first period using a discount rate
of dr. This is mathematically equivalent to the inverse of the
capital recovery factor, assuming the same rate and number of
periods is used for both calculations. In practice, you typically
use an interest rate for a capital recovery factor and a discount
rate for this.
of dr. To calculate this, we use the formula for the present value of
an annuity due, which assumes that the payments come at the beginning
of each period.
Example usage:
>>> print(
... "Net present value of a $10 / yr annuity paid for 20 years, "
... "assuming a 5 percent discount rate is ${npv:0.2f}"
... .format(npv=10 * uniform_series_to_present_value(.05, 20))
... )
Net present value of a $10 / yr annuity paid for 20 years, assuming a 5 percent discount rate is $124.62
Net present value of a $10 / yr annuity paid for 20 years, assuming a 5 percent discount rate is $130.85
Test for calculation validity compared to CRF using 7 decimal points
>>> round(uniform_series_to_present_value(.07,20),7) == \
round(1/capital_recovery_factor(.07,20),7)
True
"""
return t if dr == 0 else (1-(1+dr)**-t)/dr
return t if dr == 0 else (1-(1+dr)**-t)/dr*(1+dr)


def future_to_present_value(dr, t):
Expand Down
7 changes: 5 additions & 2 deletions switch_model/generate_input_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,9 @@ def generate_inputs(model_workspace):

# pricing_nodes.csv
node_list = list(set_gens.gen_pricing_node.unique())
node_list = node_list + load_list
node_list = [i for i in node_list if i not in ['.',np.nan]]
node_list = list(set(node_list)) # only keep unique values
pricing_nodes = pd.DataFrame(data={'PRICING_NODE':node_list})
pricing_nodes.to_csv(input_dir / 'pricing_nodes.csv', index=False)

Expand All @@ -446,8 +448,9 @@ def generate_inputs(model_workspace):
nodal_prices['timepoint'] = nodal_prices.index + 1
nodal_prices = nodal_prices.melt(id_vars=['timepoint'], var_name='pricing_node', value_name='nodal_price')
nodal_prices = nodal_prices[['pricing_node','timepoint','nodal_price']]
#add system power / demand node prices to df
nodal_prices = pd.concat([nodal_prices, system_power_cost.rename(columns={'load_zone':'pricing_node','system_power_cost':'nodal_price'})], axis=0, ignore_index=True)
# add system power / demand node prices to df
# NOTE: removed because this was adding duplicate values if one of the generators is located at the load node
#nodal_prices = pd.concat([nodal_prices, system_power_cost.rename(columns={'load_zone':'pricing_node','system_power_cost':'nodal_price'})], axis=0, ignore_index=True)
nodal_prices.to_csv(input_dir / 'nodal_prices.csv', index=False)

# dr_data.csv
Expand Down
25 changes: 13 additions & 12 deletions switch_model/generators/extensions/congestion_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def define_components(mod):
mod.DLAPLoadCostInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m, t: sum(m.zone_demand_mw[z,t] * m.nodal_price[z,t] for z in m.LOAD_ZONES))
mod.Cost_Components_Per_TP.append('DLAPLoadCostInTP')

# Pnode Revenue is earned from injecting power into the grid
mod.GenPnodeRevenue = Expression(
Expand All @@ -40,6 +41,8 @@ def define_components(mod):
mod.GenPnodeRevenueInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m,t: sum(m.GenPnodeRevenue[g,t] for g in m.NON_STORAGE_GENS))
# add Pnode revenue to objective function
mod.Cost_Components_Per_TP.append('GenPnodeRevenueInTP')

"""
mod.ExcessGenPnodeRevenue = Expression(
Expand All @@ -48,7 +51,7 @@ def define_components(mod):
mod.ExcessGenPnodeRevenueInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m,t: sum(m.ExcessGenPnodeRevenue[g,t] for g in m.NON_STORAGE_GENS))
"""
# The delivery cost is the cost of offtaking the generated energy at the demand node
mod.GenDeliveryCost = Expression(
Expand All @@ -59,24 +62,22 @@ def define_components(mod):
mod.TIMEPOINTS,
rule=lambda m,t: sum(m.GenDeliveryCost[g,t] for g in m.NON_STORAGE_GENS))
"""
mod.ExcessGenDeliveryCost = Expression(
mod.NON_STORAGE_GEN_TPS,
rule=lambda m, g, t: (m.ExcessGen[g,t] * m.nodal_price[m.gen_load_zone[g],t]))
mod.ExcessGenDeliveryCostInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m,t: sum(m.ExcessGenDeliveryCost[g,t] for g in m.NON_STORAGE_GENS))
"""
mod.GenCongestionCost = Expression(
mod.NON_STORAGE_GEN_TPS,
rule=lambda m, g, t: m.GenDeliveryCost[g,t] - m.GenPnodeRevenue[g,t])
mod.CongestionCostInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m,t: sum(m.GenCongestionCost[g,t] for g in m.NON_STORAGE_GENS))
# Add congestion cost to the objective function
mod.Cost_Components_Per_TP.append('CongestionCostInTP')

"""

def post_solve(instance, outdir):
dispatchable_congestion = [{
Expand All @@ -86,8 +87,8 @@ def post_solve(instance, outdir):
"Contract Cost": value(instance.DispatchGen[g,t] * instance.ppa_energy_cost[g] *
instance.tp_weight_in_year[t]),
"Generator Pnode Revenue": value(instance.GenPnodeRevenue[g,t]),
"Generator Delivery Cost": value(instance.GenDeliveryCost[g,t]),
"Congestion Cost": value(instance.GenCongestionCost[g,t]),
#"Generator Delivery Cost": value(instance.GenDeliveryCost[g,t]),
#"Congestion Cost": value(instance.GenCongestionCost[g,t]),
} for (g, t) in instance.DISPATCHABLE_GEN_TPS]
variable_congestion = [{
"generation_project": g,
Expand All @@ -96,8 +97,8 @@ def post_solve(instance, outdir):
"Contract Cost": value(instance.VariableGen[g,t] * instance.ppa_energy_cost[g] *
instance.tp_weight_in_year[t]),
"Generator Pnode Revenue": value(instance.GenPnodeRevenue[g,t]),
"Generator Delivery Cost": value(instance.GenDeliveryCost[g,t]),
"Congestion Cost": value(instance.GenCongestionCost[g,t]),
#"Generator Delivery Cost": value(instance.GenDeliveryCost[g,t]),
#"Congestion Cost": value(instance.GenCongestionCost[g,t]),
} for (g, t) in instance.VARIABLE_GEN_TPS]
congestion_data = dispatchable_congestion + variable_congestion
nodal_by_gen_df = pd.DataFrame(congestion_data)
Expand All @@ -107,8 +108,8 @@ def post_solve(instance, outdir):
nodal_data = [{
"timestamp": instance.tp_timestamp[t],
"Generator Pnode Revenue": value(instance.GenPnodeRevenueInTP[t]),
"Generator Delivery Cost": value(instance.GenDeliveryCostInTP[t]),
"Congestion Cost": value(instance.CongestionCostInTP[t]),
#"Generator Delivery Cost": value(instance.GenDeliveryCostInTP[t]),
#"Congestion Cost": value(instance.CongestionCostInTP[t]),
"DLAP Cost": value(instance.DLAPLoadCostInTP[t]),
} for t in instance.TIMEPOINTS]
nodal_df = pd.DataFrame(nodal_data)
Expand Down
11 changes: 0 additions & 11 deletions switch_model/generators/extensions/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,17 +343,6 @@ def State_Of_Charge_Upper_Limit_rule(m, g, t):
)
mod.Cost_Components_Per_TP.append('StorageNodalEnergyCostInTP')

# A hybrid generator should not pay the PPA cost of energy generated but stored, since this energy never crosses
# the bus, so we want to discount ExcessGenCostInTP by the amount charged; however, the storage should pay the PPA
# cost when dispatching because the energy will cross the generator bus
mod.HybridStoragePPAEnergyCostInTP = Expression(
mod.TIMEPOINTS,
rule=lambda m, t: sum(
(m.DischargeStorage[g, t] - m.ChargeStorage[g, t]) * m.ppa_energy_cost[m.storage_hybrid_generation_project[g]]
for g in m.GENS_IN_PERIOD[m.tp_period[t]]
if g in m.HYBRID_STORAGE_GENS),
doc="Summarize costs for the objective function")
mod.Cost_Components_Per_TP.append('HybridStoragePPAEnergyCostInTP')


def load_inputs(mod, switch_data, inputs_dir):
Expand Down
Loading

0 comments on commit f410385

Please sign in to comment.