Skip to content

Commit 9aa30bb

Browse files
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)
1 parent b5b1a28 commit 9aa30bb

20 files changed

+258
-44
lines changed
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
SYNOPSIS:
2+
3+
switch solve --verbose --log-run
4+
5+
This example illustrates modeling pumped hydro storage by using the hydro_simple module in concert with the storage module.
6+
7+
This adds pumped hydro to the hydro_simple example, and illustrates pumped hydro being used for arbitrage within each example day.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
base_financial_year,interest_rate,discount_rate
2+
2015,0.07,0.05
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
load_zone,fuel,period,fuel_cost
2+
South,NaturalGas,2020,4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fuel,co2_intensity,upstream_co2_intensity
2+
NaturalGas,0.05306,0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
GENERATION_PROJECT,build_year,gen_overnight_cost,gen_fixed_om,gen_storage_energy_overnight_cost
2+
S-NG_CC,2000.0,1143900.0,5868.3,.
3+
S-Central_PV-1,2000.0,2334300.0,41850.0,.
4+
S-Geothermal,1998.0,5524200.0,0.0,.
5+
Hydro,2000.0,10000000.0,100000.0,.
6+
Hydro_RoR,2000.0,1000000.0,100000.0,.
7+
Hydro_Pumped,2000.0,1000000.0,100000.0,0.0
8+
S-Geothermal,2020.0,5524200.0,0.0,.
9+
S-NG_CC,2020.0,1143900.0,5868.3,.
10+
S-Central_PV-1,2020.0,2334300.0,41850.0,.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
GENERATION_PROJECT,build_year,gen_predetermined_cap
2+
S-NG_CC,2000,5.0
3+
S-Central_PV-1,2000,1.0
4+
S-Geothermal,1998,1.0
5+
Hydro,2000,1.0
6+
Hydro_RoR,2000,1.0
7+
Hydro_Pumped,2000,5.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
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
2+
S-Geothermal,33.0,Geothermal,South,134222.0,10.0,28.83,30,0,0.0075,0.0241,0,1,0,Geothermal,.,.,.
3+
S-NG_CC,34.0,NG_CC,South,57566.6,.,3.4131,20,0,0.04,0.06,0,0,0,NaturalGas,6.705,.,.
4+
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,.,.,.
5+
Hydro,.,Hydro,South,0.0,1.0,0.1,100,0,0.019,0.05,0,0,0,Water,.,.,.
6+
Hydro_RoR,.,Hydro_RoR,South,0.0,1.0,0.0,30,0,0.019,0.05,1,0,0,Water,.,.,.
7+
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
hydro_project,timeseries,hydro_min_flow_mw,hydro_avg_flow_mw
2+
Hydro,2020_winter,0.6,0.75
3+
Hydro,2020_summer,0.2,0.6
4+
Hydro_Pumped,2020_winter,0.6,0.75
5+
Hydro_Pumped,2020_summer,0.2,0.6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
LOAD_ZONE,cost_multipliers,ccs_distance_km,dbid
2+
South,1,0,3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
LOAD_ZONE,TIMEPOINT,zone_demand_mw
2+
South,1,9.0
3+
South,2,2.5
4+
South,3,10.0
5+
South,4,4.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Core Modules
2+
switch_model
3+
switch_model.timescales
4+
switch_model.financials
5+
switch_model.balancing.load_zones
6+
switch_model.energy_sources.properties
7+
switch_model.generators.core.build
8+
switch_model.generators.core.dispatch
9+
switch_model.reporting
10+
# Custom Modules
11+
switch_model.generators.core.no_commit
12+
switch_model.energy_sources.fuel_costs.simple
13+
switch_model.generators.extensions.hydro_simple
14+
switch_model.generators.extensions.storage
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
energy_source
2+
Solar
3+
Geothermal
4+
Water
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
INVESTMENT_PERIOD,period_start,period_end
2+
2020,2017,2026
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.0.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
timepoint_id,timestamp,timeseries
2+
1,2025011512,2020_winter
3+
2,2025011600,2020_winter
4+
3,2025071512,2020_summer
5+
4,2025071600,2020_summer
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TIMESERIES,ts_period,ts_duration_of_tp,ts_num_tps,ts_scale_to_period
2+
2020_winter,2020,12,2,1826
3+
2020_summer,2020,12,2,1826
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
GENERATION_PROJECT,timepoint,gen_max_capacity_factor
2+
S-Central_PV-1,1,0.61
3+
S-Central_PV-1,2,0.0
4+
S-Central_PV-1,3,0.81
5+
S-Central_PV-1,4,0.0
6+
Hydro_RoR,1,0.25
7+
Hydro_RoR,2,0.5
8+
Hydro_RoR,3,0.2
9+
Hydro_RoR,4,0.4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
30423392.46

switch_model/generators/extensions/hydro_simple.py

+68-6
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,23 @@
2222
but the advanced framework would take longer to read and understand. To really
2323
take advantage of it, you'll also need more data than we usually have
2424
available.
25+
26+
This module can model pumped hydro systems if the storage module is listed
27+
after simple_hydro in modules.txt, and pumped hydro generation projects are
28+
flagged via gen_is_pumped_hydro. The current implementation of pumped_hydro
29+
implicitly assumes that the lower reservoir always has sufficient water in it
30+
for pumping uphill into storage. Existing resevoir energy capacity can be
31+
constrained via gen_predetermined_storage_energy_mwh as needed. If existing
32+
reservoir energy capacity is never a binding constraint for day-to-day
33+
operations of pumped hydro, leave gen_predetermined_storage_energy_mwh
34+
unspecified, and set gen_storage_energy_overnight_cost to 0 and the
35+
optimization will set the energy value to a conveniently large value.
36+
2537
"""
2638
# ToDo: Refactor this code to move the core components into a
2739
# switch_model.hydro.core module, the simplest components into
2840
# switch_model.hydro.simple, and the advanced components into
29-
# switch_model.hydro.water_network. That should set a good example
41+
# switch_model.hydro.water_network. That could set a good example
3042
# for other people who want to do other custom handling of hydro.
3143

3244
from __future__ import division
@@ -51,9 +63,19 @@ def define_components(mod):
5163
GENERATION_PROJECTS, and is determined by the inputs file
5264
hydro_timeseries.csv.
5365
66+
gen_is_pumped_hydro[g in GENERATION_PROJECTS] is an optional parameter
67+
that denotes whether a hydro project includes pumped storage. To use this
68+
pumped hydro implementation, you must include the storage module after
69+
hydro_simple in modules.txt. The storage module will look for storage
70+
generators flagged as pumped hydro and will define custom constraints for
71+
their dispatch & storage that takes into account stream flow.
72+
5473
HYDRO_GEN_TS is the set of Hydro projects and timeseries for which
5574
minimum and average flow are specified.
5675
76+
HYDRO_NONPUMPED_GEN_TS is a subset of HYDRO_GEN_TS for hydro projects that
77+
do not include pumped storage, and is used to establish flow constraints.
78+
5779
HYDRO_GEN_TPS is the set of Hydro projects and available
5880
dispatch points. This is a filtered version of GEN_TPS that
5981
only includes hydro projects.
@@ -76,7 +98,11 @@ def define_components(mod):
7698
Enforce_Hydro_Avg_Flow[(g, ts) in HYDRO_NONPUMPED_GEN_TS] is a constraint
7799
that enforces average flow levels across each timeseries. It requires the
78100
average of dispatched and spilled hydro over the course of a timeseries
79-
must equal to the corresponding hydro_avg_flow_mw parameter.
101+
must equal to the corresponding hydro_avg_flow_mw parameter. The
102+
corresponding constraint for pumped hydro is defined in the storage
103+
module (after storage decision variables are available), via an augmented
104+
version of the Track_State_Of_Charge constraint that adds average incoming
105+
streamflow and subtracts any spilled power.
80106
"""
81107

82108
mod.HYDRO_GEN_TS_RAW = Set(
@@ -87,13 +113,23 @@ def define_components(mod):
87113
)
88114
mod.HYDRO_GENS = Set(
89115
initialize=lambda m: set(g for (g, ts) in m.HYDRO_GEN_TS_RAW),
90-
doc="Dispatchable hydro projects")
116+
doc="Dispatchable hydro projects (both pumped & non-pumped)")
117+
mod.gen_is_pumped_hydro = Param(
118+
mod.GENERATION_PROJECTS,
119+
within=Boolean,
120+
default=False,
121+
validate=lambda m, value, g: (value == False) or (g in m.HYDRO_GENS))
122+
91123
mod.HYDRO_GEN_TS = Set(
92124
dimen=2,
93125
initialize=lambda m: set(
94126
(g, m.tp_ts[tp])
95127
for g in m.HYDRO_GENS
96128
for tp in m.TPS_FOR_GEN[g]))
129+
mod.HYDRO_NONPUMPED_GEN_TS = Set(
130+
dimen=2,
131+
initialize=mod.HYDRO_GEN_TS,
132+
filter=lambda m, g, ts: m.gen_is_pumped_hydro[g] == False)
97133
mod.HYDRO_GEN_TPS = Set(
98134
initialize=mod.GEN_TPS,
99135
filter=lambda m, g, t: g in m.HYDRO_GENS)
@@ -125,8 +161,8 @@ def _warn_on_extra_HYDRO_GEN_TS(m):
125161
"could indicate a benign issue where the process that built "
126162
"the dataset used simplified logic and/or didn't know the "
127163
"scheduled operating dates. If you expect those datapoints to "
128-
"be useful, then those plants need to either come online earlier "
129-
", have longer lifetimes, or have options to build new capacity "
164+
"be useful, then those plants need to either come online earlier, "
165+
"have longer lifetimes, or have options to build new capacity "
130166
"when the old capacity reaches the provided end-of-life date."
131167
"\n".format(num_impacted_generators))
132168
if extra_indexes:
@@ -153,14 +189,26 @@ def _warn_on_extra_HYDRO_GEN_TS(m):
153189
mod.HYDRO_GEN_TPS,
154190
within=NonNegativeReals)
155191
mod.Enforce_Hydro_Avg_Flow = Constraint(
156-
mod.HYDRO_GEN_TS,
192+
mod.HYDRO_NONPUMPED_GEN_TS,
157193
rule=lambda m, g, ts: (
158194
sum(m.DispatchGen[g, t] + m.SpillHydro[g,t]
159195
for t in m.TPS_IN_TS[ts]
160196
) == m.hydro_avg_flow_mw[g, ts] * m.ts_num_tps[ts]))
161197

162198
mod.min_data_check('hydro_min_flow_mw', 'hydro_avg_flow_mw')
163199

200+
def storage_module_avail_for_pumped_hydro_check(m):
201+
no_pumped_hydro = all(
202+
value(m.gen_is_pumped_hydro[g]) == False
203+
for g in m.GENERATION_PROJECTS)
204+
has_storage = (
205+
'switch_model.generators.extensions.storage' in m.module_list)
206+
return (no_pumped_hydro or has_storage)
207+
mod.storage_module_avail_for_pumped_hydro = BuildCheck(
208+
rule=storage_module_avail_for_pumped_hydro_check,
209+
doc="Ensure that the user has included the storage module if they are"
210+
"attempting to model pumped hydro.")
211+
164212

165213
def load_inputs(mod, switch_data, inputs_dir):
166214
"""
@@ -177,6 +225,15 @@ def load_inputs(mod, switch_data, inputs_dir):
177225
hydro_generation_project, timeseries, hydro_min_flow_mw,
178226
hydro_avg_flow_mw
179227
228+
To model pumped hydro projects that use both river flows and pumped
229+
storage, include the storage module in modules.txt after the hydro_simple
230+
module. You will also need to populate the gen_is_pumped_hydro &
231+
gen_storage_efficiency columns of generation_projects_info.tab for the
232+
pumped hydro projects.
233+
234+
generation_projects_info.csv
235+
GENERATION_PROJECT, ..., gen_is_pumped_hydro
236+
180237
"""
181238
switch_data.load_aug(
182239
optional=True,
@@ -185,3 +242,8 @@ def load_inputs(mod, switch_data, inputs_dir):
185242
index=mod.HYDRO_GEN_TS_RAW,
186243
param=(mod.hydro_min_flow_mw, mod.hydro_avg_flow_mw)
187244
)
245+
switch_data.load_aug(
246+
filename=os.path.join(inputs_dir, 'generation_projects_info.csv'),
247+
auto_select=True,
248+
optional_params=['gen_is_pumped_hydro'],
249+
param=(mod.gen_is_pumped_hydro,))

0 commit comments

Comments
 (0)