-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathspinning_reserves.py
More file actions
562 lines (496 loc) · 25.4 KB
/
Copy pathspinning_reserves.py
File metadata and controls
562 lines (496 loc) · 25.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# Copyright (c) 2015-2019 The Switch Authors. All rights reserved.
# Licensed under the Apache License, Version 2.0, which is in the LICENSE file.
"""
A simple and flexible model of spinning reserves that tracks the state of unit
commitment and dispatched capacity to ensures that the generation fleet has
enough up- and down- ramping capacity to satisfy reserve requirements. The
unit commitment module is a prerequisite for spinning reserves. This
formulation does not consider ramping speed or duration requirements, just MW
of upward and downward ramping capability.
Spinning reserve requirements can be customized through use of configuration
parameters and can include n-1 contingencies (either from generation units or
entire generation plants), as well as variability of load and variable
renewable resources. This lumps together regulating reserves, load following
reserves, and contingency reserves without distinguishing their timescales or
required response duration. Operating reserves at timescales with slower
responses for load following or longer-term recovery from contingencies are not
included here, but can be accounted for with the quickstart module.
Most regions and countries use distinct terminology for reserves products and
distinct procedures for determining reserve requirements. This module provides
a simple approach to spinning reserve requirements, which can be extended by
other module via registering with dynamic lists. Detailed regional studies may
need to write their own reserve modules to reflect specific regional reserve
definitions and policies.
Notes:
This formulation only considers ramping capacity (MW), not duration or speed.
The lack of duration requirements could cause problems if a significant amount
of capacity is energy limited such as demand response, storage, or hydro.
California now has a duration requirement of 3 hours for some classes of
operating reserves. The lack of ramping speed could cause issues if the
generators that are earmarked for providing spinning reserves have significant
differences in ramping speeds that are important to account for. This
formulation could be extended in the future to break reserve products into
different categories based on overall response time (ramping speed &
telemetry), and specify different reserve requirements for various response
times: <1sec, <1 min, <5min, <15min, <1hr, 1day.
One standard (nonlinear) methodology for calculating reserve requirements
looks something like: k * sqrt(sigma_load^2 + sigma_renewable^2), where k is a
constant reflecting capacity requirements (typically in the range of 3-5), and
sigma's denote standard deviation in units of MW. Depending on the study,
sigma may be calculated on timescales of seconds to minutes. Several studies
estimate the sigmas with linear approximations. Some studies set
sigma_renewable as a function of renewable output, especially for wind where
power output shows the highest variability in the 40-60% output range because
that is the steepest section of its power production curve. This formulation
is not used here because the signma_renewable term would need to be
approximated using renewable power output, making this equation non-linear
with respect to dispatch decision variables.
Other studies have used linear equations for estimating reserve requirements:
The Western Wind and Solar Integration study suggested a heuristic of 3% *
load + 5% * renewable_output for spinning reserve capacity requirements, and
the same amount for quick start capacity requirements.
Halamay 2011 derives spinning reserve requirements of +2.1% / -2.8% of load
and ~ +2% / -3% for renewables to balance natural variability, and derives
non-spinning reserve requirements and +3.5% / -4.0% of load and ~ +/- 4% for
renewables to balance hour-ahead forecast errors.
Note: Most research appears to be headed towards dynamic and probabilistic
techniques, rather than the static approximations used here.
References on operating reserves follow.
Ela, Erik, et al. "Evolution of operating reserve determination in wind power
integration studies." Power and Energy Society General Meeting, 2010 IEEE.
http://www.nrel.gov/docs/fy11osti/49100.pdf
Milligan, Michael, et al. "Operating reserves and wind power integration: An
international comparison." proc. 9th International Workshop on large-scale
integration of wind power into power systems. 2010.
http://www.nrel.gov/docs/fy11osti/49019.pdf
Halamay, Douglas A., et al. "Reserve requirement impacts of large-scale
integration of wind, solar, and ocean wave power generation." IEEE
Transactions on Sustainable Energy 2.3 (2011): 321-328.
http://nnmrec.oregonstate.edu/sites/nnmrec.oregonstate.edu/files/PES_GM_2010_HalamayVariability_y09m11d30h13m26_DAH.pdf
Ibanez, Eduardo, Ibrahim Krad, and Erik Ela. "A systematic comparison of
operating reserve methodologies." PES General Meeting| Conference &
Exposition, 2014 IEEE. http://www.nrel.gov/docs/fy14osti/61016.pdf
"""
import os
from pyomo.environ import *
dependencies = (
'switch_model.timescales',
'switch_model.balancing.load_zones',
'switch_model.balancing.operating_reserves.areas',
'switch_model.financials',
'switch_model.energy_sources.properties',
'switch_model.generators.core.build',
'switch_model.generators.core.dispatch',
'switch_model.generators.core.commit.operate',
)
def define_arguments(argparser):
group = argparser.add_argument_group(__name__)
group.add_argument('--unit-contingency', default=False,
dest='unit_contingency', action='store_true',
help=("This will enable an n-1 contingency based on a single unit of "
"a generation project falling offline. Note: This create a new "
"binary variable for each project and timepoint that has a "
"proj_unit_size specified.")
)
group.add_argument('--project-contingency', default=False,
dest='project_contingency', action='store_true',
help=("This will enable an n-1 contingency based on the entire "
"committed capacity of a generation project falling offline. "
"Unlike unit contingencies, this is a purely linear expression.")
)
group.add_argument('--spinning-requirement-rule', default=None,
dest='spinning_requirement_rule',
choices = ["Hawaii", "3+5"],
help=("Choose rules for spinning reserves requirements as a function "
"of variable renewable power and load. Hawaii uses rules "
"bootstrapped from the GE RPS study, and '3+5' requires 3%% of "
"load and 5%% of variable renewable output, based on the heuristic "
"described in the 2010 Western Wind and Solar Integration Study.")
)
def define_dynamic_lists(m):
"""
Spinning_Reserve_Up_Requirements and Spinning_Reserve_Down_Requirements
are lists of model components that contribute to spinning reserve
requirements in each balancing area and timepoint.
Spinning_Reserve_Up_Provisions and Spinning_Reserve_Down_Provisions are
lists of model components that help satisfy spinning reserve requirements
in each balancing area and timepoint.
Spinning_Reserve_Contingencies is a list of model components
describing maximum contingency events. Elements of this list will be
summarized into a Maximumcontingency variable that will be added to the
Spinning_Reserve_Up_Requirements list.
Each component in every list needs to use units of MW and be indexed by:
(b, t) in BALANCING_AREA_TIMEPOINTS.
"""
m.Spinning_Reserve_Up_Requirements = []
m.Spinning_Reserve_Down_Requirements = []
m.Spinning_Reserve_Up_Provisions = []
m.Spinning_Reserve_Down_Provisions = []
m.Spinning_Reserve_Contingencies = []
def gen_unit_contingency(m):
"""
Add components for unit-level contingencies. A generation project can
include one or more discretely sized generation units. This will model
contingencies of individual generation units that have discrete sizes
specified. Caution, this adds binary variables to the model for every
GEN_TPS for DISCRETELY_SIZED_GENS. This many binary variables can impact
runtime.
UNIT_CONTINGENCY_DISPATCH_POINTS is a subset of GEN_TPS for
DISCRETELY_SIZED_GENS
GenIsCommitted[(g,t) in UNIT_CONTINGENCY_DISPATCH_POINTS] is a binary
variable that tracks whether generation projects at least one units
committed.
Enforce_GenIsCommitted[(g,t) in UNIT_CONTINGENCY_DISPATCH_POINTS] is a
constraint that enforces the tracking behavior of GenIsCommitted.
GenUnitLargestContingency[(b,t) in BALANCING_AREA_TIMEPOINTS] is a
variable that tracks the size of the largest contingency in each balancing
area, accounting for all of the discretely sized units that are currently
committed. This is added to the dynamic list Spinning_Reserve_Contingencies.
Enforce_GenUnitLargestContingency[(g,t) in UNIT_CONTINGENCY_DISPATCH_POINTS]
is a constraint that enforces the behavior of GenUnitLargestContingency,
by making GenUnitLargestContingency >= the capacity of each of the
committed units in its balancing area.
"""
# UNIT_CONTINGENCY_DISPATCH_POINTS duplicates
# GEN_DISPATCH_POINTS_DISCRETE from generators.core.commit.discrete. I
# justify the duplication because I don't think discrete unit commitment
# should be a prerequisite for this functionality.
m.UNIT_CONTINGENCY_DISPATCH_POINTS = Set(
initialize=m.GEN_TPS,
filter=lambda m, g, tp: g in m.DISCRETELY_SIZED_GENS
)
m.GenIsCommitted = Var(
m.UNIT_CONTINGENCY_DISPATCH_POINTS,
within=Binary,
doc="Stores the status of unit committment as a binary variable."
)
m.Enforce_GenIsCommitted = Constraint(
m.UNIT_CONTINGENCY_DISPATCH_POINTS,
rule=lambda m, g, tp:
m.CommitGen[g, tp] <= m.GenIsCommitted[g, tp] * (
m._gen_max_cap_for_binary_constraints
if g not in m.CAPACITY_LIMITED_GENS
else m.gen_capacity_limit_mw[g]
)
)
m.GenUnitLargestContingency = Var(
m.BALANCING_AREA_TIMEPOINTS,
doc="Largest generating unit that could drop offline.")
def Enforce_GenUnitLargestContingency_rule(m, g, t):
b = m.zone_balancing_area[m.gen_load_zone[g]]
return (m.GenUnitLargestContingency[b,t] >=
m.GenIsCommitted[g, t] * m.gen_unit_size[g])
m.Enforce_GenUnitLargestContingency = Constraint(
m.UNIT_CONTINGENCY_DISPATCH_POINTS,
rule=Enforce_GenUnitLargestContingency_rule,
doc=("Force GenUnitLargestContingency to be at least as big as the "
"maximum unit contingency.")
)
m.Spinning_Reserve_Contingencies.append('GenUnitLargestContingency')
def gen_project_contingency(m):
"""
Add components for project-level contingencies based on committed capacity.
A generation project can include one or more discretely sized generation
units. This will model contingencies of entire generation projects -
basically entire plants tripping offline, rather than individual
generation units in a plan tripping offline.
GenProjectLargestContingency[(b,t) in BALANCING_AREA_TIMEPOINTS] is a
variable that tracks the size of the largest contingency in each balancing
area, accounting for all of the capacity that is committed. This is
added to the dynamic list Spinning_Reserve_Contingencies.
Enforce_GenProjectLargestContingency[(g,t) in GEN_TPS] is a constraint
that enforces the behavior of GenProjectLargestContingency by making
GenProjectLargestContingency >= DispatchGen
for each generation project in a balancing area. If a generation project
is capable of providing upward reserves, then CommitGenSpinningReservesUp
is added to the right hand side.
"""
m.GenProjectLargestContingency = Var(
m.BALANCING_AREA_TIMEPOINTS,
doc="Largest generating project that could drop offline.")
def Enforce_GenProjectLargestContingency_rule(m, g, t):
b = m.zone_balancing_area[m.gen_load_zone[g]]
if m.gen_can_provide_spinning_reserves[g]:
return m.GenProjectLargestContingency[b, t] >= \
m.DispatchGen[g, t] + m.CommitGenSpinningReservesUp[g, t]
else:
return m.GenProjectLargestContingency[b, t] >= m.DispatchGen[g, t]
m.Enforce_GenProjectLargestContingency = Constraint(
m.GEN_TPS,
rule=Enforce_GenProjectLargestContingency_rule,
doc=("Force GenProjectLargestContingency to be at least as big as the "
"maximum generation project contingency.")
)
m.Spinning_Reserve_Contingencies.append('GenProjectLargestContingency')
def hawaii_spinning_reserve_requirements(m):
# This may be more appropriate for a hawaii submodule until it is
# better documented and referenced.
# these parameters were found by regressing the reserve requirements from
# the GE RPS Study against wind and solar conditions each hour (see
# Dropbox/Research/Shared/Switch-Hawaii/ge_validation/source_data/
# reserve_requirements_oahu_scenarios charts.xlsx and
# Dropbox/Research/Shared/Switch-Hawaii/ge_validation/
# fit_renewable_reserves.ipynb )
# TODO: supply these parameters in input files
m.var_gen_power_reserve = Param(
m.VARIABLE_GENS, default=1.0,
doc=("Spinning reserves required to back up variable renewable "
"generators, as fraction of potential output.")
)
def var_gen_cap_reserve_limit_default(m, g):
if m.gen_energy_source[g] == 'Solar':
return 0.21288916
elif m.gen_energy_source[g] == 'Wind':
return 0.21624407
else:
raise RuntimeError()
m.var_gen_cap_reserve_limit = Param(
m.VARIABLE_GENS,
default=var_gen_cap_reserve_limit_default,
doc="Maximum spinning reserves required, as fraction of installed capacity"
)
m.HawaiiVarGenUpSpinningReserveRequirement = Expression(
m.BALANCING_AREA_TIMEPOINTS,
rule=lambda m, b, t: sum(
m.GenCapacityInTP[g, t]
* min(
m.var_gen_power_reserve[g] * m.gen_max_capacity_factor[g, t],
m.var_gen_cap_reserve_limit[g]
)
for g in m.VARIABLE_GENS
if (g, t) in m.VARIABLE_GEN_TPS and b == m.zone_balancing_area[m.gen_load_zone[g]]),
doc="The spinning reserves for backing up variable generation with Hawaii rules."
)
m.Spinning_Reserve_Up_Requirements.append('HawaiiVarGenUpSpinningReserveRequirement')
def HawaiiLoadDownSpinningReserveRequirement_rule(m, b, t):
try:
load = m.WithdrawFromCentralGrid
except AttributeError:
load = m.lz_demand_mw
return 0.10 * sum(load[z, t] for z in m.LOAD_ZONES if b == m.zone_balancing_area[z])
m.HawaiiLoadDownSpinningReserveRequirement = Expression(
m.BALANCING_AREA_TIMEPOINTS,
rule=HawaiiLoadDownSpinningReserveRequirement_rule
)
m.Spinning_Reserve_Down_Requirements.append('HawaiiLoadDownSpinningReserveRequirement')
def nrel_3_5_spinning_reserve_requirements(m):
"""
NREL35VarGenSpinningReserveRequirement[(b,t) in BALANCING_AREA_TIMEPOINTS]
is an expression for upward and downward spinning reserve requirements of
3% of load plus 5% of renewable output, based on a heuristic described in
NREL's 2010 Western Wind and Solar Integration study. It is added to the
Spinning_Reserve_Up_Requirements and Spinning_Reserve_Down_Requirements
lists. If the local_td module is available with DER accounting, load will
be set to WithdrawFromCentralGrid. Otherwise load will be set to
lz_demand_mw.
"""
def NREL35VarGenSpinningReserveRequirement_rule(m, b, t):
try:
load = m.WithdrawFromCentralGrid
except AttributeError:
load = m.lz_demand_mw
return (0.03 * sum(load[z, t] for z in m.LOAD_ZONES
if b == m.zone_balancing_area[z])
+ 0.05 * sum(m.DispatchGen[g, t] for g in m.VARIABLE_GENS
if (g, t) in m.VARIABLE_GEN_TPS and
b == m.zone_balancing_area[m.gen_load_zone[g]]))
m.NREL35VarGenSpinningReserveRequirement = Expression(
m.BALANCING_AREA_TIMEPOINTS,
rule=NREL35VarGenSpinningReserveRequirement_rule
)
m.Spinning_Reserve_Up_Requirements.append('NREL35VarGenSpinningReserveRequirement')
m.Spinning_Reserve_Down_Requirements.append('NREL35VarGenSpinningReserveRequirement')
def define_components(m):
"""
contingency_safety_factor is a parameter that increases the contingency
requirements. By default this is set to 2.0 to prevent the largest
generator from providing reserves for itself.
gen_can_provide_spinning_reserves[g] is a binary flag indicating whether
the project is allowed to provide spinning reserves.
SPINNING_RESERVE_GEN_TPS is a subset of GEN_TPS of generators that can
provide spinning reserves based on gen_can_provide_spinning_reserves.
CommitGenSpinningReservesUp[(g,t) in SPINNING_RESERVE_GEN_TPS] is a
decision variable of how much upward spinning reserve capacity to commit
(in MW).
CommitGenSpinningReservesDown[(g,t) in SPINNING_RESERVE_GEN_TPS] is a
corresponding variable for downward spinning reserves.
CommitGenSpinningReservesUp_Limit[(g,t) in SPINNING_RESERVE_GEN_TPS] and
CommitGenSpinningReservesDown_Limit constrain the
CommitGenSpinningReserves variables based on DispatchSlackUp and
DispatchSlackDown (and ChargeStorage, as applicable).
CommittedSpinningReserveUp[(b,t) in BALANCING_AREA_TIMEPOINTS] and
CommittedSpinningReserveDown are expressions summarizing the
CommitGenSpinningReserves variables for generators within each balancing
area.
CommitGenSpinningReservesUp and CommitGenSpinningReservesDown are
variables instead of aliases to DispatchSlackUp & DispatchSlackDown
because they may need to take on lower values to reduce the
project-level contigencies, especially when discrete unit commitment is
enabled, and committed capacity may exceed the amount of capacity that
is strictly needed. Having these as variables also flags them for
automatic export in model dumps and tab files, and opens up the
possibility of further customizations like adding variable costs for
spinning reserve provision.
Depending on the configuration parameters unit_contingency,
project_contingency and spinning_requirement_rule, other components may be
added by other functions which are documented above.
"""
m.contingency_safety_factor = Param(default=2.0,
doc=("The spinning reserve requiremet will be set to this value "
"times the maximum contingency. This defaults to 2 to ensure "
"that the largest generator cannot be providing contingency "
"reserves for itself."))
m.gen_can_provide_spinning_reserves = Param(
m.GENERATION_PROJECTS, within=Boolean, default=True
)
m.SPINNING_RESERVE_GEN_TPS = Set(
dimen=2,
initialize=m.GEN_TPS,
filter=lambda m, g, t: m.gen_can_provide_spinning_reserves[g])
m.CommitGenSpinningReservesUp = Var(
m.SPINNING_RESERVE_GEN_TPS,
within=NonNegativeReals
)
m.CommitGenSpinningReservesDown = Var(
m.SPINNING_RESERVE_GEN_TPS,
within=NonNegativeReals
)
m.CommitGenSpinningReservesSlackUp = Var(
m.SPINNING_RESERVE_GEN_TPS,
within=NonNegativeReals,
doc="Denotes the upward slack in spinning reserves that could be used "
"for quickstart reserves, or possibly other reserve products."
)
m.CommitGenSpinningReservesUp_Limit = Constraint(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda m, g, t: (
m.CommitGenSpinningReservesUp[g,t] +
m.CommitGenSpinningReservesSlackUp[g,t]
== m.DispatchSlackUp[g, t] +
# storage can give more up response by stopping charging
(m.ChargeStorage[g, t]
if g in getattr(m, 'STORAGE_GENS', [])
else 0.0
)
)
)
m.CommitGenSpinningReservesDown_Limit = Constraint(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda m, g, t: \
m.CommitGenSpinningReservesDown[g,t] <= m.DispatchSlackDown[g, t] +
# storage could give more down response by raising ChargeStorage
# to the maximum rate
(
(m.DispatchUpperLimit[g, t] * m.gen_store_to_release_ratio[g]
- m.ChargeStorage[g, t]
)
if g in getattr(m, 'STORAGE_GENS', [])
else 0.0
)
)
# Sum of spinning reserve capacity per balancing area and timepoint..
m.CommittedSpinningReserveUp = Expression(
m.BALANCING_AREA_TIMEPOINTS,
rule=lambda m, b, t: \
sum(m.CommitGenSpinningReservesUp[g, t]
for z in m.ZONES_IN_BALANCING_AREA[b]
for g in m.GENS_IN_ZONE[z]
if (g,t) in m.SPINNING_RESERVE_GEN_TPS
)
)
m.Spinning_Reserve_Up_Provisions.append('CommittedSpinningReserveUp')
m.CommittedSpinningReserveDown = Expression(
m.BALANCING_AREA_TIMEPOINTS,
rule=lambda m, b, t: \
sum(m.CommitGenSpinningReservesDown[g, t]
for z in m.ZONES_IN_BALANCING_AREA[b]
for g in m.GENS_IN_ZONE[z]
if (g,t) in m.SPINNING_RESERVE_GEN_TPS
)
)
m.Spinning_Reserve_Down_Provisions.append('CommittedSpinningReserveDown')
if m.options.unit_contingency:
gen_unit_contingency(m)
if m.options.project_contingency:
gen_project_contingency(m)
if m.options.spinning_requirement_rule == 'Hawaii':
hawaii_spinning_reserve_requirements(m)
elif m.options.spinning_requirement_rule == '3+5':
nrel_3_5_spinning_reserve_requirements(m)
def define_dynamic_components(m):
"""
MaximumContingency[(b,t) in BALANCING_AREA_TIMEPOINTS] is a variable that
tracks the size of the largest contingency in each balancing area,
accounting for every contingency that has been registered with
Spinning_Reserve_Contingencies.
BALANCING_AREA_TIMEPOINT_CONTINGENCIES is a set of (b, t, contingency) formed
from the cross product of the set BALANCING_AREA_TIMEPOINTS and the dynamic
list Spinning_Reserve_Contingencies.
Enforce_MaximumContingency[(b,t,contingency) in BALANCING_AREA_TIMEPOINT_CONTINGENCIES]
is a constraint that enforces the behavior of MaximumContingency by making
MaximumContingency >= contingency for each contingency registered in the
dynamic list Spinning_Reserve_Contingencies.
Satisfy_Spinning_Reserve_Up_Requirement[(b,t) in BALANCING_AREA_TIMEPOINTS]
is a constraint that ensures upward spinning reserve requirements are
being satisfied based on the sums of the two dynamic lists
Spinning_Reserve_Up_Provisions and Spinning_Reserve_Up_Requirements.
Satisfy_Spinning_Reserve_Down_Requirement[(b,t) in BALANCING_AREA_TIMEPOINTS]
is a matching constraint that uses the downward reserve lists.
"""
m.MaximumContingency = Var(
m.BALANCING_AREA_TIMEPOINTS,
doc=("Maximum of the registered Spinning_Reserve_Contingencies, after "
"multiplying by contingency_safety_factor.")
)
m.BALANCING_AREA_TIMEPOINT_CONTINGENCIES = Set(
initialize=m.BALANCING_AREA_TIMEPOINTS * m.Spinning_Reserve_Contingencies,
doc=("The set of spinning reserve contingencies, copied from the "
"dynamic list Spinning_Reserve_Contingencies to simplify the "
"process of defining one constraint per contingency in the list.")
)
m.Enforce_MaximumContingency = Constraint(
m.BALANCING_AREA_TIMEPOINT_CONTINGENCIES,
rule=lambda m, b, t, contingency:
m.MaximumContingency[b, t] >= m.contingency_safety_factor * getattr(m, contingency)[b, t]
)
m.Spinning_Reserve_Up_Requirements.append('MaximumContingency')
m.Satisfy_Spinning_Reserve_Up_Requirement = Constraint(
m.BALANCING_AREA_TIMEPOINTS,
rule=lambda m, b, t: \
sum(getattr(m, requirement)[b,t]
for requirement in m.Spinning_Reserve_Up_Requirements
) <=
sum(getattr(m, provision)[b,t]
for provision in m.Spinning_Reserve_Up_Provisions
)
)
m.Satisfy_Spinning_Reserve_Down_Requirement = Constraint(
m.BALANCING_AREA_TIMEPOINTS,
rule=lambda m, b, t: \
sum(getattr(m, requirement)[b,t]
for requirement in m.Spinning_Reserve_Down_Requirements
) <=
sum(getattr(m, provision)[b,t]
for provision in m.Spinning_Reserve_Down_Provisions
)
)
def load_inputs(m, switch_data, inputs_dir):
"""
All files & columns are optional.
generation_projects_info.csv
GENERATION_PROJECTS, ... gen_can_provide_spinning_reserves
spinning_reserve_params.csv may override the default value of
contingency_safety_factor. Note that this only contains one
header row and one data row.
"""
switch_data.load_aug(
filename=os.path.join(inputs_dir, 'generation_projects_info.csv'),
auto_select=True,
optional_params=['gen_can_provide_spinning_reserves'],
param=(m.gen_can_provide_spinning_reserves)
)
switch_data.load_aug(
filename=os.path.join(inputs_dir, 'spinning_reserve_params.csv'),
optional=True, auto_select=True,
param=(m.contingency_safety_factor,)
)