Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Costs (closes #6 #7 #12) #24

Merged
merged 3 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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