2d_constraints.gms 97.2 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
q_balance(gn(grid, node), msft(m, s, f, t))${   not p_gn(grid, node, 'boundAll')
26
27
28
29
30
                                            } .. // Energy/power balance dynamics solved using implicit Euler discretization

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

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

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

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

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

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

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

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

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

            // Dummy generation variables, for feasibility purposes
83
84
            + 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
85
    ) // END * p_stepLength
86
;
87
88

* --- Reserve Demand ----------------------------------------------------------
89
90
// 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.
91

92
q_resDemand(restypeDirectionNode(restype, up_down, node), sft(s, f, t))
93
94
    ${  ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
95
                  and sft_realized(s, f, t)]
96
        } ..
97
98
    // Reserve provision by capable units on this node
    + sum(nuft(node, unit, f, t)${nuRescapable(restype, up_down, node, unit)},
99
        + v_reserve(restype, up_down, node, unit, s, f+df_reserves(node, restype, f, t), t)
100
101
102
103
            * [ // Account for reliability of reserves
                + 1${sft_realized(s, f+df_reserves(node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_nuReserves(node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(node, restype, f, t), t)}
                ] // END * v_reserve
104
105
        ) // END sum(nuft)

106
    // Reserve provision from other reserve categories when they can be shared
107
    + sum((nuft(node, unit, f, t), restype_)${p_nuRes2Res(node, unit, restype_, up_down, restype)},
108
        + v_reserve(restype_, up_down, node, unit, s, f+df_reserves(node, restype_, f, t), t)
109
            * p_nuRes2Res(node, unit, restype_, up_down, restype)
110
111
112
113
114
            * [ // Account for reliability of reserves
                + 1${sft_realized(s, f+df_reserves(node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_nuReserves(node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(node, restype, f, t), t)}
                    * p_nuReserves(node, unit, restype_, 'reserveReliability')
                ] // END * v_reserve
115
116
        ) // END sum(nuft)

117
    // Reserve provision to this node via transfer links
118
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, up_down, node_, node)},
119
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
120
            * v_resTransferRightward(restype, up_down, node_, node, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
121
        ) // END sum(gn2n_directional)
122
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, up_down, node_, node)},
123
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
124
            * v_resTransferLeftward(restype, up_down, node, node_, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
125
126
127
128
129
130
131
132
        ) // END sum(gn2n_directional)

    =G=

    // Demand for reserves
    + ts_reserveDemand_(restype, up_down, node, f, t)${p_nReserves(node, restype, 'use_time_series')}
    + p_nReserves(node, restype, up_down)${not p_nReserves(node, restype, 'use_time_series')}

133
134
    // Reserve demand increase because of units
    + sum(nuft(node, unit, f, t)${p_nuReserves(node, unit, restype, 'reserve_increase_ratio')}, // Could be better to have 'reserve_increase_ratio' separately for up and down directions
135
        + sum(gnu(grid, node, unit), v_gen(grid, node, unit, s, f, t)) // Reserve sets and variables are currently lacking the grid dimension...
136
137
138
            * p_nuReserves(node, unit, restype, 'reserve_increase_ratio')
        ) // END sum(nuft)

139
    // Reserve provisions to another nodes via transfer links
140
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, up_down, node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
141
        + v_resTransferRightward(restype, up_down, node, node_, s, f+df_reserves(node, restype, f, t), t)
142
        ) // END sum(gn2n_directional)
143
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, up_down, node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
144
        + v_resTransferLeftward(restype, up_down, node_, node, s, f+df_reserves(node, restype, f, t), t)
145
146
147
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
148
149
    - vq_resDemand(restype, up_down, node, s, f+df_reserves(node, restype, f, t), t)
    - vq_resMissing(restype, up_down, node, s, f+df_reserves(node, restype, f, t), t)${ft_reservesFixed(node, restype, f+df_reserves(node, restype, f, t), t)}
150
;
151

152
153
154
155
* --- 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.

156
q_resDemandLargestInfeedUnit(grid, restypeDirectionNode(restype, 'up', node), unit_fail(unit_), sft(s, f, t))
157
158
159
160
    ${  ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
            and ft_realized(f, t)
            ]
161
        and p_nuReserves(node, unit_, restype, 'portion_of_infeed_to_reserve')
162
163
164
        } ..
    // Reserve provision by capable units on this node excluding the failing one
    + sum(nuft(node, unit, f, t)${nuRescapable(restype, 'up', node, unit) and (ord(unit_) ne ord(unit))},
165
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
166
167
168
169
            * [ // Account for reliability of reserves
                + 1${sft_realized(s, f+df_reserves(node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_nuReserves(node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(node, restype, f, t), t)}
                ] // END * v_reserve
170
171
172
        ) // END sum(nuft)

    // Reserve provision from other reserve categories when they can be shared
173
    + sum((nuft(node, unit, f, t), restype_)${p_nuRes2Res(node, unit, restype_, 'up', restype)},
174
        + v_reserve(restype_, 'up', node, unit, s, f+df_reserves(node, restype_, f, t), t)
175
            * p_nuRes2Res(node, unit, restype_, 'up', restype)
176
177
178
179
180
            * [ // Account for reliability of reserves
                + 1${sft_realized(s, f+df_reserves(node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
                + p_nuReserves(node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(node, restype, f, t), t)}
                    * p_nuReserves(node, unit, restype_, 'reserveReliability')
                ] // END * v_reserve
181
182
183
184
185
        ) // END sum(nuft)

    // Reserve provision to this node via transfer links
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, 'up', node_, node)},
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
186
            * v_resTransferRightward(restype, 'up', node_, node, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
187
188
189
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, 'up', node_, node)},
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
190
            * v_resTransferLeftward(restype, 'up', node, node_, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
191
192
193
194
195
        ) // END sum(gn2n_directional)

    =G=

    // Demand for reserves of the failing one
196
    v_gen(grid,node,unit_,s,f,t) * p_nuReserves(node, unit_, restype, 'portion_of_infeed_to_reserve')
197
198
199

    // Reserve provisions to another nodes via transfer links
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, 'up', node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
200
        + v_resTransferRightward(restype, 'up', node, node_, s, f+df_reserves(node, restype, f, t), t)
201
202
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, 'up', node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
203
        + v_resTransferLeftward(restype, 'up', node_, node, s, f+df_reserves(node, restype, f, t), t)
204
205
206
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
207
208
    - vq_resDemand(restype, 'up', node, s, f+df_reserves(node, restype, f, t), t)
    - vq_resMissing(restype, 'up', node, s, f+df_reserves(node, restype, f, t), t)${ft_reservesFixed(node, restype, f+df_reserves(node, restype, f, t), t)}
209
;
210
211
* --- Maximum Downward Capacity -----------------------------------------------

212
213
214
q_maxDownward(m, s, gnuft(grid, node, unit, f, t))${msft(m, s, f, t)
                                                    and {
                                                    [   ord(t) < tSolveFirst + smax(restype, p_nReserves(node, restype, 'reserve_length')) // Unit is either providing
215
216
217
218
219
220
221
222
223
224
225
226
227
228
                                                        and sum(restype, nuRescapable(restype, 'down', node, unit)) // downward reserves
                                                        ]
                                                    // 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)]
                                                        ]
229
                                                    }} ..
230
    // Energy generation/consumption
231
    + v_gen(grid, node, unit, s, f, t)
232
233

    // Considering output constraints (e.g. cV line)
234
235
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
236
            * v_gen(grid_output, node_, unit, s, f, t)
237
238
239
        ) // END sum(gngnu_constrainedOutputRatio)

    // Downward reserve participation
240
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
241
        + v_reserve(restype, 'down', node, unit, s, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
242
243
244
245
246
247
        ) // 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')
248
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
249
250
251
252
            + 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
253
254
            + 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
255
256
            ] // END v_online

257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
    // 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
                + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    * p_uCounter_runUp(unit, counter)
                ) // 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
            + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                * p_uCounter_shutdown(unit, counter)
            ) // END sum(shutdownCounter)
272

273
274
275
276
277
    // Consuming units, greater than maxCons
    // Available capacity restrictions
    - p_unit(unit, 'availability')
        * [
            // Capacity factors for flow units
278
            + sum(flowUnit(flow, unit),
279
                + ts_cf_(flow, node, f, t, s)
280
281
282
283
284
285
286
287
                ) // 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
            + p_gnu(grid, node, unit, 'unitSizeCons')
                * [
288
                    // Capacity online
289
290
                    + 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)}
291
292
293
294
295
296
297
298

                    // 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)
299
300
                    ] // END * p_gnu(unitSizeCons)
            ] // END * p_unit(availability)
301
;
302
303
304

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

305
306
307
q_maxUpward(m, s, gnuft(grid, node, unit, f, t))${msft(m, s, f, t)
                                                    and {
                                                 [   ord(t) < tSolveFirst + smax(restype, p_nReserves(node, restype, 'reserve_length')) // Unit is either providing
308
309
310
311
312
313
314
315
316
317
318
319
320
                                                    and sum(restype, nuRescapable(restype, 'up', node, unit)) // upward reserves
                                                    ]
                                                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))
                                                    ]
321
                                                }}..
322
    // Energy generation/consumption
323
    + v_gen(grid, node, unit, s, f, t)
324
325
326
327

    // Considering output constraints (e.g. cV line)
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
328
            * v_gen(grid_output, node_, unit, s, f, t)
329
330
331
        ) // END sum(gngnu_constrainedOutputRatio)

    // Upwards reserve participation
332
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
333
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
334
335
336
337
338
339
        ) // END sum(nuRescapable)

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

    // Consuming units
    + p_gnu(grid, node, unit, 'unitSizeCons')
340
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
341
342
343
344
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
345
346
            + 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)
347
348
349
350
351
352
353
            ] // 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
354
            + sum(flowUnit(flow, unit),
355
                + ts_cf_(flow, node, f, t, s)
356
357
358
359
360
361
362
363
                ) // 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')
                * [
364
                    // Capacity online
365
366
                    + 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)}
367
368
369
370
371
372
373
374

                    // 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)
375
376
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
377

378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
    // 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
                + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    * [
                        + p_uCounter_runUp(unit, counter)
                        + ( 1 - p_uCounter_runUp(unit, counter) )${ not runUpCounter(unit, counter+1) } // Final run-up phase can potentially ramp over the min load
                            * ( p_u_runUpTimeIntervalsCeil(unit) - p_u_runUpTimeIntervals(unit) )
                        ]
                ) // 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
            + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                * p_uCounter_shutdown(unit, counter)
            ) // END sum(shutdownCounter)
397
;
398

399
400
* --- Reserve Provision of Units with Investments -----------------------------

401
402
403
404
405
406
q_reserveProvision(nuRescapable(restypeDirectionNode(restype, up_down, node), unit), sft(s, f, t))${ord(t) <= tSolveFirst + p_nReserves(node, restype, 'reserve_length')
                                                                                                    and nuft(node, unit, f, t)
                                                                                                    and (unit_investLP(unit) or unit_investMIP(unit))
                                                                                                    and not ft_reservesFixed(node, restype, f+df_reserves(node, restype, f, t), t)
                                                                                                   } ..
    + v_reserve(restype, up_down, node, unit, s, f+df_reserves(node, restype, f, t), t)
407
408
409
410
411
412
413
414
415
416
417
418
419

    =L=

    + p_nuReserves(node, unit, restype, up_down)
        * [
            + sum(grid, p_gnu(grid, node, unit, 'maxGen') + p_gnu(grid, node, unit, 'maxCons') )  // Reserve sets and variables are currently lacking the grid dimension...
            + sum(t_invest(t_)${ ord(t_)<=ord(t) },
                + v_invest_LP(unit, t_)${unit_investLP(unit)}
                    * sum(grid, p_gnu(grid, node, unit, 'unitSizeTot')) // Reserve sets and variables are currently lacking the grid dimension...
                + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
                    * sum(grid, p_gnu(grid, node, unit, 'unitSizeTot')) // Reserve sets and variables are currently lacking the grid dimension...
                ) // END sum(t_)
            ]
420
421
422
423
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
424
                + ts_cf_(flow, node, f, t, s)
425
426
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
427
428
429
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

430
431
* --- Unit Startup and Shutdown -----------------------------------------------

432
q_startshut(m, s, uft_online(unit, f, t))$msft(m, s, f, t) ..
433
    // Units currently online
434
435
    + 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)}
436
437

    // Units previously online
438
439

    // The same units
440
    - 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))
441
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
442
    - 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))
443
444
445
446
                                                             and not uft_aggregator_first(unit, f, t) }

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

451
452
    =E=

453
    // Unit startup and shutdown
454

455
    // Add startup of units dt_toStartup before the current t (no start-ups for aggregator units before they become active)
456
    + sum(unitStarttype(unit, starttype),
457
        + v_startup(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))
458
        )${not [unit_aggregator(unit) and ord(t) + dt_toStartup(unit, t) <= tSolveFirst + p_unit(unit, 'lastStepNotAggregated')]} // END sum(starttype)
459

460
461
462
463
    // 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
464

465
    // Shutdown of units at time t
466
    - v_shutdown(unit, s, f, t)
467
;
468

469
*--- Startup Type -------------------------------------------------------------
470
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
471
472
473
// 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.
474

475
476
q_startuptype(m, s, starttypeConstrained(starttype), uft_online(unit, f, t))
    ${msft(m, s, f, t) and unitStarttype(unit, starttype)} ..
477
478

    // Startup type
479
    + v_startup(unit, starttype, s, f, t)
480
481
482
483

    =L=

    // Subunit shutdowns within special startup timeframe
Topi Rasku's avatar
Topi Rasku committed
484
    + sum(unitCounter(unit, counter)${dt_starttypeUnitCounter(starttype, unit, counter)},
485
        + v_shutdown(unit, s, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))
Topi Rasku's avatar
Topi Rasku committed
486
            ${t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))}
487
488
489
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
490
;
491

492

493
494
*--- Online Limits with Startup Type Constraints and Investments --------------

495
496
q_onlineLimit(m, s, uft_online(unit, f, t))${msft(m, s, f, t) and {
                                            p_unit(unit, 'minShutdownHours')
497
                                            or p_u_runUpTimeIntervals(unit)
498
499
                                            or unit_investLP(unit)
                                            or unit_investMIP(unit)
500
                                            }} ..
501
    // Online variables
502
503
    + 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)}
504
505
506
507
508
509

    =L=

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

510
    // Number of units unable to become online due to restrictions
Topi Rasku's avatar
Topi Rasku committed
511
    - sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
512
        + v_shutdown(unit, s, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
Topi Rasku's avatar
Topi Rasku committed
513
            ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
514
515
516
        ) // 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)
517
    - sum(unitAggregator_unit(unit, unit_),
Topi Rasku's avatar
Topi Rasku committed
518
        + sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
519
            + v_shutdown(unit_, s, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
Topi Rasku's avatar
Topi Rasku committed
520
                ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
521
522
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
523
524
525

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
526
527
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
528
529
530
        ) // END sum(t_invest)
;

531
532
533
534
*--- Both q_offlineAfterShutdown and q_onlineOnStartup work when there is only one unit.
*    These equations prohibit single units turning on and off at the same time step.
*    Unfortunately there seems to be no way to prohibit this when unit count is > 1.
*    (it shouldn't be worthwhile anyway if there is a startup cost, but it can fall within the solution gap).
535
536
q_onlineOnStartUp(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
537
538

    // Units currently online
539
540
    + 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)}
541
542
543
544

    =G=

    + sum(unitStarttype(unit, starttype),
545
        + v_startup(unit, starttype, s, f+df(f,t+dt_toStartup(unit, t)), t+dt_toStartup(unit, t))  //dt_toStartup displaces the time step to the one where the unit would be started up in order to reach online at t
546
547
548
      ) // END sum(starttype)
;

549
550
q_offlineAfterShutdown(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
551

552
553
554
555
556
    // Number of existing units
    + p_unit(unit, 'unitCount')

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
557
558
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
559
560
        ) // END sum(t_invest)

561
    // Units currently online
562
563
    - 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)}
564
565
566

    =G=

567
    + v_shutdown(unit, s, f, t)
568
569
;

570
571
*--- Minimum Unit Uptime ------------------------------------------------------

572
573
q_onlineMinUptime(m, s, uft_online(unit, f, t))
    ${msft(m, s, f, t) and  p_unit(unit, 'minOperationHours')} ..
574
575

    // Units currently online
576
577
    + 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)}
578
579
580
581

    =G=

    // Units that have minimum operation time requirements active
Topi Rasku's avatar
Topi Rasku committed
582
    + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
583
        + sum(unitStarttype(unit, starttype),
584
            + v_startup(unit, starttype, s, f+df(f,t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1)), t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))
Topi Rasku's avatar
Topi Rasku committed
585
                ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
586
            ) // END sum(starttype)
587
588
589
        ) // END sum(counter)

    // Units that have minimum operation time requirements active (aggregated units in the past horizon or if they have an online variable)
Topi Rasku's avatar
Topi Rasku committed
590
591
    + sum(unitAggregator_unit(unit, unit_),
        + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
592
            + sum(unitStarttype(unit, starttype),
593
                + v_startup(unit, starttype, s, f+df(f,t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1)), t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))
Topi Rasku's avatar
Topi Rasku committed
594
                    ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
595
596
597
                ) // END sum(starttype)
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
598
599
;

600
* --- Ramp Constraints --------------------------------------------------------
601
602
603
604

q_genRamp(m, s, gnuft_ramp(grid, node, unit, f, t))${  ord(t) > msStart(m, s) + 1
                                                       and msft(m, s, f, t)
                                                       } ..
605

606
    + v_genRamp(grid, node, unit, s, f, t) * p_stepLength(m, f, t)
607

608
    =E=
609

610
    // Change in generation over the interval: v_gen(t) - v_gen(t-1)
611
    + v_gen(grid, node, unit, s, f, t)
612

613
    // Unit generation at t-1 (except aggregator units right before the aggregation threshold, see next term)
614
    - v_gen(grid, node, unit, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))${not uft_aggregator_first(unit, f, t)}
615
616
    // Unit generation at t-1, aggregator units right before the aggregation threshold
    + sum(unit_${unitAggregator_unit(unit, unit_)},
617
        - v_gen(grid, node, unit_, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))
618
      )${uft_aggregator_first(unit, f, t)}
619
;
620

621
* --- Ramp Up Limits ----------------------------------------------------------
622
623
624
625

q_rampUpLimit(m, s, gnuft_ramp(grid, node, unit, f, t))${  ord(t) > msStart(m, s) + 1
                                                           and msft(m, s, f, t)
                                                           and p_gnu(grid, node, unit, 'maxRampUp')
626
627
628
629
630
                                                           and [ sum(restype, nuRescapable(restype, 'up', node, unit))
                                                                 or uft_online(unit, f, t)
                                                                 or unit_investLP(unit)
                                                                 or unit_investMIP(unit)
                                                                 ]
631
                                                           } ..
632
    + v_genRamp(grid, node, unit, s, f, t)
633
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
634
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
635
636
637
638
639
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =L=

640
    // Ramping capability of units without an online variable
641
642
643
644
645
646
647
648
649
650
651
652
    + (
        + ( p_gnu(grid, node, unit, 'maxGen') + p_gnu(grid, node, unit, 'maxCons') )${not uft_online(unit, f, t)}
        + sum(t_invest(t_)${ ord(t_)<=ord(t) },
            + v_invest_LP(unit, t_)${not uft_onlineLP(unit, f, t) and unit_investLP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
            + v_invest_MIP(unit, t_)${not uft_onlineMIP(unit, f, t) and unit_investMIP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
          )
      )
        * p_gnu(grid, node, unit, 'maxRampUp')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

653
    // Ramping capability of units with an online variable
654
    + (
655
656
        + 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)}
657
658
659
660
661
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampUp')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

662
663
664
665
666
667
668
669
670
671
672
673
674
    // Units in the run-up phase need to keep up with the run-up rate
    + p_gnu(grid, node, unit, 'unitSizeTot')
        * 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
                + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    * [
                        + p_unit(unit, 'rampSpeedToMinLoad')
                        + ( p_gnu(grid, node, unit, 'maxRampUp') - p_unit(unit, 'rampSpeedToMinLoad') )${ not runUpCounter(unit, counter+1) } // Ramp speed adjusted for the last run-up interval
                            * ( p_u_runUpTimeIntervalsCeil(unit) - p_u_runUpTimeIntervals(unit) )
                        ]
                    * 60 // Unit conversion from [p.u./min] into [p.u./h]
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)
675

676
    // Shutdown of consumption units from full load
677
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
678
        * p_gnu(grid, node, unit, 'unitSizeTot')
679
;
680

681
* --- Ramp Down Limits --------------------------------------------------------
682
683
684
685

q_rampDownLimit(m, s, gnuft_ramp(grid, node, unit, f, t))${  ord(t) > msStart(m, s) + 1
                                                             and msft(m, s, f, t)
                                                             and p_gnu(grid, node, unit, 'maxRampDown')
686
687
688
689
690
                                                             and [ sum(restype, nuRescapable(restype, 'down', node, unit))
                                                                   or uft_online(unit, f, t)
                                                                   or unit_investLP(unit)
                                                                   or unit_investMIP(unit)
                                                                   ]
691
                                                             } ..
692
    + v_genRamp(grid, node, unit, s, f, t)
693
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
694
        + v_reserve(restype, 'down', node, unit, s, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
695
696
697
698
699
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =G=

700
    // Ramping capability of units without online variable
701
702
703
704
705
706
707
708
709
710
711
712
    - (
        + ( p_gnu(grid, node, unit, 'maxGen') + p_gnu(grid, node, unit, 'maxCons') )${not uft_online(unit, f, t)}
        + sum(t_invest(t_)${ ord(t_)<=ord(t) },
            + v_invest_LP(unit, t_)${not uft_onlineLP(unit, f, t) and unit_investLP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
            + v_invest_MIP(unit, t_)${not uft_onlineMIP(unit, f, t) and unit_investMIP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
          )
      )
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

713
    // Ramping capability of units that are online
714
    - (
715
716
        + 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)}
717
718
719
720
721
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

722
    // Shutdown of generation units from full load
723
    - v_shutdown(unit, s, f, t)${   uft_online(unit, f, t)
724
725
726
                                    and gnu_output(grid, node, unit)
                                    and not uft_shutdownTrajectory(unit, f, t)
                                    }
727
        * p_gnu(grid, node, unit, 'unitSizeTot')
728

729
730
731
732
733
734
735
    // Units in shutdown phase need to keep up with the shutdown ramp 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
            + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                * [
                    + p_unit(unit, 'rampSpeedFromMinLoad')
                    + ( p_gnu(grid, node, unit, 'maxRampDown') - p_unit(unit, 'rampSpeedFromMinLoad') )${ not shutdownCounter(unit, counter-1) } // Ramp speed adjusted for the first shutdown interval
736
                        * ( p_u_shutdownTimeIntervalsCeil(unit) - p_u_shutdownTimeIntervals(unit) )
737
738
                    ]
                * 60 // Unit conversion from [p.u./min] to [p.u./h]
739
740
741
742
            // Units need to be able to shut down after shut down trajectory
            + v_shutdown(unit, s, f+df(f, t+dt_toShutdown(unit, t)), t+dt_toShutdown(unit, t))
                * p_unit(unit, 'rampSpeedFromMinload') * 60 // Unit conversion from [p.u./min] to [p.u./h]
                    * ( 1 - p_u_shutdownTimeIntervalsCeil(unit) + p_u_shutdownTimeIntervals(unit) ) // Adjusted to prevent other sub-units from ramping excessively
Topi Rasku's avatar
Topi Rasku committed
743
                    / p_stepLength(m, f, t)
744
            ) // END sum(shutdownCounter)
745
746
;

747
748
749
750
751
752
753
* --- Ramps separated into upward and downward ramps --------------------------

q_rampUpDown(m, s, gnuft_ramp(grid, node, unit, f, t))${  ord(t) > msStart(m, s) + 1
                                                          and msft(m, s, f, t)
                                                          and sum(slack, gnuft_rampCost(grid, node, unit, slack, f, t))
                                                          } ..

754
    + v_genRamp(grid, node, unit, s, f, t)
755

756
    =E=
757

758
759
    // Upward and downward ramp categories
    + sum(slack${ gnuft_rampCost(grid, node, unit, slack, f, t) },
760
761
        + v_genRampUpDown(grid, node, unit, slack, s, f, t)$upwardSlack(slack)
        - v_genRampUpDown(grid, node, unit, slack, s, f, t)$downwardSlack(slack)
762
      ) // END sum(slack)
763
764
;

Niina Helistö's avatar
Niina Helistö committed
765
* --- Upward and downward ramps constrained by slack boundaries ---------------
766
767
768
769
770

q_rampSlack(m, s, gnuft_rampCost(grid, node, unit, slack, f, t))${  ord(t) > msStart(m, s) + 1
                                                                    and msft(m, s, f, t)
                                                                    } ..

771
    + v_genRampUpDown(grid, node, unit, slack, s, f, t)
772

773
    =L=
774
775

    // Ramping capability of units without an online variable
776
777
778
779
780
781
782
783
784
785
786
    + (
        + ( p_gnu(grid, node, unit, 'maxGen') + p_gnu(grid, node, unit, 'maxCons') )${not uft_online(unit, f, t)}
        + sum(t_invest(t_)${ ord(t_)<=ord(t) },
            + v_invest_LP(unit, t_)${not uft_onlineLP(unit, f, t) and unit_investLP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
            + v_invest_MIP(unit, t_)${not uft_onlineMIP(unit, f, t) and unit_investMIP(unit)}
                * p_gnu(grid, node, unit, 'unitSizeTot')
          )
      )
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
787
788

    // Ramping capability of units with an online variable
789
    + (
790
791
        + 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)}
792
793
794
795
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
796

797
798
799
800
801
802
803
804
805
806
807
808
809
    // Units in the run-up phase need to keep up with the run-up rate
    + p_gnu(grid, node, unit, 'unitSizeTot')
        * 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
                + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    * [
                        + p_unit(unit, 'rampSpeedToMinLoad')
                        + ( p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit') - p_unit(unit, 'rampSpeedToMinLoad') )${ not runUpCounter(unit, counter+1) } // Ramp speed adjusted for the last run-up interval
                            * ( p_u_runUpTimeIntervalsCeil(unit) - p_u_runUpTimeIntervals(unit) )
                        ]
                    * 60 // Unit conversion from [p.u./min] into [p.u./h]
                ) // END sum(runUpCounter)
            ) // END sum(unitStarttype)
810
811

    // Shutdown of consumption units from full load
812
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
813
814
815
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
816

817
    // Shutdown of generation units from full load and ramping of units in the beginning of the shutdown phase
818
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_output(grid, node, unit)}
819
820
821
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
822

823
824
825
826
827
828
829
830
831
832
833
    // Units in shutdown phase need to keep up with the shutdown ramp 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
            + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                * [
                    + p_unit(unit, 'rampSpeedFromMinLoad')
                    + ( p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit') - p_unit(unit, 'rampSpeedFromMinLoad') )${ not shutdownCounter(unit, counter-1) } // Ramp speed adjusted for the first shutdown interval
                        * ( p_u_shutdownTimeIntervalsCeil(Unit) - p_u_shutdownTimeIntervals(unit) )
                    ]
                * 60 // Unit conversion from [p.u./min] to [p.u./h]
            ) // END sum(shutdownCounter)
834
;
835

836
837
* --- Fixed Output Ratio ------------------------------------------------------

838
q_outputRatioFixed(gngnu_fixedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
839
840
841
                                                                                        } ..

    // Generation in grid
842
    + v_gen(grid, node, unit, s, f, t)
843
        / p_gnu(grid, node, unit, 'conversionFactor')
844
845
846
847

    =E=

    // Generation in grid_
848
    + v_gen(grid_, node_, unit, s, f, t)
849
        / p_gnu(grid_, node_, unit, 'conversionFactor')
850
;
851
852
853

* --- Constrained Output Ratio ------------------------------------------------

854
q_outputRatioConstrained(gngnu_constrainedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
855
856
857
                                                                                                    } ..

    // Generation in grid
858
    + v_gen(grid, node, unit, s, f, t)
859
        / p_gnu(grid, node, unit, 'conversionFactor')
860
861
862
863

    =G=

    // Generation in grid_
864
    + v_gen(grid_, node_, unit, s, f, t)
865
        / p_gnu(grid_, node_, unit, 'conversionFactor')
Juha Kiviluoma's avatar
Juha Kiviluoma committed
866
;
867
868
869

* --- Direct Input-Output Conversion ------------------------------------------

870
q_conversionDirectInputOutput(s, suft(effDirect(effGroup), unit, f, t))$sft(s, f, t) ..
871
872

    // Sum over endogenous energy inputs
873
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
874
        + v_gen(grid, node, unit, s, f, t)
875
876
877
878
        ) // END sum(gnu_input)

    // Sum over fuel energy inputs
    + sum(uFuel(unit, 'main', fuel),
879
        + v_fuelUse(fuel, unit, s, f, t)
880
881
882
883
884
885
        ) // END sum(uFuel)

    =E=

    // Sum over energy outputs
    + sum(gnu_output(grid, node, unit),
886
        + v_gen(grid, node, unit, s, f, t)
887
            * [ // efficiency rate
888
                + p_effUnit(effGroup, unit, effGroup, 'slope')${ not ts_effUnit(effGroup, unit, effGroup, 'slope', f, t) }
889
                + ts_effUnit(effGroup, unit, effGroup, 'slope', f, t)
890
891
892
                ] // END * v_gen
        ) // END sum(gnu_output)

893
    // Consumption of keeping units online (no-load fuel use)
894
895
896
    + sum(gnu_output(grid, node, unit),
        + p_gnu(grid, node, unit, 'unitSizeGen')
        ) // END sum(gnu_output)
897
        * [ // Unit online state
898
899
            + 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)}
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915

            // Run-up and shutdown phase efficiency correction
            // Run-up 'online state'
            + 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
                    + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                        * p_uCounter_runUp(unit, counter)
                        / p_unit(unit, 'op00') // Scaling the p_uCounter_runUp using minload
                    ) // END sum(runUpCounter)
                ) // END sum(unitStarttype)
            // Shutdown 'online state'
            + sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
                + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                    * p_uCounter_shutdown(unit, counter)
                        / p_unit(unit, 'op00') // Scaling the p_uCounter_shutdown using minload
                ) // END sum(shutdownCounter)
916
917
            ] // END * sum(gnu_output)
        * [
918
919
            + p_effGroupUnit(effGroup, unit, 'section')${not ts_effUnit(effGroup, unit, effDirect, 'section', f, t)}
            + ts_effUnit(effGroup, unit, effGroup, 'section', f, t)
920
            ] // END * sum(gnu_output)
921
;
922
923
924

* --- SOS2 Efficiency Approximation -------------------------------------------

925
q_conversionSOS2InputIntermediate(s, suft(effLambda(effGroup), unit, f, t))$sft(s, f, t) ..
926
927

    // Sum over endogenous energy inputs
928
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
929
        + v_gen(grid, node, unit, s, f, t)
930
931
932
933
        ) // END sum(gnu_input)

    // Sum over fuel energy inputs
    + sum(uFuel(unit, 'main', fuel),
934
        + v_fuelUse(fuel, unit, s, f, t)
935
936
        ) // END sum(uFuel)

937
    =E=
938
939
940
941
942

    // Sum over the endogenous outputs of the unit
    + sum(gnu_output(grid, node, unit), p_gnu(grid, node, unit, 'unitSizeGen'))
        * [
            // Consumption of generation
943
            + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
944
                + v_sos2(unit, s, f, t, effSelector)
945
946
947
948
949
950
951
952
953
                    * [ // Operation points convert the v_sos2 variables into share of capacity used for generation
                        + p_effUnit(effGroup, unit, effSelector, 'op')${not ts_effUnit(effGroup, unit, effSelector, 'op', f, t)}
                        + ts_effUnit(effGroup, unit, effSelector, 'op', f, t)
                        ] // END * v_sos2
                    * [ // Heat rate
                        + p_effUnit(effGroup, unit, effSelector, 'slope')${not ts_effUnit(effGroup, unit, effSelector, 'slope', f, t)}
                        + ts_effUnit(effGroup, unit, effSelector, 'slope', f, t)
                        ] // END * v_sos2
                ) // END sum(effSelector)
954
           ]
955
;
956
957
958

* --- SOS 2 Efficiency Approximation Online Variables -------------------------

959
q_conversionSOS2Constraint(s, suft(effLambda(effGroup), unit, f, t))$sft(s, f, t) ..
960
961

    // Total value of the v_sos2 equals the number of online units
962
    + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
963
        + v_sos2(unit, s, f, t, effSelector)
964
965
966
967
968
        ) // END sum(effSelector)

    =E=

    // Number of units online
969
    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985

    // Run-up and shutdown phase efficiency approximation
    // Run-up 'online state'
    + 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
            + v_startup(unit, starttype, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
                * p_uCounter_runUp(unit, counter)
                / p_unit(unit, 'op00') // Scaling the p_uCounter_runUp using minload
            ) // END sum(runUpCounter)
        ) // END sum(unitStarttype)
    // Shutdown 'online state'
    + sum(shutdownCounter(unit, counter)${t_active(t+dt_trajectory(counter)) and uft_shutdownTrajectory(unit, f, t)}, // Sum over the shutdown intervals
        + v_shutdown(unit, s, f+df(f, t+dt_trajectory(counter)), t+dt_trajectory(counter))
            * p_uCounter_shutdown(unit, counter)
            / p_unit(unit, 'op00') // Scaling the p_uCounter_shutdown using minload
        ) // END sum(shutdownCounter)
986
;
987
988
989

* --- SOS 2 Efficiency Approximation Output Generation ------------------------

990
q_conversionSOS2IntermediateOutput(s, suft(effLambda(effGroup), unit, f, t))$sft(s, f, t) ..
991
992
993
994

    // Endogenous energy output
    + sum(gnu_output(grid, node, unit),
        + p_gnu(grid, node, unit, 'unitSizeGen')
995
      ) // END sum(gnu_output)
996
        * sum(effGroupSelectorUnit(effGroup, unit, effSelector),
997
            + v_sos2(unit, s, f, t, effSelector)
998
999
1000
            * [ // Operation points convert v_sos2 into share of capacity used for generation
                + p_effUnit(effGroup, unit, effSelector, 'op')${not ts_effUnit(effGroup, unit, effSelector, 'op', f, t)}
                + ts_effUnit(effGroup, unit, effSelector, 'op', f, t)