• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blue-marble / gridpath / 16251441985

07 Jul 2025 10:06PM UTC coverage: 88.949% (-0.08%) from 89.026%
16251441985

push

github

web-flow
New optional feature for Inertia reserve (#1275)

Implemented a new optional_features to model inertia reserves with all the documentation and test associated.

Inertia reserve is a grid service that units provide if they are generating and slows down the variation of the frequency when a disturbance happens on the grid, allowing more time for frequency reserves to activate.

The implementation for this optional feature follows the general structure of operational reserves in GridPath where requirements can depend on multiple factors and the sum of the inertia of projects must be greater than the requirement (with violation if allowed). There are 2 major differences between operational reserves and inertia:

	The requirement for inertia is in MWs instead of MW, which means that we must add a file in the requirement folder where the user will have to input the operating frequency of the system and the ROCOF.  The requirements that are specified by the user (% of the load, MW per timepoint ratio of project capacity/power) will then be multiplied by  (f_0 " " )/(2 *Rocof ).

	Instead of relying on headroom and footroom, Inertia relies on the nameplate capacity of units that are operating, the inertia of a project is equal to its nameplate capacity * an inertia constant that is technology/project specific if that project is generating. To implement this, an capacity_providing_inertia_rule with a default of 0 was added to projects and its definition changes based on the operational type. This inertia capacity is then multiplied by the inertia constant.

Additional changes were made to the dispatchable_load operational type to allow it to act like a synchronous condenser: The energy_requirement_factor allows a project to activate its capacity at a fraction of the energy that would normally be required. For example, when a synchronous condenser operates it consumes 1% to 3% of its nameplate capacity so while the online capacity is 100%, it power consumption ... (continued)

519 of 607 new or added lines in 32 files covered. (85.5%)

2 existing lines in 1 file now uncovered.

27310 of 30703 relevant lines covered (88.95%)

2.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.91
/gridpath/project/operations/operational_types/gen_commit_bin.py
1
# Copyright 2016-2023 Blue Marble Analytics LLC.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
#     http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14

15
"""
16
This operational types describes generation projects that can be turned on and
17
off, i.e. that have binary commitment variables associated with them. This is
18
particularly useful for production cost modeling approaches where capturing
19
the unit commitment decisions is important, e.g. when modeling a slow-starting
20
coal plant. This operational type is not compatible with new-build capacity
21
types (e.g. gen_new_lin) where the available capacity is an endogenous decision
22
variable.
23

24
The optimization makes commitment and power output decisions in every
25
timepoint. If the project is not committed (or starting up / shutting down),
26
power output is zero. If it is committed, power output can vary between a
27
pre-specified minimum stable level (greater than zero) and the project's
28
available capacity. Heat rate degradation below full load is considered.
29
These projects can be allowed to provide upward and/or downward reserves.
30

31
Startup and/or shutdown trajectories can be optionally modeled by specifying a
32
low startup and/or shutdown ramp rate.  Ramp rate limits as well us minimum up
33
and down time constraints are implemented. Starts and stops -- and the
34
associated cost and emissions -- can be tracked and constrained.
35

36
Costs for this operational type include fuel costs, variable O&M costs, and
37
startup and shutdown costs.
38

39
Interesting background papers:
40
- "Hidden power system inflexibilities imposed by traditional unit commitment
41
formulations", Morales-Espana et al. (2017).
42
- "Tight and compact MILP formulation for the thermal unit commitment problem",
43
Morales-Espana et al. (2013).
44

45
"""
46

47
from gridpath.project.operations.operational_types.common_functions import (
3✔
48
    validate_opchars,
49
)
50
from gridpath.common_functions import create_results_df
3✔
51
import gridpath.project.operations.operational_types.gen_commit_unit_common as gen_commit_unit_common
3✔
52

53

54
def add_model_components(
3✔
55
    m,
56
    d,
57
    scenario_directory,
58
    weather_iteration,
59
    hydro_iteration,
60
    availability_iteration,
61
    subproblem,
62
    stage,
63
):
64
    """
65
    See the formulation documentation in the
66
    gen_commit_unit_common.add_model_components().
67
    """
68

69
    gen_commit_unit_common.add_model_components(
3✔
70
        m=m,
71
        d=d,
72
        scenario_directory=scenario_directory,
73
        weather_iteration=weather_iteration,
74
        hydro_iteration=hydro_iteration,
75
        availability_iteration=availability_iteration,
76
        subproblem=subproblem,
77
        stage=stage,
78
        bin_or_lin_optype="gen_commit_bin",
79
    )
80

81

82
# Operational Type Methods
83
###############################################################################
84

85

86
def power_provision_rule(mod, g, tmp):
3✔
87
    """
88
    Power provision for gen_commit_bin generators is a variable constrained
89
    constrained to be between the generator's minimum stable level and its
90
    capacity if the generator is committed and 0 otherwise.
91
    """
92
    return gen_commit_unit_common.power_provision_rule(mod, g, tmp, "Bin")
3✔
93

94

95
def commitment_rule(mod, g, tmp):
3✔
96
    """
97
    Commitment decision in each timepoint
98
    """
99
    return gen_commit_unit_common.commitment_rule(mod, g, tmp, "Bin")
3✔
100

101

102
def online_capacity_rule(mod, g, tmp):
3✔
103
    """
104
    Capacity online in each timepoint.
105
    """
106
    return gen_commit_unit_common.online_capacity_rule(mod, g, tmp, "Bin")
3✔
107

108

109
def variable_om_cost_rule(mod, g, tmp):
3✔
110
    """
111
    Variable O&M cost has three components which are additive:
112
    1. A fixed variable O&M rate (cost/MWh) that doesn't change with loading
113
       levels: :code:`variable_om_cost_per_mwh`.
114
    2. A fixed variable O&M rate by period (cost/MWh) that doesn't change with
115
       loading levels: :code:`variable_om_cost_per_mwh_by_period`.
116
    3. A variable variable O&M rate that changes with the loading level,
117
       similar to the heat rates. The idea is to represent higher variable cost
118
       rates at lower loading levels. This is captured in the
119
       :code:`GenCommitBin_Variable_OM_Cost_By_LL` decision variable. If no
120
       variable O&M curve inputs are provided, this component will be zero.
121

122
    Most users will only use the first component, which is specified in the
123
    operational characteristics table.  Only operational types with
124
    commitment decisions can have the second component.
125

126
    We need to explicitly have the op type method here because of auxiliary
127
    consumption. The default method takes Project_Power_Provision_MW multiplied by
128
    the variable cost, and Project_Power_Provision_MW is equal to Provide_Power_MW
129
    minus the auxiliary consumption. The variable cost should be applied to
130
    the gross power.
131
    """
132
    return gen_commit_unit_common.variable_om_cost_rule(mod, g, tmp, "Bin")
3✔
133

134

135
def variable_om_by_period_cost_rule(mod, g, tmp):
3✔
136
    """ """
137
    return gen_commit_unit_common.variable_om_by_period_cost_rule(mod, g, tmp, "Bin")
×
138

139

140
def variable_om_by_timepoint_cost_rule(mod, g, tmp):
3✔
141
    """ """
142
    return gen_commit_unit_common.variable_om_by_timepoint_cost_rule(mod, g, tmp, "Bin")
×
143

144

145
def variable_om_cost_by_ll_rule(mod, g, tmp, s):
3✔
146
    """
147
    Variable O&M cost has two components which are additive:
148
    1. A fixed variable O&M rate (cost/MWh) that doesn't change with loading
149
       levels: :code:`variable_om_cost_per_mwh`.
150
    2. A variable variable O&M rate that changes with the loading level,
151
       similar to the heat rates. The idea is to represent higher variable cost
152
       rates at lower loading levels. This is captured in the
153
       :code:`GenCommitBin_Variable_OM_Cost_By_LL` decision variable. If no
154
       variable O&M curve inputs are provided, this component will be zero.
155

156
    Most users will only use the first component, which is specified in the
157
    operational characteristics table.  Only operational types with
158
    commitment decisions can have the second component.
159
    """
160
    return gen_commit_unit_common.variable_om_cost_by_ll_rule(mod, g, tmp, s, "Bin")
3✔
161

162

163
def startup_cost_simple_rule(mod, g, tmp):
3✔
164
    """
165
    Simple startup costs are applied in each timepoint based on the amount of
166
    capacity (in MW) that is started up in that timepoint and the startup cost
167
    parameter.
168
    """
169
    return gen_commit_unit_common.startup_cost_simple_rule(mod, g, tmp, "Bin")
×
170

171

172
def startup_cost_by_st_rule(mod, g, tmp):
3✔
173
    """
174
    Startup costs are applied in each timepoint based on the amount of capacity
175
    (in MW) that is started up in that timepoint for a given startup type and
176
    the startup cost parameter for that startup type. We take the sum across
177
    all startup types since only one startup type is active at the same time.
178
    """
179
    return gen_commit_unit_common.startup_cost_by_st_rule(mod, g, tmp, "BIN", "Bin")
3✔
180

181

182
def shutdown_cost_rule(mod, g, tmp):
3✔
183
    """
184
    Shutdown costs are applied in each timepoint based on the amount of
185
    capacity (in Mw) that is shut down in that timepoint and the shutdown
186
    cost parameter.
187
    """
188
    return gen_commit_unit_common.shutdown_cost_rule(mod, g, tmp, "Bin")
3✔
189

190

191
def fuel_burn_by_ll_rule(mod, g, tmp, s):
3✔
192
    """ """
193
    return gen_commit_unit_common.fuel_burn_by_ll_rule(mod, g, tmp, s, "Bin")
3✔
194

195

196
def startup_fuel_burn_rule(mod, g, tmp):
3✔
197
    """
198
    Startup fuel burn is applied in each timepoint based on the amount of
199
    capacity (in MW) that is started up in that timepoint and the startup
200
    fuel parameter. This does not vary by startup type.
201
    """
202
    return gen_commit_unit_common.startup_fuel_burn_rule(mod, g, tmp, "Bin")
3✔
203

204

205
def power_delta_rule(mod, g, tmp):
3✔
206
    """
207
    Ramp between this timepoint and the previous timepoint.
208
    Actual ramp rate in MW/hr depends on the duration of the timepoints.
209
    This is only used in tuning costs, so fine to skip for linked horizon's
210
    first timepoint.
211
    """
212
    return gen_commit_unit_common.power_delta_rule(mod, g, tmp, "Bin")
3✔
213

214

215
def fix_commitment(mod, g, tmp):
3✔
216
    """ """
217
    return gen_commit_unit_common.fix_commitment(mod, g, tmp, "Bin")
×
218

219

220
def operational_violation_cost_rule(mod, g, tmp):
3✔
221
    """
222

223
    :param mod:
224
    :param g:
225
    :param tmp:
226
    :return:
227
    """
228
    return gen_commit_unit_common.operational_violation_cost_rule(
3✔
229
        mod, g, tmp, "bin", "Bin"
230
    )
231

232

233
def capacity_providing_inertia_rule(mod, g, tmp):
3✔
234
    """
235
    Capacity providing inertia for GEN_VAR project is equal to the online
236
    capacity
237
    """
NEW
238
    return gen_commit_unit_common.capacity_providing_inertia_rule(mod, g, tmp, "Bin")
×
239

240

241
# Input-Output
242
###############################################################################
243

244

245
def load_model_data(
3✔
246
    mod,
247
    d,
248
    data_portal,
249
    scenario_directory,
250
    weather_iteration,
251
    hydro_iteration,
252
    availability_iteration,
253
    subproblem,
254
    stage,
255
):
256
    """
257
    :param mod:
258
    :param data_portal:
259
    :param scenario_directory:
260
    :param subproblem:
261
    :param stage:
262
    :return:
263
    """
264

265
    gen_commit_unit_common.load_model_data(
3✔
266
        mod=mod,
267
        d=d,
268
        data_portal=data_portal,
269
        scenario_directory=scenario_directory,
270
        weather_iteration=weather_iteration,
271
        hydro_iteration=hydro_iteration,
272
        availability_iteration=availability_iteration,
273
        subproblem=subproblem,
274
        stage=stage,
275
        bin_or_lin_optype="gen_commit_bin",
276
        bin_or_lin="bin",
277
        BIN_OR_LIN="BIN",
278
    )
279

280

281
def add_to_prj_tmp_results(mod):
3✔
282
    results_columns, data = gen_commit_unit_common.add_to_prj_tmp_results(
3✔
283
        mod=mod,
284
        BIN_OR_LIN="BIN",
285
        Bin_or_Lin="Bin",
286
        bin_or_lin="bin",
287
    )
288

289
    (
3✔
290
        duals_results_columns,
291
        duals_data,
292
    ) = gen_commit_unit_common.add_duals_to_dispatch_results(
293
        mod=mod,
294
        BIN_OR_LIN="BIN",
295
        Bin_or_Lin="Bin",
296
    )
297

298
    # Create DF
299
    optype_dispatch_df = create_results_df(
3✔
300
        index_columns=["project", "timepoint"],
301
        results_columns=results_columns,
302
        data=data,
303
    )
304

305
    # Get the duals
306
    optype_duals_df = create_results_df(
3✔
307
        index_columns=["project", "timepoint"],
308
        results_columns=duals_results_columns,
309
        data=duals_data,
310
    )
311

312
    # Add duals to dispatch DF
313
    results_columns += duals_results_columns
3✔
314
    for column in duals_results_columns:
3✔
315
        optype_dispatch_df[column] = None
3✔
316
    optype_dispatch_df.update(optype_duals_df)
3✔
317

318
    return results_columns, optype_dispatch_df
3✔
319

320

321
def export_results(
3✔
322
    mod,
323
    d,
324
    scenario_directory,
325
    weather_iteration,
326
    hydro_iteration,
327
    availability_iteration,
328
    subproblem,
329
    stage,
330
):
331
    """
332
    :param scenario_directory:
333
    :param subproblem:
334
    :param stage:
335
    :param mod:
336
    :param d:
337
    :return:
338
    """
339
    gen_commit_unit_common.export_linked_subproblem_inputs(
3✔
340
        mod=mod,
341
        d=d,
342
        scenario_directory=scenario_directory,
343
        weather_iteration=weather_iteration,
344
        hydro_iteration=hydro_iteration,
345
        availability_iteration=availability_iteration,
346
        subproblem=subproblem,
347
        stage=stage,
348
        BIN_OR_LIN="BIN",
349
        Bin_or_Lin="Bin",
350
        bin_or_lin="bin",
351
    )
352

353

354
def save_duals(
3✔
355
    scenario_directory,
356
    weather_iteration,
357
    hydro_iteration,
358
    availability_iteration,
359
    subproblem,
360
    stage,
361
    instance,
362
    dynamic_components,
363
):
364
    gen_commit_unit_common.save_duals(instance, "Bin")
3✔
365

366

367
# Validation
368
###############################################################################
369

370

371
def validate_inputs(
3✔
372
    scenario_id,
373
    subscenarios,
374
    weather_iteration,
375
    hydro_iteration,
376
    availability_iteration,
377
    subproblem,
378
    stage,
379
    conn,
380
):
381
    """
382
    Get inputs from database and validate the inputs
383

384
    :param subscenarios: SubScenarios object with all subscenario info
385
    :param subproblem:
386
    :param stage:
387
    :param conn: database connection
388
    :return:
389
    """
390

391
    # Validate operational chars table inputs
392
    opchar_df = validate_opchars(
3✔
393
        scenario_id,
394
        subscenarios,
395
        weather_iteration,
396
        hydro_iteration,
397
        availability_iteration,
398
        subproblem,
399
        stage,
400
        conn,
401
        "gen_commit_bin",
402
    )
403

404
    # TODO: add warning that if availabilities are partial, we treat them as
405
    #  full (because of the synced <= availability constraint with a binary
406
    #  formulation)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc