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

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

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

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

18

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

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

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

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

    =E=

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

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

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

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

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

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

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

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

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

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

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

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

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

    =G=

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

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

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

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

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

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

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

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

    =G=

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

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

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

212
213
214
q_maxDownward(m, s, gnuft(grid, node, unit, f, t))${msft(m, s, f, t)
                                                    and {
                                                    [   ord(t) < tSolveFirst + smax(restype, p_nReserves(node, restype, 'reserve_length')) // Unit is either providing
215
216
217
218
219
220
221
222
223
224
225
226
227
228
                                                        and sum(restype, nuRescapable(restype, 'down', node, unit)) // downward reserves
                                                        ]
                                                    // NOTE!!! Could be better to form a gnuft_reserves subset?
                                                    or [ // the unit has an online variable
                                                        uft_online(unit, f, t)
                                                        and [
                                                            (unit_minLoad(unit) and p_gnu(grid, node, unit, 'unitSizeGen')) // generators with a min. load
                                                            or p_gnu(grid, node, unit, 'maxCons') // or consuming units with an online variable
                                                            ]
                                                        ] // END or
                                                    or [ // consuming units with investment possibility
                                                        gnu_input(grid, node, unit)
                                                        and [unit_investLP(unit) or unit_investMIP(unit)]
                                                        ]
229
                                                    }} ..
230
    // Energy generation/consumption
231
    + v_gen(grid, node, unit, s, f, t)
232
233

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

    // Downward reserve participation
240
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
241
        + v_reserve(restype, 'down', node, unit, s, f+df_reserves(node, restype, f, t), t) // (v_reserve can be used only if the unit is capable of providing a particular reserve)
242
243
244
245
246
247
        ) // END sum(nuRescapable)

    =G= // Must be greater than minimum load or maximum consumption  (units with min-load and both generation and consumption are not allowed)

    // Generation units, greater than minload
    + p_gnu(grid, node, unit, 'unitSizeGen')
248
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
249
250
251
252
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [ // Online variables should only be generated for units with restrictions
253
254
            + v_online_LP(unit, s, f+df_central(f,t), t)${uft_onlineLP(unit, f+df_central(f,t), t)} // LP online variant
            + v_online_MIP(unit, s, f+df_central(f,t), t)${uft_onlineMIP(unit, f+df_central(f,t), t)} // MIP online variant
255
256
            ] // END v_online

Niina Helistö's avatar
Niina Helistö committed
257
258
259
    + [
        // 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
260
261
            * 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
262
                + sum(unitStarttype(unit, starttype),
263
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
264
265
266
267
268
269
270
271
                        * 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
272
            * 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
273
                + sum(unitStarttype(unit, starttype),
274
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
275
276
277
278
279
280
281
282
                        * 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
283
284
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
285
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
286
287
                    * sum(t_full(t__)${ord(t__) = ord(t) - ord(t_) + 1},
                        + p_ut_shutdown(unit, t__)
288
                        ) // END sum(t__)
Niina Helistö's avatar
Niina Helistö committed
289
290
291
292
                ) // 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')
            * (
293
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
294
295
296
                    * sum(t_full(t__)${ord(t__) = 1}, p_ut_shutdown(unit, t__))
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
297

298
299
300
301
302
    // Consuming units, greater than maxCons
    // Available capacity restrictions
    - p_unit(unit, 'availability')
        * [
            // Capacity factors for flow units
303
            + sum(flowUnit(flow, unit),
304
                + ts_cf_(flow, node, f, t, s)
305
306
307
308
309
310
311
312
                ) // 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')
                * [
313
                    // Capacity online
314
315
                    + 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)}
316
317
318
319
320
321
322
323

                    // 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)
324
325
                    ] // END * p_gnu(unitSizeCons)
            ] // END * p_unit(availability)
326
;
327
328
329

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

330
331
332
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
333
334
335
336
337
338
339
340
341
342
343
344
345
                                                    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))
                                                    ]
346
                                                }}..
347
    // Energy generation/consumption
348
    + v_gen(grid, node, unit, s, f, t)
349
350
351
352

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

    // Upwards reserve participation
357
    + sum(nuRescapable(restype, 'up', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
358
        + v_reserve(restype, 'up', node, unit, s, f+df_reserves(node, restype, f, t), t)
359
360
361
362
363
364
        ) // END sum(nuRescapable)

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

    // Consuming units
    + p_gnu(grid, node, unit, 'unitSizeCons')
365
        * sum(suft(effGroup, unit, f, t), // Uses the minimum 'lb' for the current efficiency approximation
366
367
368
369
            + p_effGroupUnit(effGroup, unit, 'lb')${not ts_effGroupUnit(effGroup, unit, 'lb', f, t)}
            + ts_effGroupUnit(effGroup, unit, 'lb', f, t)
            ) // END sum(effGroup)
        * [
370
371
            + 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)
372
373
374
375
376
377
378
            ] // 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
379
            + sum(flowUnit(flow, unit),
380
                + ts_cf_(flow, node, f, t, s)
381
382
383
384
385
386
387
388
                ) // 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')
                * [
389
                    // Capacity online
390
391
                    + 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)}
392
393
394
395
396
397
398
399

                    // 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)
400
401
                    ] // END * p_gnu(unitSizeGen)
            ] // END * p_unit(availability)
402

Niina Helistö's avatar
Niina Helistö committed
403
404
405
    + [
        // 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
406
407
            * 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
408
                + sum(unitStarttype(unit, starttype),
409
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
410
411
412
413
414
415
416
                        * 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
417
            * 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
418
                + sum(unitStarttype(unit, starttype),
419
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
420
                        * p_u_maxOutputInLastRunUpInterval(unit)
Niina Helistö's avatar
Niina Helistö committed
421
422
423
424
425
426
427
                    ) // 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
428
429
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_next(t) + dt_toShutdown(unit, t + dt_next(t))
                                    and ord(t_) < ord(t)},
430
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
431
432
433
434
435
436
437
                    * 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')
            * (
438
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
439
440
441
                    * p_u_maxOutputInFirstShutdownInterval(unit)
                ) // END * p_gnu(unitSizeGen)
        ]${uft_shutdownTrajectory(unit, f, t)}
442
;
443

444
445
* --- Reserve Provision of Units with Investments -----------------------------

446
447
448
449
450
451
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)
452
453
454
455
456
457
458
459
460
461
462
463
464

    =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_)
            ]
465
466
467
468
        * p_unit(unit, 'availability') // Taking into account availability...
        * [
            // ... and capacity factor for flow units
            + sum(flowUnit(flow, unit),
469
                + ts_cf_(flow, node, f, t, s)
470
471
                ) // END sum(flow)
            + 1${not unit_flow(unit)}
472
473
474
            ] // How to consider reserveReliability in the case of investments when we typically only have "realized" time steps?
;

475
476
* --- Unit Startup and Shutdown -----------------------------------------------

477
q_startshut(m, s, uft_online(unit, f, t))$msft(m, s, f, t) ..
478
    // Units currently online
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

    // Units previously online
483
484

    // The same units
485
    - 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))
486
                                                             and not uft_aggregator_first(unit, f, t) } // This reaches to tFirstSolve when dt = -1
487
    - 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))
488
489
490
491
                                                             and not uft_aggregator_first(unit, f, t) }

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

496
497
    =E=

498
    // Unit startup and shutdown
499

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

505
506
507
508
    // 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
509

510
    // Shutdown of units at time t
511
    - v_shutdown(unit, s, f, t)
512
;
513

514
*--- Startup Type -------------------------------------------------------------
515
// !!! NOTE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
516
517
518
// 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.
519

520
521
q_startuptype(m, s, starttypeConstrained(starttype), uft_online(unit, f, t))
    ${msft(m, s, f, t) and unitStarttype(unit, starttype)} ..
522
523

    // Startup type
524
    + v_startup(unit, starttype, s, f, t)
525
526
527
528

    =L=

    // Subunit shutdowns within special startup timeframe
Topi Rasku's avatar
Topi Rasku committed
529
    + sum(unitCounter(unit, counter)${dt_starttypeUnitCounter(starttype, unit, counter)},
530
        + 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
531
            ${t_active(t+(dt_starttypeUnitCounter(starttype, unit, counter)+1))}
532
533
534
        ) // END sum(counter)

    // NOTE: for aggregator units, shutdowns for aggregated units are not considered
535
;
536

537

538
539
*--- Online Limits with Startup Type Constraints and Investments --------------

540
541
q_onlineLimit(m, s, uft_online(unit, f, t))${msft(m, s, f, t) and {
                                            p_unit(unit, 'minShutdownHours')
542
                                            or p_u_runUpTimeIntervals(unit)
543
544
                                            or unit_investLP(unit)
                                            or unit_investMIP(unit)
545
                                            }} ..
546
    // Online variables
547
548
    + 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)}
549
550
551
552
553
554

    =L=

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

555
    // Number of units unable to become online due to restrictions
Topi Rasku's avatar
Topi Rasku committed
556
    - sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
557
        + 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
558
            ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
559
560
561
562
        ) // 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
563
        + sum(unitCounter(unit, counter)${dt_downtimeUnitCounter(unit, counter)},
564
            + 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
565
                ${t_active(t+(dt_downtimeUnitCounter(unit, counter) + 1))}
566
567
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
568
569
570

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
571
572
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
573
574
575
        ) // END sum(t_invest)
;

576
577
578
579
*--- 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).
580
581
q_onlineOnStartUp(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
582
583

    // Units currently online
584
585
    + 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)}
586
587
588
589

    =G=

    + sum(unitStarttype(unit, starttype),
590
        + 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
591
592
593
      ) // END sum(starttype)
;

594
595
q_offlineAfterShutdown(s, uft_online(unit, f, t))
    ${sft(s, f, t) and sum(starttype, unitStarttype(unit, starttype))}..
596

597
598
599
600
601
    // Number of existing units
    + p_unit(unit, 'unitCount')

    // Investments into units
    + sum(t_invest(t_)${ord(t_)<=ord(t)},
602
603
        + v_invest_LP(unit, t_)${unit_investLP(unit)}
        + v_invest_MIP(unit, t_)${unit_investMIP(unit)}
604
605
        ) // END sum(t_invest)

606
    // Units currently online
607
608
    - 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)}
609
610
611

    =G=

612
    + v_shutdown(unit, s, f, t)
613
614
;

615
616
*--- Minimum Unit Uptime ------------------------------------------------------

617
618
q_onlineMinUptime(m, s, uft_online(unit, f, t))
    ${msft(m, s, f, t) and  p_unit(unit, 'minOperationHours')} ..
619
620

    // Units currently online
621
622
    + 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)}
623
624
625
626

    =G=

    // Units that have minimum operation time requirements active
Topi Rasku's avatar
Topi Rasku committed
627
    + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
628
        + sum(unitStarttype(unit, starttype),
629
            + 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
630
                ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
631
            ) // END sum(starttype)
632
633
634
        ) // 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
635
636
    + sum(unitAggregator_unit(unit, unit_),
        + sum(unitCounter(unit, counter)${dt_uptimeUnitCounter(unit, counter)},
637
            + sum(unitStarttype(unit, starttype),
638
                + 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
639
                    ${t_active(t+(dt_uptimeUnitCounter(unit, counter)+dt_toStartup(unit, t) + 1))}
640
641
642
                ) // END sum(starttype)
            ) // END sum(counter)
        )${unit_aggregator(unit)} // END sum(unit_)
643
644
;

645
* --- Ramp Constraints --------------------------------------------------------
646
647
648
649

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

651
    + v_genRamp(grid, node, unit, s, f, t) * p_stepLength(m, f, t)
652

653
    =E=
654

655
    // Change in generation over the interval: v_gen(t) - v_gen(t-1)
656
    + v_gen(grid, node, unit, s, f, t)
657

658
    // Unit generation at t-1 (except aggregator units right before the aggregation threshold, see next term)
659
    - 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)}
660
661
    // Unit generation at t-1, aggregator units right before the aggregation threshold
    + sum(unit_${unitAggregator_unit(unit, unit_)},
662
        - v_gen(grid, node, unit_, s+ds(s,t), f+df(f,t+dt(t)), t+dt(t))
663
      )${uft_aggregator_first(unit, f, t)}
664
;
665

666
* --- Ramp Up Limits ----------------------------------------------------------
667
668
669
670

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

    =L=

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

698
    // Ramping capability of units with an online variable
699
    + (
700
701
        + 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)}
702
703
704
705
706
      )
        * 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
707
708
709
    + [
        // 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
710
711
            * 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
712
                + sum(unitStarttype(unit, starttype),
713
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
714
715
716
717
                        * p_unit(unit, 'rampSpeedToMinLoad')
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
718
        // 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
719
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
720
721
            * 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
722
                + sum(unitStarttype(unit, starttype),
723
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
724
                        * p_u_maxRampSpeedInLastRunUpInterval(unit) // could also be weighted average from 'maxRampUp' and 'rampSpeedToMinLoad'
Niina Helistö's avatar
Niina Helistö committed
725
726
727
728
                        * 60   // Unit conversion from [p.u./min] to [p.u./h]
                  ) // END sum(unitStarttype)
              ) // END sum(t_)
        ]${uft_startupTrajectory(unit, f, t)}
729

730
    // Shutdown of consumption units from full load
731
    + v_shutdown(unit, s, f, t)${uft_online(unit, f, t) and gnu_input(grid, node, unit)}
732
        * p_gnu(grid, node, unit, 'unitSizeTot')
733
;
734

735
* --- Ramp Down Limits --------------------------------------------------------
736
737
738
739

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')
740
741
742
743
744
                                                             and [ sum(restype, nuRescapable(restype, 'down', node, unit))
                                                                   or uft_online(unit, f, t)
                                                                   or unit_investLP(unit)
                                                                   or unit_investMIP(unit)
                                                                   ]
745
                                                             } ..
746
    + v_genRamp(grid, node, unit, s, f, t)
747
    - sum(nuRescapable(restype, 'down', node, unit)${ord(t) < tSolveFirst + p_nReserves(node, restype, 'reserve_length')},
748
        + 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)
749
750
751
752
753
        ) // END sum(nuRescapable)
        / p_stepLength(m, f, t)

    =G=

754
    // Ramping capability of units without online variable
755
756
757
758
759
760
761
762
763
764
765
766
    - (
        + ( 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]

767
    // Ramping capability of units that are online
768
    - (
769
770
        + 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)}
771
772
773
774
775
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnu(grid, node, unit, 'maxRampDown')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]

776
    // Shutdown of generation units from full load
777
    - v_shutdown(unit, s, f, t)${   uft_online(unit, f, t)
Niina Helistö's avatar
Niina Helistö committed
778
779
                                                 and gnu_output(grid, node, unit)
                                                 and not uft_shutdownTrajectory(unit, f, t)}
780
        * p_gnu(grid, node, unit, 'unitSizeTot')
781

Niina Helistö's avatar
Niina Helistö committed
782
783
784
    + [
        // 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
785
786
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) < ord(t) + dt(t)},
787
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
788
789
790
                    * p_unit(unit, 'rampSpeedFromMinLoad')
                    * 60   // Unit conversion from [p.u./min] to [p.u./h]
              ) // END sum(t_)
791

Niina Helistö's avatar
Niina Helistö committed
792
793
794
        // Units that are in the first time interval of the shutdown phase are limited rampSpeedFromMinLoad and maxRampDown
        - p_gnu(grid, node, unit, 'unitSizeGen')
            * (
795
                + v_shutdown(unit, s, f+df(f,t+dt(t)), t+dt(t))
Niina Helistö's avatar
Niina Helistö committed
796
797
798
799
800
801
802
                    * 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')
            * (
803
                + v_shutdown(unit, s, f, t)
Niina Helistö's avatar
Niina Helistö committed
804
805
806
807
                    * 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)}
808
809
;

810
811
812
813
814
815
816
* --- 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))
                                                          } ..

817
    + v_genRamp(grid, node, unit, s, f, t)
818

819
    =E=
820

821
822
    // Upward and downward ramp categories
    + sum(slack${ gnuft_rampCost(grid, node, unit, slack, f, t) },
823
824
        + v_genRampUpDown(grid, node, unit, slack, s, f, t)$upwardSlack(slack)
        - v_genRampUpDown(grid, node, unit, slack, s, f, t)$downwardSlack(slack)
825
      ) // END sum(slack)
826
827
;

Niina Helistö's avatar
Niina Helistö committed
828
* --- Upward and downward ramps constrained by slack boundaries ---------------
829
830
831
832
833

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

834
    + v_genRampUpDown(grid, node, unit, slack, s, f, t)
835

836
    =L=
837
838

    // Ramping capability of units without an online variable
839
840
841
842
843
844
845
846
847
848
849
    + (
        + ( 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]
850
851

    // Ramping capability of units with an online variable
852
    + (
853
854
        + 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)}
855
856
857
858
      )
        * p_gnu(grid, node, unit, 'unitSizeTot')
        * p_gnuBoundaryProperties(grid, node, unit, slack, 'rampLimit')
        * 60   // Unit conversion from [p.u./min] to [p.u./h]
859

Niina Helistö's avatar
Niina Helistö committed
860
861
862
    + [
        // Ramping of units that are in the run-up phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
863
864
            * 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
865
                + sum(unitStarttype(unit, starttype),
866
                    + v_startup(unit, starttype, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
867
868
869
870
871
                        * 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)}
872
873

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

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

Niina Helistö's avatar
Niina Helistö committed
885
886
887
    + [
        // Ramping of units that are in the shutdown phase
        + p_gnu(grid, node, unit, 'unitSizeGen')
Topi Rasku's avatar
Topi Rasku committed
888
889
            * sum(t_active(t_)${    ord(t_) >= ord(t) + dt_toShutdown(unit, t)
                                    and ord(t_) <= ord(t) + dt(t)},
890
                + v_shutdown(unit, s, f+df(f,t_), t_)
Niina Helistö's avatar
Niina Helistö committed
891
892
893
894
                    * 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)}
895
;
896

897
898
* --- Fixed Output Ratio ------------------------------------------------------

899
q_outputRatioFixed(gngnu_fixedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
900
901
902
                                                                                        } ..

    // Generation in grid
903
    + v_gen(grid, node, unit, s, f, t)
904
        / p_gnu(grid, node, unit, 'conversionFactor')
905
906
907
908

    =E=

    // Generation in grid_
909
    + v_gen(grid_, node_, unit, s, f, t)
910
        / p_gnu(grid_, node_, unit, 'conversionFactor')
911
;
912
913
914

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

915
q_outputRatioConstrained(gngnu_constrainedOutputRatio(grid, node, grid_, node_, unit), sft(s, f, t))${  uft(unit, f, t)
916
917
918
                                                                                                    } ..

    // Generation in grid
919
    + v_gen(grid, node, unit, s, f, t)
920
        / p_gnu(grid, node, unit, 'conversionFactor')
921
922
923
924

    =G=

    // Generation in grid_
925
    + v_gen(grid_, node_, unit, s, f, t)
926
        / p_gnu(grid_, node_, unit, 'conversionFactor')
Juha Kiviluoma's avatar
Juha Kiviluoma committed
927
;
928
929
930

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

931
q_conversionDirectInputOutput(s, suft(effDirect(effGroup), unit, f, t))$sft(s, f, t) ..
932
933

    // Sum over endogenous energy inputs
934
    - sum(gnu_input(grid, node, unit)${not p_gnu(grid, node, unit, 'doNotOutput')},
935
        + v_gen(grid, node, unit, s, f, t)
936
937
938
939
        ) // END sum(gnu_input)

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

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

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

997
998
999
1000
    =E=

    // Sum over energy outputs
    + sum(gnu_output(grid, node, unit),