2d_constraints.gms 168 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ontext
This file is part of Backbone.

Backbone is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Backbone is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with Backbone.  If not, see <http://www.gnu.org/licenses/>.
$offtext

18

19
* =============================================================================
20
* --- Constraint Equation Definitions -----------------------------------------
21
22
23
24
* =============================================================================

* --- Energy Balance ----------------------------------------------------------

25
26
27
q_balance(gn(grid, node), msft(m, s, f, t)) // Energy/power balance dynamics solved using implicit Euler discretization
    ${  not p_gn(grid, node, 'boundAll')
        } ..
28
29
30
31

    // The left side of the equation is the change in the state (will be zero if the node doesn't have a state)
    + p_gn(grid, node, 'energyStoredPerUnitOfState')${gn_state(grid, node)} // Unit conversion between v_state of a particular node and energy variables (defaults to 1, but can have node based values if e.g. v_state is in Kelvins and each node has a different heat storage capacity)
        * [
32
33
            + v_state(grid, node, s, f+df_central(f,t), t)                   // The difference between current
            - v_state(grid, node, s+ds_state(grid,node,s,t), f+df(f,t+dt(t)), t+dt(t))       // ... and previous state of the node
34
35
36
37
38
39
40
41
            ]

    =E=

    // The right side of the equation contains all the changes converted to energy terms
    + p_stepLength(m, f, t) // Multiply with the length of the timestep to convert power into energy
        * (
            // Self discharge out of the model boundaries
42
            - p_gn(grid, node, 'selfDischargeLoss')${ gn_state(grid, node) }
43
                * v_state(grid, node, s, f+df_central(f,t), t) // The current state of the node
44
45

            // Energy diffusion from this node to neighbouring nodes
46
            - sum(gnn_state(grid, node, to_node),
47
                + p_gnn(grid, node, to_node, 'diffCoeff')
48
                    * v_state(grid, node, s, f+df_central(f,t), t)
49
50
51
                ) // END sum(to_node)

            // Energy diffusion from neighbouring nodes to this node
52
            + sum(gnn_state(grid, from_node, node),
53
                + p_gnn(grid, from_node, node, 'diffCoeff')
54
                    * v_state(grid, from_node, s, f+df_central(f,t), t) // Incoming diffusion based on the state of the neighbouring node
55
56
57
                ) // END sum(from_node)

            // Controlled energy transfer, applies when the current node is on the left side of the connection
58
            - sum(gn2n_directional(grid, node, node_),
59
                + (1 - p_gnn(grid, node, node_, 'transferLoss')) // Reduce transfer losses
60
                    * v_transfer(grid, node, node_, s, f, t)
61
                + p_gnn(grid, node, node_, 'transferLoss') // Add transfer losses back if transfer is from this node to another node
62
                    * v_transferRightward(grid, node, node_, s, f, t)
63
64
65
                ) // END sum(node_)

            // Controlled energy transfer, applies when the current node is on the right side of the connection
66
            + sum(gn2n_directional(grid, node_, node),
67
                + v_transfer(grid, node_, node, s, f, t)
68
                - p_gnn(grid, node_, node, 'transferLoss') // Reduce transfer losses if transfer is from another node to this node
69
                    * v_transferRightward(grid, node_, node, s, f, t)
70
71
72
73
                ) // END sum(node_)

            // Interactions between the node and its units
            + sum(gnuft(grid, node, unit, f, t),
74
                + v_gen(grid, node, unit, s, f, t) // Unit energy generation and consumption
75
                )
76
77

            // Spilling energy out of the endogenous grids in the model
78
            - v_spill(grid, node, s, f, t)${node_spill(node)}
79
80

            // Power inflow and outflow timeseries to/from the node
81
            + ts_influx_(grid, node, f, t, s)   // Incoming (positive) and outgoing (negative) absolute value time series
82
83

            // Dummy generation variables, for feasibility purposes
84
85
            + vq_gen('increase', grid, node, s, f, t) // Note! When stateSlack is permitted, have to take caution with the penalties so that it will be used first
            - vq_gen('decrease', grid, node, s, f, t) // Note! When stateSlack is permitted, have to take caution with the penalties so that it will be used first
86
    ) // END * p_stepLength
87
;
88
89

* --- Reserve Demand ----------------------------------------------------------
90
91
// NOTE! Currently, there are multiple identical instances of the reserve balance equation being generated for each forecast branch even when the reserves are committed and identical between the forecasts.
// NOTE! This could be solved by formulating a new "ft_reserves" set to cover only the relevant forecast-time steps, but it would possibly make the reserves even more confusing.
92

93
94
q_resDemand(restypeDirectionGroup(restype, up_down, group), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
95
        and not [ restypeReleasedForRealization(restype)
96
                  and sft_realized(s, f, t)]
97
        } ..
98

99
    // Reserve provision by capable units on this group
100
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
101
                                          and gnuRescapable(restype, up_down, grid, node, unit)
102
                                          },
103
        + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
104
            * [ // Account for reliability of reserves
105
106
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
107
                ] // END * v_reserve
108
        ) // END sum(gnuft)
109

110
    // Reserve provision from other reserve categories when they can be shared
111
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
112
                                                      and p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
113
                                                      },
114
115
        + v_reserve(restype_, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype_, f, t), t)
            * p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
116
            * [ // Account for reliability of reserves
117
118
119
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
                    * p_gnuReserves(grid, node, unit, restype_, 'reserveReliability')
120
                ] // END * v_reserve
121
        ) // END sum(gnuft)
122

123
    // Reserve provision to this group via transfer links
124
125
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
126
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
127
                                                },
128
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
129
            * v_resTransferRightward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
130
        ) // END sum(gn2n_directional)
131
132
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
133
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
134
                                                },
135
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
136
            * v_resTransferLeftward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
137
138
139
140
141
        ) // END sum(gn2n_directional)

    =G=

    // Demand for reserves
142
143
    + ts_reserveDemand(restype, up_down, group, f, t)${p_groupReserves(group, restype, 'use_time_series')}
    + p_groupReserves(group, restype, up_down)${not p_groupReserves(group, restype, 'use_time_series')}
144

145
    // Reserve demand increase because of units
146
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
147
                                          and p_gnuReserves(grid, node, unit, restype, 'reserve_increase_ratio') // Could be better to have 'reserve_increase_ratio' separately for up and down directions
148
                                          },
149
150
        + v_gen(grid, node, unit, s, f, t)
            * p_gnuReserves(grid, node, unit, restype, 'reserve_increase_ratio')
151
152
        ) // END sum(nuft)

153
    // Reserve provisions to other groups via transfer links
154
155
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
156
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
157
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
158
        + v_resTransferRightward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
159
        ) // END sum(gn2n_directional)
160
161
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
162
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
163
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
164
        + v_resTransferLeftward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
165
166
167
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
168
169
    - vq_resDemand(restype, up_down, group, s, f+df_reservesGroup(group, restype, f, t), t)
    - vq_resMissing(restype, up_down, group, s, f+df_reservesGroup(group, restype, f, t), t)${ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t)}
170
;
171

172
173
174
175
* --- N-1 Reserve Demand ----------------------------------------------------------
// NOTE! Currently, there are multiple identical instances of the reserve balance equation being generated for each forecast branch even when the reserves are committed and identical between the forecasts.
// NOTE! This could be solved by formulating a new "ft_reserves" set to cover only the relevant forecast-time steps, but it would possibly make the reserves even more confusing.

176
177
q_resDemandLargestInfeedUnit(restypeDirectionGroup(restype, 'up', group), unit_fail(unit_), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
178
179
180
        and not [ restypeReleasedForRealization(restype)
            and ft_realized(f, t)
            ]
181
        and sum(gnGroup(grid, node, group), p_gnuReserves(grid, node, unit_, restype, 'portion_of_infeed_to_reserve'))
182
183
        and uft(unit_, f, t) // only active units
        and sum(gnGroup(grid, node, group), gnu_output(grid, node, unit_)) // only units with output capacity 'inside the group'
184
        } ..
185

186
187
    // Reserve provision by capable units on this group excluding the failing one
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
188
                                          and gnuRescapable(restype, 'up', grid, node, unit)
189
190
                                          and (ord(unit_) ne ord(unit))
                                          },
191
        + v_reserve(restype, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
192
            * [ // Account for reliability of reserves
193
194
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
195
                ] // END * v_reserve
196
197
198
        ) // END sum(nuft)

    // Reserve provision from other reserve categories when they can be shared
199
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
200
                                                      and p_gnuRes2Res(grid, node, unit, restype_, 'up', restype)
201
202
                                                      and (ord(unit_) ne ord(unit))
                                                      },
203
204
        + v_reserve(restype_, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype_, f, t), t)
            * p_gnuRes2Res(grid, node, unit, restype_, 'up', restype)
205
            * [ // Account for reliability of reserves
206
207
208
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
                    * p_gnuReserves(grid, node, unit, restype_, 'reserveReliability')
209
                ] // END * v_reserve
210
211
        ) // END sum(nuft)

212
213
214
    // Reserve provision to this group via transfer links
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
215
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node_, node)
216
                                                },
217
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
218
            * v_resTransferRightward(restype, 'up', grid, node_, node, s, f+df_reserves(grid, node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
219
        ) // END sum(gn2n_directional)
220
221
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
222
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node_, node)
223
                                                },
224
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
225
            * v_resTransferLeftward(restype, 'up', grid, node, node_, s, f+df_reserves(grid, node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
226
227
228
229
        ) // END sum(gn2n_directional)

    =G=

230
    // Demand for reserves due to a large unit that could fail
231
232
    + sum(gnGroup(grid, node, group),
        + v_gen(grid, node, unit_, s, f, t)
233
            * p_gnuReserves(grid, node, unit_, restype, 'portion_of_infeed_to_reserve')
234
        ) // END sum(gnGroup)
235

236
237
238
    // Reserve provisions to other groups via transfer links
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
239
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node, node_)
240
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
241
        + v_resTransferRightward(restype, 'up', grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
242
        ) // END sum(gn2n_directional)
243
244
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
245
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node, node_)
246
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
247
        + v_resTransferLeftward(restype, 'up', grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
248
249
250
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
251
252
    - vq_resDemand(restype, 'up', group, s, f+df_reservesGroup(group, restype, f, t), t)
    - vq_resMissing(restype, 'up', group, s, f+df_reservesGroup(group, restype, f, t), t)${ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t)}
253
;
254

255
256
* --- ROCOF Limit -- Units ----------------------------------------------------

257
258
259
q_rateOfChangeOfFrequencyUnit(group, unit_fail(unit_), sft(s, f, t))
    ${  p_groupPolicy(group, 'defaultFrequency')
        and p_groupPolicy(group, 'ROCOF')
260
261
        and uft(unit_, f, t) // only active units
        and sum(gnGroup(grid, node, group), gnu_output(grid, node, unit_)) // only units with output capacity 'inside the group'
262
263
        } ..

264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
    // Kinetic/rotational energy in the system
    + p_groupPolicy(group, 'ROCOF')*2
        * [
            + sum(gnu_output(grid, node, unit)${   ord(unit) ne ord(unit_)
                                                   and gnGroup(grid, node, group)
                                                   and gnuft(grid, node, unit, f, t)
                                                   },
                + p_gnu(grid, node, unit, 'inertia')
                    * p_gnu(grid ,node, unit, 'unitSizeMVA')
                    * [
                        + v_online_LP(unit, s, f+df_central(f,t), t)
                            ${uft_onlineLP(unit, f, t)}
                        + v_online_MIP(unit, s, f+df_central(f,t), t)
                            ${uft_onlineMIP(unit, f, t)}
                        + v_gen(grid, node, unit, s, f, t)${not uft_online(unit, f, t)}
                            / p_gnu(grid, node, unit, 'unitSizeGen')
                        ] // * p_gnu
                ) // END sum(gnu_output)
            ] // END * p_groupPolicy
283
284
285

    =G=

286
287
288
289
290
291
    // Demand for kinetic/rotational energy due to a large unit that could fail
    + p_groupPolicy(group, 'defaultFrequency')
        * sum(gnu_output(grid, node, unit_)${   gnGroup(grid, node, group)
                                                },
            + v_gen(grid, node, unit_ , s, f, t)
            ) // END sum(gnu_output)
292
;
293

294
295
296
* --- ROCOF Limit -- Transfer Links -------------------------------------------

q_rateOfChangeOfFrequencyTransfer(group, gn2n(grid, node_, node_fail), sft(s, f, t))
297
298
299
300
301
302
303
    ${  p_groupPolicy(group, 'defaultFrequency')
        and p_groupPolicy(group, 'ROCOF')
        and gnGroup(grid, node_, group) // only interconnectors where one end is 'inside the group'
        and not gnGroup(grid, node_fail, group) // and the other end is 'outside the group'
        and [ p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
              or p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
              ]
304
305
        } ..

306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
    // Kinetic/rotational energy in the system
    + p_groupPolicy(group, 'ROCOF')*2
        * [
            + sum(gnu_output(grid, node, unit)${   gnGroup(grid, node, group)
                                                   and gnuft(grid, node, unit, f, t)
                                                   },
                + p_gnu(grid, node, unit, 'inertia')
                    * p_gnu(grid ,node, unit, 'unitSizeMVA')
                    * [
                        + v_online_LP(unit, s, f+df_central(f,t), t)
                            ${uft_onlineLP(unit, f, t)}
                        + v_online_MIP(unit, s, f+df_central(f,t), t)
                            ${uft_onlineMIP(unit, f, t)}
                        + v_gen(grid, node, unit, s, f, t)${not uft_online(unit, f, t)}
                            / p_gnu(grid, node, unit, 'unitSizeGen')
                        ] // * p_gnu
                ) // END sum(gnu_output)
            ] // END * p_groupPolicy
324
325
326

    =G=

327
328
329
330
331
    // Demand for kinetic/rotational energy due to a large interconnector that could fail
    + p_groupPolicy(group, 'defaultFrequency')
        * [
            // Loss of import due to potential interconnector failures
            + p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
332
                * v_transferRightward(grid, node_fail, node_, s, f, t)${gn2n_directional(grid, node_fail, node_)}
333
334
                * (1 - p_gnn(grid, node_fail, node_, 'transferLoss') )
            + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
335
                * v_transferLeftward(grid, node_, node_fail, s, f, t)${gn2n_directional(grid, node_, node_fail)}
336
337
338
                * (1 - p_gnn(grid, node_, node_fail, 'transferLoss') )
            // Loss of export due to potential interconnector failures
            + p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
339
                * v_transferLeftward(grid, node_fail, node_, s, f, t)${gn2n_directional(grid, node_fail, node_)}
340
            + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
341
                * v_transferRightward(grid, node_, node_fail, s, f, t)${gn2n_directional(grid, node_, node_fail)}
342
            ] // END * p_groupPolicy
343
;
344

345
* --- N-1 Upward reserve demand due to a possibility that an interconnector that is transferring power to the node group fails -------------------------------------------------
346
347
348
// NOTE! Currently, there are multiple identical instances of the reserve balance equation being generated for each forecast branch even when the reserves are committed and identical between the forecasts.
// NOTE! This could be solved by formulating a new "ft_reserves" set to cover only the relevant forecast-time steps, but it would possibly make the reserves even more confusing.

349
350
q_resDemandLargestInfeedTransferUp(restypeDirectionGroup(restype, 'up', group), gn2n(grid, node_, node_fail), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
351
352
        and not [ restypeReleasedForRealization(restype)
                  and sft_realized(s, f, t)]
353
354
355
        and gn2n_directional(grid, node_, node_fail)
        and [ (gnGroup(grid, node_, group) and not gnGroup(grid, node_fail, group)) // only interconnectors where one end is 'inside the group'
              or (gnGroup(grid, node_fail, group) and not gnGroup(grid, node_, group)) // and the other end is 'outside the group'
356
              ]
357
358
359
360
        and [ p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
              or p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
              ]
        and p_groupReserves3D(group, restype, 'up', 'LossOfTrans')
361
362
        } ..

363
364
    // Reserve provision by capable units on this group
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
365
                                          and gnuRescapable(restype, 'up', grid, node, unit)
366
                                          },
367
        + v_reserve(restype, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
368
            * [ // Account for reliability of reserves
369
370
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
371
                ] // END * v_reserve
372
        ) // END sum(gnuft)
373
374

    // Reserve provision from other reserve categories when they can be shared
375
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
376
                                                      and p_gnuRes2Res(grid, node, unit, restype_, 'up', restype)
377
                                                      },
378
379
        + v_reserve(restype_, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype_, f, t), t)
            * p_gnuRes2Res(grid, node, unit, restype_, 'up', restype)
380
            * [ // Account for reliability of reserves
381
382
383
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
                    * p_gnuReserves(grid, node, unit, restype_, 'reserveReliability')
384
                ] // END * v_reserve
385
        ) // END sum(gnuft)
386

387
388
389
390
    // Reserve provision to this group via transfer links
    + sum(gn2n_directional(grid, from_node, to_node)${ gnGroup(grid, to_node, group)
                                                       and not gnGroup(grid, from_node, group)
                                                       and not (from_node(node_) and to_node(node_fail)) // excluding the failing link
391
                                                       and restypeDirectionGridNodeNode(restype, 'up', grid, from_node, to_node)
392
393
                                                       },
        + (1 - p_gnn(grid, from_node, to_node, 'transferLoss') )
394
            * v_resTransferRightward(restype, 'up', grid, from_node, to_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
395
        ) // END sum(gn2n_directional)
396
397
398
    + sum(gn2n_directional(grid, to_node, from_node)${ gnGroup(grid, to_node, group)
                                                       and not gnGroup(grid, from_node, group)
                                                       and not (to_node(node_) and from_node(node_fail)) // excluding the failing link
399
                                                       and restypeDirectionGridNodeNode(restype, 'up', grid, from_node, to_node)
400
401
                                                       },
        + (1 - p_gnn(grid, to_node, from_node, 'transferLoss') )
402
            * v_resTransferLeftward(restype, 'up', grid, to_node, from_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
403
404
405
406
        ) // END sum(gn2n_directional)

    =G=

407
408
409
410
411
    // Demand for upward reserve due to potential interconnector failures (sudden loss of import)
    + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_fail, group)}
        * v_transferRightward(grid, node_, node_fail, s, f, t) // multiply with efficiency?
    + p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_, group)}
        * v_transferLeftward(grid, node_, node_fail, s, f, t) // multiply with efficiency?
ran li's avatar
ran li committed
412

413
414
415
416
    // Reserve provisions to other groups via transfer links
    + sum(gn2n_directional(grid, from_node, to_node)${ gnGroup(grid, from_node, group)
                                                       and not gnGroup(grid, to_node, group)
                                                       and not (from_node(node_) and to_node(node_fail)) // excluding the failing link
417
                                                       and restypeDirectionGridNodeNode(restype, 'up', grid, from_node, to_node)
418
                                                       },
ran li's avatar
ran li committed
419
          // Reserve transfers to other nodes increase the reserve need of the present node
420
        + v_resTransferRightward(restype, 'up', grid, from_node, to_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
ran li's avatar
ran li committed
421
        ) // END sum(gn2n_directional)
422
423
424
    + sum(gn2n_directional(grid, to_node, from_node)${ gnGroup(grid, from_node, group)
                                                       and not gnGroup(grid, to_node, group)
                                                       and not (to_node(node_) and from_node(node_fail)) // excluding the failing link
425
                                                       and restypeDirectionGridNodeNode(restype, 'up', grid, from_node, to_node)
426
                                                       },
ran li's avatar
ran li committed
427
          // Reserve transfers to other nodes increase the reserve need of the present node
428
        + v_resTransferLeftward(restype, 'up', grid, to_node, from_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
ran li's avatar
ran li committed
429
430
431
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
432
433
    - vq_resDemand(restype, 'up', group, s, f+df_reservesGroup(group, restype, f, t), t)
    - vq_resMissing(restype, 'up', group, s, f+df_reservesGroup(group, restype, f, t), t)${ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t)}
ran li's avatar
ran li committed
434
435
;

436
* --- N-1 Downward reserve demand due to a possibility that an interconnector that is transferring power from the node group fails -------------------------------------------------
ran li's avatar
ran li committed
437
438
439
// NOTE! Currently, there are multiple identical instances of the reserve balance equation being generated for each forecast branch even when the reserves are committed and identical between the forecasts.
// NOTE! This could be solved by formulating a new "ft_reserves" set to cover only the relevant forecast-time steps, but it would possibly make the reserves even more confusing.

440
441
q_resDemandLargestInfeedTransferDown(restypeDirectionGroup(restype, 'down', group), gn2n(grid, node_, node_fail), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
ran li's avatar
ran li committed
442
443
        and not [ restypeReleasedForRealization(restype)
                  and sft_realized(s, f, t)]
444
445
446
        and gn2n_directional(grid, node_, node_fail)
        and [ (gnGroup(grid, node_, group) and not gnGroup(grid, node_fail, group)) // only interconnectors where one end is 'inside the group'
              or (gnGroup(grid, node_fail, group) and not gnGroup(grid, node_, group)) // and the other end is 'outside the group'
447
              ]
448
449
450
451
        and [ p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
              or p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
              ]
        and p_groupReserves3D(group, restype, 'down', 'LossOfTrans')
ran li's avatar
ran li committed
452
453
        } ..

454
455
    // Reserve provision by capable units on this group
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
456
                                          and gnuRescapable(restype, 'down', grid, node, unit)
457
                                          },
458
        + v_reserve(restype, 'down', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
ran li's avatar
ran li committed
459
            * [ // Account for reliability of reserves
460
461
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
ran li's avatar
ran li committed
462
                ] // END * v_reserve
463
        ) // END sum(gnuft)
ran li's avatar
ran li committed
464
465

    // Reserve provision from other reserve categories when they can be shared
466
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
467
                                                      and p_gnuRes2Res(grid, node, unit, restype_, 'down', restype)
468
                                                      },
469
470
        + v_reserve(restype_, 'down', grid, node, unit, s, f+df_reserves(grid, node, restype_, f, t), t)
            * p_gnuRes2Res(grid, node, unit, restype_, 'down', restype)
ran li's avatar
ran li committed
471
            * [ // Account for reliability of reserves
472
473
474
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
                    * p_gnuReserves(grid, node, unit, restype_, 'reserveReliability')
ran li's avatar
ran li committed
475
                ] // END * v_reserve
476
        ) // END sum(gnuft)
ran li's avatar
ran li committed
477

478
479
480
481
    // Reserve provision to this group via transfer links
    + sum(gn2n_directional(grid, from_node, to_node)${ gnGroup(grid, to_node, group)
                                                       and not gnGroup(grid, from_node, group)
                                                       and not (from_node(node_) and to_node(node_fail)) // excluding the failing link
482
                                                       and restypeDirectionGridNodeNode(restype, 'down', grid, from_node, to_node)
483
484
                                                       },
        + (1 - p_gnn(grid, from_node, to_node, 'transferLoss') )
485
            * v_resTransferRightward(restype, 'down', grid, from_node, to_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
ran li's avatar
ran li committed
486
        ) // END sum(gn2n_directional)
487
488
489
    + sum(gn2n_directional(grid, to_node, from_node)${ gnGroup(grid, to_node, group)
                                                       and not gnGroup(grid, from_node, group)
                                                       and not (to_node(node_) and from_node(node_fail)) // excluding the failing link
490
                                                       and restypeDirectionGridNodeNode(restype, 'down', grid, from_node, to_node)
491
492
                                                       },
        + (1 - p_gnn(grid, to_node, from_node, 'transferLoss') )
493
            * v_resTransferLeftward(restype, 'down', grid, to_node, from_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
ran li's avatar
ran li committed
494
495
496
497
        ) // END sum(gn2n_directional)

    =G=

498
499
500
501
502
    // Demand for downward reserve due to potential interconnector failures (sudden loss of export)
    + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_, group)}
        * v_transferRightward(grid, node_, node_fail, s, f, t)
    + p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_fail, group)}
        * v_transferLeftward(grid, node_, node_fail, s, f, t)
503

504
505
506
507
    // Reserve provisions to other groups via transfer links
    + sum(gn2n_directional(grid, from_node, to_node)${ gnGroup(grid, from_node, group)
                                                       and not gnGroup(grid, to_node, group)
                                                       and not (from_node(node_) and to_node(node_fail)) // excluding the failing link
508
                                                       and restypeDirectionGridNodeNode(restype, 'down', grid, from_node, to_node)
509
                                                       },
510
          // Reserve transfers to other nodes increase the reserve need of the present node
511
        + v_resTransferRightward(restype, 'down', grid, from_node, to_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
512
        ) // END sum(gn2n_directional)
513
514
515
    + sum(gn2n_directional(grid, to_node, from_node)${ gnGroup(grid, from_node, group)
                                                       and not gnGroup(grid, to_node, group)
                                                       and not (to_node(node_) and from_node(node_fail)) // excluding the failing link
516
                                                       and restypeDirectionGridNodeNode(restype, 'down', grid, from_node, to_node)
517
                                                       },
518
          // Reserve transfers to other nodes increase the reserve need of the present node
519
        + v_resTransferLeftward(restype, 'down', grid, to_node, from_node, s, f+df_reserves(grid, from_node, restype, f, t), t)
520
521
522
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
523
524
    - vq_resDemand(restype, 'down', group, s, f+df_reservesGroup(group, restype, f, t), t)
    - vq_resMissing(restype, 'down', group, s, f+df_reservesGroup(group, restype, f, t), t)${ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t)}
525
;
526

527
* --- N-1 reserve demand due to a possibility that an interconnector that is transferring power to/from the node group fails -------------------------------------------------
ran li's avatar
ran li committed
528
529
530
// NOTE! Currently, there are multiple identical instances of the reserve balance equation being generated for each forecast branch even when the reserves are committed and identical between the forecasts.
// NOTE! This could be solved by formulating a new "ft_reserves" set to cover only the relevant forecast-time steps, but it would possibly make the reserves even more confusing.

531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
q_resDemandLargestInfeedTransfer(restypeDirectionGroup(restype, up_down, group), gn2n(grid, node_left, node_right), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
                  and sft_realized(s, f, t)]
        and gn2n_directional(grid, node_left, node_right)
        and [ (gnGroup(grid, node_left, group) and not gnGroup(grid, node_right, group)) // only interconnectors where one end is 'inside the group'
              or (gnGroup(grid, node_right, group) and not gnGroup(grid, node_left, group)) // and the other end is 'outside the group'
              ]
        and [ p_gnn(grid, node_left, node_right, 'portion_of_transfer_to_reserve')
              or p_gnn(grid, node_right, node_left, 'portion_of_transfer_to_reserve')
              ]
        and p_groupReserves3D(group, restype, up_down, 'LossOfTrans')
        } ..

    // Reserve provision by capable units on this group
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
547
                                          and gnuRescapable(restype, up_down, grid, node, unit)
548
                                          },
549
        + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
550
            * [ // Account for reliability of reserves
551
552
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
553
554
555
556
557
                ] // END * v_reserve
        ) // END sum(gnuft)

    // Reserve provision from other reserve categories when they can be shared
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
558
                                                      and p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
559
                                                      },
560
561
        + v_reserve(restype_, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype_, f, t), t)
            * p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
562
            * [ // Account for reliability of reserves
563
564
565
                + 1${sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_gnuReserves(grid, node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(grid, node, restype, f, t), t)}
                    * p_gnuReserves(grid, node, unit, restype_, 'reserveReliability')
566
567
568
569
570
571
572
                ] // END * v_reserve
        ) // END sum(gnuft)

    // Reserve provision to this group via transfer links
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
                                                and not (sameas(node_, node_left) and sameas(node, node_right)) // excluding the failing link
573
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
574
575
                                                },
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
576
            * v_resTransferRightward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node_, restype, f, t), t)
577
578
579
580
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
                                                and not (sameas(node, node_left) and sameas(node_, node_right)) // excluding the failing link
581
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
582
583
                                                },
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
584
            * v_resTransferLeftward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node_, restype, f, t), t)
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
        ) // END sum(gn2n_directional)

    =G=

    // Demand for upward reserve due to potential interconnector failures (sudden loss of import)
    + [
        + p_gnn(grid, node_left, node_right, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_right, group)}
            * v_transferRightward(grid, node_left, node_right, s, f, t) // multiply with efficiency?
        + p_gnn(grid, node_right, node_left, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_left, group)}
            * v_transferLeftward(grid, node_left, node_right, s, f, t) // multiply with efficiency?
        ]${sameas(up_down, 'up')}
    // Demand for downward reserve due to potential interconnector failures (sudden loss of export)
    + [
        + p_gnn(grid, node_left, node_right, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_left, group)}
            * v_transferRightward(grid, node_left, node_right, s, f, t)
        + p_gnn(grid, node_right, node_left, 'portion_of_transfer_to_reserve')${gnGroup(grid, node_right, group)}
            * v_transferLeftward(grid, node_left, node_right, s, f, t)
        ]${sameas(up_down, 'down')}

    // Reserve provisions to other groups via transfer links
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
                                                and not (sameas(node, node_left) and sameas(node_, node_right)) // excluding the failing link
608
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
609
610
                                                },
          // Reserve transfers to other nodes increase the reserve need of the present node
611
        + v_resTransferRightward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
612
613
614
615
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
                                                and not (sameas(node_, node_left) and sameas(node, node_right)) // excluding the failing link
616
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
617
618
                                                },
          // Reserve transfers to other nodes increase the reserve need of the present node
619
        + v_resTransferLeftward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
620
621
622
623
624
625
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
    - vq_resDemand(restype, up_down, group, s, f+df_reservesGroup(group, restype, f, t), t)
    - vq_resMissing(restype, up_down, group, s, f+df_reservesGroup(group, restype, f, t), t)${ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t)}
;
ran li's avatar
ran li committed
626

627
628
* --- Maximum Downward Capacity -----------------------------------------------

629
q_maxDownward(gnu(grid, node, unit), msft(m, s, f, t))
630
631
    ${  gnuft(grid, node, unit, f, t)
        and {
632
633
            [   ord(t) < tSolveFirst + smax(restype, p_gnReserves(grid, node, restype, 'reserve_length')) // Unit is either providing
                and sum(restype, gnuRescapable(restype, 'down', grid, node, unit)) // downward reserves
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
                ]
            // NOTE!!! Could be better to form a gnuft_reserves subset?
            or [ // the unit has an online variable
                uft_online(unit, f, t)
                and [
                    (unit_minLoad(unit) and p_gnu(grid, node, unit, 'unitSizeGen')) // generators with a min. load
                    or p_gnu(grid, node, unit, 'maxCons') // or consuming units with an online variable
                    ]
                ] // END or
            or [ // consuming units with investment possibility
                gnu_input(grid, node, unit)
                and [unit_investLP(unit) or unit_investMIP(unit)]
                ]
        }} ..

649
    // Energy generation/consumption
650
    + v_gen(grid, node, unit, s, f, t)
651
652

    // Considering output constraints (e.g. cV line)
653
654
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
655
            * v_gen(grid_output, node_, unit, s, f, t)
656
657
658
        ) // END sum(gngnu_constrainedOutputRatio)

    // Downward reserve participation
659
660
    - sum(gnuRescapable(restype, 'down', grid, node, unit)${ord(t) < tSolveFirst + p_gnReserves(grid, node, restype, 'reserve_length')},
        + v_reserve(restype, 'down', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
661
662
663
664
665
666
        ) // END sum(nuRescapable)

    =G= // Must be greater than minimum load or maximum consumption  (units with min-load and both generation and consumption are not allowed)

    // Generation units, greater than minload
    + p_gnu(grid, node, unit, 'unitSizeGen')
667
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
668
669
670
671
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [ // Online variables should only be generated for units with restrictions
672
673
            + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f+df_central(f,t), t)} // LP online variant
            + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f+df_central(f,t), t)} // MIP online variant
674
675
            ] // END v_online

676
677
678
679
    // Units in run-up phase neet to keep up with the run-up rate
    + p_gnu(grid, node, unit, 'unitSizeGen')
        * sum(unitStarttype(unit, starttype)${uft_startupTrajectory(unit, f, t)},
            sum(runUpCounter(unit, counter)${t_active(t+dt_trajectory(counter))}, // Sum over the run-up intervals
680
681
                + [
                    + v_startup_LP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
682
                        ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
683
                    + v_startup_MIP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
684
                        ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
685
                    ]
686
                    * p_uCounter_runUpMin(unit, counter)
687
688
689
690
691
692
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)

    // Units in shutdown phase need to keep up with the shutdown rate
    + p_gnu(grid, node, unit, 'unitSizeGen')
        * sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
693
694
695
696
697
698
            + [
                + v_shutdown_LP(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
                + v_shutdown_MIP(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
                ]
699
                * p_uCounter_shutdownMin(unit, counter)
700
            ) // END sum(shutdownCounter)
701

702
703
704
705
706
    // Consuming units, greater than maxCons
    // Available capacity restrictions
    - p_unit(unit, 'availability')
        * [
            // Capacity factors for flow units
707
            + sum(flowUnit(flow, unit),
708
                + ts_cf_(flow, node, f, t, s)
709
710
711
712
713
714
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
            // Online capacity restriction
            + p_gnu(grid, node, unit, 'maxCons')${not uft_online(unit, f, t)} // Use initial maximum if no online variables
715
716
717
718
            // !!! TEMPORARY SOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            + [
                + p_gnu(grid, node, unit, 'unitSizeCons')
                + p_gnu(grid, node, unit, 'maxCons')${not p_gnu(grid, node, unit, 'unitSizeCons') > 0}
719
                    / ( p_unit(unit, 'unitCount') + 1${not p_unit(unit, 'unitCount') > 0} )
720
721
                ]
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
722
                * [
723
                    // Capacity online
724
725
                    + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
                    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
726
727
728
729
730
731
732
733

                    // Investments to additional non-online capacity
                    + sum(t_invest(t_)${    ord(t_)<=ord(t)
                                            and not uft_online(unit, f, t)
                                            },
                        + v_invest_LP(unit, t_)${unit_investLP(unit)} // NOTE! v_invest_LP also for consuming units is positive
                        + v_invest_MIP(unit, t_)${unit_investMIP(unit)} // NOTE! v_invest_MIP also for consuming units is positive
                        ) // END sum(t_invest)
734
735
                    ] // END * p_gnu(unitSizeCons)
            ] // END * p_unit(availability)
736
;
737
738
739

* --- Maximum Upwards Capacity ------------------------------------------------

740
q_maxUpward(gnu(grid, node, unit), msft(m, s, f, t))
741
742
    ${  gnuft(grid, node, unit, f, t)
        and {
743
744
            [   ord(t) < tSolveFirst + smax(restype, p_gnReserves(grid, node, restype, 'reserve_length')) // Unit is either providing
                and sum(restype, gnuRescapable(restype, 'up', grid, node, unit)) // upward reserves
745
746
747
748
749
750
751
752
753
754
755
756
757
758
                ]
            or [
                uft_online(unit, f, t) // or the unit has an online variable
                and [
                    [unit_minLoad(unit) and p_gnu(grid, node, unit, 'unitSizeCons')] // consuming units with min_load
                    or [p_gnu(grid, node, unit, 'maxGen')]                          // generators with an online variable
                    ]
                ]
            or [
                gnu_output(grid, node, unit) // generators with investment possibility
                and (unit_investLP(unit) or unit_investMIP(unit))
                ]
        }}..

759
    // Energy generation/consumption
760
    + v_gen(grid, node, unit, s, f, t)
761
762
763
764

    // Considering output constraints (e.g. cV line)
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
765
            * v_gen(grid_output, node_, unit, s, f, t)
766
767
768
        ) // END sum(gngnu_constrainedOutputRatio)

    // Upwards reserve participation
769
770
    + sum(gnuRescapable(restype, 'up', grid, node, unit)${ord(t) < tSolveFirst + p_gnReserves(grid, node, restype, 'reserve_length')},
        + v_reserve(restype, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
771
772
773
774
775
        ) // END sum(nuRescapable)

    =L= // must be less than available/online capacity

    // Consuming units
776
    - p_gnu(grid, node, unit, 'unitSizeCons')
777
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
778
779
780
781
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
782
783
            + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)} // Consuming units are restricted by their min. load (consuming is negative)
            + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)} // Consuming units are restricted by their min. load (consuming is negative)
784
785
786
787
788
789
790
            ] // END * p_gnu(unitSizeCons)

    // Generation units
    // Available capacity restrictions
    + p_unit(unit, 'availability') // Generation units are restricted by their (available) capacity
        * [
            // Capacity factor for flow units
791
            + sum(flowUnit(flow, unit),
792
                + ts_cf_(flow, node, f, t, s)
793
794
795
796
797
798
799
800
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
            // Online capacity restriction
            + p_gnu(grid, node, unit, 'maxGen')${not uft_online(unit, f, t)} // Use initial maxGen if no online variables
            + p_gnu(grid, node, unit, 'unitSizeGen')
                * [
801
                    // Capacity online
802
803
                    + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f ,t)}
                    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
804
805
806
807
808
809
810
811

                    // Investments to non-online capacity
                    + sum(t_invest(t_)${    ord(t_)<=ord(t)
                                            and not uft_online(unit, f ,t)
                                            },
                        + v_invest_LP(unit, t_)${unit_investLP(unit)}
                        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
                        ) // END sum(t_invest)
812
813
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
814

815
816
817
818
    // Units in run-up phase neet to keep up with the run-up rate
    + p_gnu(grid, node, unit, 'unitSizeGen')
        * sum(unitStarttype(unit, starttype)${uft_startupTrajectory(unit, f, t)},
            sum(runUpCounter(unit, counter)${t_active(t+dt_trajectory(counter))}, // Sum over the run-up intervals
819
820
                + [
                    + v_startup_LP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
821
                        ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
822
                    + v_startup_MIP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
823
                        ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
824
                    ]
825
                    * p_uCounter_runUpMax(unit, counter)
826
827
828
829
830
831
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)

    // Units in shutdown phase need to keep up with the shutdown rate
    + p_gnu(grid, node, unit, 'unitSizeGen')
        * sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
832
833
834
835
836
837
            + [
                + v_shutdown_LP(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
                + v_shutdown_MIP(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
                ]
838
                * p_uCounter_shutdownMax(unit, counter)
839
            ) // END sum(shutdownCounter)
840
;
841

842
843
* --- Reserve Provision of Units with Investments -----------------------------

844
845
846
q_reserveProvision(gnuRescapable(restypeDirectionGridNode(restype, up_down, grid, node), unit), sft(s, f, t))
    ${  ord(t) <= tSolveFirst + p_gnReserves(grid, node, restype, 'reserve_length')
        and gnuft(grid, node, unit, f, t)
847
        and (unit_investLP(unit) or unit_investMIP(unit))
848
849
        and not sum(restypeDirectionGridNodeGroup(restype, up_down, grid, node, group),
                    ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t))
850
851
        } ..

852
    + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
853
854
855

    =L=

856
    + p_gnuReserves(grid, node, unit, restype, up_down)
857
        * [
858
            + [ p_gnu(grid, node, unit, 'maxGen') + p_gnu(grid, node, unit, 'maxCons') ]
859
860
            + sum(t_invest(t_)${ ord(t_)<=ord(t) },
                + v_invest_LP(unit, t_)${unit_investLP(unit)}
861
                    * p_gnu(grid, node, unit, 'unitSizeTot')
862
                + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
863
                    * p_gnu(grid, node, unit, 'unitSizeTot')
864
865
                ) // END sum(t_)
            ]
866
867
868
869
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
870
                + ts_cf_(flow, node, f, t, s)
871
872
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
873
874
875
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

876
877
* --- Unit Startup and Shutdown -----------------------------------------------

878
879
880
881
q_startshut(ms(m, s), uft_online(unit, f, t))
    ${  msft(m, s, f, t)
        }..

882
    // Units currently online
883
884
    + v_online_LP (unit, s, f+df_central(f,t), t)${uft_onlineLP (unit, f, t)}
    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
885
886

    // Units previously online
887
    // The same units
888
    - v_online_LP (unit, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))${ uft_onlineLP_withPrevious(unit, f+df(f,t+dt(t)), t+dt(t))
889
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
890
    - v_online_MIP(unit, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))${ uft_onlineMIP_withPrevious(unit, f+df(f,t+dt(t)), t+dt(t))
891
892
893
894
                                                             and not uft_aggregator_first(unit, f, t) }

    // Aggregated units just before they are turned into aggregator units
    - sum(unit_${unitAggregator_unit(unit, unit_)},
895
896
        + v_online_LP (unit_, s, f+df(f,t+dt(t)), t+dt(t))${uft_onlineLP_withPrevious(unit_, f+df(f,t+dt(t)), t+dt(t))}
        + v_online_MIP(unit_, s, f+df(f,t+dt(t)), t+dt(t))${uft_onlineMIP_withPrevious(unit_, f+df(f,t+dt(t)), t+dt(t))}
897
        )${uft_aggregator_first(unit, f, t)} // END sum(unit_)
898

899
900
    =E=

901
    // Unit startup and shutdown
902

903
    // Add startup of units dt_toStartup before the current t (no start-ups for aggregator units before they become active)
904
    + sum(unitStarttype(unit, starttype),
905
        + v_startup_LP(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))
906
            ${ uft_onlineLP_withPrevious(unit, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t)) }
907
        + v_startup_MIP(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))
908
            ${ uft_onlineMIP_withPrevious(unit, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t)) }
909
        )${not [unit_aggregator(unit) and ord(t) + dt_toStartup(unit, t) <= tSolveFirst + p_unit(unit, 'lastStepNotAggregated')]} // END sum(starttype)
910

911
912
913
914
    // NOTE! According to 3d_setVariableLimits,
    // cannot start a unit if the time when the unit would become online is outside
    // the horizon when the unit has an online variable
    // --> no need to add start-ups of aggregated units to aggregator units
915

916
    // Shutdown of units at time t
917
918
919
920
    - v_shutdown_LP(unit, s, f, t)
        ${ uft_onlineLP(unit, f, t) }
    - v_shutdown_MIP(unit, s, f, t)
        ${ uft_onlineMIP(unit, f, t) }
921
;
922

923
*--- Startup Type -------------------------------------------------------------
924
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
925
926
927
// This formulation doesn't work as intended when unitCount > 1, as one recent
// shutdown allows for multiple hot/warm startups on subsequent time steps.
// Pending changes.
928

929
930
931
932
q_startuptype(ms(m, s), starttypeConstrained(starttype), uft_online(unit, f, t))
    ${  msft(m, s, f, t)
        and unitStarttype(unit, starttype)
        } ..
933
934

    // Startup type
935
936
    + v_startup_LP(unit, starttype, s, f, t)${ uft_onlineLP(unit, f, t) }
    + v_startup_MIP(unit, starttype, s, f, t)${ uft_onlineMIP(unit, f, t) }
937
938
939
940

    =L=

    // Subunit shutdowns within special startup timeframe
941
942
943
944
945
946
947
    + sum(unitCounter(unit, counter)${  dt_starttypeUnitCounter(starttype, unit, counter)
                                        and t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))
                                        },
        + v_shutdown_LP(unit, s, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))
            ${ uft_onlineLP_withPrevious(unit, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)) }
        + v_shutdown_MIP(unit, s, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))
            ${ uft_onlineMIP_withPrevious(unit, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)) }
948
949
950
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
951
;
952

953

954
955
*--- Online Limits with Startup Type Constraints and Investments --------------

956
957
958
959
960
961
962
963
964
q_onlineLimit(ms(m, s), uft_online(unit, f, t))
    ${  msft(m, s, f, t)
        and {
            p_unit(unit, 'minShutdownHours')
            or p_u_runUpTimeIntervals(unit)
            or unit_investLP(unit)
            or unit_investMIP(unit)
        }} ..

965
    // Online variables