2d_constraints.gms 97.6 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
95
96
97
    ${  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, s, 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, s, 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_, s, 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, s, 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_, s, 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, s, 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, 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)}
136
;
137
138
139

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

140
141
142
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
                                                        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)]
                                                        ]
157
                                                    }} ..
158
    // Energy generation/consumption
159
    + v_gen(grid, node, unit, s, f, t)
160
161

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

    // Downward reserve participation
168
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
169
        + 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)
170
171
172
173
174
175
        ) // 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')
176
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
177
178
179
180
            + 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
181
182
            + 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
183
184
            ] // END v_online

Niina Helistö's avatar
Niina Helistö committed
185
186
187
    + [
        // 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
188
189
            * 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
190
                + sum(unitStarttype(unit, starttype),
191
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
192
193
194
195
196
197
198
199
                        * 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
200
            * 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
201
                + sum(unitStarttype(unit, starttype),
202
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
203
204
205
206
207
208
209
210
                        * 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
211
212
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
213
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
214
215
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
216
                        ) // END sum(t__)
Niina Helistö's avatar
Niina Helistö committed
217
218
219
220
                ) // 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')
            * (
221
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
222
223
224
                    * sum(t_full(t__)${ord(t__) = 1}, p_ut_shutdown(unit, t__))
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
225

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

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

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

258
259
260
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
261
262
263
264
265
266
267
268
269
270
271
272
273
                                                    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))
                                                    ]
274
                                                }}..
275
    // Energy generation/consumption
276
    + v_gen(grid, node, unit, s, f, t)
277
278
279
280

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

    // Upwards reserve participation
285
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
286
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
287
288
289
290
291
292
        ) // END sum(nuRescapable)

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

    // Consuming units
    + p_gnu(grid, node, unit, 'unitSizeCons')
293
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
294
295
296
297
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
298
299
            + 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)
300
301
302
303
304
305
306
            ] // 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
307
            + sum(flowUnit(flow, unit),
308
                + ts_cf_(flow, node, f, t, s)
309
310
311
312
313
314
315
316
                ) // 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')
                * [
317
                    // Capacity online
318
319
                    + 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)}
320
321
322
323
324
325
326
327

                    // 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)
328
329
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
330

Niina Helistö's avatar
Niina Helistö committed
331
332
333
    + [
        // 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
334
335
            * 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
336
                + sum(unitStarttype(unit, starttype),
337
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
338
339
340
341
342
343
344
                        * 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
345
            * 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
346
                + sum(unitStarttype(unit, starttype),
347
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
348
                        * p_u_maxOutputInLastRunUpInterval(unit)
Niina Helistö's avatar
Niina Helistö committed
349
350
351
352
353
354
355
                    ) // 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
356
357
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
358
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
359
360
361
362
363
364
365
                    * 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')
            * (
366
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
367
368
369
                    * p_u_maxOutputInFirstShutdownInterval(unit)
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
370
;
371

372
373
* --- Reserve Provision of Units with Investments -----------------------------

374
375
376
377
378
379
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)
380
381
382
383
384
385
386
387
388
389
390
391
392

    =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_)
            ]
393
394
395
396
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
397
                + ts_cf_(flow, node, f, t, s)
398
399
400
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ]
401
402
403
404
405
406
        * [
            + 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?
;

407
408
* --- Unit Startup and Shutdown -----------------------------------------------

409
q_startshut(m, s, uft_online(unit, f, t))$msft(m, s, f, t) ..
410
    // Units currently online
411
412
    + 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)}
413
414

    // Units previously online
415
416

    // The same units
417
    - 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))
418
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
419
    - 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))
420
421
422
423
                                                             and not uft_aggregator_first(unit, f, t) }

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

428
429
    =E=

430
    // Unit startup and shutdown
431

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

437
438
439
440
    // 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
441

442
    // Shutdown of units at time t
443
    - v_shutdown(unit, s, f, t)
444
;
445

446
*--- Startup Type -------------------------------------------------------------
447
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
448
449
450
// 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.
451

452
453
q_startuptype(m, s, starttypeConstrained(starttype), uft_online(unit, f, t))
    ${msft(m, s, f, t) and unitStarttype(unit, starttype)} ..
454
455

    // Startup type
456
    + v_startup(unit, starttype, s, f, t)
457
458
459
460

    =L=

    // Subunit shutdowns within special startup timeframe
Topi Rasku's avatar
Topi Rasku committed
461
    + sum(unitCounter(unit, counter)${dt_starttypeUnitCounter(starttype, unit, counter)},
462
        + 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
463
            ${t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))}
464
465
466
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
467
;
468

469

470
471
*--- Online Limits with Startup Type Constraints and Investments --------------

472
473
q_onlineLimit(m, s, uft_online(unit, f, t))${msft(m, s, f, t) and {
                                            p_unit(unit, 'minShutdownHours')
474
                                            or p_u_runUpTimeIntervals(unit)
475
476
                                            or unit_investLP(unit)
                                            or unit_investMIP(unit)
477
                                            }} ..
478
    // Online variables
479
480
    + 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)}
481
482
483
484
485
486

    =L=

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

487
    // Number of units unable to become online due to restrictions
Topi Rasku's avatar
Topi Rasku committed
488
    - sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
489
        + 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
490
            ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
491
492
493
494
        ) // 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
495
        + sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
496
            + 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
497
                ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
498
499
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
500
501
502

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
503
504
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
505
506
507
        ) // END sum(t_invest)
;

508
509
510
511
*--- 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).
512
513
q_onlineOnStartUp(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
514
515

    // Units currently online
516
517
    + 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)}
518
519
520
521

    =G=

    + sum(unitStarttype(unit, starttype),
522
        + 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
523
524
525
      ) // END sum(starttype)
;

526
527
q_offlineAfterShutdown(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
528

529
530
531
532
533
    // Number of existing units
    + p_unit(unit, 'unitCount')

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
534
535
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
536
537
        ) // END sum(t_invest)

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

    =G=

544
    + v_shutdown(unit, s, f, t)
545
546
;

547
548
*--- Minimum Unit Uptime ------------------------------------------------------

549
550
q_onlineMinUptime(m, s, uft_online(unit, f, t))
    ${msft(m, s, f, t) and  p_unit(unit, 'minOperationHours')} ..
551
552

    // Units currently online
553
554
    + 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)}
555
556
557
558

    =G=

    // Units that have minimum operation time requirements active
Topi Rasku's avatar
Topi Rasku committed
559
    + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
560
        + sum(unitStarttype(unit, starttype),
561
            + 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
562
                ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
563
            ) // END sum(starttype)
564
565
566
        ) // 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
567
568
    + sum(unitAggregator_unit(unit, unit_),
        + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
569
            + sum(unitStarttype(unit, starttype),
570
                + 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
571
                    ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
572
573
574
                ) // END sum(starttype)
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
575
576
;

577
* --- Ramp Constraints --------------------------------------------------------
578
579
580
581

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

583
    + v_genRamp(grid, node, unit, s, f, t) * p_stepLength(m, f, t)
584

585
    =E=
586

587
    // Change in generation over the interval: v_gen(t) - v_gen(t-1)
588
    + v_gen(grid, node, unit, s, f, t)
589

590
    // Unit generation at t-1 (except aggregator units right before the aggregation threshold, see next term)
591
    - 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)}
592
593
    // Unit generation at t-1, aggregator units right before the aggregation threshold
    + sum(unit_${unitAggregator_unit(unit, unit_)},
594
        - v_gen(grid, node, unit_, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))
595
      )${uft_aggregator_first(unit, f, t)}
596
;
597

598
* --- Ramp Up Limits ----------------------------------------------------------
599
600
601
602

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')
603
604
605
606
607
                                                           and [ sum(restype, nuRescapable(restype, 'up', node, unit))
                                                                 or uft_online(unit, f, t)
                                                                 or unit_investLP(unit)
                                                                 or unit_investMIP(unit)
                                                                 ]
608
                                                           } ..
609
    + v_genRamp(grid, node, unit, s, f, t)
610
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
611
        + 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)
612
613
614
615
616
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =L=

617
    // Ramping capability of units without an online variable
618
619
620
621
622
623
624
625
626
627
628
629
    + (
        + ( 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]

630
    // Ramping capability of units with an online variable
631
    + (
632
633
        + 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)}
634
635
636
637
638
      )
        * 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
639
640
641
    + [
        // 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
642
643
            * 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
644
                + sum(unitStarttype(unit, starttype),
645
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
646
647
648
649
650
651
                        * 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
652
653
            * 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
654
                + sum(unitStarttype(unit, starttype),
655
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
656
657
658
659
660
                        * 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)}
661

662
    // Shutdown of consumption units from full load
663
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
664
        * p_gnu(grid, node, unit, 'unitSizeTot')
665
;
666

667
* --- Ramp Down Limits --------------------------------------------------------
668
669
670
671

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')
672
673
674
675
676
                                                             and [ sum(restype, nuRescapable(restype, 'down', node, unit))
                                                                   or uft_online(unit, f, t)
                                                                   or unit_investLP(unit)
                                                                   or unit_investMIP(unit)
                                                                   ]
677
                                                             } ..
678
    + v_genRamp(grid, node, unit, s, f, t)
679
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
680
        + 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)
681
682
683
684
685
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =G=

686
    // Ramping capability of units without online variable
687
688
689
690
691
692
693
694
695
696
697
698
    - (
        + ( 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]

699
    // Ramping capability of units that are online
700
    - (
701
702
        + 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)}
703
704
705
706
707
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

708
    // Shutdown of generation units from full load
709
    - v_shutdown(unit, s, f, t)${   uft_online(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
710
711
                                                 and gnu_output(grid, node, unit)
                                                 and not uft_shutdownTrajectory(unit, f, t)}
712
        * p_gnu(grid, node, unit, 'unitSizeTot')
713

Niina Helistö's avatar
Niina Helistö committed
714
715
716
    + [
        // 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
717
718
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) < ord(t) + dt(t)},
719
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
720
721
722
                    * p_unit(unit, 'rampSpeedFromMinLoad')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
              ) // END sum(t_)
723

Niina Helistö's avatar
Niina Helistö committed
724
725
726
        // Units that are in the first time interval of the shutdown phase are limited rampSpeedFromMinLoad and maxRampDown
        - p_gnu(grid, node, unit, 'unitSizeGen')
            * (
727
                + v_shutdown(unit, s, f+df(f,t+dt(t)), t+dt(t))
Niina Helistö's avatar
Niina Helistö committed
728
729
730
731
732
733
734
                    * 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')
            * (
735
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
736
737
738
739
                    * 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)}
740
741
;

742
743
744
745
746
747
748
* --- 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))
                                                          } ..

749
    + v_genRamp(grid, node, unit, s, f, t)
750

751
    =E=
752

753
754
    // Upward and downward ramp categories
    + sum(slack${ gnuft_rampCost(grid, node, unit, slack, f, t) },
755
756
        + v_genRampUpDown(grid, node, unit, slack, s, f, t)$upwardSlack(slack)
        - v_genRampUpDown(grid, node, unit, slack, s, f, t)$downwardSlack(slack)
757
      ) // END sum(slack)
758
759
;

Niina Helistö's avatar
Niina Helistö committed
760
* --- Upward and downward ramps constrained by slack boundaries ---------------
761
762
763
764
765

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

766
    + v_genRampUpDown(grid, node, unit, slack, s, f, t)
767

768
    =L=
769
770

    // Ramping capability of units without an online variable
771
772
773
774
775
776
777
778
779
780
781
    + (
        + ( 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]
782
783

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

Niina Helistö's avatar
Niina Helistö committed
792
793
794
    + [
        // Ramping of units that are in the run-up phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
795
796
            * 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
797
                + sum(unitStarttype(unit, starttype),
798
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
799
800
801
802
803
                        * 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)}
804
805

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

811
    // Shutdown of generation units from full load and ramping of units in the beginning of the shutdown phase
812
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_output(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

Niina Helistö's avatar
Niina Helistö committed
817
818
819
    + [
        // Ramping of units that are in the shutdown phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
820
821
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) <= ord(t) + dt(t)},
822
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
823
824
825
826
                    * 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)}
827
;
828

829
830
* --- Fixed Output Ratio ------------------------------------------------------

831
q_outputRatioFixed(gngnu_fixedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
832
833
834
                                                                                        } ..

    // Generation in grid
835
    + v_gen(grid, node, unit, s, f, t)
836
        / p_gnu(grid, node, unit, 'conversionFactor')
837
838
839
840

    =E=

    // Generation in grid_
841
    + v_gen(grid_, node_, unit, s, f, t)
842
        / p_gnu(grid_, node_, unit, 'conversionFactor')
843
;
844
845
846

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

847
q_outputRatioConstrained(gngnu_constrainedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
848
849
850
                                                                                                    } ..

    // Generation in grid
851
    + v_gen(grid, node, unit, s, f, t)
852
        / p_gnu(grid, node, unit, 'conversionFactor')
853
854
855
856

    =G=

    // Generation in grid_
857
    + v_gen(grid_, node_, unit, s, f, t)
858
        / p_gnu(grid_, node_, unit, 'conversionFactor')
Juha Kiviluoma's avatar
Juha Kiviluoma committed
859
;
860
861
862

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

863
q_conversionDirectInputOutput(s, suft(effDirect(effGroup), unit, f, t))$sft(s, f, t) ..
864
865

    // Sum over endogenous energy inputs
866
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
867
        + v_gen(grid, node, unit, s, f, t)
868
869
870
871
        ) // END sum(gnu_input)

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

875
876
    // Main fuel is not used during run-up and shutdown phases
    + [
Niina Helistö's avatar
Niina Helistö committed
877
878
879
880
        // 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
881
882
883
            * 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
884
                + sum(unitStarttype(unit, starttype),
885
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
886
887
888
889
890
891
892
893
894
                        * 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
895
            * 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
896
                + sum(unitStarttype(unit, starttype),
897
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
898
899
900
901
902
903
904
905
                        * 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
906
907
908
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)
                                    },
909
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
910
911
912
913
914
915
916
917
918
                    * 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)
            * (
919
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
920
921
922
                    * 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
923
924
925
926

    * [ // 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)
927
        ] // END * run-up phase
928

929
930
931
932
    =E=

    // Sum over energy outputs
    + sum(gnu_output(grid, node, unit),
933
        + v_gen(grid, node, unit, s, f, t)
934
            * [ // efficiency rate
935
                + p_effUnit(effGroup, unit, effGroup, 'slope')${ not ts_effUnit(effGroup, unit, effGroup, 'slope', f, t) }
936
                + ts_effUnit(effGroup, unit, effGroup, 'slope', f, t)
937
938
939
                ] // END * v_gen
        ) // END sum(gnu_output)

940
    // Consumption of keeping units online (no-load fuel use)
941
942
943
944
    + sum(gnu_output(grid, node, unit),
        + p_gnu(grid, node, unit, 'unitSizeGen')
        ) // END sum(gnu_output)
        * [
945
946
            + 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)}
947
948
            ] // END * sum(gnu_output)
        * [
949
950
            + p_effGroupUnit(effGroup, unit, 'section')${not ts_effUnit(effGroup, unit, effDirect, 'section', f, t)}
            + ts_effUnit(effGroup, unit, effGroup, 'section', f, t)
951
            ] // END * sum(gnu_output)
952
;
953
954
955

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

956
q_conversionSOS2InputIntermediate(s, suft(effLambda(effGroup), unit, f, t))$sft(s, f, t) ..
957
958

    // Sum over endogenous energy inputs
959
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
960
        + v_gen(grid, node, unit, s, f, t)
961
962
963
964
        ) // END sum(gnu_input)

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

968
    =G=
969
970
971
972
973

    // Sum over the endogenous outputs of the unit
    + sum(gnu_output(grid, node, unit), p_gnu(grid, node, unit, 'unitSizeGen'))
        * [
            // Consumption of generation
974
            + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
975
                + v_sos2(unit, s, f, t, effSelector)
976
977
978
979
980
981
982
983
984
                    * [ // 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)
985
           ]
986
;
987
988
989

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

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

    // Total value of the v_sos2 equals the number of online units
993
    + sum(effGroupSelectorUnit(effGroup, unit, effSelector),
994
        + v_sos2(unit, s, f, t, effSelector)
995
996
997
998
999
        ) // END sum(effSelector)

    =E=

    // Number of units online
1000
    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}