|
| 1 | +# Copyright (c) 2015-2019 The Switch Authors. All rights reserved. |
| 2 | +# Licensed under the Apache License, Version 2.0, which is in the LICENSE file. |
| 3 | +""" |
| 4 | +A simple model of quickstart reserves to accompany the spinning_reserves |
| 5 | +modules. Patterns from the spinning reserves modules were followed where |
| 6 | +practical to simplify implementation and review. |
| 7 | +
|
| 8 | +Unlike spinning reserves, this module does not currently implement |
| 9 | +contingency-based requirements because I lack an immediate use case for that. |
| 10 | +The contigency reserves methodology from spinning reserve modules could |
| 11 | +probably be adapted readily if needed. |
| 12 | +
|
| 13 | +For more discussion of operating reserve considerations and modeling |
| 14 | +approaches, see the spinning_reserves module. |
| 15 | +
|
| 16 | +""" |
| 17 | +import os |
| 18 | +from pyomo.environ import * |
| 19 | + |
| 20 | +dependencies = ( |
| 21 | + 'switch_model.timescales', |
| 22 | + 'switch_model.balancing.load_zones', |
| 23 | + 'switch_model.balancing.operating_reserves.areas', |
| 24 | + 'switch_model.financials', |
| 25 | + 'switch_model.energy_sources.properties', |
| 26 | + 'switch_model.generators.core.build', |
| 27 | + 'switch_model.generators.core.dispatch', |
| 28 | + 'switch_model.generators.core.commit.operate', |
| 29 | +) |
| 30 | +optional_prerequisites = ( |
| 31 | + 'switch_model.balancing.operating_reserves.spinning_reserves', |
| 32 | + 'switch_model.balancing.operating_reserves.spinning_reserves_advanced', |
| 33 | +) |
| 34 | + |
| 35 | +# Uncomment this section when more than one rule is implemented. |
| 36 | +# def define_arguments(argparser): |
| 37 | +# group = argparser.add_argument_group(__name__) |
| 38 | +# group.add_argument('--quickstart-requirement-rule', default="3+5", |
| 39 | +# dest='quickstart_requirement_rule', |
| 40 | +# choices = ["3+5"], |
| 41 | +# help=("Choose rules for quickstart reserves requirements as a function " |
| 42 | +# "of variable renewable power and load. '3+5' requires 3%% of " |
| 43 | +# "load and 5%% of variable renewable output, based on the " |
| 44 | +# "heuristic described in the 2010 Western Wind and Solar " |
| 45 | +# "Integration Study.") |
| 46 | +# ) |
| 47 | + |
| 48 | + |
| 49 | +def define_dynamic_lists(mod): |
| 50 | + """ |
| 51 | + Quickstart_Reserve_Requirements is a list of model components that |
| 52 | + contribute to quickstart reserve requirements in each balancing area and |
| 53 | + timepoint. |
| 54 | +
|
| 55 | + Quickstart_Reserve_Provisions is a list of model components that help |
| 56 | + satisfy spinning reserve requirements in each balancing area and |
| 57 | + timepoint. |
| 58 | +
|
| 59 | + Each component in both lists needs to use units of MW and be indexed by: |
| 60 | + (b, t) in BALANCING_AREA_TIMEPOINTS. |
| 61 | + """ |
| 62 | + mod.Quickstart_Reserve_Requirements = [] |
| 63 | + mod.Quickstart_Reserve_Provisions = [] |
| 64 | + |
| 65 | + |
| 66 | +def nrel_3_5_quickstart_reserve_requirements(mod): |
| 67 | + """ |
| 68 | + NREL35QuickstartRequirement[(b,t) in BALANCING_AREA_TIMEPOINTS] is |
| 69 | + an expression for quickstart reserve requirements of 3% of load plus 5% of |
| 70 | + renewable output, based on a heuristic described in NREL's 2010 Western |
| 71 | + Wind and Solar Integration study. It is added to the |
| 72 | + Quickstart_Reserve_Requirements list. If the local_td module is available |
| 73 | + with DER accounting, load will be set to WithdrawFromCentralGrid. |
| 74 | + Otherwise load will be set to lz_demand_mw. |
| 75 | + """ |
| 76 | + def NREL35QuickstartRequirement_rule(m, b, t): |
| 77 | + try: |
| 78 | + load = m.WithdrawFromCentralGrid |
| 79 | + except AttributeError: |
| 80 | + load = m.lz_demand_mw |
| 81 | + return (0.03 * sum(load[z, t] for z in m.LOAD_ZONES |
| 82 | + if b == m.zone_balancing_area[z]) |
| 83 | + + 0.05 * sum(m.DispatchGen[g, t] for g in m.VARIABLE_GENS |
| 84 | + if (g, t) in m.VARIABLE_GEN_TPS and |
| 85 | + b == m.zone_balancing_area[m.gen_load_zone[g]])) |
| 86 | + mod.NREL35QuickstartRequirement = Expression( |
| 87 | + mod.BALANCING_AREA_TIMEPOINTS, |
| 88 | + rule=NREL35QuickstartRequirement_rule |
| 89 | + ) |
| 90 | + mod.Quickstart_Reserve_Requirements.append('NREL35QuickstartRequirement') |
| 91 | + |
| 92 | + |
| 93 | +def define_components(mod): |
| 94 | + """ |
| 95 | + gen_can_provide_quickstart_reserves[g] is a binary flag indicating whether |
| 96 | + a generation project can provide quickstart reserves. Default to False for |
| 97 | + baseload & variable generators, otherwise defaults to True. |
| 98 | +
|
| 99 | + QUICKSTART_RESERVE_GEN_TPS is a subset of GEN_TPS for generators that |
| 100 | + have gen_can_provide_quickstart_reserves set to True. |
| 101 | +
|
| 102 | + CommitQuickstartReserves[(g,t) in QUICKSTART_RESERVE_GEN_TPS] is a |
| 103 | + decision variable of how much quickstart reserve capacity to commit |
| 104 | + (in MW). |
| 105 | +
|
| 106 | + CommitQuickstartReserves_Limit[(g,t) in SPINNING_RESERVE_GEN_TPS] |
| 107 | + constrain the CommitGenSpinningReserves variables based on CommitSlackUp |
| 108 | + (and CommitGenSpinningReservesSlackUp as applicable). |
| 109 | + |
| 110 | + For example, if discrete unit commitment is enabled, and a 5MW hydro |
| 111 | + generator is fully committed but only providing 2MW of power and 1 MW of |
| 112 | + spinning reserves, the remaining 2MW of capacity (summarized in |
| 113 | + CommitGenSpinningReservesSlackUp) could be committed to quickstart |
| 114 | + reserves. |
| 115 | +
|
| 116 | + CommittedQuickstartReserves[(b,t) in BALANCING_AREA_TIMEPOINTS] is an |
| 117 | + expression summarizing the CommitQuickstartReserves variables for |
| 118 | + generators within each balancing area. |
| 119 | +
|
| 120 | + See also: NREL35QuickstartRequirement defined in the function above. |
| 121 | + """ |
| 122 | + mod.gen_can_provide_quickstart_reserves = Param( |
| 123 | + mod.GENERATION_PROJECTS, |
| 124 | + within=Boolean, |
| 125 | + default=lambda m, g: not (m.gen_is_baseload[g] or m.gen_is_variable[g]), |
| 126 | + doc="Denotes whether a generation project can provide quickstart " |
| 127 | + "reserves. Default to false for baseload & variable generators, " |
| 128 | + "otherwise true. Can be explicitly specified in an input file." |
| 129 | + ) |
| 130 | + mod.QUICKSTART_RESERVE_GEN_TPS = Set( |
| 131 | + dimen=2, |
| 132 | + within=mod.GEN_TPS, |
| 133 | + initialize=lambda m: set( |
| 134 | + (g,t) |
| 135 | + for g in m.GENERATION_PROJECTS |
| 136 | + if m.gen_can_provide_quickstart_reserves[g] |
| 137 | + for t in m.TPS_FOR_GEN[g] |
| 138 | + ) |
| 139 | + ) |
| 140 | + mod.CommitQuickstartReserves = Var( |
| 141 | + mod.QUICKSTART_RESERVE_GEN_TPS, |
| 142 | + within=NonNegativeReals |
| 143 | + ) |
| 144 | + def CommitQuickstartReserves_Limit_rule(m, g, t): |
| 145 | + limit = m.CommitSlackUp[g,t] |
| 146 | + try: |
| 147 | + limit += m.CommitGenSpinningReservesSlackUp[g,t] |
| 148 | + except (AttributeError, KeyError): |
| 149 | + pass |
| 150 | + return (m.CommitQuickstartReserves[g,t] <= limit) |
| 151 | + mod.CommitQuickstartReserves_Limit = Constraint( |
| 152 | + mod.QUICKSTART_RESERVE_GEN_TPS, |
| 153 | + doc="Constrain committed quickstart reserves to uncommited capacity " |
| 154 | + "plus any dispatch slack that is not committed to spinning " |
| 155 | + "reserves (if applicable).", |
| 156 | + rule=CommitQuickstartReserves_Limit_rule |
| 157 | + ) |
| 158 | + |
| 159 | + mod.CommittedQuickstartReserves = Expression( |
| 160 | + mod.BALANCING_AREA_TIMEPOINTS, |
| 161 | + doc="Sum of committed quickstart reserve capacity per balancing " |
| 162 | + "area and timepoint.", |
| 163 | + rule=lambda m, b, t: ( |
| 164 | + sum(m.CommitQuickstartReserves[g, t] |
| 165 | + for z in m.ZONES_IN_BALANCING_AREA[b] |
| 166 | + for g in m.GENS_IN_ZONE[z] |
| 167 | + if (g,t) in m.QUICKSTART_RESERVE_GEN_TPS |
| 168 | + ) |
| 169 | + ) |
| 170 | + ) |
| 171 | + mod.Quickstart_Reserve_Provisions.append('CommittedQuickstartReserves') |
| 172 | + |
| 173 | + # These rules are in a separate function in anticipation of additional |
| 174 | + # rulesets eventually being defined and selectable via command line |
| 175 | + # arguments. |
| 176 | + nrel_3_5_quickstart_reserve_requirements(mod) |
| 177 | + |
| 178 | + |
| 179 | +def define_dynamic_components(mod): |
| 180 | + """ |
| 181 | + Satisfy_Quickstart_Requirement[(b,t) in BALANCING_AREA_TIMEPOINTS] |
| 182 | + is a constraint that ensures quickstart reserve requirements are |
| 183 | + being satisfied based on the sum of the dynamic lists |
| 184 | + Quickstart_Reserve_Requirements & Quickstart_Reserve_Provisions. |
| 185 | + """ |
| 186 | + mod.Satisfy_Quickstart_Requirement = Constraint( |
| 187 | + mod.BALANCING_AREA_TIMEPOINTS, |
| 188 | + rule=lambda m, b, t: ( |
| 189 | + sum(getattr(m, provision)[b,t] |
| 190 | + for provision in m.Quickstart_Reserve_Provisions |
| 191 | + ) >= |
| 192 | + sum(getattr(m, requirement)[b,t] |
| 193 | + for requirement in m.Quickstart_Reserve_Requirements |
| 194 | + ) |
| 195 | + ) |
| 196 | + ) |
| 197 | + |
| 198 | + |
| 199 | +def load_inputs(mod, switch_data, inputs_dir): |
| 200 | + """ |
| 201 | + All files & columns are optional. See notes above for default values. |
| 202 | +
|
| 203 | + generation_projects_info.csv |
| 204 | + GENERATION_PROJECTS, ... gen_can_provide_quickstart_reserves |
| 205 | + """ |
| 206 | + switch_data.load_aug( |
| 207 | + filename=os.path.join(inputs_dir, 'generation_projects_info.csv'), |
| 208 | + auto_select=True, |
| 209 | + optional_params=['gen_can_provide_quickstart_reserves'], |
| 210 | + param=(mod.gen_can_provide_quickstart_reserves) |
| 211 | + ) |
0 commit comments