From 9aa30bb6fe18ab30e73dc73620dedf8e00fd6c13 Mon Sep 17 00:00:00 2001 From: "josiah.johnston" Date: Thu, 25 Jul 2019 14:07:03 -0700 Subject: [PATCH] Generalized implementation of simple pumped hydro with an example/test case. This implementation requires the sequence of simple_hydro, storage in modules.txt. My goals were modeling accuracy, backwards compatibility, avoiding redundancy, checks to aid debugging of common errors, and allow pumped hydro to participate in reserves like any other generator. Pumped hydro is different from normal hydro because of need to the track energy balance, and that average dispatch is not solely determined by average stream flow (but also includes storage decisions). It's also different from normal storage because energy tracking needs to include both stream inflow and any spilled water. In this implementation, if any pumped hydro generators are specified, the simple_hydro module will validate that the storage module is also included. The pre-existing rules for maintaining average stream flow are restricted to non-pumped hydro. Special rules for pumped hydro energy tracking and streamflow are implemented with an extra few lines in the storage energy tracking constraint, which capture the mathematical requirements of average daily balancing and additionally tracks state-of-charge. If the pumped hydro terms are not available, the storage module will skip that stanza without complaint. Storage documentation updates: Describe how StateOfCharge is unbound for one timepoint in each timeseries (that is, the optimization can choose to start a day with an arbitrary storage level), because I found myself having to re-read through equations to convince myself of that behavior. Explicit describe units of various energy-based components in the storage model (to reduce confusion between power & energy). Rewrap line endings to conform to PEP-8 recommendations for reading two files side-by-side in a single screen (or a single file in large font on a small screen) --- examples/pumped_hydro_simple/README.md | 7 + .../pumped_hydro_simple/inputs/financials.csv | 2 + .../pumped_hydro_simple/inputs/fuel_cost.csv | 2 + examples/pumped_hydro_simple/inputs/fuels.csv | 2 + .../inputs/gen_build_costs.csv | 10 ++ .../inputs/gen_build_predetermined.csv | 7 + .../inputs/generation_projects_info.csv | 7 + .../inputs/hydro_timeseries.csv | 5 + .../pumped_hydro_simple/inputs/load_zones.csv | 2 + examples/pumped_hydro_simple/inputs/loads.csv | 5 + .../pumped_hydro_simple/inputs/modules.txt | 14 ++ .../inputs/non_fuel_energy_sources.csv | 4 + .../pumped_hydro_simple/inputs/periods.csv | 2 + .../inputs/switch_inputs_version.txt | 1 + .../pumped_hydro_simple/inputs/timepoints.csv | 5 + .../pumped_hydro_simple/inputs/timeseries.csv | 3 + .../inputs/variable_capacity_factors.csv | 9 ++ .../outputs/total_cost.txt | 1 + .../generators/extensions/hydro_simple.py | 74 ++++++++- switch_model/generators/extensions/storage.py | 140 +++++++++++++----- 20 files changed, 258 insertions(+), 44 deletions(-) create mode 100644 examples/pumped_hydro_simple/README.md create mode 100644 examples/pumped_hydro_simple/inputs/financials.csv create mode 100644 examples/pumped_hydro_simple/inputs/fuel_cost.csv create mode 100644 examples/pumped_hydro_simple/inputs/fuels.csv create mode 100644 examples/pumped_hydro_simple/inputs/gen_build_costs.csv create mode 100644 examples/pumped_hydro_simple/inputs/gen_build_predetermined.csv create mode 100644 examples/pumped_hydro_simple/inputs/generation_projects_info.csv create mode 100644 examples/pumped_hydro_simple/inputs/hydro_timeseries.csv create mode 100644 examples/pumped_hydro_simple/inputs/load_zones.csv create mode 100644 examples/pumped_hydro_simple/inputs/loads.csv create mode 100644 examples/pumped_hydro_simple/inputs/modules.txt create mode 100644 examples/pumped_hydro_simple/inputs/non_fuel_energy_sources.csv create mode 100644 examples/pumped_hydro_simple/inputs/periods.csv create mode 100644 examples/pumped_hydro_simple/inputs/switch_inputs_version.txt create mode 100644 examples/pumped_hydro_simple/inputs/timepoints.csv create mode 100644 examples/pumped_hydro_simple/inputs/timeseries.csv create mode 100644 examples/pumped_hydro_simple/inputs/variable_capacity_factors.csv create mode 100644 examples/pumped_hydro_simple/outputs/total_cost.txt diff --git a/examples/pumped_hydro_simple/README.md b/examples/pumped_hydro_simple/README.md new file mode 100644 index 000000000..318cc24a0 --- /dev/null +++ b/examples/pumped_hydro_simple/README.md @@ -0,0 +1,7 @@ +SYNOPSIS: + + switch solve --verbose --log-run + +This example illustrates modeling pumped hydro storage by using the hydro_simple module in concert with the storage module. + +This adds pumped hydro to the hydro_simple example, and illustrates pumped hydro being used for arbitrage within each example day. diff --git a/examples/pumped_hydro_simple/inputs/financials.csv b/examples/pumped_hydro_simple/inputs/financials.csv new file mode 100644 index 000000000..c162f5372 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/financials.csv @@ -0,0 +1,2 @@ +base_financial_year,interest_rate,discount_rate +2015,0.07,0.05 diff --git a/examples/pumped_hydro_simple/inputs/fuel_cost.csv b/examples/pumped_hydro_simple/inputs/fuel_cost.csv new file mode 100644 index 000000000..7783a8744 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/fuel_cost.csv @@ -0,0 +1,2 @@ +load_zone,fuel,period,fuel_cost +South,NaturalGas,2020,4 diff --git a/examples/pumped_hydro_simple/inputs/fuels.csv b/examples/pumped_hydro_simple/inputs/fuels.csv new file mode 100644 index 000000000..54dfca062 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/fuels.csv @@ -0,0 +1,2 @@ +fuel,co2_intensity,upstream_co2_intensity +NaturalGas,0.05306,0 diff --git a/examples/pumped_hydro_simple/inputs/gen_build_costs.csv b/examples/pumped_hydro_simple/inputs/gen_build_costs.csv new file mode 100644 index 000000000..52c360d46 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/gen_build_costs.csv @@ -0,0 +1,10 @@ +GENERATION_PROJECT,build_year,gen_overnight_cost,gen_fixed_om,gen_storage_energy_overnight_cost +S-NG_CC,2000.0,1143900.0,5868.3,. +S-Central_PV-1,2000.0,2334300.0,41850.0,. +S-Geothermal,1998.0,5524200.0,0.0,. +Hydro,2000.0,10000000.0,100000.0,. +Hydro_RoR,2000.0,1000000.0,100000.0,. +Hydro_Pumped,2000.0,1000000.0,100000.0,0.0 +S-Geothermal,2020.0,5524200.0,0.0,. +S-NG_CC,2020.0,1143900.0,5868.3,. +S-Central_PV-1,2020.0,2334300.0,41850.0,. diff --git a/examples/pumped_hydro_simple/inputs/gen_build_predetermined.csv b/examples/pumped_hydro_simple/inputs/gen_build_predetermined.csv new file mode 100644 index 000000000..3dfba0315 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/gen_build_predetermined.csv @@ -0,0 +1,7 @@ +GENERATION_PROJECT,build_year,gen_predetermined_cap +S-NG_CC,2000,5.0 +S-Central_PV-1,2000,1.0 +S-Geothermal,1998,1.0 +Hydro,2000,1.0 +Hydro_RoR,2000,1.0 +Hydro_Pumped,2000,5.0 diff --git a/examples/pumped_hydro_simple/inputs/generation_projects_info.csv b/examples/pumped_hydro_simple/inputs/generation_projects_info.csv new file mode 100644 index 000000000..08f59f315 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/generation_projects_info.csv @@ -0,0 +1,7 @@ +GENERATION_PROJECT,gen_dbid,gen_tech,gen_load_zone,gen_connect_cost_per_mw,gen_capacity_limit_mw,gen_variable_om,gen_max_age,gen_min_build_capacity,gen_scheduled_outage_rate,gen_forced_outage_rate,gen_is_variable,gen_is_baseload,gen_is_cogen,gen_energy_source,gen_full_load_heat_rate,gen_is_pumped_hydro,gen_storage_efficiency +S-Geothermal,33.0,Geothermal,South,134222.0,10.0,28.83,30,0,0.0075,0.0241,0,1,0,Geothermal,.,.,. +S-NG_CC,34.0,NG_CC,South,57566.6,.,3.4131,20,0,0.04,0.06,0,0,0,NaturalGas,6.705,.,. +S-Central_PV-1,41.0,Central_PV,South,74881.9,2.0,0.0,20,0,0.0,0.02,1,0,0,Solar,.,.,. +Hydro,.,Hydro,South,0.0,1.0,0.1,100,0,0.019,0.05,0,0,0,Water,.,.,. +Hydro_RoR,.,Hydro_RoR,South,0.0,1.0,0.0,30,0,0.019,0.05,1,0,0,Water,.,.,. +Hydro_Pumped,.,Hydro_Pumped,South,0.0,5.0,0.1,100,0,0.019,0.05,0,0,0,Water,.,1.0,0.75 diff --git a/examples/pumped_hydro_simple/inputs/hydro_timeseries.csv b/examples/pumped_hydro_simple/inputs/hydro_timeseries.csv new file mode 100644 index 000000000..235674f49 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/hydro_timeseries.csv @@ -0,0 +1,5 @@ +hydro_project,timeseries,hydro_min_flow_mw,hydro_avg_flow_mw +Hydro,2020_winter,0.6,0.75 +Hydro,2020_summer,0.2,0.6 +Hydro_Pumped,2020_winter,0.6,0.75 +Hydro_Pumped,2020_summer,0.2,0.6 diff --git a/examples/pumped_hydro_simple/inputs/load_zones.csv b/examples/pumped_hydro_simple/inputs/load_zones.csv new file mode 100644 index 000000000..9810665f2 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/load_zones.csv @@ -0,0 +1,2 @@ +LOAD_ZONE,cost_multipliers,ccs_distance_km,dbid +South,1,0,3 diff --git a/examples/pumped_hydro_simple/inputs/loads.csv b/examples/pumped_hydro_simple/inputs/loads.csv new file mode 100644 index 000000000..ede325070 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/loads.csv @@ -0,0 +1,5 @@ +LOAD_ZONE,TIMEPOINT,zone_demand_mw +South,1,9.0 +South,2,2.5 +South,3,10.0 +South,4,4.0 diff --git a/examples/pumped_hydro_simple/inputs/modules.txt b/examples/pumped_hydro_simple/inputs/modules.txt new file mode 100644 index 000000000..b2530fc71 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/modules.txt @@ -0,0 +1,14 @@ +# Core Modules +switch_model +switch_model.timescales +switch_model.financials +switch_model.balancing.load_zones +switch_model.energy_sources.properties +switch_model.generators.core.build +switch_model.generators.core.dispatch +switch_model.reporting +# Custom Modules +switch_model.generators.core.no_commit +switch_model.energy_sources.fuel_costs.simple +switch_model.generators.extensions.hydro_simple +switch_model.generators.extensions.storage diff --git a/examples/pumped_hydro_simple/inputs/non_fuel_energy_sources.csv b/examples/pumped_hydro_simple/inputs/non_fuel_energy_sources.csv new file mode 100644 index 000000000..512d1b81e --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/non_fuel_energy_sources.csv @@ -0,0 +1,4 @@ +energy_source +Solar +Geothermal +Water diff --git a/examples/pumped_hydro_simple/inputs/periods.csv b/examples/pumped_hydro_simple/inputs/periods.csv new file mode 100644 index 000000000..27c58e07f --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/periods.csv @@ -0,0 +1,2 @@ +INVESTMENT_PERIOD,period_start,period_end +2020,2017,2026 diff --git a/examples/pumped_hydro_simple/inputs/switch_inputs_version.txt b/examples/pumped_hydro_simple/inputs/switch_inputs_version.txt new file mode 100644 index 000000000..e01025862 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/switch_inputs_version.txt @@ -0,0 +1 @@ +2.0.5 diff --git a/examples/pumped_hydro_simple/inputs/timepoints.csv b/examples/pumped_hydro_simple/inputs/timepoints.csv new file mode 100644 index 000000000..bcbd3da19 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/timepoints.csv @@ -0,0 +1,5 @@ +timepoint_id,timestamp,timeseries +1,2025011512,2020_winter +2,2025011600,2020_winter +3,2025071512,2020_summer +4,2025071600,2020_summer diff --git a/examples/pumped_hydro_simple/inputs/timeseries.csv b/examples/pumped_hydro_simple/inputs/timeseries.csv new file mode 100644 index 000000000..d6638f534 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/timeseries.csv @@ -0,0 +1,3 @@ +TIMESERIES,ts_period,ts_duration_of_tp,ts_num_tps,ts_scale_to_period +2020_winter,2020,12,2,1826 +2020_summer,2020,12,2,1826 diff --git a/examples/pumped_hydro_simple/inputs/variable_capacity_factors.csv b/examples/pumped_hydro_simple/inputs/variable_capacity_factors.csv new file mode 100644 index 000000000..3b55edf84 --- /dev/null +++ b/examples/pumped_hydro_simple/inputs/variable_capacity_factors.csv @@ -0,0 +1,9 @@ +GENERATION_PROJECT,timepoint,gen_max_capacity_factor +S-Central_PV-1,1,0.61 +S-Central_PV-1,2,0.0 +S-Central_PV-1,3,0.81 +S-Central_PV-1,4,0.0 +Hydro_RoR,1,0.25 +Hydro_RoR,2,0.5 +Hydro_RoR,3,0.2 +Hydro_RoR,4,0.4 diff --git a/examples/pumped_hydro_simple/outputs/total_cost.txt b/examples/pumped_hydro_simple/outputs/total_cost.txt new file mode 100644 index 000000000..00b93007f --- /dev/null +++ b/examples/pumped_hydro_simple/outputs/total_cost.txt @@ -0,0 +1 @@ +30423392.46 diff --git a/switch_model/generators/extensions/hydro_simple.py b/switch_model/generators/extensions/hydro_simple.py index f5260483b..123d00cf7 100644 --- a/switch_model/generators/extensions/hydro_simple.py +++ b/switch_model/generators/extensions/hydro_simple.py @@ -22,11 +22,23 @@ but the advanced framework would take longer to read and understand. To really take advantage of it, you'll also need more data than we usually have available. + +This module can model pumped hydro systems if the storage module is listed +after simple_hydro in modules.txt, and pumped hydro generation projects are +flagged via gen_is_pumped_hydro. The current implementation of pumped_hydro +implicitly assumes that the lower reservoir always has sufficient water in it +for pumping uphill into storage. Existing resevoir energy capacity can be +constrained via gen_predetermined_storage_energy_mwh as needed. If existing +reservoir energy capacity is never a binding constraint for day-to-day +operations of pumped hydro, leave gen_predetermined_storage_energy_mwh +unspecified, and set gen_storage_energy_overnight_cost to 0 and the +optimization will set the energy value to a conveniently large value. + """ # ToDo: Refactor this code to move the core components into a # switch_model.hydro.core module, the simplest components into # switch_model.hydro.simple, and the advanced components into -# switch_model.hydro.water_network. That should set a good example +# switch_model.hydro.water_network. That could set a good example # for other people who want to do other custom handling of hydro. from __future__ import division @@ -51,9 +63,19 @@ def define_components(mod): GENERATION_PROJECTS, and is determined by the inputs file hydro_timeseries.csv. + gen_is_pumped_hydro[g in GENERATION_PROJECTS] is an optional parameter + that denotes whether a hydro project includes pumped storage. To use this + pumped hydro implementation, you must include the storage module after + hydro_simple in modules.txt. The storage module will look for storage + generators flagged as pumped hydro and will define custom constraints for + their dispatch & storage that takes into account stream flow. + HYDRO_GEN_TS is the set of Hydro projects and timeseries for which minimum and average flow are specified. + HYDRO_NONPUMPED_GEN_TS is a subset of HYDRO_GEN_TS for hydro projects that + do not include pumped storage, and is used to establish flow constraints. + HYDRO_GEN_TPS is the set of Hydro projects and available dispatch points. This is a filtered version of GEN_TPS that only includes hydro projects. @@ -76,7 +98,11 @@ def define_components(mod): Enforce_Hydro_Avg_Flow[(g, ts) in HYDRO_NONPUMPED_GEN_TS] is a constraint that enforces average flow levels across each timeseries. It requires the average of dispatched and spilled hydro over the course of a timeseries - must equal to the corresponding hydro_avg_flow_mw parameter. + must equal to the corresponding hydro_avg_flow_mw parameter. The + corresponding constraint for pumped hydro is defined in the storage + module (after storage decision variables are available), via an augmented + version of the Track_State_Of_Charge constraint that adds average incoming + streamflow and subtracts any spilled power. """ mod.HYDRO_GEN_TS_RAW = Set( @@ -87,13 +113,23 @@ def define_components(mod): ) mod.HYDRO_GENS = Set( initialize=lambda m: set(g for (g, ts) in m.HYDRO_GEN_TS_RAW), - doc="Dispatchable hydro projects") + doc="Dispatchable hydro projects (both pumped & non-pumped)") + mod.gen_is_pumped_hydro = Param( + mod.GENERATION_PROJECTS, + within=Boolean, + default=False, + validate=lambda m, value, g: (value == False) or (g in m.HYDRO_GENS)) + mod.HYDRO_GEN_TS = Set( dimen=2, initialize=lambda m: set( (g, m.tp_ts[tp]) for g in m.HYDRO_GENS for tp in m.TPS_FOR_GEN[g])) + mod.HYDRO_NONPUMPED_GEN_TS = Set( + dimen=2, + initialize=mod.HYDRO_GEN_TS, + filter=lambda m, g, ts: m.gen_is_pumped_hydro[g] == False) mod.HYDRO_GEN_TPS = Set( initialize=mod.GEN_TPS, filter=lambda m, g, t: g in m.HYDRO_GENS) @@ -125,8 +161,8 @@ def _warn_on_extra_HYDRO_GEN_TS(m): "could indicate a benign issue where the process that built " "the dataset used simplified logic and/or didn't know the " "scheduled operating dates. If you expect those datapoints to " - "be useful, then those plants need to either come online earlier " - ", have longer lifetimes, or have options to build new capacity " + "be useful, then those plants need to either come online earlier, " + "have longer lifetimes, or have options to build new capacity " "when the old capacity reaches the provided end-of-life date." "\n".format(num_impacted_generators)) if extra_indexes: @@ -153,7 +189,7 @@ def _warn_on_extra_HYDRO_GEN_TS(m): mod.HYDRO_GEN_TPS, within=NonNegativeReals) mod.Enforce_Hydro_Avg_Flow = Constraint( - mod.HYDRO_GEN_TS, + mod.HYDRO_NONPUMPED_GEN_TS, rule=lambda m, g, ts: ( sum(m.DispatchGen[g, t] + m.SpillHydro[g,t] for t in m.TPS_IN_TS[ts] @@ -161,6 +197,18 @@ def _warn_on_extra_HYDRO_GEN_TS(m): mod.min_data_check('hydro_min_flow_mw', 'hydro_avg_flow_mw') + def storage_module_avail_for_pumped_hydro_check(m): + no_pumped_hydro = all( + value(m.gen_is_pumped_hydro[g]) == False + for g in m.GENERATION_PROJECTS) + has_storage = ( + 'switch_model.generators.extensions.storage' in m.module_list) + return (no_pumped_hydro or has_storage) + mod.storage_module_avail_for_pumped_hydro = BuildCheck( + rule=storage_module_avail_for_pumped_hydro_check, + doc="Ensure that the user has included the storage module if they are" + "attempting to model pumped hydro.") + def load_inputs(mod, switch_data, inputs_dir): """ @@ -177,6 +225,15 @@ def load_inputs(mod, switch_data, inputs_dir): hydro_generation_project, timeseries, hydro_min_flow_mw, hydro_avg_flow_mw + To model pumped hydro projects that use both river flows and pumped + storage, include the storage module in modules.txt after the hydro_simple + module. You will also need to populate the gen_is_pumped_hydro & + gen_storage_efficiency columns of generation_projects_info.tab for the + pumped hydro projects. + + generation_projects_info.csv + GENERATION_PROJECT, ..., gen_is_pumped_hydro + """ switch_data.load_aug( optional=True, @@ -185,3 +242,8 @@ def load_inputs(mod, switch_data, inputs_dir): index=mod.HYDRO_GEN_TS_RAW, param=(mod.hydro_min_flow_mw, mod.hydro_avg_flow_mw) ) + switch_data.load_aug( + filename=os.path.join(inputs_dir, 'generation_projects_info.csv'), + auto_select=True, + optional_params=['gen_is_pumped_hydro'], + param=(mod.gen_is_pumped_hydro,)) diff --git a/switch_model/generators/extensions/storage.py b/switch_model/generators/extensions/storage.py index e3e4fc191..a3ba85450 100644 --- a/switch_model/generators/extensions/storage.py +++ b/switch_model/generators/extensions/storage.py @@ -5,15 +5,28 @@ This module defines storage technologies. It builds on top of generic generators, adding components for deciding how much energy to build into storage, when to charge, energy accounting, etc. + +To do: + +* Add optional ability to exogenously constrain StateOfCharge based on +input files. See notes under the Track_State_Of_Charge documentation below. +* Update the PumpedHydro features to work with the hydro_system module, +and use full mass balance accounting. """ from pyomo.environ import * import os, collections from switch_model.financials import capital_recovery_factor as crf -dependencies = 'switch_model.timescales', 'switch_model.balancing.load_zones',\ - 'switch_model.financials', 'switch_model.energy_sources.properties', \ - 'switch_model.generators.core.build', 'switch_model.generators.core.dispatch' +dependencies = ( + 'switch_model.timescales', + 'switch_model.balancing.load_zones', + 'switch_model.financials', + 'switch_model.energy_sources.properties', + 'switch_model.generators.core.build', + 'switch_model.generators.core.dispatch' +) +optional_prerequisites = ('switch_model.generators.extensions.hydro_simple',) def define_components(mod): """ @@ -35,11 +48,11 @@ def define_components(mod): for extended time perios, then those behaviors will need to be modeled in more detail. - gen_store_to_release_ratio[STORAGE_GENS] describes the maximum rate - that energy can be stored, expressed as a ratio of discharge power - capacity. This is an optional parameter and will default to 1. If a - storage project has 1 MW of dischage capacity and a gen_store_to_release_ratio - of 1.2, then it can consume up to 1.2 MW of power while charging. + gen_store_to_release_ratio[STORAGE_GENS] describes the maximum rate that + energy can be stored, expressed as a ratio of discharge power capacity. + This is an optional parameter and will default to 1. If a storage project + has 1 MW of dischage capacity and a gen_store_to_release_ratio of 1.2, + then it can consume up to 1.2 MW of power while charging. gen_storage_energy_to_power_ratio[STORAGE_GENS], if specified, restricts the storage capacity (in MWh) to be a fixed multiple of the output @@ -66,37 +79,60 @@ def define_components(mod): decision variable. This is analogous to gen_predetermined_cap, but in units of energy of storage capacity (MWh) rather than power (MW). - BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS] - is a decision of how much energy capacity to build onto a storage - project. This is analogous to BuildGen, but for energy rather than power. + BuildStorageEnergy[(g, bld_yr) in STORAGE_GEN_BLD_YRS] is a decision of + how much energy capacity to build onto a storage project in units of MWh. + This is analogous to BuildGen, but for energy rather than power. - StorageEnergyInstallCosts[PERIODS] is an expression of the - annual costs incurred by the BuildStorageEnergy decision. + StorageEnergyInstallCosts[PERIODS] is an expression of the annual costs + incurred by the BuildStorageEnergy decision, in units of $/MWh. StorageEnergyCapacity[g, period] is an expression describing the - cumulative available energy capacity of BuildStorageEnergy. This is - analogous to GenCapacity. + cumulative available energy capacity of BuildStorageEnergy in units of + MWh. This is analogous to GenCapacity. STORAGE_GEN_TPS is the subset of GEN_TPS, restricted to storage projects. - ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch - decision of how much to charge a storage project in each timepoint. + ChargeStorage[(g, t) in STORAGE_GEN_TPS] is a dispatch decision of how + much to charge a storage project in each timepoint in units of MW. StorageNetCharge[LOAD_ZONE, TIMEPOINT] is an expression describing the - aggregate impact of ChargeStorage in each load zone and timepoint. + aggregate impact of ChargeStorage in each load zone and timepoint in units + of MW. Charge_Storage_Upper_Limit[(g, t) in STORAGE_GEN_TPS] constrains ChargeStorage to available power capacity (accounting for gen_store_to_release_ratio) - StateOfCharge[(g, t) in STORAGE_GEN_TPS] is a variable - for tracking state of charge. This value stores the state of charge at - the end of each timepoint for each storage project. - - Track_State_Of_Charge[(g, t) in STORAGE_GEN_TPS] constrains - StateOfCharge based on the StateOfCharge in the previous timepoint, - ChargeStorage and DispatchGen. + StateOfCharge[(g, t) in STORAGE_GEN_TPS] is a variable for tracking state + of charge, in units of MWh. This value stores the state of charge at the + end of each timepoint for each storage project. + + Track_State_Of_Charge[(g, t) in STORAGE_GEN_TPS] constrains StateOfCharge + based on the StateOfCharge in the previous timepoint, ChargeStorage and + DispatchGen. If pumped hydro projects are available (defined in the + optional prerequisite simple_hydro module), this constraint will also + include average stream inflow and spilled water for pumped hydro projects. + + With the current circular implementation of each timeseries, + StateOfCharge[first] = StateOfCharge[last] + net_energy. This effectively + allows each timeseries to begin and end with any given charge level (0 to + 100% of energy capacity), and the optimization process will choose a value + that is convenient. + + For some applications, you may wish to constrain the StateOfCharge values + to pre-determined values, which would require writing a new module that + reads data and applies a constraint to StateOfCharge at the beginning of + each day - either as a fixed amount of energy (MWh) or as a fraction of + energy storage capacity (%). Or alternately, updating this file to include + that behavior as an optional feature. + + Other applications may require linking consecutive timeseries so that the + StateOfCharge carries over from one timeseries to the next. That would + require writing a new version of the timescales module and redefining + tp_previous to use sequential indexing rather than consecutive. Note, that + strategy is not required for 8760 hourly/annual production cost models; + those problems can define a single timeseries of an entire year. State_Of_Charge_Upper_Limit[(g, t) in STORAGE_GEN_TPS] constrains StateOfCharge based on installed energy capacity. @@ -214,10 +250,23 @@ def Charge_Storage_Upper_Limit_rule(m, g, t): within=NonNegativeReals) def Track_State_Of_Charge_rule(m, g, t): - return m.StateOfCharge[g, t] == \ - m.StateOfCharge[g, m.tp_previous[t]] + \ + new_energy = ( + m.StateOfCharge[g, m.tp_previous[t]] + (m.ChargeStorage[g, t] * m.gen_storage_efficiency[g] - m.DispatchGen[g, t]) * m.tp_duration_hrs[t] + ) + # Try to update the energy balance for pumped hydro from the + # hydro_simple module, but skip it if hydro terms haven't been + # defined. + try: + if m.gen_is_pumped_hydro[g]: + ts = m.tp_ts[t] + new_energy += ( + m.hydro_avg_flow_mw[g, ts] - m.SpillHydro[g, t] + ) * m.tp_duration_hrs[t] + except AttributeError: + pass + return m.StateOfCharge[g, t] == new_energy mod.Track_State_Of_Charge = Constraint( mod.STORAGE_GEN_TPS, rule=Track_State_Of_Charge_rule) @@ -257,25 +306,40 @@ def load_inputs(mod, switch_data, inputs_dir): GENERATION_PROJECT, build_year, ... gen_storage_energy_overnight_cost - gen_build_predetermined.tab + gen_build_predetermined.csv GENERATION_PROJECT, build_year, ..., gen_predetermined_storage_energy_mwh* """ - # TODO: maybe move these columns to a storage_gen_info file to avoid the weird index - # reading and avoid having to create these extra columns for all projects; - # Alternatively, say that these values are specified for _all_ projects (maybe with None - # as default) and then define STORAGE_GENS as the subset of projects for which - # gen_storage_efficiency has been specified, then require valid settings for all - # STORAGE_GENS. + # TODO: consider moving these columns to a storage_gen_info file to avoid + # avoid having to create these extra columns for all projects, and to + # allow unambiguous marking of storage projects without looking for + # non-empty gen_storage_efficiency columns. This is similar to how + # simple_hydro specifies input files. Pro: less empty values for + # non-storage projects. Con: another input file to keep track of. + # Alternatively, say that these values are specified for _all_ projects + # (maybe with None as default) and then define STORAGE_GENS as the subset + # of projects for which gen_storage_efficiency has been specified, then + # require valid settings for all STORAGE_GENS. + # Note: The current implementation is similar to the 2nd option above, but + # requires checking `g in mod.gen_storage_efficiency` rather than checking + # `mod.gen_storage_efficiency[g] is not None`. switch_data.load_aug( filename=os.path.join(inputs_dir, 'generation_projects_info.csv'), auto_select=True, - optional_params=['gen_store_to_release_ratio', 'gen_storage_energy_to_power_ratio', 'gen_storage_max_cycles_per_year'], - param=(mod.gen_storage_efficiency, mod.gen_store_to_release_ratio, mod.gen_storage_energy_to_power_ratio, mod.gen_storage_max_cycles_per_year)) + optional_params=[ + 'gen_store_to_release_ratio', + 'gen_storage_energy_to_power_ratio', + 'gen_storage_max_cycles_per_year'], + param=( + mod.gen_storage_efficiency, + mod.gen_store_to_release_ratio, + mod.gen_storage_energy_to_power_ratio, + mod.gen_storage_max_cycles_per_year)) # Base the set of storage projects on storage efficiency being specified. - # TODO: define this in a more normal way + # TODO: define this in a more normal way, possibly with a binary flag + # gen_is_storage. switch_data.data()['STORAGE_GENS'] = { None: list(switch_data.data(name='gen_storage_efficiency').keys())} switch_data.load_aug( @@ -284,7 +348,7 @@ def load_inputs(mod, switch_data, inputs_dir): param=(mod.gen_storage_energy_overnight_cost)) switch_data.load_aug( optional=True, - filename=os.path.join(inputs_dir, 'gen_build_predetermined.tab'), + filename=os.path.join(inputs_dir, 'gen_build_predetermined.csv'), auto_select=True, param=(mod.gen_predetermined_storage_energy_mwh))