2d_constraints.gms 96.4 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
* =============================================================================

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

q_balance(gn(grid, node), mft(m, f, t))${   not p_gn(grid, node, 'boundAll')
                                            } .. // 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)
        * [
            + v_state(grid, node, f+df_central(f,t), t)                   // The difference between current
            - v_state(grid, node, f+df(f,t+dt(t)), t+dt(t))                     // ... and previous state of the node
            ]

    =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
42
            - p_gn(grid, node, 'selfDischargeLoss')${ gn_state(grid, node) }
                * v_state(grid, node, 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, 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, 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_, 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_, 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
66
            + sum(node_${ gn2n_directional(grid, node_, node) },
                + v_transfer(grid, node_, node, 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, f, t)
69
70
71
72
73
                ) // END sum(node_)

            // Interactions between the node and its units
            + sum(gnuft(grid, node, unit, f, t),
                + v_gen(grid, node, unit, f, t) // Unit energy generation and consumption
74
                )
75
76
77
78
79
80
81
82
83
84
85

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

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

            // Dummy generation variables, for feasibility purposes
            + vq_gen('increase', grid, node, 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, f, t) // Note! When stateSlack is permitted, have to take caution with the penalties so that it will be used first
    ) // 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
93
94
95
96
97
q_resDemand(restypeDirectionNode(restype, up_down, node), ft(f, t))
    ${  ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
            and ft_realized(f, t)
            ]
        } ..
98
99
    // Reserve provision by capable units on this node
    + sum(nuft(node, unit, f, t)${nuRescapable(restype, up_down, node, unit)},
100
        + v_reserve(restype, up_down, node, unit, f+df_reserves(node, restype, f, t), t)
101
102
103
        ) // END sum(nuft)

    // Reserve provision to this node via transfer links
104
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, up_down, node_, node)},
105
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
106
            * v_resTransferRightward(restype, up_down, node_, node, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
107
        ) // END sum(gn2n_directional)
108
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, up_down, node_, node)},
109
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
110
            * v_resTransferLeftward(restype, up_down, node, node_, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
111
112
113
114
115
116
117
118
        ) // 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')}

119
120
    // 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
121
        + sum(gnu(grid, node, unit), v_gen(grid, node, unit, f, t)) // Reserve sets and variables are currently lacking the grid dimension...
122
123
124
            * p_nuReserves(node, unit, restype, 'reserve_increase_ratio')
        ) // END sum(nuft)

125
    // Reserve provisions to another nodes via transfer links
126
    + 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
127
        + v_resTransferRightward(restype, up_down, node, node_, f+df_reserves(node, restype, f, t), t)
128
        ) // END sum(gn2n_directional)
129
    + 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
130
        + v_resTransferLeftward(restype, up_down, node_, node, f+df_reserves(node, restype, f, t), t)
131
132
133
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
134
135
    - vq_resDemand(restype, up_down, node, f+df_reserves(node, restype, f, t), t)
    - vq_resMissing(restype, up_down, node, f+df_reserves(node, restype, f, t), t)${ft_reservesFixed(node, restype, f+df_reserves(node, restype, f, t), t)}
136
;
137
138
139

* --- Maximum Downward Capacity -----------------------------------------------

140
q_maxDownward(m, gnuft(grid, node, unit, f, t))${   [   ord(t) < tSolveFirst + smax(restype, p_nReserves(node, restype, 'reserve_length')) // Unit is either providing
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
                                                        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)]
                                                        ]
                                                    } ..
    // Energy generation/consumption
    + v_gen(grid, node, unit, f, t)

    // Considering output constraints (e.g. cV line)
160
161
162
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
            * v_gen(grid_output, node_, unit, f, t)
163
164
165
        ) // END sum(gngnu_constrainedOutputRatio)

    // Downward reserve participation
166
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
167
        + v_reserve(restype, 'down', node, unit, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
168
169
170
171
172
173
        ) // 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')
174
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
175
176
177
178
            + 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
179
180
            + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f+df_central(f,t), t)} // LP online variant
            + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f+df_central(f,t), t)} // MIP online variant
181
182
            ] // END v_online

Niina Helistö's avatar
Niina Helistö committed
183
184
185
    + [
        // Units that are in the run-up phase need to keep up with the run-up ramp rate (contained in p_ut_runUp)
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
186
187
            * sum(t_active(t_)${    ord(t_) > ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and ord(t_) <= ord(t)},
Niina Helistö's avatar
Niina Helistö committed
188
                + sum(unitStarttype(unit, starttype),
189
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
190
191
192
193
194
195
196
197
                        * sum(t_full(t__)${ord(t__) = p_u_runUpTimeIntervalsCeil(unit) - ord(t) - dt_next(t) + 1 + ord(t_)}, // last step in the interval
                            + p_ut_runUp(unit, t__)
*                                * 1 // test values [0,1] to provide some flexibility
                            ) // END sum(t__)
                    ) // END sum(unitStarttype)
                ) // END sum(t_)
        // Units that are in the last time interval of the run-up phase are limited by the minimum load (contained in p_ut_runUp(unit, 't00000'))
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
198
            * sum(t_active(t_)${ ord(t_) = ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t)) },
Niina Helistö's avatar
Niina Helistö committed
199
                + sum(unitStarttype(unit, starttype),
200
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
201
202
203
204
205
206
207
208
                        * sum(t_full(t__)${ord(t__) = 1}, p_ut_runUp(unit, t__))
                    ) // END sum(unitStarttype)
                ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}

    + [
        // Units that are in the shutdown phase need to keep up with the shutdown ramp rate (contained in p_ut_shutdown)
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
209
210
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
211
                + v_shutdown(unit, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
212
213
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
214
                        ) // END sum(t__)
Niina Helistö's avatar
Niina Helistö committed
215
216
217
218
                ) // END sum(t_)
        // Units that are in the first time interval of the shutdown phase are limited by the minimum load (contained in p_ut_shutdown(unit, 't00000'))
        + p_gnu(grid, node, unit, 'unitSizeGen')
            * (
219
                + v_shutdown(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
220
221
222
                    * sum(t_full(t__)${ord(t__) = 1}, p_ut_shutdown(unit, t__))
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
223

224
225
226
227
228
    // Consuming units, greater than maxCons
    // Available capacity restrictions
    - p_unit(unit, 'availability')
        * [
            // Capacity factors for flow units
229
            + sum(flowUnit(flow, unit),
230
231
232
233
234
235
236
237
238
                + ts_cf_(flow, node, f, t)
                ) // 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')
                * [
239
                    // Capacity online
240
241
                    + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
                    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
242
243
244
245
246
247
248
249

                    // 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)
250
251
                    ] // END * p_gnu(unitSizeCons)
            ] // END * p_unit(availability)
252
;
253
254
255

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

256
q_maxUpward(m, gnuft(grid, node, unit, f, t))${ [   ord(t) < tSolveFirst + smax(restype, p_nReserves(node, restype, 'reserve_length')) // Unit is either providing
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
                                                    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))
                                                    ]
                                                }..
    // Energy generation/consumption
    + v_gen(grid, node, unit, f, t)

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

    // Upwards reserve participation
281
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
282
        + v_reserve(restype, 'up', node, unit, f+df_reserves(node, restype, f, t), t)
283
284
285
286
287
288
        ) // END sum(nuRescapable)

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

    // Consuming units
    + p_gnu(grid, node, unit, 'unitSizeCons')
289
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
290
291
292
293
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
294
295
            + v_online_LP(unit, 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, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)} // Consuming units are restricted by their min. load (consuming is negative)
296
297
298
299
300
301
302
            ] // 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
303
            + sum(flowUnit(flow, unit),
304
305
306
307
308
309
310
311
312
                + ts_cf_(flow, node, f, t)
                ) // 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')
                * [
313
                    // Capacity online
314
315
                    + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f ,t)}
                    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
316
317
318
319
320
321
322
323

                    // 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)
324
325
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
326

Niina Helistö's avatar
Niina Helistö committed
327
328
329
    + [
        // Units that are in the run-up phase need to keep up with the run-up ramp rate (contained in p_ut_runUp)
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
330
331
            * sum(t_active(t_)${    ord(t_) > ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and ord(t_) <= ord(t)},
Niina Helistö's avatar
Niina Helistö committed
332
                + sum(unitStarttype(unit, starttype),
333
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
334
335
336
337
338
339
340
                        * sum(t_full(t__)${ord(t__) = p_u_runUpTimeIntervalsCeil(unit) - ord(t) - dt_next(t) + 1 + ord(t_)}, // last step in the interval
                            + p_ut_runUp(unit, t__)
                            ) // END sum(t__)
                    ) // END sum(unitStarttype)
                ) // END sum(t_)
        // Units that are in the last time interval of the run-up phase are limited by the p_u_maxOutputInLastRunUpInterval
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
341
            * sum(t_active(t_)${ ord(t_) = ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t)) },
Niina Helistö's avatar
Niina Helistö committed
342
                + sum(unitStarttype(unit, starttype),
343
                    + v_startup(unit, starttype, f+df(f,t_), t_)
344
                        * p_u_maxOutputInLastRunUpInterval(unit)
Niina Helistö's avatar
Niina Helistö committed
345
346
347
348
349
350
351
                    ) // END sum(unitStarttype)
                ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}

    + [
        // Units that are in the shutdown phase need to keep up with the shutdown ramp rate (contained in p_ut_shutdown)
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
352
353
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
354
                + v_shutdown(unit, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
355
356
357
358
359
360
361
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
                        ) // END sum(t__)
                ) // END sum(t_)
        // Units that are in the first time interval of the shutdown phase are limited by p_u_maxOutputInFirstShutdownInterval
        + p_gnu(grid, node, unit, 'unitSizeGen')
            * (
362
                + v_shutdown(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
363
364
365
                    * p_u_maxOutputInFirstShutdownInterval(unit)
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
366
;
367

368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
* --- Reserve Provision of Units with Investments -----------------------------

q_reserveProvision(nuRescapable(restypeDirectionNode(restype, up_down, node), unit), ft(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, f+df_reserves(node, restype, f, t), t)

    =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_)
            ]
389
390
391
392
393
394
395
396
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
                + ts_cf_(flow, node, f, t)
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ]
397
398
399
400
401
402
        * [
            + 1${ft_realized(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 ft_realized(f+df_reserves(node, restype, f, t), t)}
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

403
404
* --- Unit Startup and Shutdown -----------------------------------------------

405
q_startshut(m, uft_online(unit, f, t)) ..
406
407
408
    // Units currently online
    + v_online_LP (unit, f+df_central(f,t), t)${uft_onlineLP (unit, f, t)}
    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
409
410

    // Units previously online
411
412

    // The same units
413
    - v_online_LP (unit, f+df(f,t+dt(t)), t+dt(t))${ uft_onlineLP_withPrevious(unit, f+df(f,t+dt(t)), t+dt(t))
414
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
415
    - v_online_MIP(unit, f+df(f,t+dt(t)), t+dt(t))${ uft_onlineMIP_withPrevious(unit, f+df(f,t+dt(t)), t+dt(t))
416
417
418
419
                                                             and not uft_aggregator_first(unit, f, t) }

    // Aggregated units just before they are turned into aggregator units
    - sum(unit_${unitAggregator_unit(unit, unit_)},
420
421
        + v_online_LP (unit_, 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_, f+df(f,t+dt(t)), t+dt(t))${uft_onlineMIP_withPrevious(unit_, f+df(f,t+dt(t)), t+dt(t))}
422
        )${uft_aggregator_first(unit, f, t)} // END sum(unit_)
423

424
425
    =E=

426
    // Unit startup and shutdown
427

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

433
434
435
436
    // 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
437

438
    // Shutdown of units at time t
439
    - v_shutdown(unit, f, t)
440
;
441

442
*--- Startup Type -------------------------------------------------------------
443
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
444
445
446
// 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.
447

448
q_startuptype(m, starttypeConstrained(starttype), uft_online(unit, f, t))${ unitStarttype(unit, starttype) } ..
449
450

    // Startup type
451
    + v_startup(unit, starttype, f, t)
452
453
454
455

    =L=

    // Subunit shutdowns within special startup timeframe
Topi Rasku's avatar
Topi Rasku committed
456
457
458
    + sum(unitCounter(unit, counter)${dt_starttypeUnitCounter(starttype, unit, counter)},
        + v_shutdown(unit, f+df(f,t+(dt_starttypeUnitCounter(starttype, unit, counter)+1)), t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))
            ${t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))}
459
460
461
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
462
;
463

464

465
466
*--- Online Limits with Startup Type Constraints and Investments --------------

467
q_onlineLimit(m, uft_online(unit, f, t))${  p_unit(unit, 'minShutdownHours')
468
                                            or p_u_runUpTimeIntervals(unit)
469
470
471
472
                                            or unit_investLP(unit)
                                            or unit_investMIP(unit)
                                            } ..
    // Online variables
473
474
    + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f ,t)}
475
476
477
478
479
480

    =L=

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

481
    // Number of units unable to become online due to restrictions
Topi Rasku's avatar
Topi Rasku committed
482
483
484
    - sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
        + v_shutdown(unit, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
            ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
485
486
487
488
        ) // 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)
    - sum(unit_${unitAggregator_unit(unit, unit_)},
Topi Rasku's avatar
Topi Rasku committed
489
490
491
        + sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
            + v_shutdown(unit_, f+df(f,t+(dt_downtimeUnitCounter(unit, counter) + 1)), t+(dt_downtimeUnitCounter(unit, counter) + 1))
                ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
492
493
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
494
495
496

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
497
498
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
499
500
501
        ) // END sum(t_invest)
;

502
503
504
505
*--- 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).
506
507
508
509
510
511
512
513
514
q_onlineOnStartUp(uft_online(unit, f, t))${sum(starttype, unitStarttype(unit, starttype))}..

    // Units currently online
    + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}

    =G=

    + sum(unitStarttype(unit, starttype),
515
        + v_startup(unit, starttype, 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
516
517
518
519
520
      ) // END sum(starttype)
;

q_offlineAfterShutdown(uft_online(unit, f, t))${sum(starttype, unitStarttype(unit, starttype))}..

521
522
523
524
525
    // Number of existing units
    + p_unit(unit, 'unitCount')

    // 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
        ) // END sum(t_invest)

530
531
532
533
534
535
    // Units currently online
    - v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    - v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}

    =G=

536
    + v_shutdown(unit, f, t)
537
538
;

539
540
*--- Minimum Unit Uptime ------------------------------------------------------

541
q_onlineMinUptime(m, uft_online(unit, f, t))${  p_unit(unit, 'minOperationHours')
542
543
544
                                                } ..

    // Units currently online
545
546
    + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
547
548
549
550

    =G=

    // Units that have minimum operation time requirements active
Topi Rasku's avatar
Topi Rasku committed
551
    + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
552
        + sum(unitStarttype(unit, starttype),
Topi Rasku's avatar
Topi Rasku committed
553
554
            + v_startup(unit, starttype, f+df(f,t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1)), t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))
                ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
555
            ) // END sum(starttype)
556
557
558
        ) // 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
559
560
    + sum(unitAggregator_unit(unit, unit_),
        + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
561
            + sum(unitStarttype(unit, starttype),
Topi Rasku's avatar
Topi Rasku committed
562
563
                + v_startup(unit, starttype, f+df(f,t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1)), t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))
                    ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
564
565
566
                ) // END sum(starttype)
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
567
568
;

569
* --- Ramp Constraints --------------------------------------------------------
570
571
572
573

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

575
    + v_genRamp(grid, node, unit, f, t) * p_stepLength(m, f, t)
576

577
    =E=
578

579
    // Change in generation over the interval: v_gen(t) - v_gen(t-1)
580
    + v_gen(grid, node, unit, f, t)
581

582
583
584
585
    // Unit generation at t-1 (except aggregator units right before the aggregation threshold, see next term)
    - v_gen(grid, node, unit, f+df(f,t+dt(t)), t+dt(t))${not uft_aggregator_first(unit, f, t)}
    // Unit generation at t-1, aggregator units right before the aggregation threshold
    + sum(unit_${unitAggregator_unit(unit, unit_)},
586
        - v_gen(grid, node, unit_, f+df(f,t+dt(t)), t+dt(t))
587
      )${uft_aggregator_first(unit, f, t)}
588
;
589

590
* --- Ramp Up Limits ----------------------------------------------------------
591
592
593
594

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')
595
596
597
598
599
                                                           and [ sum(restype, nuRescapable(restype, 'up', node, unit))
                                                                 or uft_online(unit, f, t)
                                                                 or unit_investLP(unit)
                                                                 or unit_investMIP(unit)
                                                                 ]
600
                                                           } ..
601
    + v_genRamp(grid, node, unit, f, t)
602
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
603
        + v_reserve(restype, 'up', node, unit, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
604
605
606
607
608
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =L=

609
    // Ramping capability of units without an online variable
610
611
612
613
614
615
616
617
618
619
620
621
    + (
        + ( 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]

622
    // Ramping capability of units with an online variable
623
624
625
626
627
628
629
630
    + (
        + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
        + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampUp')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

Niina Helistö's avatar
Niina Helistö committed
631
632
633
    + [
        // Units that are in the run-up phase need to keep up with the run-up ramp rate
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
634
635
            * sum(t_active(t_)${    ord(t_) > ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and ord(t_) <= ord(t)},
Niina Helistö's avatar
Niina Helistö committed
636
                + sum(unitStarttype(unit, starttype),
637
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
638
639
640
641
642
643
                        * p_unit(unit, 'rampSpeedToMinLoad')
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
        // Units that are in the last time interval of the run-up phase are limited by rampSpeedToMinLoad and maxRampUp
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
644
645
            * sum(t_active(t_)${    ord(t_) = ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and uft_startupTrajectory(unit, f, t)},
Niina Helistö's avatar
Niina Helistö committed
646
                + sum(unitStarttype(unit, starttype),
647
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
648
649
650
651
652
                        * max(p_unit(unit, 'rampSpeedToMinLoad'), p_gnu(grid, node, unit, 'maxRampUp')) // could also be weighted average from 'maxRampUp' and 'rampSpeedToMinLoad'
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}
653

654
    // Shutdown of consumption units from full load
655
    + v_shutdown(unit, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
656
        * p_gnu(grid, node, unit, 'unitSizeTot')
657
;
658

659
* --- Ramp Down Limits --------------------------------------------------------
660
661
662
663

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')
664
665
666
667
668
                                                             and [ sum(restype, nuRescapable(restype, 'down', node, unit))
                                                                   or uft_online(unit, f, t)
                                                                   or unit_investLP(unit)
                                                                   or unit_investMIP(unit)
                                                                   ]
669
                                                             } ..
670
    + v_genRamp(grid, node, unit, f, t)
671
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
672
        + v_reserve(restype, 'down', node, unit, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
673
674
675
676
677
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =G=

678
    // Ramping capability of units without online variable
679
680
681
682
683
684
685
686
687
688
689
690
    - (
        + ( 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]

691
    // Ramping capability of units that are online
692
693
694
695
696
697
698
699
    - (
        + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
        + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

700
    // Shutdown of generation units from full load
701
    - v_shutdown(unit, f, t)${   uft_online(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
702
703
                                                 and gnu_output(grid, node, unit)
                                                 and not uft_shutdownTrajectory(unit, f, t)}
704
        * p_gnu(grid, node, unit, 'unitSizeTot')
705

Niina Helistö's avatar
Niina Helistö committed
706
707
708
    + [
        // Units that are in the shutdown phase need to keep up with the shutdown ramp rate
        - p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
709
710
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) < ord(t) + dt(t)},
711
                + v_shutdown(unit, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
712
713
714
                    * p_unit(unit, 'rampSpeedFromMinLoad')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
              ) // END sum(t_)
715

Niina Helistö's avatar
Niina Helistö committed
716
717
718
        // Units that are in the first time interval of the shutdown phase are limited rampSpeedFromMinLoad and maxRampDown
        - p_gnu(grid, node, unit, 'unitSizeGen')
            * (
719
                + v_shutdown(unit, f+df(f,t+dt(t)), t+dt(t))
Niina Helistö's avatar
Niina Helistö committed
720
721
722
723
724
725
726
                    * max(p_unit(unit, 'rampSpeedFromMinLoad'), p_gnu(grid, node, unit, 'maxRampDown')) // could also be weighted average from 'maxRampDown' and 'rampSpeedFromMinLoad'
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
                ) // END * p_gnu(unitSizeGen)

        // Units just starting the shutdown phase are limited by the maxRampDown
        - p_gnu(grid, node, unit, 'unitSizeGen')
            * (
727
                + v_shutdown(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
728
729
730
731
                    * p_gnu(grid, node, unit, 'maxRampDown')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
732
733
;

734
735
736
737
738
739
740
* --- 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))
                                                          } ..

741
    + v_genRamp(grid, node, unit, f, t)
742

743
    =E=
744

745
746
747
748
749
    // Upward and downward ramp categories
    + sum(slack${ gnuft_rampCost(grid, node, unit, slack, f, t) },
        + v_genRampUpDown(grid, node, unit, slack, f, t)$upwardSlack(slack)
        - v_genRampUpDown(grid, node, unit, slack, f, t)$downwardSlack(slack)
      ) // END sum(slack)
750
751
;

Niina Helistö's avatar
Niina Helistö committed
752
* --- Upward and downward ramps constrained by slack boundaries ---------------
753
754
755
756
757

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

758
    + v_genRampUpDown(grid, node, unit, slack, f, t)
759

760
    =L=
761
762

    // Ramping capability of units without an online variable
763
764
765
766
767
768
769
770
771
772
773
    + (
        + ( 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]
774
775

    // Ramping capability of units with an online variable
776
777
778
779
780
781
782
    + (
        + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
        + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
783

Niina Helistö's avatar
Niina Helistö committed
784
785
786
    + [
        // Ramping of units that are in the run-up phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
787
788
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and ord(t_) <= ord(t)},
Niina Helistö's avatar
Niina Helistö committed
789
                + sum(unitStarttype(unit, starttype),
790
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
791
792
793
794
795
                        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}
796
797

    // Shutdown of consumption units from full load
798
    + v_shutdown(unit, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
799
800
801
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
802

803
    // Shutdown of generation units from full load and ramping of units in the beginning of the shutdown phase
804
    + v_shutdown(unit, f, t)${uft_online(unit, f, t) and gnu_output(grid, node, unit)}
805
806
807
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
808

Niina Helistö's avatar
Niina Helistö committed
809
810
811
    + [
        // Ramping of units that are in the shutdown phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
812
813
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) <= ord(t) + dt(t)},
814
                + v_shutdown(unit, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
815
816
817
818
                    * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
              ) // END sum(t_)
        ]${uft_shutdownTrajectory(unit, f, t)}
819
;
820

821
822
823
824
825
826
827
* --- Fixed Output Ratio ------------------------------------------------------

q_outputRatioFixed(gngnu_fixedOutputRatio(grid, node, grid_, node_, unit), ft(f, t))${  uft(unit, f, t)
                                                                                        } ..

    // Generation in grid
    + v_gen(grid, node, unit, f, t)
828
        / p_gnu(grid, node, unit, 'conversionFactor')
829
830
831
832
833

    =E=

    // Generation in grid_
    + v_gen(grid_, node_, unit, f, t)
834
        / p_gnu(grid_, node_, unit, 'conversionFactor')
835
;
836
837
838
839
840
841
842
843

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

q_outputRatioConstrained(gngnu_constrainedOutputRatio(grid, node, grid_, node_, unit), ft(f, t))${  uft(unit, f, t)
                                                                                                    } ..

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

    =G=

    // Generation in grid_
    + v_gen(grid_, node_, unit, f, t)
850
        / p_gnu(grid_, node_, unit, 'conversionFactor')
Juha Kiviluoma's avatar
Juha Kiviluoma committed
851
;
852
853
854

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

855
q_conversionDirectInputOutput(suft(effDirect(effGroup), unit, f, t)) ..
856
857

    // Sum over endogenous energy inputs
858
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
859
860
861
862
863
864
865
866
        + v_gen(grid, node, unit, f, t)
        ) // END sum(gnu_input)

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

867
868
    // Main fuel is not used during run-up and shutdown phases
    + [
Niina Helistö's avatar
Niina Helistö committed
869
870
871
872
        // Units that are in the run-up phase need to keep up with the run-up ramp rate (contained in p_ut_runUp)
        + sum(gnu_output(grid, node, unit)$uft_startupTrajectory(unit, f, t),
            + p_gnu(grid, node, unit, 'unitSizeGen')
          ) // END sum(gnu_output)
Topi Rasku's avatar
Topi Rasku committed
873
874
875
            * sum(t_active(t_)${    ord(t_) > ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t))
                                    and ord(t_) <= ord(t)
                                    },
Niina Helistö's avatar
Niina Helistö committed
876
                + sum(unitStarttype(unit, starttype),
877
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
878
879
880
881
882
883
884
885
886
                        * sum(t_full(t__)${ ord(t__) = p_u_runUpTimeIntervalsCeil(unit) - ord(t) - dt_next(t) + 1 + ord(t_) }, // last step in the interval
                            + p_ut_runUp(unit, t__)
                          ) // END sum(t__)
                  ) // END sum(unitStarttype)
              )  // END sum(t_)
        // Units that are in the last time interval of the run-up phase are limited by the minimum load (contained in p_ut_runUp(unit, 't00000'))
        + sum(gnu_output(grid, node, unit)$uft_startupTrajectory(unit, f, t),
            + p_gnu(grid, node, unit, 'unitSizeGen')
          ) // END sum(gnu_output)
Topi Rasku's avatar
Topi Rasku committed
887
            * sum(t_active(t_)${ ord(t_) = ord(t) + dt_next(t) + dt_toStartup(unit, t + dt_next(t)) },
Niina Helistö's avatar
Niina Helistö committed
888
                + sum(unitStarttype(unit, starttype),
889
                    + v_startup(unit, starttype, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
890
891
892
893
894
895
896
897
                        * sum(t_full(t__)${ord(t__) = 1}, p_ut_runUp(unit, t__))
                  ) // END sum(unitStarttype)
              )  // END sum(t_)

        // Units that are in the shutdown phase need to keep up with the shutdown ramp rate (contained in p_ut_shutdown)
        + sum(gnu_output(grid, node, unit)$uft_shutdownTrajectory(unit, f, t),
            + p_gnu(grid, node, unit, 'unitSizeGen')
          ) // END sum(gnu_output)
Topi Rasku's avatar
Topi Rasku committed
898
899
900
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)
                                    },
901
                + v_shutdown(unit, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
902
903
904
905
906
907
908
909
910
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
                        ) // END sum(t__)
                ) // END sum(t_)
        // Units that are in the first time interval of the shutdown phase are limited by the minimum load (contained in p_ut_shutdown(unit, 't00000'))
        + sum(gnu_output(grid, node, unit)$uft_shutdownTrajectory(unit, f, t),
            + p_gnu(grid, node, unit, 'unitSizeGen')
          ) // END sum(gnu_output)
            * (
911
                + v_shutdown(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
912
913
914
                    * sum(t_full(t__)${ord(t__) = 1}, p_ut_shutdown(unit, t__))
                ) // END * p_gnu(unitSizeGen)
        ]${uft_startupTrajectory(unit, f, t) or uft_shutdownTrajectory(unit, f, t)} // END run-up and shutdown phases
915
916
917
918

    * [ // Heat rate
        + p_effUnit(effGroup, unit, effGroup, 'slope')${ not ts_effUnit(effGroup, unit, effGroup, 'slope', f, t) }
        + ts_effUnit(effGroup, unit, effGroup, 'slope', f, t)
919
        ] // END * run-up phase
920

921
922
923
924
925
    =E=

    // Sum over energy outputs
    + sum(gnu_output(grid, node, unit),
        + v_gen(grid, node, unit, f, t)
926
            * [ // efficiency rate
927
                + p_effUnit(effGroup, unit, effGroup, 'slope')${ not ts_effUnit(effGroup, unit, effGroup, 'slope', f, t) }
928
                + ts_effUnit(effGroup, unit, effGroup, 'slope', f, t)
929
930
931
                ] // END * v_gen
        ) // END sum(gnu_output)

932
    // Consumption of keeping units online (no-load fuel use)
933
934
935
936
    + sum(gnu_output(grid, node, unit),
        + p_gnu(grid, node, unit, 'unitSizeGen')
        ) // END sum(gnu_output)
        * [
937
938
            + v_online_LP(unit, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
            + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
939
940
            ] // END * sum(gnu_output)
        * [
941
942
            + p_effGroupUnit(effGroup, unit, 'section')${not ts_effUnit(effGroup, unit, effDirect, 'section', f, t)}
            + ts_effUnit(effGroup, unit, effGroup, 'section', f, t)
943
            ] // END * sum(gnu_output)
944
;
945
946
947

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

948
949
950
q_conversionSOS2InputIntermediate(suft(effLambda(effGroup), unit, f, t)) ..

    // Sum over endogenous energy inputs
951
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
952
953
954
955
956
957
958
959
        + v_gen(grid, node, unit, f, t)
        ) // END sum(gnu_input)

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

960
    =G=
961
962
963
964
965

    // Sum over the endogenous outputs of the unit
    + sum(gnu_output(grid, node, unit), p_gnu(grid, node, unit, 'unitSizeGen'))
        * [
            // Consumption of generation
966
            + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
967
968
969
970
971
972
973
974
975
976
                + v_sos2(unit, f, t, effSelector)
                    * [ // 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)
977
           ]
978
;
979
980
981
982
983
984

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

q_conversionSOS2Constraint(suft(effLambda(effGroup), unit, f, t)) ..

    // Total value of the v_sos2 equals the number of online units
985
    + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
986
987
988
989
990
991
        + v_sos2(unit, f, t, effSelector)
        ) // END sum(effSelector)

    =E=

    // Number of units online
992
    + v_online_MIP(unit, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
993
;
994
995
996
997
998
999
1000

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

q_conversionSOS2IntermediateOutput(suft(effLambda(effGroup), unit, f, t)) ..

    // Endogenous energy output
    + sum(gnu_output(grid, node, unit),