Skip to content

Unit commitment smaller problem #128

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 42 additions & 27 deletions switch_model/balancing/operating_reserves/spinning_reserves.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ def define_arguments(argparser):
"load and 5% of variable renewable output, based on the heuristic "
"described in the 2010 Western Wind and Solar Integration Study.")
)
group.add_argument('--spinning-reserves-no-vars', default=False,
dest='spinning_reserves_no_vars', action='store_true',
help=("Implement spinning reserves as aliases to aliases to "
"DispatchSlackUp & DispatchSlackDown rather than decision "
"variables to reduce problem size.")
)



Expand Down Expand Up @@ -389,33 +395,42 @@ def define_components(m):
dimen=2,
initialize=m.GEN_TPS,
filter=lambda m, g, t: m.gen_can_provide_spinning_reserves[g])
# 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.
m.CommitGenSpinningReservesUp = Var(
m.SPINNING_RESERVE_GEN_TPS,
within=NonNegativeReals
)
m.CommitGenSpinningReservesDown = Var(
m.SPINNING_RESERVE_GEN_TPS,
within=NonNegativeReals
)
m.CommitGenSpinningReservesUp_Limit = Constraint(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda m, g, t: \
m.CommitGenSpinningReservesUp[g,t] <= m.DispatchSlackUp[g, t]
)
m.CommitGenSpinningReservesDown_Limit = Constraint(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda m, g, t: \
m.CommitGenSpinningReservesDown[g,t] <= m.DispatchSlackDown[g, t]
)
if m.options.spinning_reserves_no_vars:
m.CommitGenSpinningReservesUp = Expression(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda mod, g, t: mod.DispatchSlackUp[g, t]
)
m.CommitGenSpinningReservesDown = Expression(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda mod, g, t: mod.DispatchSlackDown[g, t]
)
else:
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.DispatchSlackUp[g, t]
)
)
m.CommitGenSpinningReservesDown_Limit = Constraint(
m.SPINNING_RESERVE_GEN_TPS,
rule=lambda m, g, t: (
m.CommitGenSpinningReservesDown[g,t] <= m.DispatchSlackDown[g, t]
)
)

# Sum of spinning reserve capacity per balancing area and timepoint..
m.CommittedSpinningReserveUp = Expression(
Expand Down
27 changes: 21 additions & 6 deletions switch_model/generators/core/commit/fuel_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,29 @@ def FUEL_USE_SEGMENTS_FOR_GEN_default_rule(m, g):
for (intercept, slope) in m.FUEL_USE_SEGMENTS_FOR_GEN[g]
]
)
def GenFuelUseRate_Calculate_rule(m, g, t, intercept, inc_heat_rate):
# If there is only a single line segment, fully constrain fuel use so
# it can be simplified out of the model during pre-processing.
# Otherwise, set it as an inequality.
# If Startup variables are not defined, then skip startup fuel use.
fuel_req = intercept * m.CommitGen[g, t] + inc_heat_rate * m.DispatchGen[g, t]
try:
# Startup fuel is a one-shot fuel expenditure, but the rest of
# this expression has a units of heat/hr, so convert startup fuel
# requirements into an average over this timepoint.
fuel_req += m.StartupGenCapacity[g, t] * m.gen_startup_fuel[g] / m.tp_duration_hrs[t]
except (AttributeError, KeyError):
pass
fuel_used = sum(m.GenFuelUseRate[g, t, f] for f in m.FUELS_FOR_GEN[g])
if len(m.FUEL_USE_SEGMENTS_FOR_GEN[g]) > 1:
rule = fuel_used >= fuel_req
else:
rule = fuel_used == fuel_req
return rule
mod.GenFuelUseRate_Calculate = Constraint(
mod.GEN_TPS_FUEL_PIECEWISE_CONS_SET,
rule=lambda m, g, t, intercept, incremental_heat_rate: (
sum(m.GenFuelUseRate[g, t, f] for f in m.FUELS_FOR_GEN[g]) >=
# Do the startup
m.StartupGenCapacity[g, t] * m.gen_startup_fuel[g] / m.tp_duration_hrs[t] +
intercept * m.CommitGen[g, t] +
incremental_heat_rate * m.DispatchGen[g, t]))
rule=GenFuelUseRate_Calculate_rule
)

# TODO: switch to defining heat rates as a collection of (output_mw, fuel_mmbtu_per_h) points;
# read those directly as normal sets, then derive the project heat rate curves from those
Expand Down
110 changes: 65 additions & 45 deletions switch_model/generators/core/commit/operate.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
"""
Defines model components to describe unit commitment of projects for the
Switch model. This module is mutually exclusive with the
operations.no_commit module which specifies simplified dispatch
constraints. If you want to use this module directly in a list of switch
modules (instead of including the package operations.unitcommit), you will also
need to include the module operations.unitcommit.fuel_use.
...generators.core.no_commit module which specifies simplified dispatch
constraints. This module has a post-requisite of
switch_model.generators.core.commit.fuel_use.
"""
from __future__ import division

Expand All @@ -20,6 +19,14 @@
'switch_model.generators.core.build', 'switch_model.generators.core.dispatch'
)

def define_arguments(argparser):
group = argparser.add_argument_group(__name__)
group.add_argument('--do-not-track-startup-or-shutdown', default=False,
dest='do_not_track_startup_or_shutdown', action='store_true',
help=("Skip tracking startup & shutdown in unit commitment to reduce "
"the number of decision variables & constraints.")
)

def define_components(mod):
"""

Expand Down Expand Up @@ -81,6 +88,9 @@ def define_components(mod):

-- StartupGenCapacity and ShutdownGenCapacity --

This section can be disabled by using the --do-not-track-startup-or-shutdown
command line option.

The capacity started up or shutdown is completely determined by
the change in CommitGen from one hour to the next, but we can't
calculate these directly within the linear program because linear
Expand Down Expand Up @@ -254,6 +264,51 @@ def define_components(mod):
mod.GEN_TPS,
rule=lambda m, g, t: (
m.CommitGen[g, t] - m.CommitLowerLimit[g, t]))

if not mod.options.do_not_track_startup_or_shutdown:
track_startup_and_shutdown(mod)

# Dispatch limits relative to committed capacity.
mod.gen_min_load_fraction = Param(
mod.GENERATION_PROJECTS,
within=PercentFraction,
default=lambda m, g: 1.0 if m.gen_is_baseload[g] else 0.0)
mod.gen_min_load_fraction_TP = Param(
mod.GEN_TPS,
default=lambda m, g, t: m.gen_min_load_fraction[g])
mod.DispatchLowerLimit = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.CommitGen[g, t] * m.gen_min_load_fraction_TP[g, t]))

def DispatchUpperLimit_expr(m, g, t):
if g in m.VARIABLE_GENS:
return m.CommitGen[g, t]*m.gen_max_capacity_factor[g, t]
else:
return m.CommitGen[g, t]
mod.DispatchUpperLimit = Expression(
mod.GEN_TPS,
rule=DispatchUpperLimit_expr)

mod.Enforce_Dispatch_Lower_Limit = Constraint(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchLowerLimit[g, t] <= m.DispatchGen[g, t]))
mod.Enforce_Dispatch_Upper_Limit = Constraint(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchGen[g, t] <= m.DispatchUpperLimit[g, t]))
mod.DispatchSlackUp = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchUpperLimit[g, t] - m.DispatchGen[g, t]))
mod.DispatchSlackDown = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchGen[g, t] - m.DispatchLowerLimit[g, t]))


def track_startup_and_shutdown(mod):
# StartupGenCapacity & ShutdownGenCapacity (at start of each timepoint)
mod.StartupGenCapacity = Var(
mod.GEN_TPS,
Expand Down Expand Up @@ -371,45 +426,6 @@ def min_time_rule(m, g, tp, up):
rule=lambda *a: min_time_rule(*a, up=False)
)

# Dispatch limits relative to committed capacity.
mod.gen_min_load_fraction = Param(
mod.GENERATION_PROJECTS,
within=PercentFraction,
default=lambda m, g: 1.0 if m.gen_is_baseload[g] else 0.0)
mod.gen_min_load_fraction_TP = Param(
mod.GEN_TPS,
default=lambda m, g, t: m.gen_min_load_fraction[g])
mod.DispatchLowerLimit = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.CommitGen[g, t] * m.gen_min_load_fraction_TP[g, t]))

def DispatchUpperLimit_expr(m, g, t):
if g in m.VARIABLE_GENS:
return m.CommitGen[g, t]*m.gen_max_capacity_factor[g, t]
else:
return m.CommitGen[g, t]
mod.DispatchUpperLimit = Expression(
mod.GEN_TPS,
rule=DispatchUpperLimit_expr)

mod.Enforce_Dispatch_Lower_Limit = Constraint(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchLowerLimit[g, t] <= m.DispatchGen[g, t]))
mod.Enforce_Dispatch_Upper_Limit = Constraint(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchGen[g, t] <= m.DispatchUpperLimit[g, t]))
mod.DispatchSlackUp = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchUpperLimit[g, t] - m.DispatchGen[g, t]))
mod.DispatchSlackDown = Expression(
mod.GEN_TPS,
rule=lambda m, g, t: (
m.DispatchGen[g, t] - m.DispatchLowerLimit[g, t]))


def load_inputs(mod, switch_data, inputs_dir):
"""
Expand All @@ -432,12 +448,16 @@ def load_inputs(mod, switch_data, inputs_dir):
gen_max_commit_fraction_TP, gen_min_load_fraction_TP

"""
if mod.options.do_not_track_startup_or_shutdown:
params = (mod.gen_min_load_fraction,)
else:
params = (mod.gen_min_load_fraction, mod.gen_startup_fuel,
mod.gen_startup_om, mod.gen_min_uptime, mod.gen_min_downtime)
switch_data.load_aug(
optional=True,
filename=os.path.join(inputs_dir, 'generation_projects_info.tab'),
auto_select=True,
param=(mod.gen_min_load_fraction, mod.gen_startup_fuel,
mod.gen_startup_om, mod.gen_min_uptime, mod.gen_min_downtime))
param=params)
switch_data.load_aug(
optional=True,
filename=os.path.join(inputs_dir, 'gen_timepoint_commit_bounds.tab'),
Expand Down