2d_constraints.gms 176 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
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')
27
        and p_gn(grid, node, 'nodeBalance')
28
        } ..
29
30
31
32

    // 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)
        * [
33
34
            + 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
35
36
37
38
39
40
41
42
            ]

    =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
43
            - p_gn(grid, node, 'selfDischargeLoss')${ gn_state(grid, node) }
44
                * v_state(grid, node, s, f+df_central(f,t), t) // The current state of the node
45
46

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

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

            // Controlled energy transfer, applies when the current node is on the left side of the connection
59
            - sum(gn2n_directional(grid, node, node_),
60
61
62
63
64
65
                + v_transfer(grid, node, node_, s, f, t)
                + [
                    + p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
                    + ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
                    ] // Reduce transfer losses if transfer is from another node to this node
                    * v_transferLeftward(grid, node, node_, s, f, t)
66
67
68
                ) // END sum(node_)

            // Controlled energy transfer, applies when the current node is on the right side of the connection
69
            + sum(gn2n_directional(grid, node_, node),
70
                + v_transfer(grid, node_, node, s, f, t)
71
72
73
74
                - [
                    + p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
                    + ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
                    ] // Reduce transfer losses if transfer is from another node to this node
75
                    * v_transferRightward(grid, node_, node, s, f, t)
76
77
78
79
                ) // END sum(node_)

            // Interactions between the node and its units
            + sum(gnuft(grid, node, unit, f, t),
80
                + v_gen(grid, node, unit, s, f, t) // Unit energy generation and consumption
81
                )
82
83

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

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

            // Dummy generation variables, for feasibility purposes
90
91
            + 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
92
    ) // END * p_stepLength
93
;
94
95

* --- Reserve Demand ----------------------------------------------------------
96
97
// 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.
98

99
100
q_resDemand(restypeDirectionGroup(restype, up_down, group), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
101
        and not [ restypeReleasedForRealization(restype)
102
                  and sft_realized(s, f, t)]
103
        and not restype_inertia(restype)
104
        } ..
105

106
    // Reserve provision by capable units on this group
107
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
108
                                          and gnuRescapable(restype, up_down, grid, node, unit)
109
                                          },
110
        + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
111
            * [ // Account for reliability of reserves
112
113
                + 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)}
114
                ] // END * v_reserve
115
        ) // END sum(gnuft)
116

117
    // Reserve provision from other reserve categories when they can be shared
118
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
119
                                                      and p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
120
                                                      },
121
122
        + 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)
123
            * [ // Account for reliability of reserves
124
125
126
                + 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')
127
                ] // END * v_reserve
128
        ) // END sum(gnuft)
129

130
    // Reserve provision to this group via transfer links
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
136
137
138
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
139
            * 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
140
        ) // END sum(gn2n_directional)
141
142
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
143
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
144
                                                },
145
146
147
148
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
149
            * 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
150
151
152
153
154
        ) // END sum(gn2n_directional)

    =G=

    // Demand for reserves
155
156
    + ts_reserveDemand(restype, up_down, group, f, t)${p_groupReserves(group, restype, 'useTimeSeries')}
    + p_groupReserves(group, restype, up_down)${not p_groupReserves(group, restype, 'useTimeSeries')}
157

158
    // Reserve demand increase because of units
159
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
160
                                          and p_gnuReserves(grid, node, unit, restype, 'reserve_increase_ratio') // Could be better to have 'reserve_increase_ratio' separately for up and down directions
161
                                          },
162
163
        + v_gen(grid, node, unit, s, f, t)
            * p_gnuReserves(grid, node, unit, restype, 'reserve_increase_ratio')
164
165
        ) // END sum(nuft)

166
    // Reserve provisions to other groups via transfer links
167
168
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
169
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
170
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
171
        + v_resTransferRightward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
172
        ) // END sum(gn2n_directional)
173
174
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
175
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
176
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
177
        + v_resTransferLeftward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
178
179
180
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
181
182
    - 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)}
183
;
184

185
186
187
188
* --- 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.

189
190
q_resDemandLargestInfeedUnit(restypeDirectionGroup(restype, 'up', group), unit_fail(unit_), sft(s, f, t))
    ${  ord(t) < tSolveFirst + p_groupReserves(group, restype, 'reserve_length')
191
192
193
        and not [ restypeReleasedForRealization(restype)
            and ft_realized(f, t)
            ]
194
        and sum(gnGroup(grid, node, group), p_gnuReserves(grid, node, unit_, restype, 'portion_of_infeed_to_reserve'))
195
196
        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'
197
        } ..
198

199
200
    // Reserve provision by capable units on this group excluding the failing one
    + sum(gnuft(grid, node, unit, f, t)${ gnGroup(grid, node, group)
201
                                          and gnuRescapable(restype, 'up', grid, node, unit)
202
203
                                          and (ord(unit_) ne ord(unit))
                                          },
204
        + v_reserve(restype, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
205
            * [ // Account for reliability of reserves
206
207
                + 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)}
208
                ] // END * v_reserve
209
210
211
        ) // END sum(nuft)

    // Reserve provision from other reserve categories when they can be shared
212
    + sum((gnuft(grid, node, unit, f, t), restype_)${ gnGroup(grid, node, group)
213
                                                      and p_gnuRes2Res(grid, node, unit, restype_, 'up', restype)
214
215
                                                      and (ord(unit_) ne ord(unit))
                                                      },
216
217
        + 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)
218
            * [ // Account for reliability of reserves
219
220
221
                + 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')
222
                ] // END * v_reserve
223
224
        ) // END sum(nuft)

225
226
227
    // Reserve provision to this group via transfer links
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
228
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node_, node)
229
                                                },
230
231
232
233
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
234
            * 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
235
        ) // END sum(gn2n_directional)
236
237
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
238
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node_, node)
239
                                                },
240
241
242
243
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
244
            * 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
245
246
247
248
        ) // END sum(gn2n_directional)

    =G=

249
    // Demand for reserves due to a large unit that could fail
250
251
    + sum(gnGroup(grid, node, group),
        + v_gen(grid, node, unit_, s, f, t)
252
            * p_gnuReserves(grid, node, unit_, restype, 'portion_of_infeed_to_reserve')
253
        ) // END sum(gnGroup)
254

255
256
257
    // Reserve provisions to other groups via transfer links
    + sum(gn2n_directional(grid, node, node_)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
258
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node, node_)
259
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
260
        + v_resTransferRightward(restype, 'up', grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
261
        ) // END sum(gn2n_directional)
262
263
    + sum(gn2n_directional(grid, node_, node)${ gnGroup(grid, node, group)
                                                and not gnGroup(grid, node_, group)
264
                                                and restypeDirectionGridNodeNode(restype, 'up', grid, node, node_)
265
                                                },   // If trasferring reserves to another node, increase your own reserves by same amount
266
        + v_resTransferLeftward(restype, 'up', grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
267
268
269
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
270
271
    - 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)}
272
;
273

274
275
* --- ROCOF Limit -- Units ----------------------------------------------------

276
277
278
q_rateOfChangeOfFrequencyUnit(group, unit_fail(unit_), sft(s, f, t))
    ${  p_groupPolicy(group, 'defaultFrequency')
        and p_groupPolicy(group, 'ROCOF')
279
        and p_groupPolicy(group, 'dynamicInertia')
280
281
        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'
282
283
        } ..

284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
    // 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)}
299
                            / p_gnu(grid, node, unit, 'unitSize')
300
301
302
                        ] // * p_gnu
                ) // END sum(gnu_output)
            ] // END * p_groupPolicy
303
304
305

    =G=

306
307
308
309
310
311
    // 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)
312
;
313

314
315
316
* --- ROCOF Limit -- Transfer Links -------------------------------------------

q_rateOfChangeOfFrequencyTransfer(group, gn2n(grid, node_, node_fail), sft(s, f, t))
317
318
    ${  p_groupPolicy(group, 'defaultFrequency')
        and p_groupPolicy(group, 'ROCOF')
319
        and p_groupPolicy(group, 'dynamicInertia')
320
321
322
323
324
        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')
              ]
325
326
        } ..

327
328
329
330
331
332
333
334
335
336
337
338
339
340
    // 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)}
341
                            / p_gnu(grid, node, unit, 'unitSize')
342
343
344
                        ] // * p_gnu
                ) // END sum(gnu_output)
            ] // END * p_groupPolicy
345
346
347

    =G=

348
349
350
351
352
    // 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')
353
                * v_transferRightward(grid, node_fail, node_, s, f, t)${gn2n_directional(grid, node_fail, node_)}
354
355
356
357
                * [1
                    - p_gnn(grid, node_fail, node_, 'transferLoss')${not gn2n_timeseries(grid, node_fail, node_, 'transferLoss')}
                    - ts_gnn_(grid, node_fail, node_, 'transferLoss', f, t)${gn2n_timeseries(grid, node_fail, node_, 'transferLoss')}
                    ]
358
            + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
359
                * v_transferLeftward(grid, node_, node_fail, s, f, t)${gn2n_directional(grid, node_, node_fail)}
360
361
362
363
                * [1
                    - p_gnn(grid, node_fail, node_, 'transferLoss')${not gn2n_timeseries(grid, node_fail, node_, 'transferLoss')}
                    - ts_gnn_(grid, node_fail, node_, 'transferLoss', f, t)${gn2n_timeseries(grid, node_fail, node_, 'transferLoss')}
                    ]
364
365
            // Loss of export due to potential interconnector failures
            + p_gnn(grid, node_fail, node_, 'portion_of_transfer_to_reserve')
366
                * v_transferLeftward(grid, node_fail, node_, s, f, t)${gn2n_directional(grid, node_fail, node_)}
367
            + p_gnn(grid, node_, node_fail, 'portion_of_transfer_to_reserve')
368
                * v_transferRightward(grid, node_, node_fail, s, f, t)${gn2n_directional(grid, node_, node_fail)}
369
            ] // END * p_groupPolicy
370
;
371

372
* --- 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
373
374
375
// 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.

376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
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)
392
                                          and gnuRescapable(restype, up_down, grid, node, unit)
393
                                          },
394
        + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
395
            * [ // Account for reliability of reserves
396
397
                + 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)}
398
399
400
401
402
                ] // 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)
403
                                                      and p_gnuRes2Res(grid, node, unit, restype_, up_down, restype)
404
                                                      },
405
406
        + 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)
407
            * [ // Account for reliability of reserves
408
409
410
                + 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')
411
412
413
414
415
416
417
                ] // 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
418
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
419
                                                },
420
421
422
423
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
424
            * v_resTransferRightward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node_, restype, f, t), t)
425
426
427
428
        ) // 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
429
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node_, node)
430
                                                },
431
432
433
434
        + [1
            - p_gnn(grid, node_, node, 'transferLoss')${not gn2n_timeseries(grid, node_, node, 'transferLoss')}
            - ts_gnn_(grid, node_, node, 'transferLoss', f, t)${gn2n_timeseries(grid, node_, node, 'transferLoss')}
            ]
435
            * v_resTransferLeftward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node_, restype, f, t), t)
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
        ) // 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
459
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
460
461
                                                },
          // Reserve transfers to other nodes increase the reserve need of the present node
462
        + v_resTransferRightward(restype, up_down, grid, node, node_, s, f+df_reserves(grid, node, restype, f, t), t)
463
464
465
466
        ) // 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
467
                                                and restypeDirectionGridNodeNode(restype, up_down, grid, node, node_)
468
469
                                                },
          // Reserve transfers to other nodes increase the reserve need of the present node
470
        + v_resTransferLeftward(restype, up_down, grid, node_, node, s, f+df_reserves(grid, node, restype, f, t), t)
471
472
473
474
475
476
        ) // 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
477

478
479
* --- Maximum Downward Capacity -----------------------------------------------

480
q_maxDownward(gnu(grid, node, unit), msft(m, s, f, t))
481
    ${  gnuft(grid, node, unit, f, t)
482
        and (p_gnu(grid, node, unit, 'capacity') or p_gnu(grid, node, unit, 'unitSize'))
483
        and {
484
485
            [   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
486
487
488
489
490
                ]
            // NOTE!!! Could be better to form a gnuft_reserves subset?
            or [ // the unit has an online variable
                uft_online(unit, f, t)
                and [
491
492
                    (unit_minLoad(unit) and gnu_output(grid, node, unit)) // generating units with a min. load
                    or gnu_input(grid, node, unit)                       // or consuming units with an online variable
493
494
495
496
497
498
499
500
                    ]
                ] // END or
            or [ // consuming units with investment possibility
                gnu_input(grid, node, unit)
                and [unit_investLP(unit) or unit_investMIP(unit)]
                ]
        }} ..

501
    // Energy generation/consumption
502
    + v_gen(grid, node, unit, s, f, t)
503
504

    // Downward reserve participation
505
506
507
    - sum(gnuRescapable(restype, 'down', grid, node, unit)${ ord(t) < tSolveFirst + p_gnReserves(grid, node, restype, 'reserve_length')
                                                             and not gnuOfflineRescapable(restype, grid, node, unit)
                                                             },
508
        + 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)
509
510
511
512
513
        ) // 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
514
    + p_gnu(grid, node, unit, 'unitSize')$gnu_output(grid, node, unit)
515
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
516
517
518
519
            + 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
520
521
            + 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
522
523
            ] // END v_online

524
    // Units in run-up phase neet to keep up with the run-up rate
525
    + p_gnu(grid, node, unit, 'unitSize')$gnu_output(grid, node, unit)
526
527
        * 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
528
529
                + [
                    + v_startup_LP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
530
                        ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
531
                    + v_startup_MIP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
532
                        ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
533
                    ]
534
                    * p_uCounter_runUpMin(unit, counter)
535
536
537
538
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)

    // Units in shutdown phase need to keep up with the shutdown rate
539
    + p_gnu(grid, node, unit, 'unitSize')$gnu_output(grid, node, unit)
540
        * sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
541
542
543
544
545
546
            + [
                + 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)) }
                ]
547
                * p_uCounter_shutdownMin(unit, counter)
548
            ) // END sum(shutdownCounter)
549

550
551
    // Consuming units, greater than maxCons
    // Available capacity restrictions
552
    - p_unit(unit, 'availability')$gnu_input(grid, node, unit)
553
554
        * [
            // Capacity factors for flow units
555
            + sum(flowUnit(flow, unit),
556
                + ts_cf_(flow, node, s, f, t)
557
558
559
560
561
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
            // Online capacity restriction
562
            + p_gnu(grid, node, unit, 'capacity')${not uft_online(unit, f, t)} // Use initial maximum if no online variables
563
564
            // !!! TEMPORARY SOLUTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            + [
565
566
                + p_gnu(grid, node, unit, 'unitSize')
                + p_gnu(grid, node, unit, 'capacity')${not p_gnu(grid, node, unit, 'unitSize') > 0}
567
                    / ( p_unit(unit, 'unitCount') + 1${not p_unit(unit, 'unitCount') > 0} )
568
569
                ]
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
570
                * [
571
                    // Capacity online
572
573
                    + 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)}
574
575

                    // Investments to additional non-online capacity
576
577
                    + v_invest_LP(unit)${unit_investLP(unit) and not uft_online(unit, f, t)} // NOTE! v_invest_LP also for consuming units is positive
                    + v_invest_MIP(unit)${unit_investMIP(unit) and not uft_online(unit, f, t)} // NOTE! v_invest_MIP also for consuming units is positive
578
                    ] // END * p_gnu(unitSize)
579
            ] // END * p_unit(availability)
580
;
581

582
583
* --- Maximum Downward Capacity for Production/Consumption, Online Reserves and Offline Reserves ---

Niina Helistö's avatar
Niina Helistö committed
584
q_maxDownwardOfflineReserve(gnu(grid, node, unit), msft(m, s, f, t))
585
    ${  gnuft(grid, node, unit, f, t)
586
        and (p_gnu(grid, node, unit, 'capacity') or p_gnu(grid, node, unit, 'unitSize'))
587
        and {
588
589
            [   ord(t) < tSolveFirst + smax(restype, p_gnReserves(grid, node, restype, 'reserve_length')) // Unit is providing
                and sum(restype, gnuRescapable(restype, 'down', grid, node, unit)) // downward reserves
590
591
592
                ]
        }

593
         and {  sum(restype, gnuOfflineRescapable(restype, grid, node, unit))}  // and it can provide some reserve products although being offline
594
595
596
597
598
599
600

}..

    // Energy generation/consumption
    + v_gen(grid, node, unit, s, f, t)

    // Downward reserve participation
601
602
    - 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)
603
604
605
606
607
608
        ) // END sum(nuRescapable)

    =G= // Must be greater than maximum consumption

    // Consuming units
    // Available capacity restrictions
609
    - p_unit(unit, 'availability')$gnu_input(grid, node, unit) // Consumption units are also restricted by their (available) capacity
610
611
612
        * [
            // Capacity factors for flow units
            + sum(flowUnit(flow, unit),
613
                + ts_cf_(flow, node, s, f, t)
614
615
616
617
618
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
            // Existing capacity
619
            + p_gnu(grid, node, unit, 'capacity')
620
621
            // Investments to new capacity
            + [
622
                + p_gnu(grid, node, unit, 'unitSize')
623
624
                ]
                * [
625
626
                    + v_invest_LP(unit)${unit_investLP(unit)}
                    + v_invest_MIP(unit)${unit_investMIP(unit)}
627
                    ] // END * p_gnu(unitSize)
628
629
630
631
            ] // END * p_unit(availability)

;

632
* --- Maximum Upwards Capacity for Production/Consumption and Online Reserves ---
633

634
q_maxUpward(gnu(grid, node, unit), msft(m, s, f, t))
635
    ${  gnuft(grid, node, unit, f, t)
636
        and (p_gnu(grid, node, unit, 'capacity') or p_gnu(grid, node, unit, 'unitSize'))
637
        and {
638
639
            [   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
640
641
642
643
                ]
            or [
                uft_online(unit, f, t) // or the unit has an online variable
                and [
644
645
                    [unit_minLoad(unit) and gnu_input(grid, node, unit)] // consuming units with min_load
                    or gnu_output(grid, node, unit)                      // generators with an online variable
646
647
648
649
650
651
                    ]
                ]
            or [
                gnu_output(grid, node, unit) // generators with investment possibility
                and (unit_investLP(unit) or unit_investMIP(unit))
                ]
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
652
653
654
             }
                 }..

655

656
    // Energy generation/consumption
657
    + v_gen(grid, node, unit, s, f, t)
658
659

    // Upwards reserve participation
660
661
662
    + sum(gnuRescapable(restype, 'up', grid, node, unit)${ ord(t) < tSolveFirst + p_gnReserves(grid, node, restype, 'reserve_length')
                                                           and not gnuOfflineRescapable(restype, grid, node, unit)
                                                           },
663
        + v_reserve(restype, 'up', grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
664
665
666
667
668
        ) // END sum(nuRescapable)

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

    // Consuming units
669
    - p_gnu(grid, node, unit, 'unitSize')$gnu_input(grid, node, unit)
670
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
671
672
673
674
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
675
676
            + 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)
677
            ] // END * p_gnu(unitSize)
678
679
680

    // Generation units
    // Available capacity restrictions
681
    + p_unit(unit, 'availability')$gnu_output(grid, node, unit) // Generation units are restricted by their (available) capacity
682
683
        * [
            // Capacity factor for flow units
684
            + sum(flowUnit(flow, unit),
685
                + ts_cf_(flow, node, s, f, t)
686
687
688
689
690
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
            // Online capacity restriction
691
692
            + p_gnu(grid, node, unit, 'capacity')${not uft_online(unit, f, t)} // Use initial capacity if no online variables
            + p_gnu(grid, node, unit, 'unitSize')
693
                * [
694
                    // Capacity online
695
696
                    + 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)}
697
698

                    // Investments to non-online capacity
699
700
                    + v_invest_LP(unit)${unit_investLP(unit) and not uft_online(unit, f ,t)}
                    + v_invest_MIP(unit)${unit_investMIP(unit) and not uft_online(unit, f ,t)}
701
                    ] // END * p_gnu(unitSize)
702
            ] // END * p_unit(availability)
703

704
    // Units in run-up phase neet to keep up with the run-up rate
705
    + p_gnu(grid, node, unit, 'unitSize')$gnu_output(grid, node, unit)
706
707
        * 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
708
709
                + [
                    + v_startup_LP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
710
                        ${ uft_onlineLP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
711
                    + v_startup_MIP(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
712
                        ${ uft_onlineMIP_withPrevious(unit, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter)) }
713
                    ]
714
                    * p_uCounter_runUpMax(unit, counter)
715
716
717
718
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)

    // Units in shutdown phase need to keep up with the shutdown rate
719
    + p_gnu(grid, node, unit, 'unitSize')$gnu_output(grid, node, unit)
720
        * sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
721
722
723
724
725
726
            + [
                + 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)) }
                ]
727
                * p_uCounter_shutdownMax(unit, counter)
728
            ) // END sum(shutdownCounter)
729
;
730

731
* --- Maximum Upwards Capacity for Production/Consumption, Online Reserves and Offline Reserves ---
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
732

Niina Helistö's avatar
Niina Helistö committed
733
q_maxUpwardOfflineReserve(gnu(grid, node, unit), msft(m, s, f, t))
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
734
    ${  gnuft(grid, node, unit, f, t)
735
        and (p_gnu(grid, node, unit, 'capacity') or p_gnu(grid, node, unit, 'unitSize'))
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
736
        and {
737
738
            [   ord(t) < tSolveFirst + smax(restype, p_gnReserves(grid, node, restype, 'reserve_length')) // Unit is providing
                and sum(restype, gnuRescapable(restype, 'up', grid, node, unit)) // upward reserves
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
739
740
741
                ]
        }

742
         and {  sum(restype, gnuOfflineRescapable(restype, grid, node, unit))}  // and it can provide some reserve products although being offline
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
743
744
745
746
747
748
749

}..

    // Energy generation/consumption
    + v_gen(grid, node, unit, s, f, t)

    // Upwards reserve participation
750
751
    + 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)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
752
753
        ) // END sum(nuRescapable)

754
    =L= // must be less than available capacity
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
755
756
757

    // Generation units
    // Available capacity restrictions
758
    + p_unit(unit, 'availability')$gnu_output(grid, node, unit) // Generation units are restricted by their (available) capacity
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
759
760
761
        * [
            // Capacity factor for flow units
            + sum(flowUnit(flow, unit),
762
                + ts_cf_(flow, node, s, f, t)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
763
764
765
766
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // END * p_unit(availability)
        * [
767
            // Capacity restriction
768
            + p_gnu(grid, node, unit, 'unitSize')
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
769
                * [
770
771
                    // Existing capacity
                    + p_unit(unit, 'unitCount')
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
772

773
                    // Investments to new capacity
774
775
                    + v_invest_LP(unit)${unit_investLP(unit)}
                    + v_invest_MIP(unit)${unit_investMIP(unit)}
776
                    ] // END * p_gnu(unitSize)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
777
778
779
            ] // END * p_unit(availability)
;

780
781
* --- Reserve Provision of Units with Investments -----------------------------

782
783
784
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)
785
        and (unit_investLP(unit) or unit_investMIP(unit))
786
787
        and not sum(restypeDirectionGridNodeGroup(restype, up_down, grid, node, group),
                    ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t))
788
789
        } ..

790
    + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
791
792
793

    =L=

794
    + p_gnuReserves(grid, node, unit, restype, up_down)
795
        * [
796
            + p_gnu(grid, node, unit, 'capacity')
797
798
799
800
            + v_invest_LP(unit)${unit_investLP(unit)}
                * p_gnu(grid, node, unit, 'unitSize')
            + v_invest_MIP(unit)${unit_investMIP(unit)}
                * p_gnu(grid, node, unit, 'unitSize')
801
            ]
802
803
804
805
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
806
                + ts_cf_(flow, node, s, f, t)
807
808
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
809
810
811
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

812
* --- Online Reserve Provision of Units with Online Variables -----------------
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
813

814
815
816
817
818
q_reserveProvisionOnline(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)
        and not sum(restypeDirectionGridNodeGroup(restype, up_down, grid, node, group),
                    ft_reservesFixed(group, restype, f+df_reservesGroup(group, restype, f, t), t))
819
        and uft_online(unit, f ,t)
820
        and not gnuOfflineRescapable(restype, grid, node, unit)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
821
822
        }..

823
    + v_reserve(restype, up_down, grid, node, unit, s, f+df_reserves(grid, node, restype, f, t), t)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
824
825
826

    =L=

827
    + p_gnuReserves(grid, node, unit, restype, up_down)
828
        * p_gnu(grid, node, unit, 'unitSize')
829
830
831
        * [
            + 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)}
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
832
833
834
835
836
            ]
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
837
                + ts_cf_(flow, node, s, f, t)
Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
838
839
840
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
841

Ciara O'Dwyer's avatar
Ciara O'Dwyer committed
842
843
844
;


845
846
* --- Unit Startup and Shutdown -----------------------------------------------

847
848
849
850
q_startshut(ms(m, s), uft_online(unit, f, t))
    ${  msft(m, s, f, t)
        }..

851
    // Units currently online
852
853
    + 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)}
854
855

    // Units previously online
856
    // The same units
857
    - 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))
858
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
859
    - 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))
860
861
862
863
                                                             and not uft_aggregator_first(unit, f, t) }

    // Aggregated units just before they are turned into aggregator units
    - sum(unit_${unitAggregator_unit(unit, unit_)},
864
865
        + 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))}
866
        )${uft_aggregator_first(unit, f, t)} // END sum(unit_)
867

868
869
    =E=

870
    // Unit startup and shutdown
871

872
    // Add startup of units dt_toStartup before the current t (no start-ups for aggregator units before they become active)
873
    + sum(unitStarttype(unit, starttype),
874
        + v_startup_LP(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))
875
            ${ uft_onlineLP_withPrevious(unit, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t)) }
876
        + v_startup_MIP(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))
877
            ${ uft_onlineMIP_withPrevious(unit, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t)) }
878
        )${not [unit_aggregator(unit) and ord(t) + dt_toStartup(unit, t) <= tSolveFirst + p_unit(unit, 'lastStepNotAggregated')]} // END sum(starttype)
879

880
881
882
883
    // 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
884

885
    // Shutdown of units at time t
886
887
888
889
    - v_shutdown_LP(unit, s, f, t)
        ${ uft_onlineLP(unit, f, t) }
    - v_shutdown_MIP(unit, s, f, t)
        ${ uft_onlineMIP(unit, f, t) }
890
;
891

892
*--- Startup Type -------------------------------------------------------------
893
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
894
895
896
// 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.
897

898
899
900
901
q_startuptype(ms(m, s), starttypeConstrained(starttype), uft_online(unit, f, t))
    ${  msft(m, s, f, t)
        and unitStarttype(unit, starttype)
        } ..
902
903

    // Startup type
904
905
    + 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) }
906
907
908
909

    =L=

    // Subunit shutdowns within special startup timeframe
910
911
912
913
914
915
916
    + 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)) }
917
918
919
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
920
;
921

922

923
924
*--- Online Limits with Startup Type Constraints and Investments --------------

925
926
927
928
929
930
931
932
933
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)
        }} ..

934
    // Online variables
935
936
    + 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)}
937
938
939
940
941
942

    =L=

    // Number of existing units
    + p_unit(unit, 'unitCount')

943
    // Number of units unable to become online due to restrictions
944
945
946
947
948
949
950
    - sum(unitCounter(unit, counter)${  dt_downtimeUnitCounter(unit, counter)
                                        and t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))
                                        },
        + v_shutdown_LP(unit, s, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
            ${ uft_onlineLP_withPrevious(unit, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1)) }
        + v_shutdown_MIP(unit, s, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
            ${ uft_onlineMIP_withPrevious(unit, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1)) }
951
952
953
        ) // END sum(counter)

    // Number of units unable to become online due to restrictions (aggregated units in the past horizon or if they have an online variable)
954
    - sum(unitAggregator_unit(unit, unit_),
955
956
957
958
959
960
961
        + sum(unitCounter(unit, counter)${  dt_downtimeUnitCounter(unit, counter)
                                            and t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))
                                            },
            + v_shutdown_LP(unit_, s, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
                ${ uft_onlineLP_withPrevious(unit_, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1)) }
            + v_shutdown_MIP(unit_, s, f+df(f,t