From b58c968155cc68e244f3866ff1a7c50a7c8f5c40 Mon Sep 17 00:00:00 2001 From: "josiah.johnston" Date: Thu, 3 Oct 2019 13:41:51 -0400 Subject: [PATCH 1/3] Add option to define spinning reserves as expressions (rather than variables and constraints) to reduce the problem size. --- .../operating_reserves/spinning_reserves.py | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/switch_model/balancing/operating_reserves/spinning_reserves.py b/switch_model/balancing/operating_reserves/spinning_reserves.py index 54801c055..ed9ec91fc 100644 --- a/switch_model/balancing/operating_reserves/spinning_reserves.py +++ b/switch_model/balancing/operating_reserves/spinning_reserves.py @@ -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.") + ) @@ -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( From 5e9c3e92cb27b7057a51890c58036b3e747b0fc2 Mon Sep 17 00:00:00 2001 From: "josiah.johnston" Date: Thu, 3 Oct 2019 13:45:09 -0400 Subject: [PATCH 2/3] Simplify fuel use definition when simple inputs are available. When a single-line segment is available for a part-load heat rate curve (which is the most common situation), specify fuel use as an equality constraint that can be simplified out during optimizer pre-solve steps, and only use an inequality when multiple line segments are provided. Also, be robust to the situation of startup not being tracked. --- .../generators/core/commit/fuel_use.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/switch_model/generators/core/commit/fuel_use.py b/switch_model/generators/core/commit/fuel_use.py index 5e0d1394f..afcdf5c01 100644 --- a/switch_model/generators/core/commit/fuel_use.py +++ b/switch_model/generators/core/commit/fuel_use.py @@ -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 From 019d2741e8c9ca2ddc0907e9956a9429a434596d Mon Sep 17 00:00:00 2001 From: "josiah.johnston" Date: Thu, 3 Oct 2019 13:47:12 -0400 Subject: [PATCH 3/3] Provide an option to skip tracking startup & shutdown during unit commitment to reduce problem size. The default behavior is still to track startup & shutdown. --- .../generators/core/commit/operate.py | 110 +++++++++++------- 1 file changed, 65 insertions(+), 45 deletions(-) diff --git a/switch_model/generators/core/commit/operate.py b/switch_model/generators/core/commit/operate.py index cb414dbcb..b8db3639f 100644 --- a/switch_model/generators/core/commit/operate.py +++ b/switch_model/generators/core/commit/operate.py @@ -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 @@ -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): """ @@ -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 @@ -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, @@ -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): """ @@ -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'),