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( 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 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'),