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

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

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

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

18

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

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

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

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

    =E=

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

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

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

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

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

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

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

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

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

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

92
q_resDemand(restypeDirectionNode(restype, up_down, node), sft(s, f, t))
93
94
    ${  ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
95
                  and sft_realized(s, f, t)]
96
        } ..
97
98
    // Reserve provision by capable units on this node
    + sum(nuft(node, unit, f, t)${nuRescapable(restype, up_down, node, unit)},
99
        + v_reserve(restype, up_down, node, unit, s, f+df_reserves(node, restype, f, t), t)
100
101
        ) // END sum(nuft)

102
    // Reserve provision from other reserve categories when they can be shared
103
    + sum((nuft(node, unit, f, t), restype_)${p_nuRes2Res(node, unit, restype_, up_down, restype)},
104
        + v_reserve(restype_, up_down, node, unit, s, f+df_reserves(node, restype_, f, t), t)
105
            * p_nuRes2Res(node, unit, restype_, up_down, restype)
106
107
        ) // END sum(nuft)

108
    // Reserve provision to this node via transfer links
109
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, up_down, node_, node)},
110
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
111
            * 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
112
        ) // END sum(gn2n_directional)
113
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, up_down, node_, node)},
114
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
115
            * 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
116
117
118
119
120
121
122
123
        ) // 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')}

124
125
    // 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
126
        + sum(gnu(grid, node, unit), v_gen(grid, node, unit, s, f, t)) // Reserve sets and variables are currently lacking the grid dimension...
127
128
129
            * p_nuReserves(node, unit, restype, 'reserve_increase_ratio')
        ) // END sum(nuft)

130
    // Reserve provisions to another nodes via transfer links
131
    + 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
132
        + v_resTransferRightward(restype, up_down, node, node_, s, f+df_reserves(node, restype, f, t), t)
133
        ) // END sum(gn2n_directional)
134
    + 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
135
        + v_resTransferLeftward(restype, up_down, node_, node, s, f+df_reserves(node, restype, f, t), t)
136
137
138
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
139
140
    - 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)}
141
;
142

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

147
q_resDemand_Infeed(grid, restypeDirectionNode(restype, 'up', node), sft(s, f, t), unit_fail(unit_))
148
149
150
151
    ${  ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')
        and not [ restypeReleasedForRealization(restype)
            and ft_realized(f, t)
            ]
152
        and p_nReserves(node, restype, 'Infeed2Cover')
153
154
155
        } ..
    // Reserve provision by capable units on this node excluding the failing one
    + sum(nuft(node, unit, f, t)${nuRescapable(restype, 'up', node, unit) and (ord(unit_) ne ord(unit))},
156
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
157
158
159
        ) // END sum(nuft)

    // Reserve provision from other reserve categories when they can be shared
160
    + sum((nuft(node, unit, f, t), restype_)${p_nuRes2Res(node, unit, restype_, 'up', restype)},
161
        + v_reserve(restype_, 'up', node, unit, s, f+df_reserves(node, restype_, f, t), t)
162
            * p_nuRes2Res(node, unit, restype_, 'up', restype)
163
164
165
166
167
        ) // END sum(nuft)

    // Reserve provision to this node via transfer links
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, 'up', node_, node)},
        + (1 - p_gnn(grid, node_, node, 'transferLoss') )
168
            * v_resTransferRightward(restype, 'up', node_, node, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
169
170
171
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, 'up', node_, node)},
        + (1 - p_gnn(grid, node, node_, 'transferLoss') )
172
            * v_resTransferLeftward(restype, 'up', node, node_, s, f+df_reserves(node_, restype, f, t), t) // Reserves from another node - reduces the need for reserves in the node
173
174
175
176
177
        ) // END sum(gn2n_directional)

    =G=

    // Demand for reserves of the failing one
178
    v_gen(grid,node,unit_,s,f,t)*p_nReserves(node, restype, 'Infeed2Cover')
179
180
181

    // 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
182
        + sum(gnu(grid, node, unit), v_gen(grid, node, unit, s, f, t)) // Reserve sets and variables are currently lacking the grid dimension...
183
184
185
186
187
            * p_nuReserves(node, unit, restype, 'reserve_increase_ratio')
        ) // END sum(nuft)

    // Reserve provisions to another nodes via transfer links
    + sum(gn2n_directional(grid, node, node_)${restypeDirectionNodeNode(restype, 'up', node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
188
        + v_resTransferRightward(restype, 'up', node, node_, s, f+df_reserves(node, restype, f, t), t)
189
190
        ) // END sum(gn2n_directional)
    + sum(gn2n_directional(grid, node_, node)${restypeDirectionNodeNode(restype, 'up', node_, node)},   // If trasferring reserves to another node, increase your own reserves by same amount
191
        + v_resTransferLeftward(restype, 'up', node_, node, s, f+df_reserves(node, restype, f, t), t)
192
193
194
        ) // END sum(gn2n_directional)

    // Reserve demand feasibility dummy variables
195
196
    - vq_resDemand(restype, 'up', node, s, f+df_reserves(node, restype, f, t), t)
    - vq_resMissing(restype, 'up', node, s, f+df_reserves(node, restype, f, t), t)${ft_reservesFixed(node, restype, f+df_reserves(node, restype, f, t), t)}
197
;
198
199
* --- Maximum Downward Capacity -----------------------------------------------

200
201
202
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
                                                        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)]
                                                        ]
217
                                                    }} ..
218
    // Energy generation/consumption
219
    + v_gen(grid, node, unit, s, f, t)
220
221

    // Considering output constraints (e.g. cV line)
222
223
    + sum(gngnu_constrainedOutputRatio(grid, node, grid_output, node_, unit),
        + p_gnu(grid_output, node_, unit, 'cV')
224
            * v_gen(grid_output, node_, unit, s, f, t)
225
226
227
        ) // END sum(gngnu_constrainedOutputRatio)

    // Downward reserve participation
228
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
229
        + 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)
230
231
232
233
234
235
        ) // 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')
236
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
237
238
239
240
            + 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
241
242
            + 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
243
244
            ] // END v_online

Niina Helistö's avatar
Niina Helistö committed
245
246
247
    + [
        // 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
248
249
            * 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
250
                + sum(unitStarttype(unit, starttype),
251
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
252
253
254
255
256
257
258
259
                        * 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
260
            * 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
261
                + sum(unitStarttype(unit, starttype),
262
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
263
264
265
266
267
268
269
270
                        * 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
271
272
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
273
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
274
275
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
276
                        ) // END sum(t__)
Niina Helistö's avatar
Niina Helistö committed
277
278
279
280
                ) // 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')
            * (
281
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
282
283
284
                    * sum(t_full(t__)${ord(t__) = 1}, p_ut_shutdown(unit, t__))
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
285

286
287
288
289
290
    // Consuming units, greater than maxCons
    // Available capacity restrictions
    - p_unit(unit, 'availability')
        * [
            // Capacity factors for flow units
291
            + sum(flowUnit(flow, unit),
292
                + ts_cf_(flow, node, f, t, s)
293
294
295
296
297
298
299
300
                ) // 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')
                * [
301
                    // Capacity online
302
303
                    + 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)}
304
305
306
307
308
309
310
311

                    // 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)
312
313
                    ] // END * p_gnu(unitSizeCons)
            ] // END * p_unit(availability)
314
;
315
316
317

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

318
319
320
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
321
322
323
324
325
326
327
328
329
330
331
332
333
                                                    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))
                                                    ]
334
                                                }}..
335
    // Energy generation/consumption
336
    + v_gen(grid, node, unit, s, f, t)
337
338
339
340

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

    // Upwards reserve participation
345
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
346
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
347
348
349
350
351
352
        ) // END sum(nuRescapable)

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

    // Consuming units
    + p_gnu(grid, node, unit, 'unitSizeCons')
353
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
354
355
356
357
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
358
359
            + 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)
360
361
362
363
364
365
366
            ] // 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
367
            + sum(flowUnit(flow, unit),
368
                + ts_cf_(flow, node, f, t, s)
369
370
371
372
373
374
375
376
                ) // 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')
                * [
377
                    // Capacity online
378
379
                    + 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)}
380
381
382
383
384
385
386
387

                    // 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)
388
389
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
390

Niina Helistö's avatar
Niina Helistö committed
391
392
393
    + [
        // 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
394
395
            * 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
396
                + sum(unitStarttype(unit, starttype),
397
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
398
399
400
401
402
403
404
                        * 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
405
            * 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
406
                + sum(unitStarttype(unit, starttype),
407
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
408
                        * p_u_maxOutputInLastRunUpInterval(unit)
Niina Helistö's avatar
Niina Helistö committed
409
410
411
412
413
414
415
                    ) // 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
416
417
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
418
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
419
420
421
422
423
424
425
                    * 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')
            * (
426
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
427
428
429
                    * p_u_maxOutputInFirstShutdownInterval(unit)
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
430
;
431

432
433
* --- Reserve Provision of Units with Investments -----------------------------

434
435
436
437
438
439
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)
440
441
442
443
444
445
446
447
448
449
450
451
452

    =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_)
            ]
453
454
455
456
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
457
                + ts_cf_(flow, node, f, t, s)
458
459
460
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
            ]
461
        * [
462
463
            + 1${sft_realized(s, f+df_reserves(node, restype, f, t), t)} // reserveReliability limits the reliability of reserves locked ahead of time.
            + p_nuReserves(node, unit, restype, 'reserveReliability')${not sft_realized(s, f+df_reserves(node, restype, f, t), t)}
464
465
466
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

467
468
* --- Unit Startup and Shutdown -----------------------------------------------

469
q_startshut(m, s, uft_online(unit, f, t))$msft(m, s, f, t) ..
470
    // Units currently online
471
472
    + 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)}
473
474

    // Units previously online
475
476

    // The same units
477
    - 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))
478
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
479
    - 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))
480
481
482
483
                                                             and not uft_aggregator_first(unit, f, t) }

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

488
489
    =E=

490
    // Unit startup and shutdown
491

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

497
498
499
500
    // 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
501

502
    // Shutdown of units at time t
503
    - v_shutdown(unit, s, f, t)
504
;
505

506
*--- Startup Type -------------------------------------------------------------
507
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
508
509
510
// 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.
511

512
513
q_startuptype(m, s, starttypeConstrained(starttype), uft_online(unit, f, t))
    ${msft(m, s, f, t) and unitStarttype(unit, starttype)} ..
514
515

    // Startup type
516
    + v_startup(unit, starttype, s, f, t)
517
518
519
520

    =L=

    // Subunit shutdowns within special startup timeframe
Topi Rasku's avatar
Topi Rasku committed
521
    + sum(unitCounter(unit, counter)${dt_starttypeUnitCounter(starttype, unit, counter)},
522
        + 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
523
            ${t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))}
524
525
526
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
527
;
528

529

530
531
*--- Online Limits with Startup Type Constraints and Investments --------------

532
533
q_onlineLimit(m, s, uft_online(unit, f, t))${msft(m, s, f, t) and {
                                            p_unit(unit, 'minShutdownHours')
534
                                            or p_u_runUpTimeIntervals(unit)
535
536
                                            or unit_investLP(unit)
                                            or unit_investMIP(unit)
537
                                            }} ..
538
    // Online variables
539
540
    + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f ,t)}
541
542
543
544
545
546

    =L=

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

547
    // Number of units unable to become online due to restrictions
Topi Rasku's avatar
Topi Rasku committed
548
    - sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
549
        + 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
550
            ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
551
552
553
554
        ) // 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
555
        + sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
556
            + 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
557
                ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
558
559
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
560
561
562

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
563
564
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
565
566
567
        ) // END sum(t_invest)
;

568
569
570
571
*--- 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).
572
573
q_onlineOnStartUp(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
574
575

    // Units currently online
576
577
    + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f, t)}
    + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f, t)}
578
579
580
581

    =G=

    + sum(unitStarttype(unit, starttype),
582
        + 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
583
584
585
      ) // END sum(starttype)
;

586
587
q_offlineAfterShutdown(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
588

589
590
591
592
593
    // Number of existing units
    + p_unit(unit, 'unitCount')

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
594
595
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
596
597
        ) // END sum(t_invest)

598
    // Units currently online
599
600
    - 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)}
601
602
603

    =G=

604
    + v_shutdown(unit, s, f, t)
605
606
;

607
608
*--- Minimum Unit Uptime ------------------------------------------------------

609
610
q_onlineMinUptime(m, s, uft_online(unit, f, t))
    ${msft(m, s, f, t) and  p_unit(unit, 'minOperationHours')} ..
611
612

    // Units currently online
613
614
    + 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)}
615
616
617
618

    =G=

    // Units that have minimum operation time requirements active
Topi Rasku's avatar
Topi Rasku committed
619
    + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
620
        + sum(unitStarttype(unit, starttype),
621
            + 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
622
                ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
623
            ) // END sum(starttype)
624
625
626
        ) // 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
627
628
    + sum(unitAggregator_unit(unit, unit_),
        + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
629
            + sum(unitStarttype(unit, starttype),
630
                + 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
631
                    ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
632
633
634
                ) // END sum(starttype)
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
635
636
;

637
* --- Ramp Constraints --------------------------------------------------------
638
639
640
641

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

643
    + v_genRamp(grid, node, unit, s, f, t) * p_stepLength(m, f, t)
644

645
    =E=
646

647
    // Change in generation over the interval: v_gen(t) - v_gen(t-1)
648
    + v_gen(grid, node, unit, s, f, t)
649

650
    // Unit generation at t-1 (except aggregator units right before the aggregation threshold, see next term)
651
    - 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)}
652
653
    // Unit generation at t-1, aggregator units right before the aggregation threshold
    + sum(unit_${unitAggregator_unit(unit, unit_)},
654
        - v_gen(grid, node, unit_, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))
655
      )${uft_aggregator_first(unit, f, t)}
656
;
657

658
* --- Ramp Up Limits ----------------------------------------------------------
659
660
661
662

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')
663
664
665
666
667
                                                           and [ sum(restype, nuRescapable(restype, 'up', node, unit))
                                                                 or uft_online(unit, f, t)
                                                                 or unit_investLP(unit)
                                                                 or unit_investMIP(unit)
                                                                 ]
668
                                                           } ..
669
    + v_genRamp(grid, node, unit, s, f, t)
670
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
671
        + 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)
672
673
674
675
676
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =L=

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

690
    // Ramping capability of units with an online variable
691
    + (
692
693
        + 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)}
694
695
696
697
698
      )
        * 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
699
700
701
    + [
        // 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
702
703
            * 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
704
                + sum(unitStarttype(unit, starttype),
705
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
706
707
708
709
                        * p_unit(unit, 'rampSpeedToMinLoad')
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
710
        // Units that are in the last time interval of the run-up phase are limited by p_u_maxRampSpeedInLastRunUpInterval(unit)
Niina Helistö's avatar
Niina Helistö committed
711
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
712
713
            * 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
714
                + sum(unitStarttype(unit, starttype),
715
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
716
                        * p_u_maxRampSpeedInLastRunUpInterval(unit) // could also be weighted average from 'maxRampUp' and 'rampSpeedToMinLoad'
Niina Helistö's avatar
Niina Helistö committed
717
718
719
720
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}
721

722
    // Shutdown of consumption units from full load
723
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
724
        * p_gnu(grid, node, unit, 'unitSizeTot')
725
;
726

727
* --- Ramp Down Limits --------------------------------------------------------
728
729
730
731

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')
732
733
734
735
736
                                                             and [ sum(restype, nuRescapable(restype, 'down', node, unit))
                                                                   or uft_online(unit, f, t)
                                                                   or unit_investLP(unit)
                                                                   or unit_investMIP(unit)
                                                                   ]
737
                                                             } ..
738
    + v_genRamp(grid, node, unit, s, f, t)
739
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
740
        + 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)
741
742
743
744
745
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =G=

746
    // Ramping capability of units without online variable
747
748
749
750
751
752
753
754
755
756
757
758
    - (
        + ( 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]

759
    // Ramping capability of units that are online
760
    - (
761
762
        + 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)}
763
764
765
766
767
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

768
    // Shutdown of generation units from full load
769
    - v_shutdown(unit, s, f, t)${   uft_online(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
770
771
                                                 and gnu_output(grid, node, unit)
                                                 and not uft_shutdownTrajectory(unit, f, t)}
772
        * p_gnu(grid, node, unit, 'unitSizeTot')
773

Niina Helistö's avatar
Niina Helistö committed
774
775
776
    + [
        // 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
777
778
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) < ord(t) + dt(t)},
779
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
780
781
782
                    * p_unit(unit, 'rampSpeedFromMinLoad')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
              ) // END sum(t_)
783

Niina Helistö's avatar
Niina Helistö committed
784
785
786
        // Units that are in the first time interval of the shutdown phase are limited rampSpeedFromMinLoad and maxRampDown
        - p_gnu(grid, node, unit, 'unitSizeGen')
            * (
787
                + v_shutdown(unit, s, f+df(f,t+dt(t)), t+dt(t))
Niina Helistö's avatar
Niina Helistö committed
788
789
790
791
792
793
794
                    * 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')
            * (
795
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
796
797
798
799
                    * 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)}
800
801
;

802
803
804
805
806
807
808
* --- 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))
                                                          } ..

809
    + v_genRamp(grid, node, unit, s, f, t)
810

811
    =E=
812

813
814
    // Upward and downward ramp categories
    + sum(slack${ gnuft_rampCost(grid, node, unit, slack, f, t) },
815
816
        + v_genRampUpDown(grid, node, unit, slack, s, f, t)$upwardSlack(slack)
        - v_genRampUpDown(grid, node, unit, slack, s, f, t)$downwardSlack(slack)
817
      ) // END sum(slack)
818
819
;

Niina Helistö's avatar
Niina Helistö committed
820
* --- Upward and downward ramps constrained by slack boundaries ---------------
821
822
823
824
825

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

826
    + v_genRampUpDown(grid, node, unit, slack, s, f, t)
827

828
    =L=
829
830

    // Ramping capability of units without an online variable
831
832
833
834
835
836
837
838
839
840
841
    + (
        + ( 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]
842
843

    // Ramping capability of units with an online variable
844
    + (
845
846
        + 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)}
847
848
849
850
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
851

Niina Helistö's avatar
Niina Helistö committed
852
853
854
    + [
        // Ramping of units that are in the run-up phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
855
856
            * 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
857
                + sum(unitStarttype(unit, starttype),
858
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
859
860
861
862
863
                        * 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)}
864
865

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

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

Niina Helistö's avatar
Niina Helistö committed
877
878
879
    + [
        // Ramping of units that are in the shutdown phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
880
881
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) <= ord(t) + dt(t)},
882
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
883
884
885
886
                    * 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)}
887
;
888

889
890
* --- Fixed Output Ratio ------------------------------------------------------

891
q_outputRatioFixed(gngnu_fixedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
892
893
894
                                                                                        } ..

    // Generation in grid
895
    + v_gen(grid, node, unit, s, f, t)
896
        / p_gnu(grid, node, unit, 'conversionFactor')
897
898
899
900

    =E=

    // Generation in grid_
901
    + v_gen(grid_, node_, unit, s, f, t)
902
        / p_gnu(grid_, node_, unit, 'conversionFactor')
903
;
904
905
906

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

907
q_outputRatioConstrained(gngnu_constrainedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
908
909
910
                                                                                                    } ..

    // Generation in grid
911
    + v_gen(grid, node, unit, s, f, t)
912
        / p_gnu(grid, node, unit, 'conversionFactor')
913
914
915
916

    =G=

    // Generation in grid_
917
    + v_gen(grid_, node_, unit, s, f, t)
918
        / p_gnu(grid_, node_, unit, 'conversionFactor')
Juha Kiviluoma's avatar
Juha Kiviluoma committed
919
;
920
921
922

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

923
q_conversionDirectInputOutput(s, suft(effDirect(effGroup), unit, f, t))$sft(s, f, t) ..
924
925

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

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

935
936
    // Main fuel is not used during run-up and shutdown phases
    + [
Niina Helistö's avatar
Niina Helistö committed
937
938
939
940
        // 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
941
942
943
            * 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
944
                + sum(unitStarttype(unit, starttype),
945
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
946
947
948
949
950
951
952
953
954
                        * 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
955
            * 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
956
                + sum(unitStarttype(unit, starttype),
957
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
958
959
960
961
962
963
964
965
                        * 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
966
967
968
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)
                                    },
969
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
970
971
972
973
974
975
976
977
978
                    * 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)
            * (
979
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
980
981
982
                    * 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
983
984
985
986

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

989
990
991
992
    =E=

    // Sum over energy outputs
    + sum(gnu_output(grid, node, unit),
993
        + v_gen(grid, node, unit, s, f, t)
994
            * [ // efficiency rate
995
                + p_effUnit(effGroup, unit, effGroup, 'slope')${ not ts_effUnit(effGroup, unit, effGroup, 'slope', f, t) }
996
                + ts_effUnit(effGroup, unit, effGroup, 'slope', f, t)
997
998
999
                ] // END * v_gen
        ) // END sum(gnu_output)

1000
    // Consumption of keeping units online (no-load fuel use)