From 9c976ea62e7f6384737f06b83ae03b3fc02297d4 Mon Sep 17 00:00:00 2001 From: grgmiller Date: Tue, 10 Aug 2021 16:59:18 -0700 Subject: [PATCH 1/3] fixes #12 #6 --- CHANGELOG.md | 12 + switch_model/financials.py | 12 +- switch_model/generate_input_files.py | 5 +- .../extensions/congestion_pricing.py | 27 +- switch_model/generators/extensions/storage.py | 11 - switch_model/reporting/summary_report.ipynb | 1736 +++++++++-------- switch_model/run_scenarios.ipynb | 46 +- 7 files changed, 925 insertions(+), 924 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fbc59..0dceda8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +------------------------------------------------------------------------------- +Commmit 2021.08.XX +------------------------------------------------------------------------------- +Fixes #4, #6, #7, #12 + +^^ Fix commit DATE + +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) + - Update 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 + ------------------------------------------------------------------------------- Commmit 2021.08.10 ------------------------------------------------------------------------------- diff --git a/switch_model/financials.py b/switch_model/financials.py index b79b736..8274db7 100644 --- a/switch_model/financials.py +++ b/switch_model/financials.py @@ -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): diff --git a/switch_model/generate_input_files.py b/switch_model/generate_input_files.py index 1d6d4ff..969cffd 100644 --- a/switch_model/generate_input_files.py +++ b/switch_model/generate_input_files.py @@ -446,8 +446,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 diff --git a/switch_model/generators/extensions/congestion_pricing.py b/switch_model/generators/extensions/congestion_pricing.py index cbd54a5..3e19582 100644 --- a/switch_model/generators/extensions/congestion_pricing.py +++ b/switch_model/generators/extensions/congestion_pricing.py @@ -29,7 +29,8 @@ def define_components(mod): # Calculate the cost we pay for load at the DLAP 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)) + rule=lambda m, t: sum(m.zone_demand_mw[z,t] * m.system_power_cost[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( @@ -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( @@ -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( @@ -59,14 +62,14 @@ 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, @@ -74,9 +77,7 @@ def define_components(mod): 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 = [{ @@ -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, @@ -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) @@ -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) diff --git a/switch_model/generators/extensions/storage.py b/switch_model/generators/extensions/storage.py index b1a68f1..e9b5147 100644 --- a/switch_model/generators/extensions/storage.py +++ b/switch_model/generators/extensions/storage.py @@ -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): diff --git a/switch_model/reporting/summary_report.ipynb b/switch_model/reporting/summary_report.ipynb index 76a10c4..2da948d 100644 --- a/switch_model/reporting/summary_report.ipynb +++ b/switch_model/reporting/summary_report.ipynb @@ -2,148 +2,147 @@ "cells": [ { "cell_type": "markdown", - "metadata": {}, "source": [ "# Scenario Report" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "# Copyright (c) 2021 *****************. All rights reserved.\r\n", + "# Licensed under the Apache License, Version 2.0, which is in the LICENSE file.\r\n", + "\r\n", + "from pathlib import Path\r\n", + "import pandas as pd\r\n", + "import plotly.express as px\r\n", + "import plotly\r\n", + "import plotly.graph_objects as go \r\n", + "import numpy as np\r\n", + "\r\n", + "#get the name of the current directory to specify the scenario name and identify the output directory\r\n", + "scenario_name = str(Path.cwd()).split('\\\\')[-1]\r\n", + "if scenario_name == 'inputs':\r\n", + " data_dir = Path.cwd() / '../outputs/'\r\n", + " inputs_dir = Path.cwd() / '../inputs/'\r\n", + " scenario_name = 'N/A'\r\n", + "else:\r\n", + " data_dir = Path.cwd() / f'../../outputs/{scenario_name}/'\r\n", + " inputs_dir = Path.cwd() / f'../../inputs/{scenario_name}/'\r\n", + "\r\n", + "#define formatting options/functions for outputs\r\n", + "pd.options.display.float_format = '{:,.2f}'.format\r\n", + "def format_currency(x): \r\n", + " try:\r\n", + " formatted = '$ {:,.2f}'.format(x)\r\n", + " except ValueError:\r\n", + " formatted = x\r\n", + " return formatted\r\n", + "def format_percent(x): \r\n", + " try:\r\n", + " formatted = '{:,.2f}%'.format(x)\r\n", + " except ValueError:\r\n", + " formatted = x\r\n", + " return formatted\r\n", + "\r\n", + "#allow the notebook to display plots in html report\r\n", + "###################################################\r\n", + "plotly.offline.init_notebook_mode()\r\n", + "\r\n", + "print(f'Scenario Name: {scenario_name}')" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:41:58.309907Z", - "iopub.status.busy": "2021-06-03T23:41:58.309907Z", - "iopub.status.idle": "2021-06-03T23:41:59.115211Z", - "shell.execute_reply": "2021-06-03T23:41:59.115211Z" + "iopub.execute_input": "2021-08-10T22:22:50.646474Z", + "iopub.status.busy": "2021-08-10T22:22:50.630505Z", + "iopub.status.idle": "2021-08-10T22:22:51.630435Z", + "shell.execute_reply": "2021-08-10T22:22:51.630435Z" } - }, - "outputs": [], - "source": [ - "# Copyright (c) 2021 *****************. All rights reserved.\n", - "# Licensed under the Apache License, Version 2.0, which is in the LICENSE file.\n", - "\n", - "from pathlib import Path\n", - "import pandas as pd\n", - "import plotly.express as px\n", - "import plotly\n", - "import plotly.graph_objects as go \n", - "import numpy as np\n", - "\n", - "#get the name of the current directory to specify the scenario name and identify the output directory\n", - "scenario_name = str(Path.cwd()).split('\\\\')[-1]\n", - "if scenario_name == 'inputs':\n", - " data_dir = Path.cwd() / '../outputs/'\n", - " inputs_dir = Path.cwd() / '../inputs/'\n", - " scenario_name = 'N/A'\n", - "else:\n", - " data_dir = Path.cwd() / f'../../outputs/{scenario_name}/'\n", - " inputs_dir = Path.cwd() / f'../../inputs/{scenario_name}/'\n", - "\n", - "#define formatting options/functions for outputs\n", - "pd.options.display.float_format = '{:,.2f}'.format\n", - "def format_currency(x): \n", - " try:\n", - " formatted = '$ {:,.2f}'.format(x)\n", - " except ValueError:\n", - " formatted = x\n", - " return formatted\n", - "def format_percent(x): \n", - " try:\n", - " formatted = '{:,.2f}%'.format(x)\n", - " except ValueError:\n", - " formatted = x\n", - " return formatted\n", - "\n", - "#allow the notebook to display plots in html report\n", - "###################################################\n", - "plotly.offline.init_notebook_mode()\n", - "\n", - "print(f'Scenario Name: {scenario_name}')" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Portfolio Renewable Percentage" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "# Calculate Time-coincident renewable % and annual renewable %\r\n", + "##############################################################\r\n", + "\r\n", + "# load load balance data \r\n", + "load_balance = pd.read_csv(data_dir / 'load_balance.csv', parse_dates=True)\r\n", + "\r\n", + "# get the list of generation column names that actually exist in the dataframe\r\n", + "generation_columns_list = ['ZoneTotalGeneratorDispatch','ZoneTotalVariableGeneration','ZoneTotalStorageDispatch']\r\n", + "generation_columns_available_list = [i for i in load_balance.columns if i in generation_columns_list]\r\n", + "\r\n", + "# Calculate total generation from all sources\r\n", + "load_balance['Total_Generation'] = load_balance[generation_columns_available_list].sum(axis=1) \r\n", + "load_balance['Total_Generation_No_Storage'] = load_balance[['ZoneTotalGeneratorDispatch','ZoneTotalVariableGeneration']].sum(axis=1)\r\n", + "\r\n", + "# get the list of load column names that actually exist in the dataframe\r\n", + "load_columns_list = ['zone_demand_mw','ZoneTotalStorageCharge','ShiftDemand']\r\n", + "load_columns_available_list = [i for i in load_balance.columns if i in load_columns_list]\r\n", + "\r\n", + "# Calculate total demand from all sources\r\n", + "load_balance['Total_Demand'] = load_balance[load_columns_available_list].sum(axis=1) \r\n", + "\r\n", + "\r\n", + "# calculate net generation, treating storage as a supply-side resource\r\n", + "if 'ZoneTotalStorageCharge' in load_columns_available_list:\r\n", + " load_balance['Net_Generation'] = load_balance['Total_Generation'] - load_balance['ZoneTotalStorageCharge']\r\n", + "\r\n", + " # calculate storage losses\r\n", + " storage_losses = load_balance['ZoneTotalStorageCharge'].sum() - load_balance['ZoneTotalStorageDispatch'].sum()\r\n", + "else:\r\n", + " load_balance['Net_Generation'] = load_balance['Total_Generation']\r\n", + "\r\n", + " # set storage losses = 0 if no storage in scenario\r\n", + " storage_losses = 0\r\n", + "\r\n", + "# calculate the time-coincident generation\r\n", + "# this calculation currently assumes that the batteries are only charging from contracted renewable generation\r\n", + "load_balance['Time_Coincident_Generation'] = load_balance[['Total_Generation','Total_Demand']].min(axis=1)\r\n", + "load_balance['Time_Coincident_Generation_No_Storage'] = load_balance[['Total_Generation_No_Storage','zone_demand_mw']].min(axis=1)\r\n", + "\r\n", + "# TODO: Determine if SystemPower is ever used when not needed, which would affect the calculation\r\n", + "#calculate the time-coincident renewable percentage\r\n", + "try:\r\n", + " tc_percent_renewable = (1 - load_balance['SystemPower'].sum() / load_balance['Total_Demand'].sum()) * 100\r\n", + "except KeyError:\r\n", + " # if the SystemPower data is missing, then it is likely a 100% Time coincident scenario\r\n", + " tc_percent_renewable = load_balance['Time_Coincident_Generation'].sum() / load_balance['Total_Demand'].sum() * 100\r\n", + "\r\n", + "# calculate the time-coincident renewable % ignoring storage\r\n", + "tc_no_storage_percent_renewable = load_balance['Time_Coincident_Generation_No_Storage'].sum() / load_balance['zone_demand_mw'].sum() * 100\r\n", + "\r\n", + "#tc_no_storage_percent_renewable = load_balance['Time_Coincident_Generation'].sum() / load_balance['zone_demand_mw'].sum() * 100\r\n", + "annual_percent_renewable = load_balance['Net_Generation'].sum() / load_balance['zone_demand_mw'].sum() * 100\r\n", + "\r\n", + "print(f'Time-coincident renewable percentage (with storage dispatch): {np.round(tc_percent_renewable, decimals=2)}%')\r\n", + "print(f'Time-coincident renewable percentage (without storage dispatch): {np.round(tc_no_storage_percent_renewable, decimals=2)}%')\r\n", + "print(f'Annual volumetric renewable percentage: {annual_percent_renewable.round(decimals=2)}%')" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:41:59.131509Z", - "iopub.status.busy": "2021-06-03T23:41:59.131509Z", - "iopub.status.idle": "2021-06-03T23:41:59.180711Z", - "shell.execute_reply": "2021-06-03T23:41:59.180711Z" + "iopub.execute_input": "2021-08-10T22:22:51.642461Z", + "iopub.status.busy": "2021-08-10T22:22:51.642461Z", + "iopub.status.idle": "2021-08-10T22:22:51.684053Z", + "shell.execute_reply": "2021-08-10T22:22:51.684053Z" }, "tags": [] - }, - "outputs": [], - "source": [ - "# Calculate Time-coincident renewable % and annual renewable %\n", - "##############################################################\n", - "\n", - "# load load balance data \n", - "load_balance = pd.read_csv(data_dir / 'load_balance.csv', parse_dates=True)\n", - "\n", - "# get the list of generation column names that actually exist in the dataframe\n", - "generation_columns_list = ['ZoneTotalGeneratorDispatch','ZoneTotalVariableGeneration','ZoneTotalStorageDispatch']\n", - "generation_columns_available_list = [i for i in load_balance.columns if i in generation_columns_list]\n", - "\n", - "# Calculate total generation from all sources\n", - "load_balance['Total_Generation'] = load_balance[generation_columns_available_list].sum(axis=1) \n", - "load_balance['Total_Generation_No_Storage'] = load_balance[['ZoneTotalGeneratorDispatch','ZoneTotalVariableGeneration']].sum(axis=1)\n", - "\n", - "# get the list of load column names that actually exist in the dataframe\n", - "load_columns_list = ['zone_demand_mw','ZoneTotalStorageCharge','ShiftDemand']\n", - "load_columns_available_list = [i for i in load_balance.columns if i in load_columns_list]\n", - "\n", - "# Calculate total demand from all sources\n", - "load_balance['Total_Demand'] = load_balance[load_columns_available_list].sum(axis=1) \n", - "\n", - "\n", - "# calculate net generation, treating storage as a supply-side resource\n", - "if 'ZoneTotalStorageCharge' in load_columns_available_list:\n", - " load_balance['Net_Generation'] = load_balance['Total_Generation'] - load_balance['ZoneTotalStorageCharge']\n", - "\n", - " # calculate storage losses\n", - " storage_losses = load_balance['ZoneTotalStorageCharge'].sum() - load_balance['ZoneTotalStorageDispatch'].sum()\n", - "else:\n", - " load_balance['Net_Generation'] = load_balance['Total_Generation']\n", - "\n", - " # set storage losses = 0 if no storage in scenario\n", - " storage_losses = 0\n", - "\n", - "# calculate the time-coincident generation\n", - "# this calculation currently assumes that the batteries are only charging from contracted renewable generation\n", - "load_balance['Time_Coincident_Generation'] = load_balance[['Total_Generation','Total_Demand']].min(axis=1)\n", - "load_balance['Time_Coincident_Generation_No_Storage'] = load_balance[['Total_Generation_No_Storage','zone_demand_mw']].min(axis=1)\n", - "\n", - "# TODO: Determine if SystemPower is ever used when not needed, which would affect the calculation\n", - "#calculate the time-coincident renewable percentage\n", - "try:\n", - " tc_percent_renewable = (1 - load_balance['SystemPower'].sum() / load_balance['Total_Demand'].sum()) * 100\n", - "except KeyError:\n", - " # if the SystemPower data is missing, then it is likely a 100% Time coincident scenario\n", - " tc_percent_renewable = load_balance['Time_Coincident_Generation'].sum() / load_balance['Total_Demand'].sum() * 100\n", - "\n", - "# calculate the time-coincident renewable % ignoring storage\n", - "tc_no_storage_percent_renewable = load_balance['Time_Coincident_Generation_No_Storage'].sum() / load_balance['zone_demand_mw'].sum() * 100\n", - "\n", - "#tc_no_storage_percent_renewable = load_balance['Time_Coincident_Generation'].sum() / load_balance['zone_demand_mw'].sum() * 100\n", - "annual_percent_renewable = load_balance['Net_Generation'].sum() / load_balance['zone_demand_mw'].sum() * 100\n", - "\n", - "print(f'Time-coincident renewable percentage (with storage dispatch): {np.round(tc_percent_renewable, decimals=2)}%')\n", - "print(f'Time-coincident renewable percentage (without storage dispatch): {np.round(tc_no_storage_percent_renewable, decimals=2)}%')\n", - "print(f'Annual volumetric renewable percentage: {annual_percent_renewable.round(decimals=2)}%')" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Generator Portfolio\n", "The sunburst chart describes the built portfolio at various levels of detail, which shows how the outer rings relate to the inner rings\n", @@ -152,791 +151,811 @@ "- Outer ring: specific project name\n", "\n", "For example, individual projects in the outer ring belong to a specific technology type in the middle ring, which can either be part of the existing/contracted portfolio, or the additional portfolio.\n" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "# Prepare data for sunburst chart of portfolio\r\n", + "##############################################\r\n", + "\r\n", + "#load capacity costs\r\n", + "portfolio = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','GenCapacity'])\r\n", + "portfolio = portfolio[portfolio['GenCapacity'] > 0]\r\n", + "portfolio = portfolio.rename(columns={'GenCapacity':'MW'})\r\n", + "\r\n", + "# load data about predetermined generators\r\n", + "existing = pd.read_csv(inputs_dir / 'gen_build_predetermined.csv', usecols=['GENERATION_PROJECT']).rename(columns={'GENERATION_PROJECT':'generation_project'})\r\n", + "\r\n", + "\r\n", + "# add column indicating which generators are contracted or additional builds\r\n", + "\r\n", + "existing['Status'] = 'Contracted'\r\n", + "portfolio = portfolio.merge(existing, how='left', on='generation_project').fillna('Additional')\r\n", + "\r\n", + "\r\n", + "#split the prefix of the generator name from the rest of the string\r\n", + "tech_column = [i.split('_')[0] for i in portfolio['generation_project']]\r\n", + "generator_name = [' '.join(i.split('_')[1:]) for i in portfolio['generation_project']]\r\n", + "\r\n", + "#create a new tecnhology column based on the prefix and drop the generator name\r\n", + "portfolio['Technology'] = tech_column\r\n", + "portfolio['generation_project'] = generator_name\r\n", + "\r\n", + "portfolio['MW'] = portfolio['MW'].round(1)\r\n", + "\r\n", + "portfolio = portfolio.sort_values(by=['Status','Technology'])\r\n", + "\r\n", + "\r\n", + "portfolio_sunburst = px.sunburst(portfolio, path=['Status','Technology','generation_project'], values='MW', color='Technology', color_discrete_map={'HYDRO':'Purple',\r\n", + " 'ONWIND':'Blue',\r\n", + " 'OFFWIND':'Navy',\r\n", + " 'PV':'Yellow',\r\n", + " 'PVHYBRID':'Yellow',\r\n", + " 'CSP':'Orange',\r\n", + " 'GEO':'Sienna',\r\n", + " 'STORAGE':'Green',\r\n", + " 'STORAGEHYBRID':'Green',\r\n", + " '(?)':'Black'},\r\n", + " width=1000, height=1000,\r\n", + " title='Energy Portfolio by Project Name, Technology Type, and Contract Status (MW)')\r\n", + "portfolio_sunburst.update_traces(textinfo='label+value')\r\n", + "portfolio_sunburst.show()" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:41:59.180711Z", - "iopub.status.busy": "2021-06-03T23:41:59.180711Z", - "iopub.status.idle": "2021-06-03T23:41:59.597321Z", - "shell.execute_reply": "2021-06-03T23:41:59.597321Z" + "iopub.execute_input": "2021-08-10T22:22:51.692045Z", + "iopub.status.busy": "2021-08-10T22:22:51.692045Z", + "iopub.status.idle": "2021-08-10T22:22:52.296889Z", + "shell.execute_reply": "2021-08-10T22:22:52.296889Z" } - }, - "outputs": [], - "source": [ - "# Prepare data for sunburst chart of portfolio\n", - "##############################################\n", - "\n", - "#load capacity costs\n", - "portfolio = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','GenCapacity'])\n", - "portfolio = portfolio[portfolio['GenCapacity'] > 0]\n", - "portfolio = portfolio.rename(columns={'GenCapacity':'MW'})\n", - "\n", - "# load data about predetermined generators\n", - "existing = pd.read_csv(inputs_dir / 'gen_build_predetermined.csv', usecols=['GENERATION_PROJECT']).rename(columns={'GENERATION_PROJECT':'generation_project'})\n", - "\n", - "\n", - "# add column indicating which generators are contracted or additional builds\n", - "\n", - "existing['Status'] = 'Contracted'\n", - "portfolio = portfolio.merge(existing, how='left', on='generation_project').fillna('Additional')\n", - "\n", - "\n", - "#split the prefix of the generator name from the rest of the string\n", - "tech_column = [i.split('_')[0] for i in portfolio['generation_project']]\n", - "generator_name = [' '.join(i.split('_')[1:]) for i in portfolio['generation_project']]\n", - "\n", - "#create a new tecnhology column based on the prefix and drop the generator name\n", - "portfolio['Technology'] = tech_column\n", - "portfolio['generation_project'] = generator_name\n", - "\n", - "portfolio['MW'] = portfolio['MW'].round(1)\n", - "\n", - "portfolio = portfolio.sort_values(by=['Status','Technology'])\n", - "\n", - "\n", - "portfolio_sunburst = px.sunburst(portfolio, path=['Status','Technology','generation_project'], values='MW', color='Technology', color_discrete_map={'HYDRO':'Purple',\n", - " 'ONWIND':'Blue',\n", - " 'OFFWIND':'Navy',\n", - " 'PV':'Yellow',\n", - " 'PVHYBRID':'Yellow',\n", - " 'CSP':'Orange',\n", - " 'GEO':'Sienna',\n", - " 'STORAGE':'Green',\n", - " 'STORAGEHYBRID':'Green',\n", - " '(?)':'Black'},\n", - " width=1000, height=1000,\n", - " title='Energy Portfolio by Project Name, Technology Type, and Contract Status (MW)')\n", - "portfolio_sunburst.update_traces(textinfo='label+value')\n", - "portfolio_sunburst.show()" - ] + } }, { "cell_type": "code", "execution_count": null, + "source": [ + "# Load generator Costs\r\n", + "######################\r\n", + "try:\r\n", + " generator_costs = pd.read_csv(data_dir / 'nodal_costs_by_gen.csv')\r\n", + " #calculate annual sums\r\n", + " generator_costs = generator_costs.groupby('generation_project').sum().round(decimals=2).reset_index()\r\n", + " #drop data with zero values\r\n", + "\r\n", + " generator_costs = generator_costs[generator_costs['DispatchGen_MW'] > 0]\r\n", + "\r\n", + " generator_costs = generator_costs.rename(columns={'DispatchGen_MW':'Generation MWh', 'Generator Pnode Revenue':'Pnode Revenue', 'Generator Delivery Cost':'Delivery Cost'})\r\n", + "\r\n", + "\r\n", + " generator_costs = generator_costs[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost']]\r\n", + "\r\n", + " # Load Storage Costs\r\n", + " ####################\r\n", + "\r\n", + " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=True, usecols=['generation_project','load_zone','timestamp','ChargeMW','DispatchMW','StorageDispatchPnodeCost'])\r\n", + "\r\n", + " #merge DLAP cost data into the storage data\r\n", + " system_power_cost = pd.read_csv(Path.cwd() / 'system_power_cost.csv', usecols=['load_zone','system_power_cost'])\r\n", + " system_power_cost['timestamp'] = storage['timestamp'].iloc[:8760]\r\n", + " storage = storage.merge(system_power_cost, how='left', on=['load_zone','timestamp'])\r\n", + "\r\n", + " #calculate delivery cost\r\n", + " storage['Dispatch_Delivery_Cost'] = storage['DispatchMW'] * storage['system_power_cost']\r\n", + " storage['Hybrid_Charge_Delivery_Discount'] = - storage['ChargeMW'] * storage['system_power_cost']\r\n", + "\r\n", + " # Calculate Pnode Revenues\r\n", + " storage['Pnode Revenue'] = - storage['StorageDispatchPnodeCost']\r\n", + " storage.loc[storage['Pnode Revenue'] < 0,'Pnode Revenue'] = 0 \r\n", + " \r\n", + " # Pnode revenues for hybrid projects should be set to 0 since the revenue is already counted from the generator side\r\n", + " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Pnode Revenue'] = 0\r\n", + "\r\n", + " storage = storage.groupby('generation_project').sum().round(decimals=2).reset_index()\r\n", + " storage = storage[storage['DispatchMW'] > 0]\r\n", + "\r\n", + " # Adjust hybrid storage calculations\r\n", + " ####################################\r\n", + " # for hybrid projects, discount the dispatch delivery cost by the discount for charging\r\n", + " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] = storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] + storage.loc[storage['generation_project'].str.contains('HYBRID'),'Hybrid_Charge_Delivery_Discount']\r\n", + "\r\n", + " #for hybrid projects, set the Generation MWh to 0, since we already counted this in the generation side of the project\r\n", + " storage.loc[storage['generation_project'].str.contains('HYBRID'),'DispatchMW'] = 0\r\n", + "\r\n", + " #merge in the contract costs\r\n", + " storage_contract = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','Annual_PPA_Capacity_Cost'])\r\n", + " storage = storage.merge(storage_contract, how='left', on='generation_project').fillna(0)\r\n", + "\r\n", + " storage = storage.rename(columns={'StorageDispatchPnodeCost':'Energy Arbitrage','Annual_PPA_Capacity_Cost':'Contract Cost','Dispatch_Delivery_Cost':'Delivery Cost','DispatchMW':'Generation MWh'})\r\n", + "\r\n", + " #only keep columns to merge\r\n", + " storage = storage[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost','Energy Arbitrage']]\r\n", + "\r\n", + " #rename the hybrid projects\r\n", + " #rename storage hybrid to paired generator name\r\n", + " hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\r\n", + " hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\r\n", + " hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\r\n", + " storage['generation_project'] = storage['generation_project'].replace(hybrid_pair)\r\n", + "\r\n", + " # Merge Data Together\r\n", + " #####################\r\n", + "\r\n", + " generator_costs = generator_costs.append(storage).fillna(0)\r\n", + " generator_costs = generator_costs.groupby('generation_project').sum().reset_index()\r\n", + "\r\n", + " #calculate congestion cost\r\n", + " generator_costs['Congestion Cost'] = generator_costs['Delivery Cost'] - generator_costs['Pnode Revenue']\r\n", + "\r\n", + " # calculate per MWh costs\r\n", + " generator_costs['Contract Cost per MWh'] = generator_costs['Contract Cost'] / generator_costs['Generation MWh']\r\n", + " generator_costs['Congestion Cost per MWh'] = generator_costs['Congestion Cost'] / generator_costs['Generation MWh']\r\n", + " generator_costs['Energy Arbitrage per MWh'] = generator_costs['Energy Arbitrage'] / generator_costs['Generation MWh']\r\n", + " generator_costs['Total Cost per MWh'] = generator_costs['Contract Cost per MWh'] + generator_costs['Congestion Cost per MWh'] + generator_costs['Energy Arbitrage per MWh']\r\n", + "\r\n", + " generator_costs['Technology'] = [i.split('_')[0] for i in generator_costs['generation_project']]\r\n", + "\r\n", + " generator_costs = generator_costs.sort_values(by='Total Cost per MWh', ascending=True)\r\n", + "\r\n", + " generator_costs = generator_costs.round(decimals=2)\r\n", + "\r\n", + " # Prepare Data for graph\r\n", + " ########################\r\n", + " generator_costs_melted = generator_costs[['generation_project', 'Contract Cost per MWh','Congestion Cost per MWh','Energy Arbitrage per MWh']].rename(columns={'Contract Cost per MWh':'Contract Cost','Congestion Cost per MWh':'Congestion Cost','Energy Arbitrage per MWh':'Energy Arbitrage'}).melt(id_vars='generation_project', var_name='Cost', value_name='$/MWh')\r\n", + " generator_costs_melted['$/MWh'] = generator_costs_melted['$/MWh']\r\n", + "\r\n", + " generator_cost_fig = px.bar(generator_costs_melted, title='Average Generator Cost per MWh Generated', x='generation_project', y='$/MWh', color='Cost', color_discrete_map={'Contract Cost': 'Green','Congestion Cost': 'Orange', 'Energy Arbitrage':'Blue'})\r\n", + "\r\n", + "\r\n", + " generator_cost_fig.add_scatter(x=generator_costs.generation_project, y=generator_costs['Total Cost per MWh'], mode='markers+text', text=generator_costs['Total Cost per MWh'], textposition='top center', line=dict(color='black', width=1), name='Total Cost')\r\n", + "\r\n", + " generator_cost_fig.show()\r\n", + "\r\n", + "except FileNotFoundError:\r\n", + " pass" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:41:59.612766Z", - "iopub.status.busy": "2021-06-03T23:41:59.597321Z", - "iopub.status.idle": "2021-06-03T23:42:00.293070Z", - "shell.execute_reply": "2021-06-03T23:42:00.293070Z" + "iopub.execute_input": "2021-08-10T22:22:52.317690Z", + "iopub.status.busy": "2021-08-10T22:22:52.313688Z", + "iopub.status.idle": "2021-08-10T22:22:52.908392Z", + "shell.execute_reply": "2021-08-10T22:22:52.908392Z" } - }, - "outputs": [], - "source": [ - "# Load generator Costs\n", - "######################\n", - "try:\n", - " generator_costs = pd.read_csv(data_dir / 'nodal_costs_by_gen.csv')\n", - " #calculate annual sums\n", - " generator_costs = generator_costs.groupby('generation_project').sum().round(decimals=2).reset_index()\n", - " #drop data with zero values\n", - "\n", - " generator_costs = generator_costs[generator_costs['DispatchGen_MW'] > 0]\n", - "\n", - " generator_costs = generator_costs.rename(columns={'DispatchGen_MW':'Generation MWh', 'Generator Pnode Revenue':'Pnode Revenue', 'Generator Delivery Cost':'Delivery Cost'})\n", - "\n", - "\n", - " generator_costs = generator_costs[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost']]\n", - "\n", - " # Load Storage Costs\n", - " ####################\n", - "\n", - " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=True, usecols=['generation_project','load_zone','timestamp','ChargeMW','DispatchMW','StorageDispatchPnodeCost'])\n", - "\n", - " #merge DLAP cost data into the storage data\n", - " system_power_cost = pd.read_csv(Path.cwd() / 'system_power_cost.csv', usecols=['load_zone','system_power_cost'])\n", - " system_power_cost['timestamp'] = storage['timestamp'].iloc[:8760]\n", - " storage = storage.merge(system_power_cost, how='left', on=['load_zone','timestamp'])\n", - "\n", - " #calculate delivery cost\n", - " storage['Dispatch_Delivery_Cost'] = storage['DispatchMW'] * storage['system_power_cost']\n", - " storage['Hybrid_Charge_Delivery_Discount'] = - storage['ChargeMW'] * storage['system_power_cost']\n", - "\n", - " # Calculate Pnode Revenues\n", - " storage['Pnode Revenue'] = - storage['StorageDispatchPnodeCost']\n", - " storage.loc[storage['Pnode Revenue'] < 0,'Pnode Revenue'] = 0 \n", - " \n", - " # Pnode revenues for hybrid projects should be set to 0 since the revenue is already counted from the generator side\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Pnode Revenue'] = 0\n", - "\n", - " storage = storage.groupby('generation_project').sum().round(decimals=2).reset_index()\n", - " storage = storage[storage['DispatchMW'] > 0]\n", - "\n", - " # Adjust hybrid storage calculations\n", - " ####################################\n", - " # for hybrid projects, discount the dispatch delivery cost by the discount for charging\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] = storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] + storage.loc[storage['generation_project'].str.contains('HYBRID'),'Hybrid_Charge_Delivery_Discount']\n", - "\n", - " #for hybrid projects, set the Generation MWh to 0, since we already counted this in the generation side of the project\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'DispatchMW'] = 0\n", - "\n", - " #merge in the contract costs\n", - " storage_contract = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','Annual_PPA_Capacity_Cost'])\n", - " storage = storage.merge(storage_contract, how='left', on='generation_project').fillna(0)\n", - "\n", - " storage = storage.rename(columns={'StorageDispatchPnodeCost':'Energy Arbitrage','Annual_PPA_Capacity_Cost':'Contract Cost','Dispatch_Delivery_Cost':'Delivery Cost','DispatchMW':'Generation MWh'})\n", - "\n", - " #only keep columns to merge\n", - " storage = storage[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost','Energy Arbitrage']]\n", - "\n", - " #rename the hybrid projects\n", - " #rename storage hybrid to paired generator name\n", - " hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\n", - " hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\n", - " hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\n", - " storage['generation_project'] = storage['generation_project'].replace(hybrid_pair)\n", - "\n", - " # Merge Data Together\n", - " #####################\n", - "\n", - " generator_costs = generator_costs.append(storage).fillna(0)\n", - " generator_costs = generator_costs.groupby('generation_project').sum().reset_index()\n", - "\n", - " #calculate congestion cost\n", - " generator_costs['Congestion Cost'] = generator_costs['Delivery Cost'] - generator_costs['Pnode Revenue']\n", - "\n", - " # calculate per MWh costs\n", - " generator_costs['Contract Cost per MWh'] = generator_costs['Contract Cost'] / generator_costs['Generation MWh']\n", - " generator_costs['Congestion Cost per MWh'] = generator_costs['Congestion Cost'] / generator_costs['Generation MWh']\n", - " generator_costs['Energy Arbitrage per MWh'] = generator_costs['Energy Arbitrage'] / generator_costs['Generation MWh']\n", - " generator_costs['Total Cost per MWh'] = generator_costs['Contract Cost per MWh'] + generator_costs['Congestion Cost per MWh'] + generator_costs['Energy Arbitrage per MWh']\n", - "\n", - " generator_costs['Technology'] = [i.split('_')[0] for i in generator_costs['generation_project']]\n", - "\n", - " generator_costs = generator_costs.sort_values(by='Total Cost per MWh', ascending=True)\n", - "\n", - " generator_costs = generator_costs.round(decimals=2)\n", - "\n", - " # Prepare Data for graph\n", - " ########################\n", - " generator_costs_melted = generator_costs[['generation_project', 'Contract Cost per MWh','Congestion Cost per MWh','Energy Arbitrage per MWh']].rename(columns={'Contract Cost per MWh':'Contract Cost','Congestion Cost per MWh':'Congestion Cost','Energy Arbitrage per MWh':'Energy Arbitrage'}).melt(id_vars='generation_project', var_name='Cost', value_name='$/MWh')\n", - " generator_costs_melted['$/MWh'] = generator_costs_melted['$/MWh']\n", - "\n", - " generator_cost_fig = px.bar(generator_costs_melted, title='Average Generator Cost per MWh Generated', x='generation_project', y='$/MWh', color='Cost', color_discrete_map={'Contract Cost': 'Green','Congestion Cost': 'Orange', 'Energy Arbitrage':'Blue'})\n", - "\n", - "\n", - " generator_cost_fig.add_scatter(x=generator_costs.generation_project, y=generator_costs['Total Cost per MWh'], mode='markers+text', text=generator_costs['Total Cost per MWh'], textposition='top center', line=dict(color='black', width=1), name='Total Cost')\n", - "\n", - " generator_cost_fig.show()\n", - "\n", - "except FileNotFoundError:\n", - " pass" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Delivered Energy Mix\n", "This is similar to a power content label, showing the percentage of energy from each source used to serve load." - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "total_demand = load_balance['zone_demand_mw'].sum()\r\n", + "try:\r\n", + " system_power_consumed = load_balance['SystemPower'].sum()\r\n", + "except KeyError:\r\n", + " system_power_consumed = 0\r\n", + "generated_power_consumed = total_demand - system_power_consumed\r\n", + "\r\n", + "# Calculate generated energy mix\r\n", + "generation_mix = pd.read_csv(data_dir / 'dispatch_annual_summary.csv', usecols=['gen_energy_source','Energy_GWh_typical_yr'])\r\n", + "# calculate the % generation from each technology\r\n", + "generation_mix['Energy_GWh_typical_yr'] = generation_mix['Energy_GWh_typical_yr'] / generation_mix['Energy_GWh_typical_yr'].sum()\r\n", + "# calculate the total MWh of consumed power from each source\r\n", + "generation_mix['Energy_GWh_typical_yr'] = generation_mix['Energy_GWh_typical_yr'] * generated_power_consumed\r\n", + "# rename the columns\r\n", + "generation_mix = generation_mix.rename(columns={'gen_energy_source':'Source','Energy_GWh_typical_yr':'Delivered MWh'})\r\n", + "# add a row for system power\r\n", + "generation_mix = generation_mix.append({'Source':'System_Power','Delivered MWh':system_power_consumed}, ignore_index=True)\r\n", + "\r\n", + "\r\n", + "generation_mix['Delivered MWh'] = generation_mix['Delivered MWh'].round(decimals=0)\r\n", + "\r\n", + "# only keep non-zero data\r\n", + "generation_mix = generation_mix[generation_mix['Delivered MWh'] > 0]\r\n", + "\r\n", + "# rename water to Hydro\r\n", + "generation_mix = generation_mix.replace('Water','Hydro')\r\n", + "\r\n", + "delivered_energy_pie = px.pie(generation_mix, values='Delivered MWh', names='Source', title='Delivered Energy Mix (MWh)', color='Source', color_discrete_map={'Hydro':'Purple',\r\n", + " 'Onshore_Wind':'Blue',\r\n", + " 'Offshore_Wind':'Navy',\r\n", + " 'Solar':'Yellow',\r\n", + " 'Geothermal':'Sienna',\r\n", + " 'System_Power':'Red'},\r\n", + "width=600, height=600\r\n", + " )\r\n", + "delivered_energy_pie.update_traces(textinfo='percent+label+value')\r\n", + "delivered_energy_pie.show()\r\n" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.300025Z", - "iopub.status.busy": "2021-06-03T23:42:00.299028Z", - "iopub.status.idle": "2021-06-03T23:42:00.354813Z", - "shell.execute_reply": "2021-06-03T23:42:00.354813Z" + "iopub.execute_input": "2021-08-10T22:22:52.916387Z", + "iopub.status.busy": "2021-08-10T22:22:52.916387Z", + "iopub.status.idle": "2021-08-10T22:22:52.991221Z", + "shell.execute_reply": "2021-08-10T22:22:52.991221Z" } - }, - "outputs": [], - "source": [ - "total_demand = load_balance['zone_demand_mw'].sum()\n", - "try:\n", - " system_power_consumed = load_balance['SystemPower'].sum()\n", - "except KeyError:\n", - " system_power_consumed = 0\n", - "generated_power_consumed = total_demand - system_power_consumed\n", - "\n", - "# Calculate generated energy mix\n", - "generation_mix = pd.read_csv(data_dir / 'dispatch_annual_summary.csv', usecols=['gen_energy_source','Energy_GWh_typical_yr'])\n", - "# calculate the % generation from each technology\n", - "generation_mix['Energy_GWh_typical_yr'] = generation_mix['Energy_GWh_typical_yr'] / generation_mix['Energy_GWh_typical_yr'].sum()\n", - "# calculate the total MWh of consumed power from each source\n", - "generation_mix['Energy_GWh_typical_yr'] = generation_mix['Energy_GWh_typical_yr'] * generated_power_consumed\n", - "# rename the columns\n", - "generation_mix = generation_mix.rename(columns={'gen_energy_source':'Source','Energy_GWh_typical_yr':'Delivered MWh'})\n", - "# add a row for system power\n", - "generation_mix = generation_mix.append({'Source':'System_Power','Delivered MWh':system_power_consumed}, ignore_index=True)\n", - "\n", - "\n", - "generation_mix['Delivered MWh'] = generation_mix['Delivered MWh'].round(decimals=0)\n", - "\n", - "# only keep non-zero data\n", - "generation_mix = generation_mix[generation_mix['Delivered MWh'] > 0]\n", - "\n", - "# rename water to Hydro\n", - "generation_mix = generation_mix.replace('Water','Hydro')\n", - "\n", - "delivered_energy_pie = px.pie(generation_mix, values='Delivered MWh', names='Source', title='Delivered Energy Mix (MWh)', color='Source', color_discrete_map={'Hydro':'Purple',\n", - " 'Onshore_Wind':'Blue',\n", - " 'Offshore_Wind':'Navy',\n", - " 'Solar':'Yellow',\n", - " 'Geothermal':'Sienna',\n", - " 'System_Power':'Red'},\n", - "width=600, height=600\n", - " )\n", - "delivered_energy_pie.update_traces(textinfo='percent+label+value')\n", - "delivered_energy_pie.show()\n" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Delivered Electricity Cost Summary" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "costs_itemized_df = pd.read_csv(data_dir / 'costs_itemized.csv')\r\n", + "try:\r\n", + " nodal_costs_df = pd.read_csv(data_dir / 'nodal_costs.csv')\r\n", + " nodal_costs_exist = True\r\n", + "except FileNotFoundError:\r\n", + " nodal_costs_exist = False\r\n", + "\r\n", + "# Energy Contract Cost\r\n", + "energy_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'GenPPACostInTP']['AnnualCost_Real'].to_numpy()[0]\r\n", + "# check for discount from hybrid generation\r\n", + "try:\r\n", + " hybrid_storage_energy_discount = costs_itemized_df[costs_itemized_df['Component'] == 'HybridStoragePPAEnergyCostInTP']['AnnualCost_Real'].to_numpy()[0]\r\n", + " energy_contract_cost = energy_contract_cost + hybrid_storage_energy_discount\r\n", + "except IndexError:\r\n", + " pass\r\n", + "\r\n", + "# Capacity Contract Cost\r\n", + "capacity_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'TotalGenCapacityCost']['AnnualCost_Real'].to_numpy()[0]\r\n", + "\r\n", + "# Delivered Energy Cost\r\n", + "if nodal_costs_exist:\r\n", + " delivered_energy_cost = nodal_costs_df['DLAP Cost'].sum()\r\n", + "\r\n", + "# Generator Pnode Revenue\r\n", + "if nodal_costs_exist:\r\n", + " generator_pnode_revenue = - nodal_costs_df['Generator Pnode Revenue'].sum()\r\n", + "\r\n", + "# Storage Energy Arbitrage\r\n", + "try:\r\n", + " storage_energy_arbitrage_cost = costs_itemized_df[costs_itemized_df['Component'] == 'StorageNodalEnergyCostInTP']['AnnualCost_Real'].to_numpy()[0]\r\n", + "except IndexError:\r\n", + " storage_energy_arbitrage_cost = 0\r\n", + "\r\n", + "# Resource Adequacy Costs\r\n", + "try:\r\n", + " ra_open_position_cost = costs_itemized_df[costs_itemized_df['Component'] == 'TotalRAOpenPositionCost']['AnnualCost_Real'].to_numpy()[0]\r\n", + "except IndexError:\r\n", + " ra_open_position_cost = 0\r\n", + "\r\n", + "try:\r\n", + " # Build the cost table\r\n", + " cost = {\r\n", + " 'Cost Component': ['Energy Contract Cost','Storage Capacity Cost','Delivered Energy Cost','Generator Pnode Revenue','Storage Energy Arbitrage','Resource Adequacy Open Position Cost'],\r\n", + " 'Annual Real Cost': [energy_contract_cost, capacity_contract_cost, delivered_energy_cost, generator_pnode_revenue, storage_energy_arbitrage_cost, ra_open_position_cost]\r\n", + " }\r\n", + "\r\n", + " cost_summary = pd.DataFrame(cost)\r\n", + " cost_summary = cost_summary.round(decimals=2)\r\n", + " #drop any rows with zero values\r\n", + " cost_summary = cost_summary[cost_summary['Annual Real Cost'] != 0]\r\n", + "\r\n", + " #add a total row\r\n", + " cost_summary = cost_summary.append({'Cost Component':'TOTAL','Annual Real Cost':cost_summary['Annual Real Cost'].sum()}, ignore_index=True)\r\n", + "\r\n", + " # add a column for delivered cost\r\n", + " cost_summary['Delivered Cost per MWh'] = (cost_summary['Annual Real Cost'] / total_demand).round(decimals=2)\r\n", + "\r\n", + " # export calculated costs to csv\r\n", + " unformatted_cost = cost_summary.copy()\r\n", + " unformatted_cost.to_csv(data_dir / 'total_cost_of_energy.csv')\r\n", + "\r\n", + " cost_summary['Delivered Cost per MWh'] = cost_summary['Delivered Cost per MWh'].apply(format_currency)\r\n", + " cost_summary['Annual Real Cost'] = cost_summary['Annual Real Cost'].apply(format_currency)\r\n", + " cost_summary = cost_summary.set_index('Cost Component')\r\n", + "\r\n", + " display(cost_summary)\r\n", + "except NameError:\r\n", + " pass" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.354813Z", - "iopub.status.busy": "2021-06-03T23:42:00.354813Z", - "iopub.status.idle": "2021-06-03T23:42:00.429353Z", - "shell.execute_reply": "2021-06-03T23:42:00.429353Z" + "iopub.execute_input": "2021-08-10T22:22:53.003208Z", + "iopub.status.busy": "2021-08-10T22:22:53.003208Z", + "iopub.status.idle": "2021-08-10T22:22:53.068212Z", + "shell.execute_reply": "2021-08-10T22:22:53.068212Z" } - }, - "outputs": [], - "source": [ - "costs_itemized_df = pd.read_csv(data_dir / 'costs_itemized.csv')\n", - "try:\n", - " nodal_costs_df = pd.read_csv(data_dir / 'nodal_costs.csv')\n", - " nodal_costs_exist = True\n", - "except FileNotFoundError:\n", - " nodal_costs_exist = False\n", - "\n", - "# Energy Contract Cost\n", - "energy_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'GenPPACostInTP']['AnnualCost_Real'].to_numpy()[0]\n", - "# check for discount from hybrid generation\n", - "try:\n", - " hybrid_storage_energy_discount = costs_itemized_df[costs_itemized_df['Component'] == 'HybridStoragePPAEnergyCostInTP']['AnnualCost_Real'].to_numpy()[0]\n", - " energy_contract_cost = energy_contract_cost + hybrid_storage_energy_discount\n", - "except IndexError:\n", - " pass\n", - "\n", - "# Capacity Contract Cost\n", - "capacity_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'TotalGenCapacityCost']['AnnualCost_Real'].to_numpy()[0]\n", - "\n", - "# Delivered Energy Cost\n", - "if nodal_costs_exist:\n", - " delivered_energy_cost = nodal_costs_df['DLAP Cost'].sum()\n", - "\n", - "# Generator Pnode Revenue\n", - "if nodal_costs_exist:\n", - " generator_pnode_revenue = - nodal_costs_df['Generator Pnode Revenue'].sum()\n", - "\n", - "# Storage Energy Arbitrage\n", - "try:\n", - " storage_energy_arbitrage_cost = costs_itemized_df[costs_itemized_df['Component'] == 'StorageNodalEnergyCostInTP']['AnnualCost_Real'].to_numpy()[0]\n", - "except IndexError:\n", - " storage_energy_arbitrage_cost = 0\n", - "\n", - "# Resource Adequacy Costs\n", - "try:\n", - " ra_open_position_cost = costs_itemized_df[costs_itemized_df['Component'] == 'TotalRAOpenPositionCost']['AnnualCost_Real'].to_numpy()[0]\n", - "except IndexError:\n", - " ra_open_position_cost = 0\n", - "\n", - "try:\n", - " # Build the cost table\n", - " cost = {\n", - " 'Cost Component': ['Energy Contract Cost','Storage Capacity Cost','Delivered Energy Cost','Generator Pnode Revenue','Storage Energy Arbitrage','Resource Adequacy Open Position Cost'],\n", - " 'Annual Real Cost': [energy_contract_cost, capacity_contract_cost, delivered_energy_cost, generator_pnode_revenue, storage_energy_arbitrage_cost, ra_open_position_cost]\n", - " }\n", - "\n", - " cost_summary = pd.DataFrame(cost)\n", - " cost_summary = cost_summary.round(decimals=2)\n", - " #drop any rows with zero values\n", - " cost_summary = cost_summary[cost_summary['Annual Real Cost'] != 0]\n", - "\n", - " #add a total row\n", - " cost_summary = cost_summary.append({'Cost Component':'TOTAL','Annual Real Cost':cost_summary['Annual Real Cost'].sum()}, ignore_index=True)\n", - "\n", - " # add a column for delivered cost\n", - " cost_summary['Delivered Cost per MWh'] = (cost_summary['Annual Real Cost'] / total_demand).round(decimals=2)\n", - "\n", - " # export calculated costs to csv\n", - " unformatted_cost = cost_summary.copy()\n", - " unformatted_cost.to_csv(data_dir / 'total_cost_of_energy.csv')\n", - "\n", - " cost_summary['Delivered Cost per MWh'] = cost_summary['Delivered Cost per MWh'].apply(format_currency)\n", - " cost_summary['Annual Real Cost'] = cost_summary['Annual Real Cost'].apply(format_currency)\n", - " cost_summary = cost_summary.set_index('Cost Component')\n", - "\n", - " display(cost_summary)\n", - "except NameError:\n", - " pass" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Excess Generation Summary" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "try:\r\n", + " total_generation_mwh = load_balance['Total_Generation_No_Storage'].sum()\r\n", + " excess_tc_generation_mwh = (load_balance['Total_Generation_No_Storage'] - load_balance['Time_Coincident_Generation']).sum()\r\n", + " excess_volumetric_generation_mwh = load_balance['Total_Generation_No_Storage'].sum() - load_balance['Total_Demand'].sum() \r\n", + "\r\n", + "\r\n", + " generation_avg_contract_cost_per_mwh = energy_contract_cost / total_generation_mwh\r\n", + " generation_avg_pnode_revenue_per_mwh = generator_pnode_revenue / total_generation_mwh\r\n", + "\r\n", + " if excess_tc_generation_mwh > 0:\r\n", + " print(f'Excess generation (RECs) by annual volume: {int(excess_volumetric_generation_mwh)} MWh')\r\n", + " print(f'Excess time-coincident generation (hourly RECs): {int(excess_tc_generation_mwh)} MWh\\n')\r\n", + " \r\n", + " print(f'Average Contract Cost of Generation: {format_currency(generation_avg_contract_cost_per_mwh)} / MWh generated')\r\n", + " print(f'Average Pnode Cost of Generation: {format_currency(generation_avg_pnode_revenue_per_mwh)} / MWh generated')\r\n", + " print(f'Average Total Cost of Generation: {format_currency(generation_avg_contract_cost_per_mwh+generation_avg_pnode_revenue_per_mwh)} / MWh generated\\n')\r\n", + " \r\n", + " print(f'Total Contract Cost of Excess Time-coincident Generation: {format_currency(generation_avg_contract_cost_per_mwh*excess_tc_generation_mwh)}')\r\n", + " print(f'Total Pnode Cost of Excess Time-coincident Generation: {format_currency(generation_avg_pnode_revenue_per_mwh*excess_tc_generation_mwh)}')\r\n", + " print(f'Total Cost of Excess Time-coincident Generation: {format_currency((generation_avg_contract_cost_per_mwh+generation_avg_pnode_revenue_per_mwh)*excess_tc_generation_mwh)}')\r\n", + " else:\r\n", + " print('No excess generation')\r\n", + "except NameError:\r\n", + " pass" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.429353Z", - "iopub.status.busy": "2021-06-03T23:42:00.429353Z", - "iopub.status.idle": "2021-06-03T23:42:00.445082Z", - "shell.execute_reply": "2021-06-03T23:42:00.445082Z" + "iopub.execute_input": "2021-08-10T22:22:53.076203Z", + "iopub.status.busy": "2021-08-10T22:22:53.076203Z", + "iopub.status.idle": "2021-08-10T22:22:53.080258Z", + "shell.execute_reply": "2021-08-10T22:22:53.080258Z" } - }, - "outputs": [], - "source": [ - "try:\n", - " total_generation_mwh = load_balance['Total_Generation_No_Storage'].sum()\n", - " excess_tc_generation_mwh = (load_balance['Total_Generation_No_Storage'] - load_balance['Time_Coincident_Generation']).sum()\n", - " excess_volumetric_generation_mwh = load_balance['Total_Generation_No_Storage'].sum() - load_balance['Total_Demand'].sum() \n", - "\n", - "\n", - " generation_avg_contract_cost_per_mwh = energy_contract_cost / total_generation_mwh\n", - " generation_avg_pnode_revenue_per_mwh = generator_pnode_revenue / total_generation_mwh\n", - "\n", - " if excess_tc_generation_mwh > 0:\n", - " print(f'Excess generation (RECs) by annual volume: {int(excess_volumetric_generation_mwh)} MWh')\n", - " print(f'Excess time-coincident generation (hourly RECs): {int(excess_tc_generation_mwh)} MWh\\n')\n", - " \n", - " print(f'Average Contract Cost of Generation: {format_currency(generation_avg_contract_cost_per_mwh)} / MWh generated')\n", - " print(f'Average Pnode Cost of Generation: {format_currency(generation_avg_pnode_revenue_per_mwh)} / MWh generated')\n", - " print(f'Average Total Cost of Generation: {format_currency(generation_avg_contract_cost_per_mwh+generation_avg_pnode_revenue_per_mwh)} / MWh generated\\n')\n", - " \n", - " print(f'Total Contract Cost of Excess Time-coincident Generation: {format_currency(generation_avg_contract_cost_per_mwh*excess_tc_generation_mwh)}')\n", - " print(f'Total Pnode Cost of Excess Time-coincident Generation: {format_currency(generation_avg_pnode_revenue_per_mwh*excess_tc_generation_mwh)}')\n", - " print(f'Total Cost of Excess Time-coincident Generation: {format_currency((generation_avg_contract_cost_per_mwh+generation_avg_pnode_revenue_per_mwh)*excess_tc_generation_mwh)}')\n", - " else:\n", - " print('No excess generation')\n", - "except NameError:\n", - " pass" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Resource Adequacy Open Position" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "try:\r\n", + " ra_open = pd.read_csv(data_dir / 'RA_open_position.csv')\r\n", + " \r\n", + " #where RA Position is negative, it indicates an open position\r\n", + " ra_open['Color'] = np.where(ra_open[\"RA_Position_MW\"]<0, 'Open Position', 'Excess RA')\r\n", + "\r\n", + " #create a plot of monthly RA position\r\n", + " monthly_ra_open_fig = px.bar(ra_open, x='Month', y='RA_Position_MW', facet_col='RA_Requirement', color='Color', color_discrete_map={'Excess RA':'green', 'Open Position':'red'}, title='Monthly RA position by requirement')\r\n", + " monthly_ra_open_fig.for_each_annotation(lambda a: a.update(text=a.text.replace(\"RA_Requirement=\", \"\")))\r\n", + "\r\n", + " sellable_flex = ra_open.copy()\r\n", + " sellable_flex = sellable_flex[(sellable_flex['RA_Requirement'] == 'flexible_RA') & (sellable_flex['RA_Position_MW'] > 0)]\r\n", + "\r\n", + " #flex RA must be paired with regular RA to sell, so we need to limit it\r\n", + " def calculate_sellable_flex(row, ra_open):\r\n", + " # get the month number\r\n", + " month = row['Month']\r\n", + " #get the amount of excess system RA\r\n", + " system_ra_open_MW = ra_open.loc[(ra_open['RA_Requirement'] == 'system_RA') & (ra_open['Month'] == month), 'Excess_RA_MW'].item()\r\n", + " # find the minimum of the excess system RA and excess flex RA\r\n", + " sellable = min(row['RA_Position_MW'], system_ra_open_MW)\r\n", + " return sellable \r\n", + " sellable_flex.loc[:,'Sellable_Flex_MW'] = sellable_flex.apply(lambda row: calculate_sellable_flex(row, ra_open), axis=1)\r\n", + "\r\n", + " #re-calculate the sellable position of flex RA\r\n", + " sellable_flex['Excess_RA_Value'] = sellable_flex['RA_Value'] * sellable_flex['Sellable_Flex_MW']\r\n", + "\r\n", + " #calculate how much system could be sold for subtracting the local\r\n", + " sellable_system = ra_open[(ra_open['RA_Requirement'] == 'system_RA')]\r\n", + "\r\n", + " local_RA = ra_open[(ra_open['RA_Requirement'] != 'system_RA') & (ra_open['RA_Requirement'] != 'flexible_RA')][['Month','Excess_RA_MW']]\r\n", + " local_RA = local_RA.groupby('Month').sum().reset_index().rename(columns={'Excess_RA_MW':'Local_RA_MW'})\r\n", + "\r\n", + " #merge local RA data into system data\r\n", + " sellable_system = sellable_system.merge(local_RA, how='left', on='Month').fillna(0)\r\n", + " sellable_system['Sellable_System_MW'] = sellable_system['Excess_RA_MW'] - sellable_system['Local_RA_MW']\r\n", + "\r\n", + " sellable_system['Excess_RA_Value'] = sellable_system['RA_Value'] * sellable_system['Sellable_System_MW']\r\n", + "\r\n", + " #calculate total RA costs and value\r\n", + " total_RA_open_cost = ra_open['Open_Position_Cost'].sum()\r\n", + " excess_flex_RA_value = sellable_flex['Excess_RA_Value'].sum()\r\n", + " excess_local_RA_value = ra_open.loc[(ra_open['RA_Requirement'] != 'system_RA') & (ra_open['RA_Requirement'] != 'flexible_RA'), 'Excess_RA_Value'].sum()\r\n", + " excess_system_RA_value = sellable_system['Excess_RA_Value'].sum()\r\n", + "\r\n", + " #calculate the best-case RA cost if all RA can be sold at full market value\r\n", + " #ra_cost_best_case = (total_RA_open_cost - (excess_flex_RA_value + excess_local_RA_value + excess_system_RA_value)) / system_demand_mwh\r\n", + "\r\n", + " #create cost summary of RA costs\r\n", + " ################################\r\n", + "\r\n", + " ra_costs = ra_open.copy()\r\n", + " #groupby RA requirement\r\n", + " ra_costs = ra_costs.groupby('RA_Requirement').mean().drop(columns=['Period','Month'])\r\n", + " #rename columns\r\n", + " ra_costs = ra_costs.rename(columns={'Open_Position_MW': 'Monthly_Avg_Open_Position_MW', 'Open_Position_Cost': 'Monthly_Avg_Open_Position_Cost','RA_Requirement_Need_MW':'Monthly_Avg_RA_Need_MW','Available_RA_Capacity_MW':'Monthly_Avg_Available_RA_MW'})\r\n", + "\r\n", + " \r\n", + " #calculate the annual total\r\n", + " ra_costs['Annual_Open_Position_Cost'] = ra_costs['Monthly_Avg_Open_Position_Cost'] * 12\r\n", + " ra_costs['$/MWh delivered'] = ra_costs['Annual_Open_Position_Cost'] / total_demand\r\n", + " #ra_costs['Weighted Cost'] = ra_costs['RA_Cost'] / 1000\r\n", + " #ra_costs['Unit'] = '$/kW-mo'\r\n", + " ra_costs = ra_costs[['$/MWh delivered']]\r\n", + " #ra_costs = ra_costs.drop(columns=['Monthly_Avg_Open_Position_Cost','Monthly_Avg_RA_Need_MW','Monthly_Avg_Available_RA_MW','Monthly_Avg_Open_Position_MW','Annual_Open_Position_Cost','RA_Position_MW', 'Excess_RA_MW','RA_Cost','RA_Value','Excess_RA_Value'])\r\n", + "\r\n", + " #merge RA cost data into cost_summary\r\n", + " ra_costs = ra_costs.reset_index().rename(columns={'RA_Requirement':'Cost Component'})\r\n", + "\r\n", + " print(f'Resale value of Excess RA: {format_currency(excess_flex_RA_value + excess_local_RA_value + excess_system_RA_value)}')\r\n", + "except FileNotFoundError:\r\n", + " pass" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.445082Z", - "iopub.status.busy": "2021-06-03T23:42:00.445082Z", - "iopub.status.idle": "2021-06-03T23:42:00.599206Z", - "shell.execute_reply": "2021-06-03T23:42:00.599206Z" + "iopub.execute_input": "2021-08-10T22:22:53.096580Z", + "iopub.status.busy": "2021-08-10T22:22:53.096580Z", + "iopub.status.idle": "2021-08-10T22:22:53.308781Z", + "shell.execute_reply": "2021-08-10T22:22:53.304784Z" }, "tags": [] - }, - "outputs": [], - "source": [ - "try:\n", - " ra_open = pd.read_csv(data_dir / 'RA_open_position.csv')\n", - " \n", - " #where RA Position is negative, it indicates an open position\n", - " ra_open['Color'] = np.where(ra_open[\"RA_Position_MW\"]<0, 'Open Position', 'Excess RA')\n", - "\n", - " #create a plot of monthly RA position\n", - " monthly_ra_open_fig = px.bar(ra_open, x='Month', y='RA_Position_MW', facet_col='RA_Requirement', color='Color', color_discrete_map={'Excess RA':'green', 'Open Position':'red'}, title='Monthly RA position by requirement')\n", - " monthly_ra_open_fig.for_each_annotation(lambda a: a.update(text=a.text.replace(\"RA_Requirement=\", \"\")))\n", - "\n", - " sellable_flex = ra_open.copy()\n", - " sellable_flex = sellable_flex[(sellable_flex['RA_Requirement'] == 'flexible_RA') & (sellable_flex['RA_Position_MW'] > 0)]\n", - "\n", - " #flex RA must be paired with regular RA to sell, so we need to limit it\n", - " def calculate_sellable_flex(row, ra_open):\n", - " # get the month number\n", - " month = row['Month']\n", - " #get the amount of excess system RA\n", - " system_ra_open_MW = ra_open.loc[(ra_open['RA_Requirement'] == 'system_RA') & (ra_open['Month'] == month), 'Excess_RA_MW'].item()\n", - " # find the minimum of the excess system RA and excess flex RA\n", - " sellable = min(row['RA_Position_MW'], system_ra_open_MW)\n", - " return sellable \n", - " sellable_flex.loc[:,'Sellable_Flex_MW'] = sellable_flex.apply(lambda row: calculate_sellable_flex(row, ra_open), axis=1)\n", - "\n", - " #re-calculate the sellable position of flex RA\n", - " sellable_flex['Excess_RA_Value'] = sellable_flex['RA_Value'] * sellable_flex['Sellable_Flex_MW']\n", - "\n", - " #calculate how much system could be sold for subtracting the local\n", - " sellable_system = ra_open[(ra_open['RA_Requirement'] == 'system_RA')]\n", - "\n", - " local_RA = ra_open[(ra_open['RA_Requirement'] != 'system_RA') & (ra_open['RA_Requirement'] != 'flexible_RA')][['Month','Excess_RA_MW']]\n", - " local_RA = local_RA.groupby('Month').sum().reset_index().rename(columns={'Excess_RA_MW':'Local_RA_MW'})\n", - "\n", - " #merge local RA data into system data\n", - " sellable_system = sellable_system.merge(local_RA, how='left', on='Month').fillna(0)\n", - " sellable_system['Sellable_System_MW'] = sellable_system['Excess_RA_MW'] - sellable_system['Local_RA_MW']\n", - "\n", - " sellable_system['Excess_RA_Value'] = sellable_system['RA_Value'] * sellable_system['Sellable_System_MW']\n", - "\n", - " #calculate total RA costs and value\n", - " total_RA_open_cost = ra_open['Open_Position_Cost'].sum()\n", - " excess_flex_RA_value = sellable_flex['Excess_RA_Value'].sum()\n", - " excess_local_RA_value = ra_open.loc[(ra_open['RA_Requirement'] != 'system_RA') & (ra_open['RA_Requirement'] != 'flexible_RA'), 'Excess_RA_Value'].sum()\n", - " excess_system_RA_value = sellable_system['Excess_RA_Value'].sum()\n", - "\n", - " #calculate the best-case RA cost if all RA can be sold at full market value\n", - " #ra_cost_best_case = (total_RA_open_cost - (excess_flex_RA_value + excess_local_RA_value + excess_system_RA_value)) / system_demand_mwh\n", - "\n", - " #create cost summary of RA costs\n", - " ################################\n", - "\n", - " ra_costs = ra_open.copy()\n", - " #groupby RA requirement\n", - " ra_costs = ra_costs.groupby('RA_Requirement').mean().drop(columns=['Period','Month'])\n", - " #rename columns\n", - " ra_costs = ra_costs.rename(columns={'Open_Position_MW': 'Monthly_Avg_Open_Position_MW', 'Open_Position_Cost': 'Monthly_Avg_Open_Position_Cost','RA_Requirement_Need_MW':'Monthly_Avg_RA_Need_MW','Available_RA_Capacity_MW':'Monthly_Avg_Available_RA_MW'})\n", - "\n", - " \n", - " #calculate the annual total\n", - " ra_costs['Annual_Open_Position_Cost'] = ra_costs['Monthly_Avg_Open_Position_Cost'] * 12\n", - " ra_costs['$/MWh delivered'] = ra_costs['Annual_Open_Position_Cost'] / total_demand\n", - " #ra_costs['Weighted Cost'] = ra_costs['RA_Cost'] / 1000\n", - " #ra_costs['Unit'] = '$/kW-mo'\n", - " ra_costs = ra_costs[['$/MWh delivered']]\n", - " #ra_costs = ra_costs.drop(columns=['Monthly_Avg_Open_Position_Cost','Monthly_Avg_RA_Need_MW','Monthly_Avg_Available_RA_MW','Monthly_Avg_Open_Position_MW','Annual_Open_Position_Cost','RA_Position_MW', 'Excess_RA_MW','RA_Cost','RA_Value','Excess_RA_Value'])\n", - "\n", - " #merge RA cost data into cost_summary\n", - " ra_costs = ra_costs.reset_index().rename(columns={'RA_Requirement':'Cost Component'})\n", - "\n", - " print(f'Resale value of Excess RA: {format_currency(excess_flex_RA_value + excess_local_RA_value + excess_system_RA_value)}')\n", - "except FileNotFoundError:\n", - " pass" - ] + } }, { "cell_type": "code", "execution_count": null, + "source": [ + "try:\r\n", + " monthly_ra_open_fig.show()\r\n", + "except:\r\n", + " print('Resource Adequacy was not considered in this scenario')" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.599206Z", - "iopub.status.busy": "2021-06-03T23:42:00.599206Z", - "iopub.status.idle": "2021-06-03T23:42:00.615196Z", - "shell.execute_reply": "2021-06-03T23:42:00.615196Z" + "iopub.execute_input": "2021-08-10T22:22:53.316779Z", + "iopub.status.busy": "2021-08-10T22:22:53.312777Z", + "iopub.status.idle": "2021-08-10T22:22:53.332507Z", + "shell.execute_reply": "2021-08-10T22:22:53.332507Z" } - }, - "outputs": [], - "source": [ - "try:\n", - " monthly_ra_open_fig.show()\n", - "except:\n", - " print('Resource Adequacy was not considered in this scenario')" - ] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "# Supply and Demand Balance" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:00.630448Z", - "iopub.status.busy": "2021-06-03T23:42:00.630448Z", - "iopub.status.idle": "2021-06-03T23:42:05.114906Z", - "shell.execute_reply": "2021-06-03T23:42:05.114906Z" - } - }, - "outputs": [], "source": [ - "# Set up data for dispatch timeseries graph\n", - "###########################################\n", - "\n", - "dispatch = pd.read_csv(data_dir / 'dispatch.csv', usecols=['timestamp','generation_project','DispatchGen_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\n", - "\n", - "#create a dictionary that matches hybrid storage to generator\n", - "hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\n", - "hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\n", - "hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\n", - "\n", - "try:\n", - " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=['timestamp'], infer_datetime_format=True)\n", - " #merge charging data for hybrid storage projects into the dataframe\n", - " hybrid_dispatch = storage.copy()[['generation_project','timestamp','ChargeMW','DispatchMW']]\n", - " hybrid_dispatch.loc[:,'generation_project'] = hybrid_dispatch['generation_project'].replace(hybrid_pair)\n", - " dispatch = dispatch.merge(hybrid_dispatch, how='left', on=['generation_project','timestamp']).fillna(0)\n", - "\n", - " # subtract out charging energy from generation from hybrid projects to get true generation\n", - " dispatch.loc[:,'DispatchGen_MW'] = dispatch['DispatchGen_MW'] - dispatch['ChargeMW'] + dispatch['DispatchMW']\n", - " #drop the hybrid columns\n", - " dispatch = dispatch.drop(columns=['ChargeMW','DispatchMW'])\n", - " # rename the column\n", - " dispatch = dispatch.rename(columns={'DispatchGen_MW':'DispatchMW'})\n", - "\n", - " #append storage \n", - " storage_dispatch = storage[['generation_project','timestamp','DispatchMW']]\n", - " storage_dispatch = storage_dispatch[~storage_dispatch['generation_project'].str.contains('HYBRID')]\n", - " dispatch = dispatch.append(storage_dispatch)\n", - "\n", - " # Setup storage charging timeseries data\n", - " ########################################\n", - " storage_charge = storage[['generation_project','timestamp','ChargeMW']]\n", - " storage_charge = storage_charge[~storage_charge['generation_project'].str.contains('HYBRID')]\n", - " storage_charge = storage_charge.groupby('timestamp').sum().reset_index()\n", - "\n", - "except FileNotFoundError:\n", - " #calculate total generation\n", - " dispatch = dispatch.rename(columns={'DispatchGen_MW':'DispatchMW'})\n", - "\n", - "#add system power data if available\n", - "try:\n", - " system_power_use = pd.read_csv(data_dir / 'system_power.csv', usecols=['timestamp','System_Power_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\n", - " system_power_use.loc[:,'generation_project'] = 'SYSTEM_POWER'\n", - " system_power_use = system_power_use.rename(columns={'System_Power_MW':'DispatchMW'})\n", - " dispatch = dispatch.append(system_power_use)\n", - "\n", - "except FileNotFoundError:\n", - " pass\n", - "\n", - "#add technology column\n", - "dispatch['Technology'] = [i.split('_')[0] for i in dispatch['generation_project']]\n", - "\n", - "# group the data by technology type\n", - "dispatch_by_tech = dispatch.groupby(['Technology','timestamp']).sum().reset_index()\n", - "#only keep observations greater than 0\n", - "dispatch_by_tech = dispatch_by_tech[dispatch_by_tech['DispatchMW'] > 0]\n", - "# Setup load timeseries data\n", - "############################\n", - "\n", - "load = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], parse_dates=['timestamp'], infer_datetime_format=True)\n", - "\n", - "# Create Figure\n", - "###############\n", - "\n", - "color_map = {'HYDRO':'Purple',\n", - " 'ONWIND':'Blue',\n", - " 'OFFWIND':'Navy',\n", - " 'PV':'Yellow',\n", - " 'PVHYBRID':'GreenYellow',\n", - " 'CSP':'Orange',\n", - " 'GEO':'Sienna',\n", - " 'STORAGE':'Green',\n", - " 'STORAGEHYBRID':'GreenYellow',\n", - " 'SYSTEM':'Red'}\n", - "\n", - "dispatch_fig = px.area(dispatch_by_tech, x='timestamp', y='DispatchMW', color='Technology', color_discrete_map=color_map, labels={'DispatchMW':'MW','timestamp':'Datetime','Technology':'Key'})\n", - "dispatch_fig.update_traces(line={'width':0})\n", - "#dispatch_fig.update_traces(hoveron=\"points+fills\")\n", - "dispatch_fig.layout.template = 'plotly_white'\n", - "#hoveron = 'points+fills'\n", - "\n", - "try:\n", - " # add charging line\n", - " dispatch_fig.add_scatter(x=storage_charge.timestamp, y=(storage_charge.ChargeMW + load.zone_demand_mw), line=dict(color='green', width=4), name='Load + Storage Charge')\n", - "except NameError:\n", - " pass\n", - "\n", - "# add load line\n", - "dispatch_fig.add_scatter(x=load.timestamp, y=load.zone_demand_mw, line=dict(color='black', width=4), name='Demand')\n", - "\n", - "dispatch_fig.update_xaxes(\n", - " rangeslider_visible=True,\n", - " rangeselector=dict(\n", - " buttons=list([\n", - " dict(count=1, label=\"1d\", step=\"day\", stepmode=\"backward\"),\n", - " dict(count=7, label=\"1w\", step=\"day\", stepmode=\"backward\"),\n", - " dict(count=1, label=\"1m\", step=\"month\", stepmode=\"backward\"),\n", - " dict(step=\"all\")\n", - " ])))\n", - "\n", + "# Set up data for dispatch timeseries graph\r\n", + "###########################################\r\n", + "\r\n", + "dispatch = pd.read_csv(data_dir / 'dispatch.csv', usecols=['timestamp','generation_project','DispatchGen_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + "\r\n", + "\"\"\"\r\n", + "\r\n", + "#create a dictionary that matches hybrid storage to generator\r\n", + "hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\r\n", + "hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\r\n", + "hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\r\n", + "\"\"\"\r\n", + "\r\n", + "try:\r\n", + " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + " \"\"\"\r\n", + " #merge charging data for hybrid storage projects into the dataframe\r\n", + " hybrid_dispatch = storage.copy()[['generation_project','timestamp','ChargeMW','DispatchMW']]\r\n", + " hybrid_dispatch.loc[:,'generation_project'] = hybrid_dispatch['generation_project'].replace(hybrid_pair)\r\n", + " dispatch = dispatch.merge(hybrid_dispatch, how='left', on=['generation_project','timestamp']).fillna(0)\r\n", + "\r\n", + " # subtract out charging energy from generation from hybrid projects to get true generation\r\n", + " dispatch.loc[:,'DispatchGen_MW'] = dispatch['DispatchGen_MW'] - dispatch['ChargeMW'] + dispatch['DispatchMW']\r\n", + " #drop the hybrid columns\r\n", + " dispatch = dispatch.drop(columns=['ChargeMW','DispatchMW'])\r\n", + " \"\"\"\r\n", + " # rename the column\r\n", + " dispatch = dispatch.rename(columns={'DispatchGen_MW':'DispatchMW'})\r\n", + "\r\n", + "\r\n", + " #append storage \r\n", + " storage_dispatch = storage[['generation_project','timestamp','DispatchMW']]\r\n", + " #storage_dispatch = storage_dispatch[~storage_dispatch['generation_project'].str.contains('HYBRID')]\r\n", + " dispatch = dispatch.append(storage_dispatch)\r\n", + "\r\n", + " # Setup storage charging timeseries data\r\n", + " ########################################\r\n", + " storage_charge = storage[['generation_project','timestamp','ChargeMW']]\r\n", + " #storage_charge = storage_charge[~storage_charge['generation_project'].str.contains('HYBRID')]\r\n", + " storage_charge = storage_charge.groupby('timestamp').sum().reset_index()\r\n", + "\r\n", + "except FileNotFoundError:\r\n", + " #calculate total generation\r\n", + " dispatch = dispatch.rename(columns={'DispatchGen_MW':'DispatchMW'})\r\n", + "\r\n", + "#add system power data if available\r\n", + "try:\r\n", + " system_power_use = pd.read_csv(data_dir / 'system_power.csv', usecols=['timestamp','System_Power_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + " system_power_use.loc[:,'generation_project'] = 'SYSTEM_POWER'\r\n", + " system_power_use = system_power_use.rename(columns={'System_Power_MW':'DispatchMW'})\r\n", + " dispatch = dispatch.append(system_power_use)\r\n", + "\r\n", + "except FileNotFoundError:\r\n", + " pass\r\n", + "\r\n", + "#add technology column\r\n", + "dispatch['Technology'] = [i.split('_')[0] for i in dispatch['generation_project']]\r\n", + "\r\n", + "# replace the technology name of the RE portion of a hybrid with just the name of the technology\r\n", + "for tech in list(dispatch['Technology'].unique()):\r\n", + " if 'HYBRID' in tech and 'STORAGE' not in tech:\r\n", + " dispatch.loc[dispatch['Technology'] == tech, 'Technology'] = tech.replace('HYBRID','')\r\n", + "\r\n", + "# group the data by technology type\r\n", + "dispatch_by_tech = dispatch.groupby(['Technology','timestamp']).sum().reset_index()\r\n", + "#only keep observations greater than 0\r\n", + "dispatch_by_tech = dispatch_by_tech[dispatch_by_tech['DispatchMW'] > 0]\r\n", + "# Setup load timeseries data\r\n", + "############################\r\n", + "\r\n", + "load = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + "\r\n", + "# Create Figure\r\n", + "###############\r\n", + "\r\n", + "color_map = {'HYDRO':'Purple',\r\n", + " 'ONWIND':'Blue',\r\n", + " 'OFFWIND':'Navy',\r\n", + " 'PV':'Yellow',\r\n", + " 'CSP':'Orange',\r\n", + " 'GEO':'Sienna',\r\n", + " 'STORAGE':'Green',\r\n", + " 'STORAGEHYBRID':'GreenYellow',\r\n", + " 'SYSTEM':'Red'}\r\n", + "\r\n", + "dispatch_fig = px.area(dispatch_by_tech, x='timestamp', y='DispatchMW', color='Technology', color_discrete_map=color_map, labels={'DispatchMW':'MW','timestamp':'Datetime','Technology':'Key'})\r\n", + "dispatch_fig.update_traces(line={'width':0})\r\n", + "#dispatch_fig.update_traces(hoveron=\"points+fills\")\r\n", + "dispatch_fig.layout.template = 'plotly_white'\r\n", + "#hoveron = 'points+fills'\r\n", + "\r\n", + "try:\r\n", + " # add charging line\r\n", + " dispatch_fig.add_scatter(x=storage_charge.timestamp, y=(storage_charge.ChargeMW + load.zone_demand_mw), line=dict(color='green', width=4), name='Load + Storage Charge')\r\n", + "except NameError:\r\n", + " pass\r\n", + "\r\n", + "# add load line\r\n", + "dispatch_fig.add_scatter(x=load.timestamp, y=load.zone_demand_mw, line=dict(color='black', width=4), name='Demand')\r\n", + "\r\n", + "dispatch_fig.update_xaxes(\r\n", + " rangeslider_visible=True,\r\n", + " rangeselector=dict(\r\n", + " buttons=list([\r\n", + " dict(count=1, label=\"1d\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=7, label=\"1w\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=1, label=\"1m\", step=\"month\", stepmode=\"backward\"),\r\n", + " dict(step=\"all\")\r\n", + " ])))\r\n", + "\r\n", "dispatch_fig.show()" - ] + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:05.114906Z", - "iopub.status.busy": "2021-06-03T23:42:05.114906Z", - "iopub.status.idle": "2021-06-03T23:42:06.254015Z", - "shell.execute_reply": "2021-06-03T23:42:06.254015Z" - } - }, - "outputs": [], "source": [ - "# Battery State of Charge data\n", - "##############################\n", - "\n", - "try:\n", - "\n", - " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=['timestamp'], infer_datetime_format=True)\n", - " \n", - " soc = storage.pivot(index='timestamp', columns='generation_project', values='StateOfCharge')\n", - " #remove columns where all values are 0\n", - " soc = soc.loc[:, (soc != 0).any(axis=0)]\n", - " \n", - " #load storage capacity\n", - " storage_energy_capacity = pd.read_csv(data_dir / 'storage_builds.csv', usecols=['generation_project','OnlineEnergyCapacityMWh'], index_col='generation_project')\n", - "\n", - " #create another dictionary of storage energy capacity summed by storage type\n", - " storage_energy_capacity = storage_energy_capacity.reset_index()\n", - " storage_energy_capacity['generation_project'] = [i.split('_')[0] for i in storage_energy_capacity['generation_project']]\n", - " storage_energy_capacity = storage_energy_capacity.groupby('generation_project').sum()\n", - " grouped_storage_energy_capacity_dict = storage_energy_capacity.to_dict()['OnlineEnergyCapacityMWh']\n", - "\n", - "\n", - " #sum by storage type\n", - " soc.columns = [i.split('_')[0] for i in soc.columns]\n", - " \n", - " soc = soc.groupby(soc.columns, axis=1).sum()\n", - " \n", - " #divide by the total capacity to get state of charge\n", - " soc = soc.div(soc.assign(**grouped_storage_energy_capacity_dict))\n", - " \n", - " #soc.index = pd.to_datetime(soc.index)\n", - " \n", - " soc.head(5)\n", - "\n", - " soc_fig = px.line(soc, x=soc.index, y=list(soc.columns), color_discrete_map={'STORAGE':'green','STORAGEHYBRID':'yellowgreen'}, labels={'timestamp':'Datetime','value':'%'}, title='Storage State of Charge')\n", - "\n", - " soc_fig.update_xaxes(\n", - " rangeslider_visible=True,\n", - " rangeselector=dict(\n", - " buttons=list([\n", - " dict(count=1, label=\"1d\", step=\"day\", stepmode=\"backward\"),\n", - " dict(count=7, label=\"1w\", step=\"day\", stepmode=\"backward\"),\n", - " dict(count=1, label=\"1m\", step=\"month\", stepmode=\"backward\"),\n", - " dict(step=\"all\")\n", - " ])))\n", - "\n", - " soc_fig.show()\n", - " \n", - "except FileNotFoundError:\n", - " pass" - ] + "# Nodal Energy Cost data\r\n", + "nodal_prices = pd.read_csv(inputs_dir / 'nodal_prices.csv')\r\n", + "timestamps = pd.read_csv(inputs_dir / 'timepoints.csv', parse_dates=['timestamp'], usecols=['timepoint_id','timestamp'])\r\n", + "# merge the timestamp data\r\n", + "nodal_prices = nodal_prices.merge(timestamps, how='left', left_on='timepoint', right_on='timepoint_id')\r\n", + "\r\n", + "nodal_fig = px.line(nodal_prices, x='timestamp', y='nodal_price', color='pricing_node', labels={'nodal_price':'$/MWh','timestamp':'Datetime','pricing_node':'Node'}, template='plotly_white')\r\n", + "nodal_fig.update_xaxes(\r\n", + " rangeslider_visible=True,\r\n", + " rangeselector=dict(\r\n", + " buttons=list([\r\n", + " dict(count=1, label=\"1d\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=7, label=\"1w\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=1, label=\"1m\", step=\"month\", stepmode=\"backward\"),\r\n", + " dict(step=\"all\")\r\n", + " ])))\r\n", + "nodal_fig.show()" + ], + "outputs": [], + "metadata": {} }, { "cell_type": "code", "execution_count": null, + "source": [ + "# Battery State of Charge data\r\n", + "##############################\r\n", + "\r\n", + "try:\r\n", + "\r\n", + " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + " \r\n", + " soc = storage.pivot(index='timestamp', columns='generation_project', values='StateOfCharge')\r\n", + " #remove columns where all values are 0\r\n", + " soc = soc.loc[:, (soc != 0).any(axis=0)]\r\n", + " \r\n", + " #load storage capacity\r\n", + " storage_energy_capacity = pd.read_csv(data_dir / 'storage_builds.csv', usecols=['generation_project','OnlineEnergyCapacityMWh'], index_col='generation_project')\r\n", + "\r\n", + " #create another dictionary of storage energy capacity summed by storage type\r\n", + " storage_energy_capacity = storage_energy_capacity.reset_index()\r\n", + " storage_energy_capacity['generation_project'] = [i.split('_')[0] for i in storage_energy_capacity['generation_project']]\r\n", + " storage_energy_capacity = storage_energy_capacity.groupby('generation_project').sum()\r\n", + " grouped_storage_energy_capacity_dict = storage_energy_capacity.to_dict()['OnlineEnergyCapacityMWh']\r\n", + "\r\n", + "\r\n", + " #sum by storage type\r\n", + " soc.columns = [i.split('_')[0] for i in soc.columns]\r\n", + " \r\n", + " soc = soc.groupby(soc.columns, axis=1).sum()\r\n", + " \r\n", + " #divide by the total capacity to get state of charge\r\n", + " soc = soc.div(soc.assign(**grouped_storage_energy_capacity_dict))\r\n", + " \r\n", + " #soc.index = pd.to_datetime(soc.index)\r\n", + " \r\n", + " soc.head(5)\r\n", + "\r\n", + " soc_fig = px.line(soc, x=soc.index, y=list(soc.columns), color_discrete_map={'STORAGE':'green','STORAGEHYBRID':'yellowgreen'}, labels={'timestamp':'Datetime','value':'%'}, title='Storage State of Charge')\r\n", + "\r\n", + " soc_fig.update_xaxes(\r\n", + " rangeslider_visible=True,\r\n", + " rangeselector=dict(\r\n", + " buttons=list([\r\n", + " dict(count=1, label=\"1d\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=7, label=\"1w\", step=\"day\", stepmode=\"backward\"),\r\n", + " dict(count=1, label=\"1m\", step=\"month\", stepmode=\"backward\"),\r\n", + " dict(step=\"all\")\r\n", + " ])))\r\n", + "\r\n", + " soc_fig.show()\r\n", + " \r\n", + "except FileNotFoundError:\r\n", + " pass" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:06.290227Z", - "iopub.status.busy": "2021-06-03T23:42:06.290227Z", - "iopub.status.idle": "2021-06-03T23:42:08.193681Z", - "shell.execute_reply": "2021-06-03T23:42:08.193681Z" + "iopub.execute_input": "2021-08-10T22:22:59.841686Z", + "iopub.status.busy": "2021-08-10T22:22:59.840686Z", + "iopub.status.idle": "2021-08-10T22:23:01.240157Z", + "shell.execute_reply": "2021-08-10T22:23:01.241155Z" } - }, - "outputs": [], - "source": [ - "mh_dispatch = dispatch.copy()\n", - "mh_dispatch = mh_dispatch.set_index('timestamp')\n", - "\n", - "#groupby month and hour\n", - "mh_dispatch = mh_dispatch.groupby(['generation_project', mh_dispatch.index.month, mh_dispatch.index.hour], axis=0).mean()\n", - "mh_dispatch.index = mh_dispatch.index.rename(['Project','Month','Hour'])\n", - "mh_dispatch = mh_dispatch.reset_index()\n", - "\n", - "#add a technology column\n", - "mh_dispatch['Technology'] = mh_dispatch['Project'].str.split('_', expand=True)[0]\n", - "\n", - "mh_dispatch = mh_dispatch[mh_dispatch['DispatchMW'] > 0]\n", - "\n", - "\n", - "#load data\n", - "mh_load = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], index_col='timestamp', parse_dates=True, infer_datetime_format=True).rename(columns={'zone_demand_mw':'DEMAND'})\n", - "mh_load = mh_load.groupby([mh_load.index.month, mh_load.index.hour], axis=0).mean()\n", - "mh_load.index = mh_load.index.rename(['Month','Hour'])\n", - "mh_load = mh_load.reset_index()\n", - "\n", - "try:\n", - " mh_charge = storage_charge.copy().set_index('timestamp')\n", - " mh_charge = mh_charge.groupby([mh_charge.index.month, mh_charge.index.hour], axis=0).mean()\n", - " mh_charge.index = mh_charge.index.rename(['Month','Hour'])\n", - " mh_charge = mh_charge.reset_index()\n", - " #merge load data\n", - " mh_charge = mh_charge.merge(mh_load, how='left', on=['Month','Hour'])\n", - " mh_charge['ChargeMW'] = mh_charge['ChargeMW'] + mh_charge['DEMAND']\n", - "except NameError:\n", - " pass\n", - "\n", - "\n", - "# Generate the Figure\n", - "#####################\n", - "\n", - "color_map = {'HYDRO':'Purple',\n", - " 'ONWIND':'Blue',\n", - " 'OFFWIND':'Navy',\n", - " 'PV':'Yellow',\n", - " 'PVHYBRID':'GreenYellow',\n", - " 'CSP':'Orange',\n", - " 'GEO':'Sienna',\n", - " 'STORAGE':'Green',\n", - " 'STORAGEHYBRID':'GreenYellow',\n", - " 'SYSTEM':'Red'}\n", - "\n", - "mh_fig = px.area(mh_dispatch, x='Hour', y='DispatchMW', facet_col='Month', color='Technology', line_group='Project', color_discrete_map=color_map, facet_col_wrap=6, width=1000, height=600, title='Month-Hour Average Generation Profiles')\n", - "mh_fig.layout.template = 'plotly_white'\n", - "mh_fig.update_traces(line={'dash':'dot'})\n", - "\n", - "try:\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 1, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 1, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=1, name='STORAGE_Charge', showlegend=True)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 2, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 2, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=2, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 3, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 3, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=3, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 4, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 4, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=4, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 5, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 5, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=5, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 6, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 6, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=6, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 7, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 7, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=1, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 8, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 8, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=2, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 9, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 9, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=3, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 10, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 10, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=4, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 11, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 11, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=5, name='STORAGE_Charge', showlegend=False)\n", - " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 12, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 12, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=6, name='STORAGE_Charge', showlegend=False)\n", - "except (NameError, KeyError) as e:\n", - " pass\n", - "\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 1, 'Hour'], y=mh_load.loc[mh_load['Month'] == 1, 'DEMAND'], line=dict(color='black', width=4), row=2, col=1, name='Demand', showlegend=True)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 2, 'Hour'], y=mh_load.loc[mh_load['Month'] == 2, 'DEMAND'], line=dict(color='black', width=4), row=2, col=2, name='Demand',showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 3, 'Hour'], y=mh_load.loc[mh_load['Month'] == 3, 'DEMAND'], line=dict(color='black', width=4), row=2, col=3, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 4, 'Hour'], y=mh_load.loc[mh_load['Month'] == 4, 'DEMAND'], line=dict(color='black', width=4), row=2, col=4, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 5, 'Hour'], y=mh_load.loc[mh_load['Month'] == 5, 'DEMAND'], line=dict(color='black', width=4), row=2, col=5, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 6, 'Hour'], y=mh_load.loc[mh_load['Month'] == 6, 'DEMAND'], line=dict(color='black', width=4), row=2, col=6, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 7, 'Hour'], y=mh_load.loc[mh_load['Month'] == 7, 'DEMAND'], line=dict(color='black', width=4), row=1, col=1, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 8, 'Hour'], y=mh_load.loc[mh_load['Month'] == 8, 'DEMAND'], line=dict(color='black', width=4), row=1, col=2, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 9, 'Hour'], y=mh_load.loc[mh_load['Month'] == 9, 'DEMAND'], line=dict(color='black', width=4), row=1, col=3, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 10, 'Hour'], y=mh_load.loc[mh_load['Month'] == 10, 'DEMAND'], line=dict(color='black', width=4), row=1, col=4, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 11, 'Hour'], y=mh_load.loc[mh_load['Month'] == 11, 'DEMAND'], line=dict(color='black', width=4), row=1, col=5, name='Demand', showlegend=False)\n", - "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 12, 'Hour'], y=mh_load.loc[mh_load['Month'] == 12, 'DEMAND'], line=dict(color='black', width=4), row=1, col=6, name='Demand', showlegend=False)\n", - "\n", - "month_names = ['July', 'August', 'September', 'October', 'November', 'December','January', 'February', 'March', 'April', 'May', 'June']\n", - "for i, a in enumerate(mh_fig.layout.annotations):\n", - " a.text = month_names[i]\n", - "\n", - "\n", - "mh_fig.update_xaxes(dtick=3)\n", - "mh_fig.show()" - ] + } }, { "cell_type": "code", "execution_count": null, + "source": [ + "mh_dispatch = dispatch.copy()\r\n", + "mh_dispatch = mh_dispatch.set_index('timestamp')\r\n", + "\r\n", + "#groupby month and hour\r\n", + "mh_dispatch = mh_dispatch.groupby(['generation_project', mh_dispatch.index.month, mh_dispatch.index.hour], axis=0).mean()\r\n", + "mh_dispatch.index = mh_dispatch.index.rename(['Project','Month','Hour'])\r\n", + "mh_dispatch = mh_dispatch.reset_index()\r\n", + "\r\n", + "#add a technology column\r\n", + "mh_dispatch['Technology'] = mh_dispatch['Project'].str.split('_', expand=True)[0]\r\n", + "\r\n", + "mh_dispatch = mh_dispatch[mh_dispatch['DispatchMW'] > 0]\r\n", + "\r\n", + "\r\n", + "#load data\r\n", + "mh_load = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], index_col='timestamp', parse_dates=True, infer_datetime_format=True).rename(columns={'zone_demand_mw':'DEMAND'})\r\n", + "mh_load = mh_load.groupby([mh_load.index.month, mh_load.index.hour], axis=0).mean()\r\n", + "mh_load.index = mh_load.index.rename(['Month','Hour'])\r\n", + "mh_load = mh_load.reset_index()\r\n", + "\r\n", + "try:\r\n", + " mh_charge = storage_charge.copy().set_index('timestamp')\r\n", + " mh_charge = mh_charge.groupby([mh_charge.index.month, mh_charge.index.hour], axis=0).mean()\r\n", + " mh_charge.index = mh_charge.index.rename(['Month','Hour'])\r\n", + " mh_charge = mh_charge.reset_index()\r\n", + " #merge load data\r\n", + " mh_charge = mh_charge.merge(mh_load, how='left', on=['Month','Hour'])\r\n", + " mh_charge['ChargeMW'] = mh_charge['ChargeMW'] + mh_charge['DEMAND']\r\n", + "except NameError:\r\n", + " pass\r\n", + "\r\n", + "\r\n", + "# Generate the Figure\r\n", + "#####################\r\n", + "\r\n", + "color_map = {'HYDRO':'Purple',\r\n", + " 'ONWIND':'Blue',\r\n", + " 'OFFWIND':'Navy',\r\n", + " 'PV':'Yellow',\r\n", + " 'PVHYBRID':'GreenYellow',\r\n", + " 'CSP':'Orange',\r\n", + " 'GEO':'Sienna',\r\n", + " 'STORAGE':'Green',\r\n", + " 'STORAGEHYBRID':'GreenYellow',\r\n", + " 'SYSTEM':'Red'}\r\n", + "\r\n", + "mh_fig = px.area(mh_dispatch, x='Hour', y='DispatchMW', facet_col='Month', color='Technology', line_group='Project', color_discrete_map=color_map, facet_col_wrap=6, width=1000, height=600, title='Month-Hour Average Generation Profiles')\r\n", + "mh_fig.layout.template = 'plotly_white'\r\n", + "mh_fig.update_traces(line={'dash':'dot'})\r\n", + "\r\n", + "try:\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 1, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 1, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=1, name='STORAGE_Charge', showlegend=True)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 2, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 2, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=2, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 3, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 3, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=3, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 4, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 4, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=4, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 5, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 5, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=5, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 6, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 6, 'ChargeMW'], line=dict(color='green', width=4), row=2, col=6, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 7, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 7, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=1, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 8, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 8, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=2, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 9, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 9, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=3, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 10, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 10, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=4, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 11, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 11, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=5, name='STORAGE_Charge', showlegend=False)\r\n", + " mh_fig.add_scatter(x=mh_charge.loc[mh_charge['Month'] == 12, 'Hour'], y=mh_charge.loc[mh_charge['Month'] == 12, 'ChargeMW'], line=dict(color='green', width=4), row=1, col=6, name='STORAGE_Charge', showlegend=False)\r\n", + "except (NameError, KeyError) as e:\r\n", + " pass\r\n", + "\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 1, 'Hour'], y=mh_load.loc[mh_load['Month'] == 1, 'DEMAND'], line=dict(color='black', width=4), row=2, col=1, name='Demand', showlegend=True)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 2, 'Hour'], y=mh_load.loc[mh_load['Month'] == 2, 'DEMAND'], line=dict(color='black', width=4), row=2, col=2, name='Demand',showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 3, 'Hour'], y=mh_load.loc[mh_load['Month'] == 3, 'DEMAND'], line=dict(color='black', width=4), row=2, col=3, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 4, 'Hour'], y=mh_load.loc[mh_load['Month'] == 4, 'DEMAND'], line=dict(color='black', width=4), row=2, col=4, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 5, 'Hour'], y=mh_load.loc[mh_load['Month'] == 5, 'DEMAND'], line=dict(color='black', width=4), row=2, col=5, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 6, 'Hour'], y=mh_load.loc[mh_load['Month'] == 6, 'DEMAND'], line=dict(color='black', width=4), row=2, col=6, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 7, 'Hour'], y=mh_load.loc[mh_load['Month'] == 7, 'DEMAND'], line=dict(color='black', width=4), row=1, col=1, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 8, 'Hour'], y=mh_load.loc[mh_load['Month'] == 8, 'DEMAND'], line=dict(color='black', width=4), row=1, col=2, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 9, 'Hour'], y=mh_load.loc[mh_load['Month'] == 9, 'DEMAND'], line=dict(color='black', width=4), row=1, col=3, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 10, 'Hour'], y=mh_load.loc[mh_load['Month'] == 10, 'DEMAND'], line=dict(color='black', width=4), row=1, col=4, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 11, 'Hour'], y=mh_load.loc[mh_load['Month'] == 11, 'DEMAND'], line=dict(color='black', width=4), row=1, col=5, name='Demand', showlegend=False)\r\n", + "mh_fig.add_scatter(x=mh_load.loc[mh_load['Month'] == 12, 'Hour'], y=mh_load.loc[mh_load['Month'] == 12, 'DEMAND'], line=dict(color='black', width=4), row=1, col=6, name='Demand', showlegend=False)\r\n", + "\r\n", + "month_names = ['July', 'August', 'September', 'October', 'November', 'December','January', 'February', 'March', 'April', 'May', 'June']\r\n", + "for i, a in enumerate(mh_fig.layout.annotations):\r\n", + " a.text = month_names[i]\r\n", + "\r\n", + "\r\n", + "mh_fig.update_xaxes(dtick=3)\r\n", + "mh_fig.show()" + ], + "outputs": [], "metadata": { "execution": { - "iopub.execute_input": "2021-06-03T23:42:08.209306Z", - "iopub.status.busy": "2021-06-03T23:42:08.209306Z", - "iopub.status.idle": "2021-06-03T23:42:09.841684Z", - "shell.execute_reply": "2021-06-03T23:42:09.841684Z" + "iopub.execute_input": "2021-08-10T22:23:01.284959Z", + "iopub.status.busy": "2021-08-10T22:23:01.253513Z", + "iopub.status.idle": "2021-08-10T22:23:03.330070Z", + "shell.execute_reply": "2021-08-10T22:23:03.334067Z" } - }, - "outputs": [], + } + }, + { + "cell_type": "code", + "execution_count": null, "source": [ "# get month-hour shape of open energy position\n", "##############################################\n", @@ -974,27 +993,27 @@ "\n", "mh_mismatch_fig.show()\n", "\n" - ] + ], + "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2021-08-10T22:23:03.342055Z", + "iopub.status.busy": "2021-08-10T22:23:03.342055Z", + "iopub.status.idle": "2021-08-10T22:23:04.722972Z", + "shell.execute_reply": "2021-08-10T22:23:04.723970Z" + } + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "## Battery Cycles" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:09.841684Z", - "iopub.status.busy": "2021-06-03T23:42:09.841684Z", - "iopub.status.idle": "2021-06-03T23:42:09.879200Z", - "shell.execute_reply": "2021-06-03T23:42:09.877190Z" - } - }, - "outputs": [], "source": [ "try:\n", " cycles = pd.read_csv(data_dir / 'storage_cycle_count.csv', usecols=['generation_project','storage_max_annual_cycles','Battery_Cycle_Count'])\n", @@ -1008,28 +1027,27 @@ "except FileNotFoundError:\n", " cycles = \"No batteries in model\"\n", "cycles" - ] + ], + "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2021-08-10T22:23:04.730982Z", + "iopub.status.busy": "2021-08-10T22:23:04.729956Z", + "iopub.status.idle": "2021-08-10T22:23:04.756364Z", + "shell.execute_reply": "2021-08-10T22:23:04.755369Z" + } + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "# Generator Assumptions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:09.879200Z", - "iopub.status.busy": "2021-06-03T23:42:09.879200Z", - "iopub.status.idle": "2021-06-03T23:42:09.910879Z", - "shell.execute_reply": "2021-06-03T23:42:09.910879Z" - }, - "tags": [] - }, - "outputs": [], "source": [ "pd.set_option('display.max_rows',100)\n", "gen_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_energy_cost','gen_capacity_limit_mw'])\n", @@ -1037,27 +1055,28 @@ "gen_assumptions = gen_assumptions.sort_values(by='GENERATION_PROJECT')\n", "gen_assumptions = gen_assumptions.set_index('GENERATION_PROJECT')\n", "gen_assumptions" - ] + ], + "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2021-08-10T22:23:04.764345Z", + "iopub.status.busy": "2021-08-10T22:23:04.763356Z", + "iopub.status.idle": "2021-08-10T22:23:04.786411Z", + "shell.execute_reply": "2021-08-10T22:23:04.787409Z" + }, + "tags": [] + } }, { "cell_type": "markdown", - "metadata": {}, "source": [ "# Storage Assumptions" - ] + ], + "metadata": {} }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:09.910879Z", - "iopub.status.busy": "2021-06-03T23:42:09.910879Z", - "iopub.status.idle": "2021-06-03T23:42:09.942114Z", - "shell.execute_reply": "2021-06-03T23:42:09.942114Z" - } - }, - "outputs": [], "source": [ "storage_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'storage_roundtrip_efficiency','storage_charge_to_discharge_ratio','storage_energy_to_power_ratio','storage_leakage_loss','storage_hybrid_generation_project','storage_hybrid_capacity_ratio'])\n", "#change capacity cost to $/kw-mo\n", @@ -1077,20 +1096,20 @@ "storage_assumptions = storage_assumptions.set_index('GENERATION_PROJECT')\n", "storage_assumptions = storage_assumptions[['gen_tech','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'RTE','storage_hours','charge/discharge_ratio','soc_leakage_loss','paired_hybrid_gen','hybrid_capacity_ratio']]\n", "storage_assumptions" - ] + ], + "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2021-08-10T22:23:04.795387Z", + "iopub.status.busy": "2021-08-10T22:23:04.794355Z", + "iopub.status.idle": "2021-08-10T22:23:04.817875Z", + "shell.execute_reply": "2021-08-10T22:23:04.816878Z" + } + } }, { "cell_type": "code", "execution_count": null, - "metadata": { - "execution": { - "iopub.execute_input": "2021-06-03T23:42:09.942114Z", - "iopub.status.busy": "2021-06-03T23:42:09.942114Z", - "iopub.status.idle": "2021-06-03T23:42:09.995429Z", - "shell.execute_reply": "2021-06-03T23:42:09.995429Z" - } - }, - "outputs": [], "source": [ "summary = pd.DataFrame(columns=['Scenario Name'], data=['test'])\n", "\n", @@ -1130,13 +1149,25 @@ "summary.columns = [f'{scenario_name}']\n", "\n", "summary.to_csv(data_dir / 'scenario_summary.csv')" - ] + ], + "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2021-08-10T22:23:04.846798Z", + "iopub.status.busy": "2021-08-10T22:23:04.845800Z", + "iopub.status.idle": "2021-08-10T22:23:04.864748Z", + "shell.execute_reply": "2021-08-10T22:23:04.863717Z" + } + } } ], "metadata": { + "interpreter": { + "hash": "b5cd7b7571e6318f5fd6adc9cee12d73eccc93ada83e657cc8f1c05b47d15a2e" + }, "kernelspec": { "name": "python3", - "display_name": "Python 3.8.8 64-bit ('switch_247': conda)" + "display_name": "Python 3.8.10 64-bit ('switch_247': conda)" }, "language_info": { "codemirror_mode": { @@ -1148,10 +1179,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.8" - }, - "interpreter": { - "hash": "b065fb8279f6ab6af7337e67a789f1d7000bc2016e03c59cc129bcbe7412eab5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/switch_model/run_scenarios.ipynb b/switch_model/run_scenarios.ipynb index 0b5be31..b11e79b 100644 --- a/switch_model/run_scenarios.ipynb +++ b/switch_model/run_scenarios.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "source": [ "# Copyright (c) 2021 *****************. All rights reserved.\r\n", "# Licensed under the Apache License, Version 2.0, which is in the LICENSE file.\r\n", @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "source": [ "# specify the location of the model runs folder you will be using, relative to the switch_model directory\r\n", "# the default location is the MODEL_RUNS directory within the Git repo\r\n", @@ -65,7 +65,7 @@ "model_runs_folder = '../MODEL_RUNS'\r\n", "\r\n", "# enter the name of the folder within your MODEL_RUNS folder where the input and output files for this specific model run are located\r\n", - "model_run_name = 'generic_office_example'\r\n", + "model_run_name = 'test_PCE'\r\n", "\r\n", "model_workspace = Path.cwd() / f'{model_runs_folder}/{model_run_name}'" ], @@ -84,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "source": [ "# check if the directory exists\r\n", "if os.path.exists(model_workspace / 'inputs'):\r\n", @@ -128,15 +128,7 @@ " print('Generating inputs now...')\r\n", " generate_input_files.generate_inputs(model_workspace)" ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Input files already generated with current software version (0.1.0)\n" - ] - } - ], + "outputs": [], "metadata": {} }, { @@ -156,22 +148,13 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "source": [ "num_processors = mp.cpu_count()\r\n", "print(f'This machine has {num_processors} CPU cores')\r\n", "print(f'We recommend running no more than {num_processors-1} threads in parallel')" ], - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "This machine has 4 CPU cores\n", - "We recommend running no more than 3 threads in parallel\n" - ] - } - ], + "outputs": [], "metadata": {} }, { @@ -183,22 +166,11 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "source": [ "os.system(f'start cmd /k \"cd {model_runs_folder}\\{model_run_name} & activate switch_247 & switch solve-scenarios\"') " ], - "outputs": [ - { - "output_type": "execute_result", - "data": { - "text/plain": [ - "0" - ] - }, - "metadata": {}, - "execution_count": 7 - } - ], + "outputs": [], "metadata": {} } ] From bf6e38ee86888b43143e05913dd1eb94dfc88799 Mon Sep 17 00:00:00 2001 From: grgmiller Date: Wed, 11 Aug 2021 09:58:27 -0700 Subject: [PATCH 2/3] start fixes on reporting --- switch_model/generate_input_files.py | 2 ++ switch_model/generators/extensions/congestion_pricing.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/switch_model/generate_input_files.py b/switch_model/generate_input_files.py index 969cffd..2ff0c84 100644 --- a/switch_model/generate_input_files.py +++ b/switch_model/generate_input_files.py @@ -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) diff --git a/switch_model/generators/extensions/congestion_pricing.py b/switch_model/generators/extensions/congestion_pricing.py index 3e19582..7a1e3e1 100644 --- a/switch_model/generators/extensions/congestion_pricing.py +++ b/switch_model/generators/extensions/congestion_pricing.py @@ -29,7 +29,7 @@ def define_components(mod): # Calculate the cost we pay for load at the DLAP mod.DLAPLoadCostInTP = Expression( mod.TIMEPOINTS, - rule=lambda m, t: sum(m.zone_demand_mw[z,t] * m.system_power_cost[z,t] for z in m.LOAD_ZONES)) + 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 From 16739a6b391b0df8d1850f0ba4e4236a7360e6a4 Mon Sep 17 00:00:00 2001 From: grgmiller Date: Wed, 11 Aug 2021 13:46:31 -0700 Subject: [PATCH 3/3] fixes #6 #7 #12 cost updates --- CHANGELOG.md | 7 +- switch_model/reporting/summary_report.ipynb | 404 ++++++++++---------- 2 files changed, 203 insertions(+), 208 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dceda8..559da41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,13 @@ ------------------------------------------------------------------------------- -Commmit 2021.08.XX +Commmit 2021.08.11 ------------------------------------------------------------------------------- Fixes #4, #6, #7, #12 -^^ Fix commit DATE - 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) - - Update 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 + - 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 diff --git a/switch_model/reporting/summary_report.ipynb b/switch_model/reporting/summary_report.ipynb index 2da948d..afc741c 100644 --- a/switch_model/reporting/summary_report.ipynb +++ b/switch_model/reporting/summary_report.ipynb @@ -218,114 +218,116 @@ "cell_type": "code", "execution_count": null, "source": [ - "# Load generator Costs\r\n", - "######################\r\n", - "try:\r\n", - " generator_costs = pd.read_csv(data_dir / 'nodal_costs_by_gen.csv')\r\n", - " #calculate annual sums\r\n", - " generator_costs = generator_costs.groupby('generation_project').sum().round(decimals=2).reset_index()\r\n", - " #drop data with zero values\r\n", - "\r\n", - " generator_costs = generator_costs[generator_costs['DispatchGen_MW'] > 0]\r\n", - "\r\n", - " generator_costs = generator_costs.rename(columns={'DispatchGen_MW':'Generation MWh', 'Generator Pnode Revenue':'Pnode Revenue', 'Generator Delivery Cost':'Delivery Cost'})\r\n", - "\r\n", - "\r\n", - " generator_costs = generator_costs[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost']]\r\n", - "\r\n", - " # Load Storage Costs\r\n", - " ####################\r\n", - "\r\n", - " storage = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=True, usecols=['generation_project','load_zone','timestamp','ChargeMW','DispatchMW','StorageDispatchPnodeCost'])\r\n", - "\r\n", - " #merge DLAP cost data into the storage data\r\n", - " system_power_cost = pd.read_csv(Path.cwd() / 'system_power_cost.csv', usecols=['load_zone','system_power_cost'])\r\n", - " system_power_cost['timestamp'] = storage['timestamp'].iloc[:8760]\r\n", - " storage = storage.merge(system_power_cost, how='left', on=['load_zone','timestamp'])\r\n", - "\r\n", - " #calculate delivery cost\r\n", - " storage['Dispatch_Delivery_Cost'] = storage['DispatchMW'] * storage['system_power_cost']\r\n", - " storage['Hybrid_Charge_Delivery_Discount'] = - storage['ChargeMW'] * storage['system_power_cost']\r\n", - "\r\n", - " # Calculate Pnode Revenues\r\n", - " storage['Pnode Revenue'] = - storage['StorageDispatchPnodeCost']\r\n", - " storage.loc[storage['Pnode Revenue'] < 0,'Pnode Revenue'] = 0 \r\n", - " \r\n", - " # Pnode revenues for hybrid projects should be set to 0 since the revenue is already counted from the generator side\r\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Pnode Revenue'] = 0\r\n", - "\r\n", - " storage = storage.groupby('generation_project').sum().round(decimals=2).reset_index()\r\n", - " storage = storage[storage['DispatchMW'] > 0]\r\n", - "\r\n", - " # Adjust hybrid storage calculations\r\n", - " ####################################\r\n", - " # for hybrid projects, discount the dispatch delivery cost by the discount for charging\r\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] = storage.loc[storage['generation_project'].str.contains('HYBRID'),'Dispatch_Delivery_Cost'] + storage.loc[storage['generation_project'].str.contains('HYBRID'),'Hybrid_Charge_Delivery_Discount']\r\n", - "\r\n", - " #for hybrid projects, set the Generation MWh to 0, since we already counted this in the generation side of the project\r\n", - " storage.loc[storage['generation_project'].str.contains('HYBRID'),'DispatchMW'] = 0\r\n", - "\r\n", - " #merge in the contract costs\r\n", - " storage_contract = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','Annual_PPA_Capacity_Cost'])\r\n", - " storage = storage.merge(storage_contract, how='left', on='generation_project').fillna(0)\r\n", + "# Load Storage Costs\r\n", + "storage_costs = pd.read_csv(data_dir / 'storage_dispatch.csv', parse_dates=True, usecols=['generation_project','load_zone','timestamp','ChargeMW','DispatchMW','StorageDispatchPnodeCost'])\r\n", + "\r\n", + "# merge system power cost data\r\n", + "storage_costs = storage_costs.merge(system_power_cost, how='left', on=['timestamp','load_zone'])\r\n", + "\r\n", + "# split the hybrid generators into a separate df\r\n", + "hybrid_costs = storage_costs.copy()[storage_costs['generation_project'].str.contains('HYBRID')]\r\n", + "storage_costs = storage_costs[~storage_costs['generation_project'].str.contains('HYBRID')]\r\n", + "\r\n", + "\r\n", + "## STORAGE\r\n", + "# calculate delivery cost \r\n", + "storage_costs['Delivery Cost'] = storage_costs['DispatchMW'] * storage_costs['system_power_cost']\r\n", + "# calculate Pnode revenue from discharge\r\n", + "storage_costs['Pnode Revenue'] = - storage_costs['StorageDispatchPnodeCost']\r\n", + "storage_costs.loc[storage_costs['Pnode Revenue'] < 0,'Pnode Revenue'] = 0 \r\n", + "# calculate congestion cost\r\n", + "storage_costs['Congestion Cost'] = storage_costs['Delivery Cost'] - storage_costs['Pnode Revenue']\r\n", + "# calculate annual sums and drop data with zero values\r\n", + "storage_costs = storage_costs.groupby('generation_project').sum().reset_index()\r\n", + "storage_costs = storage_costs[storage_costs['DispatchMW'] > 0]\r\n", + "#merge in the contract costs\r\n", + "storage_contract = pd.read_csv(data_dir / 'gen_cap.csv', usecols=['generation_project','Annual_PPA_Capacity_Cost'])\r\n", + "storage_costs = storage_costs.merge(storage_contract, how='left', on='generation_project').fillna(0)\r\n", + "# clean up columns\r\n", + "storage_costs = storage_costs.rename(columns={'StorageDispatchPnodeCost':'Energy Arbitrage','Annual_PPA_Capacity_Cost':'Contract Cost','DispatchMW':'Generation MWh'})\r\n", + "storage_costs = storage_costs[['generation_project','Generation MWh','Contract Cost','Energy Arbitrage','Pnode Revenue', 'Delivery Cost', 'Congestion Cost']]\r\n", + "\r\n", + "## HYBRID\r\n", + "# calculate net delivery cost \r\n", + "hybrid_costs['Delivery Cost'] = (hybrid_costs['DispatchMW'] - hybrid_costs['ChargeMW']) * hybrid_costs['system_power_cost']\r\n", + "# calculate net Pnode revenue\r\n", + "hybrid_costs['Pnode Revenue'] = - hybrid_costs['StorageDispatchPnodeCost']\r\n", + "# calculate net congestion cost\r\n", + "hybrid_costs['Congestion Cost'] = hybrid_costs['Delivery Cost'] - hybrid_costs['Pnode Revenue']\r\n", + "# calculate annual sums and drop data with zero values\r\n", + "hybrid_costs = hybrid_costs.groupby('generation_project').sum().reset_index()\r\n", + "hybrid_costs = hybrid_costs[hybrid_costs['DispatchMW'] > 0]\r\n", + "#merge in the contract costs\r\n", + "hybrid_costs = hybrid_costs.merge(storage_contract, how='left', on='generation_project').fillna(0)\r\n", + "# calculate net generation\r\n", + "hybrid_costs['Generation MWh'] = hybrid_costs['DispatchMW'] - hybrid_costs['ChargeMW']\r\n", + "# clean up columns\r\n", + "hybrid_costs = hybrid_costs.rename(columns={'Annual_PPA_Capacity_Cost':'Contract Cost'})\r\n", + "hybrid_costs = hybrid_costs[['generation_project','Generation MWh','Contract Cost','Pnode Revenue', 'Delivery Cost', 'Congestion Cost']]\r\n", + "#rename storage hybrid to paired generator name\r\n", + "hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\r\n", + "hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\r\n", + "hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\r\n", + "hybrid_costs['generation_project'] = hybrid_costs['generation_project'].replace(hybrid_pair)\r\n", "\r\n", - " storage = storage.rename(columns={'StorageDispatchPnodeCost':'Energy Arbitrage','Annual_PPA_Capacity_Cost':'Contract Cost','Dispatch_Delivery_Cost':'Delivery Cost','DispatchMW':'Generation MWh'})\r\n", + "# load generator data\r\n", + "generator_costs = pd.read_csv(data_dir / 'nodal_costs_by_gen.csv')\r\n", + "# clean up the columns\r\n", + "generator_costs = generator_costs.rename(columns={'DispatchGen_MW':'Generation MWh', 'Generator Pnode Revenue':'Pnode Revenue'})\r\n", "\r\n", - " #only keep columns to merge\r\n", - " storage = storage[['generation_project','Generation MWh','Contract Cost','Pnode Revenue','Delivery Cost','Energy Arbitrage']]\r\n", + "# load generator load zone data and merge into generator costs data\r\n", + "generator_load_zones = pd.read_csv(inputs_dir / 'generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_load_zone']).rename(columns={'GENERATION_PROJECT':'generation_project', 'gen_load_zone':'load_zone'})\r\n", + "generator_costs = generator_costs.merge(generator_load_zones, how='left', on='generation_project')\r\n", "\r\n", - " #rename the hybrid projects\r\n", - " #rename storage hybrid to paired generator name\r\n", - " hybrid_pair = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','storage_hybrid_generation_project'])\r\n", - " hybrid_pair = hybrid_pair[hybrid_pair['storage_hybrid_generation_project'] != \".\"]\r\n", - " hybrid_pair = dict(zip(hybrid_pair.GENERATION_PROJECT, hybrid_pair.storage_hybrid_generation_project))\r\n", - " storage['generation_project'] = storage['generation_project'].replace(hybrid_pair)\r\n", + "# load system power cost and merge into generator cost data\r\n", + "system_power_cost = pd.read_csv(inputs_dir / 'system_power_cost.csv', usecols=['load_zone','system_power_cost'])\r\n", + "system_power_cost['timestamp'] = generator_costs['timestamp'].iloc[:8760]\r\n", + "generator_costs = generator_costs.merge(system_power_cost, how='left', on=['timestamp','load_zone'])\r\n", "\r\n", - " # Merge Data Together\r\n", - " #####################\r\n", + "# calculate delivery costs and congestion cost\r\n", + "generator_costs['Delivery Cost'] = generator_costs['Generation MWh'] * generator_costs['system_power_cost']\r\n", + "generator_costs['Congestion Cost'] = generator_costs['Delivery Cost'] - generator_costs['Pnode Revenue']\r\n", "\r\n", - " generator_costs = generator_costs.append(storage).fillna(0)\r\n", - " generator_costs = generator_costs.groupby('generation_project').sum().reset_index()\r\n", + "# clean up columns\r\n", + "generator_costs = generator_costs[['generation_project','Generation MWh','Contract Cost','Pnode Revenue', 'Delivery Cost', 'Congestion Cost']]\r\n", "\r\n", - " #calculate congestion cost\r\n", - " generator_costs['Congestion Cost'] = generator_costs['Delivery Cost'] - generator_costs['Pnode Revenue']\r\n", + "# sum the costs for the entire year\r\n", + "# calculate annual sums and drop data with zero values\r\n", + "generator_costs = generator_costs.groupby('generation_project').sum().reset_index()\r\n", + "generator_costs = generator_costs[generator_costs['Generation MWh'] > 0]\r\n", "\r\n", - " # calculate per MWh costs\r\n", - " generator_costs['Contract Cost per MWh'] = generator_costs['Contract Cost'] / generator_costs['Generation MWh']\r\n", - " generator_costs['Congestion Cost per MWh'] = generator_costs['Congestion Cost'] / generator_costs['Generation MWh']\r\n", - " generator_costs['Energy Arbitrage per MWh'] = generator_costs['Energy Arbitrage'] / generator_costs['Generation MWh']\r\n", - " generator_costs['Total Cost per MWh'] = generator_costs['Contract Cost per MWh'] + generator_costs['Congestion Cost per MWh'] + generator_costs['Energy Arbitrage per MWh']\r\n", + "# append the hybrid and storage data and group the hybrid components together\r\n", + "generator_costs = generator_costs.append(hybrid_costs).fillna(0)\r\n", + "generator_costs = generator_costs.append(storage_costs).fillna(0)\r\n", + "generator_costs = generator_costs.groupby('generation_project').sum().reset_index()\r\n", "\r\n", - " generator_costs['Technology'] = [i.split('_')[0] for i in generator_costs['generation_project']]\r\n", + "# calculate per MWh costs\r\n", + "generator_costs['Contract Cost per MWh'] = generator_costs['Contract Cost'] / generator_costs['Generation MWh']\r\n", + "generator_costs['Congestion Cost per MWh'] = generator_costs['Congestion Cost'] / generator_costs['Generation MWh']\r\n", + "generator_costs['Energy Arbitrage per MWh'] = generator_costs['Energy Arbitrage'] / generator_costs['Generation MWh']\r\n", + "generator_costs['Total Cost per MWh'] = generator_costs['Contract Cost per MWh'] + generator_costs['Congestion Cost per MWh'] + generator_costs['Energy Arbitrage per MWh']\r\n", "\r\n", - " generator_costs = generator_costs.sort_values(by='Total Cost per MWh', ascending=True)\r\n", + "generator_costs['Technology'] = [i.split('_')[0] for i in generator_costs['generation_project']]\r\n", "\r\n", - " generator_costs = generator_costs.round(decimals=2)\r\n", + "generator_costs = generator_costs.sort_values(by='Total Cost per MWh', ascending=True)\r\n", "\r\n", - " # Prepare Data for graph\r\n", - " ########################\r\n", - " generator_costs_melted = generator_costs[['generation_project', 'Contract Cost per MWh','Congestion Cost per MWh','Energy Arbitrage per MWh']].rename(columns={'Contract Cost per MWh':'Contract Cost','Congestion Cost per MWh':'Congestion Cost','Energy Arbitrage per MWh':'Energy Arbitrage'}).melt(id_vars='generation_project', var_name='Cost', value_name='$/MWh')\r\n", - " generator_costs_melted['$/MWh'] = generator_costs_melted['$/MWh']\r\n", + "generator_costs = generator_costs.round(decimals=2)\r\n", "\r\n", - " generator_cost_fig = px.bar(generator_costs_melted, title='Average Generator Cost per MWh Generated', x='generation_project', y='$/MWh', color='Cost', color_discrete_map={'Contract Cost': 'Green','Congestion Cost': 'Orange', 'Energy Arbitrage':'Blue'})\r\n", + "# Prepare Data for graph\r\n", + "########################\r\n", + "generator_costs_melted = generator_costs[['generation_project', 'Contract Cost per MWh','Congestion Cost per MWh','Energy Arbitrage per MWh']] \\\r\n", + " .rename(columns={'Contract Cost per MWh':'Contract Cost','Congestion Cost per MWh':'Congestion Cost','Energy Arbitrage per MWh':'Energy Arbitrage'}) \\\r\n", + " .melt(id_vars='generation_project', var_name='Cost', value_name='$/MWh')\r\n", "\r\n", + "generator_cost_fig = px.bar(generator_costs_melted, title='Average Generator Cost per MWh Generated', x='generation_project', y='$/MWh', color='Cost', color_discrete_map={'Contract Cost': 'Green','Congestion Cost': 'Orange', 'Energy Arbitrage':'Blue'})\r\n", "\r\n", - " generator_cost_fig.add_scatter(x=generator_costs.generation_project, y=generator_costs['Total Cost per MWh'], mode='markers+text', text=generator_costs['Total Cost per MWh'], textposition='top center', line=dict(color='black', width=1), name='Total Cost')\r\n", "\r\n", - " generator_cost_fig.show()\r\n", + "generator_cost_fig.add_scatter(x=generator_costs.generation_project, y=generator_costs['Total Cost per MWh'], mode='markers+text', text=generator_costs['Total Cost per MWh'], textposition='top center', line=dict(color='black', width=1), name='Total Cost')\r\n", "\r\n", - "except FileNotFoundError:\r\n", - " pass" + "generator_cost_fig.show()\r\n" ], "outputs": [], - "metadata": { - "execution": { - "iopub.execute_input": "2021-08-10T22:22:52.317690Z", - "iopub.status.busy": "2021-08-10T22:22:52.313688Z", - "iopub.status.idle": "2021-08-10T22:22:52.908392Z", - "shell.execute_reply": "2021-08-10T22:22:52.908392Z" - } - } + "metadata": {} }, { "cell_type": "markdown", @@ -407,12 +409,6 @@ "\r\n", "# Energy Contract Cost\r\n", "energy_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'GenPPACostInTP']['AnnualCost_Real'].to_numpy()[0]\r\n", - "# check for discount from hybrid generation\r\n", - "try:\r\n", - " hybrid_storage_energy_discount = costs_itemized_df[costs_itemized_df['Component'] == 'HybridStoragePPAEnergyCostInTP']['AnnualCost_Real'].to_numpy()[0]\r\n", - " energy_contract_cost = energy_contract_cost + hybrid_storage_energy_discount\r\n", - "except IndexError:\r\n", - " pass\r\n", "\r\n", "# Capacity Contract Cost\r\n", "capacity_contract_cost = costs_itemized_df[costs_itemized_df['Component'] == 'TotalGenCapacityCost']['AnnualCost_Real'].to_numpy()[0]\r\n", @@ -465,7 +461,7 @@ "\r\n", " display(cost_summary)\r\n", "except NameError:\r\n", - " pass" + " pass\r\n" ], "outputs": [], "metadata": { @@ -773,7 +769,7 @@ "# merge the timestamp data\r\n", "nodal_prices = nodal_prices.merge(timestamps, how='left', left_on='timepoint', right_on='timepoint_id')\r\n", "\r\n", - "nodal_fig = px.line(nodal_prices, x='timestamp', y='nodal_price', color='pricing_node', labels={'nodal_price':'$/MWh','timestamp':'Datetime','pricing_node':'Node'}, template='plotly_white')\r\n", + "nodal_fig = px.line(nodal_prices, x='timestamp', y='nodal_price', color='pricing_node', labels={'nodal_price':'$/MWh','timestamp':'Datetime','pricing_node':'Node'}, title='Nodal Prices', template='plotly_white')\r\n", "nodal_fig.update_xaxes(\r\n", " rangeslider_visible=True,\r\n", " rangeselector=dict(\r\n", @@ -957,42 +953,42 @@ "cell_type": "code", "execution_count": null, "source": [ - "# get month-hour shape of open energy position\n", - "##############################################\n", - "\n", - "dispatch_gen = pd.read_csv(data_dir / 'dispatch.csv', usecols=['timestamp','DispatchGen_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\n", - "\n", - "#sum by timestamp\n", - "dispatch_gen = dispatch_gen.groupby('timestamp').sum().reset_index()\n", - "\n", - "#sum to get total generation\n", - "dispatch_gen['Generation_MW'] = dispatch_gen['DispatchGen_MW']\n", - "\n", - "#merge generation_load_mismatch data\n", - "generation_load_mismatch = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], parse_dates=['timestamp'], infer_datetime_format=True)\n", - "\n", - "generation_load_mismatch = generation_load_mismatch.merge(dispatch_gen[['timestamp','Generation_MW']], how='left', on='timestamp')\n", - "\n", - "#subtract generation from generation_load_mismatch to get open position and replace negative values with zero\n", - "generation_load_mismatch['Overgeneration_MW'] = generation_load_mismatch['Generation_MW'] - generation_load_mismatch['zone_demand_mw']\n", - "#generation_load_mismatch.loc[generation_load_mismatch['generation_load_mismatch_MW'] < 0, 'generation_load_mismatch_MW'] = 0\n", - "\n", - "#generation_load_mismatch['timestamp'] = pd.to_datetime(generation_load_mismatch['timestamp'])\n", - "\n", - "generation_load_mismatch = generation_load_mismatch.set_index('timestamp')\n", - "\n", - "generation_load_mismatch_mh = generation_load_mismatch.groupby([generation_load_mismatch.index.month, generation_load_mismatch.index.hour], axis=0).mean()\n", - "generation_load_mismatch_mh.index = generation_load_mismatch_mh.index.rename(['Month','Hour'])\n", - "generation_load_mismatch_mh = generation_load_mismatch_mh.reset_index()\n", - "\n", - "mh_mismatch_fig = px.area(generation_load_mismatch_mh, x='Hour', y='Overgeneration_MW', facet_col='Month', facet_col_wrap=6, width=1000, height=600, title='Shape of over- and under-generation, excluding energy storage')\n", - "mh_mismatch_fig.update_xaxes(dtick=3)\n", - "month_names = ['July', 'August', 'September', 'October', 'November', 'December','January', 'February', 'March', 'April', 'May', 'June']\n", - "for i, a in enumerate(mh_mismatch_fig.layout.annotations):\n", - " a.text = month_names[i]\n", - "\n", - "mh_mismatch_fig.show()\n", - "\n" + "# get month-hour shape of open energy position\r\n", + "##############################################\r\n", + "\r\n", + "dispatch_gen = pd.read_csv(data_dir / 'dispatch.csv', usecols=['timestamp','DispatchGen_MW'], parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + "\r\n", + "#sum by timestamp\r\n", + "dispatch_gen = dispatch_gen.groupby('timestamp').sum().reset_index()\r\n", + "\r\n", + "#sum to get total generation\r\n", + "dispatch_gen['Generation_MW'] = dispatch_gen['DispatchGen_MW']\r\n", + "\r\n", + "#merge generation_load_mismatch data\r\n", + "generation_load_mismatch = pd.read_csv(data_dir / 'load_balance.csv', usecols=['timestamp','zone_demand_mw'], parse_dates=['timestamp'], infer_datetime_format=True)\r\n", + "\r\n", + "generation_load_mismatch = generation_load_mismatch.merge(dispatch_gen[['timestamp','Generation_MW']], how='left', on='timestamp')\r\n", + "\r\n", + "#subtract generation from generation_load_mismatch to get open position and replace negative values with zero\r\n", + "generation_load_mismatch['Overgeneration_MW'] = generation_load_mismatch['Generation_MW'] - generation_load_mismatch['zone_demand_mw']\r\n", + "#generation_load_mismatch.loc[generation_load_mismatch['generation_load_mismatch_MW'] < 0, 'generation_load_mismatch_MW'] = 0\r\n", + "\r\n", + "#generation_load_mismatch['timestamp'] = pd.to_datetime(generation_load_mismatch['timestamp'])\r\n", + "\r\n", + "generation_load_mismatch = generation_load_mismatch.set_index('timestamp')\r\n", + "\r\n", + "generation_load_mismatch_mh = generation_load_mismatch.groupby([generation_load_mismatch.index.month, generation_load_mismatch.index.hour], axis=0).mean()\r\n", + "generation_load_mismatch_mh.index = generation_load_mismatch_mh.index.rename(['Month','Hour'])\r\n", + "generation_load_mismatch_mh = generation_load_mismatch_mh.reset_index()\r\n", + "\r\n", + "mh_mismatch_fig = px.area(generation_load_mismatch_mh, x='Hour', y='Overgeneration_MW', facet_col='Month', facet_col_wrap=6, width=1000, height=600, title='Shape of over- and under-generation, excluding energy storage')\r\n", + "mh_mismatch_fig.update_xaxes(dtick=3)\r\n", + "month_names = ['July', 'August', 'September', 'October', 'November', 'December','January', 'February', 'March', 'April', 'May', 'June']\r\n", + "for i, a in enumerate(mh_mismatch_fig.layout.annotations):\r\n", + " a.text = month_names[i]\r\n", + "\r\n", + "mh_mismatch_fig.show()\r\n", + "\r\n" ], "outputs": [], "metadata": { @@ -1015,17 +1011,17 @@ "cell_type": "code", "execution_count": null, "source": [ - "try:\n", - " cycles = pd.read_csv(data_dir / 'storage_cycle_count.csv', usecols=['generation_project','storage_max_annual_cycles','Battery_Cycle_Count'])\n", - " cycles = cycles.round(decimals=2)\n", - " cycles = cycles[cycles['Battery_Cycle_Count'] > 0]\n", - " storage_energy_capacity = pd.read_csv(data_dir / 'storage_builds.csv', usecols=['generation_project','OnlineEnergyCapacityMWh'])\n", - " cycles = cycles.merge(storage_energy_capacity, how='left', on='generation_project')\n", - " cycles['Battery_Cycle_Count'] = cycles['Battery_Cycle_Count'] / cycles['OnlineEnergyCapacityMWh']\n", - " cycles = cycles.drop(columns=['OnlineEnergyCapacityMWh']).set_index('generation_project').round(decimals=0)\n", - " cycles['Battery_Cycle_Count'] = cycles['Battery_Cycle_Count'].astype(int)\n", - "except FileNotFoundError:\n", - " cycles = \"No batteries in model\"\n", + "try:\r\n", + " cycles = pd.read_csv(data_dir / 'storage_cycle_count.csv', usecols=['generation_project','storage_max_annual_cycles','Battery_Cycle_Count'])\r\n", + " cycles = cycles.round(decimals=2)\r\n", + " cycles = cycles[cycles['Battery_Cycle_Count'] > 0]\r\n", + " storage_energy_capacity = pd.read_csv(data_dir / 'storage_builds.csv', usecols=['generation_project','OnlineEnergyCapacityMWh'])\r\n", + " cycles = cycles.merge(storage_energy_capacity, how='left', on='generation_project')\r\n", + " cycles['Battery_Cycle_Count'] = cycles['Battery_Cycle_Count'] / cycles['OnlineEnergyCapacityMWh']\r\n", + " cycles = cycles.drop(columns=['OnlineEnergyCapacityMWh']).set_index('generation_project').round(decimals=0)\r\n", + " cycles['Battery_Cycle_Count'] = cycles['Battery_Cycle_Count'].astype(int)\r\n", + "except FileNotFoundError:\r\n", + " cycles = \"No batteries in model\"\r\n", "cycles" ], "outputs": [], @@ -1049,11 +1045,11 @@ "cell_type": "code", "execution_count": null, "source": [ - "pd.set_option('display.max_rows',100)\n", - "gen_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_energy_cost','gen_capacity_limit_mw'])\n", - "gen_assumptions = gen_assumptions[gen_assumptions['gen_energy_source'] != 'Electricity']\n", - "gen_assumptions = gen_assumptions.sort_values(by='GENERATION_PROJECT')\n", - "gen_assumptions = gen_assumptions.set_index('GENERATION_PROJECT')\n", + "pd.set_option('display.max_rows',100)\r\n", + "gen_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_energy_cost','gen_capacity_limit_mw'])\r\n", + "gen_assumptions = gen_assumptions[gen_assumptions['gen_energy_source'] != 'Electricity']\r\n", + "gen_assumptions = gen_assumptions.sort_values(by='GENERATION_PROJECT')\r\n", + "gen_assumptions = gen_assumptions.set_index('GENERATION_PROJECT')\r\n", "gen_assumptions" ], "outputs": [], @@ -1078,23 +1074,23 @@ "cell_type": "code", "execution_count": null, "source": [ - "storage_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'storage_roundtrip_efficiency','storage_charge_to_discharge_ratio','storage_energy_to_power_ratio','storage_leakage_loss','storage_hybrid_generation_project','storage_hybrid_capacity_ratio'])\n", - "#change capacity cost to $/kw-mo\n", - "storage_assumptions['ppa_capacity_cost'] = storage_assumptions['ppa_capacity_cost'] / 12000\n", - "\n", - "storage_assumptions = storage_assumptions.rename(columns={\n", - " 'storage_roundtrip_efficiency':'RTE',\n", - " 'storage_charge_to_discharge_ratio':'charge/discharge_ratio',\n", - " 'storage_energy_to_power_ratio':'storage_hours',\n", - " 'storage_leakage_loss':'soc_leakage_loss',\n", - " 'storage_hybrid_generation_project':'paired_hybrid_gen',\n", - " 'storage_hybrid_capacity_ratio':'hybrid_capacity_ratio'\n", - "})\n", - "\n", - "storage_assumptions = storage_assumptions[storage_assumptions['gen_energy_source'] == 'Electricity']\n", - "storage_assumptions = storage_assumptions.sort_values(by='GENERATION_PROJECT')\n", - "storage_assumptions = storage_assumptions.set_index('GENERATION_PROJECT')\n", - "storage_assumptions = storage_assumptions[['gen_tech','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'RTE','storage_hours','charge/discharge_ratio','soc_leakage_loss','paired_hybrid_gen','hybrid_capacity_ratio']]\n", + "storage_assumptions = pd.read_csv('generation_projects_info.csv', usecols=['GENERATION_PROJECT','gen_tech','gen_energy_source','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'storage_roundtrip_efficiency','storage_charge_to_discharge_ratio','storage_energy_to_power_ratio','storage_leakage_loss','storage_hybrid_generation_project','storage_hybrid_capacity_ratio'])\r\n", + "#change capacity cost to $/kw-mo\r\n", + "storage_assumptions['ppa_capacity_cost'] = storage_assumptions['ppa_capacity_cost'] / 12000\r\n", + "\r\n", + "storage_assumptions = storage_assumptions.rename(columns={\r\n", + " 'storage_roundtrip_efficiency':'RTE',\r\n", + " 'storage_charge_to_discharge_ratio':'charge/discharge_ratio',\r\n", + " 'storage_energy_to_power_ratio':'storage_hours',\r\n", + " 'storage_leakage_loss':'soc_leakage_loss',\r\n", + " 'storage_hybrid_generation_project':'paired_hybrid_gen',\r\n", + " 'storage_hybrid_capacity_ratio':'hybrid_capacity_ratio'\r\n", + "})\r\n", + "\r\n", + "storage_assumptions = storage_assumptions[storage_assumptions['gen_energy_source'] == 'Electricity']\r\n", + "storage_assumptions = storage_assumptions.sort_values(by='GENERATION_PROJECT')\r\n", + "storage_assumptions = storage_assumptions.set_index('GENERATION_PROJECT')\r\n", + "storage_assumptions = storage_assumptions[['gen_tech','gen_reliability_area','ppa_capacity_cost','gen_capacity_limit_mw', 'RTE','storage_hours','charge/discharge_ratio','soc_leakage_loss','paired_hybrid_gen','hybrid_capacity_ratio']]\r\n", "storage_assumptions" ], "outputs": [], @@ -1111,43 +1107,43 @@ "cell_type": "code", "execution_count": null, "source": [ - "summary = pd.DataFrame(columns=['Scenario Name'], data=['test'])\n", - "\n", - "summary['Scenario Name'] = scenario_name\n", - "\n", - "#Goal Data\n", - "summary['Time-coincident Delivered %'] = tc_percent_renewable\n", - "summary['Time-coincident Generation %'] = tc_no_storage_percent_renewable\n", - "summary['Annual Volumetric Renewable %'] = annual_percent_renewable\n", - "\n", - "unformatted_cost = unformatted_cost.rename(columns={'Annual Real Cost': ' (Annual)',\t'Delivered Cost per MWh': ' (per MWh)'}).melt(id_vars=['Cost Component'], var_name='type', value_name=0)\n", - "unformatted_cost['col_name'] = unformatted_cost['Cost Component'] + unformatted_cost['type']\n", - "\n", - "\n", - "summary = pd.concat([summary, unformatted_cost[['col_name',0]].set_index('col_name').T], axis=1)\n", - "\n", - "#Portfolio Mix\n", - "portfolio_summary = portfolio[['MW','Status','Technology']].groupby(['Status','Technology']).sum().reset_index()\n", - "portfolio_summary['Description'] = portfolio_summary['Status'] + \" \" + portfolio_summary['Technology']\n", - "portfolio_summary = portfolio_summary.drop(columns=['Status','Technology'])\n", - "portfolio_summary = portfolio_summary.set_index('Description').transpose().reset_index(drop=True).add_prefix('MW Capacity from ')\n", - "\n", - "summary = pd.concat([summary, portfolio_summary], axis=1)\n", - "\n", - "#Load\n", - "summary['Customer Load GWh'] = load['zone_demand_mw'].sum(axis=0) / 1000\n", - "try:\n", - " summary['Total Load with Storage GWh'] = (load['zone_demand_mw'].sum(axis=0) + storage_charge['ChargeMW'].sum(axis=0)) / 1000\n", - "except (KeyError, NameError):\n", - " summary['Total Load with Storage GWh'] = summary['Customer Load GWh']\n", - "\n", - "#Generation Mix\n", - "generation_summary = generation_mix.set_index('Source').transpose().reset_index(drop=True).add_prefix('GWh Generation from ')\n", - "summary = pd.concat([summary, generation_summary], axis=1)\n", - "\n", - "summary = summary.transpose()\n", - "summary.columns = [f'{scenario_name}']\n", - "\n", + "summary = pd.DataFrame(columns=['Scenario Name'], data=['test'])\r\n", + "\r\n", + "summary['Scenario Name'] = scenario_name\r\n", + "\r\n", + "#Goal Data\r\n", + "summary['Time-coincident Delivered %'] = tc_percent_renewable\r\n", + "summary['Time-coincident Generation %'] = tc_no_storage_percent_renewable\r\n", + "summary['Annual Volumetric Renewable %'] = annual_percent_renewable\r\n", + "\r\n", + "unformatted_cost = unformatted_cost.rename(columns={'Annual Real Cost': ' (Annual)',\t'Delivered Cost per MWh': ' (per MWh)'}).melt(id_vars=['Cost Component'], var_name='type', value_name=0)\r\n", + "unformatted_cost['col_name'] = unformatted_cost['Cost Component'] + unformatted_cost['type']\r\n", + "\r\n", + "\r\n", + "summary = pd.concat([summary, unformatted_cost[['col_name',0]].set_index('col_name').T], axis=1)\r\n", + "\r\n", + "#Portfolio Mix\r\n", + "portfolio_summary = portfolio[['MW','Status','Technology']].groupby(['Status','Technology']).sum().reset_index()\r\n", + "portfolio_summary['Description'] = portfolio_summary['Status'] + \" \" + portfolio_summary['Technology']\r\n", + "portfolio_summary = portfolio_summary.drop(columns=['Status','Technology'])\r\n", + "portfolio_summary = portfolio_summary.set_index('Description').transpose().reset_index(drop=True).add_prefix('MW Capacity from ')\r\n", + "\r\n", + "summary = pd.concat([summary, portfolio_summary], axis=1)\r\n", + "\r\n", + "#Load\r\n", + "summary['Customer Load GWh'] = load['zone_demand_mw'].sum(axis=0) / 1000\r\n", + "try:\r\n", + " summary['Total Load with Storage GWh'] = (load['zone_demand_mw'].sum(axis=0) + storage_charge['ChargeMW'].sum(axis=0)) / 1000\r\n", + "except (KeyError, NameError):\r\n", + " summary['Total Load with Storage GWh'] = summary['Customer Load GWh']\r\n", + "\r\n", + "#Generation Mix\r\n", + "generation_summary = generation_mix.set_index('Source').transpose().reset_index(drop=True).add_prefix('GWh Generation from ')\r\n", + "summary = pd.concat([summary, generation_summary], axis=1)\r\n", + "\r\n", + "summary = summary.transpose()\r\n", + "summary.columns = [f'{scenario_name}']\r\n", + "\r\n", "summary.to_csv(data_dir / 'scenario_summary.csv')" ], "outputs": [],