• 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

89.81
/gridpath/project/operations/operational_types/__init__.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
Describe operational constraints on generation, storage, and DR projects.
17

18
This module contains the defaults for the operational type module methods (
19
the standard methods used by the operational type modules to interact with
20
the rest of the model).
21
If an operational type module method is not specified in an operational type
22
module, these defaults are used.
23
"""
24

25
from pyomo.environ import Set
3✔
26

27
from gridpath.auxiliary.auxiliary import get_required_subtype_modules
3✔
28
from gridpath.project.operations.common_functions import load_operational_type_modules
3✔
29

30

31
def add_model_components(
3✔
32
    m,
33
    d,
34
    scenario_directory,
35
    weather_iteration,
36
    hydro_iteration,
37
    availability_iteration,
38
    subproblem,
39
    stage,
40
):
41
    """
42
    +-------------------------------------------------------------------------+
43
    | Sets                                                                    |
44
    +=========================================================================+
45
    | | :code:`GEN_COMMIT_BINLIN`                                             |
46
    | | *Defined over*: :code:`GEN_COMMIT_BIN`                                |
47
    |                                                                         |
48
    | Union of the GEN_COMMIT_BIN and GEN_COMMIT_LIN sets if they exist. We   |
49
    | use this set to limit membership in the GEN_W_CYCLE_SELECT set to these |
50
    | operational types.                                                      |
51
    +-------------------------------------------------------------------------+
52

53
    """
54
    # Import needed operational modules
55
    required_operational_modules = get_required_subtype_modules(
3✔
56
        scenario_directory=scenario_directory,
57
        weather_iteration=weather_iteration,
58
        hydro_iteration=hydro_iteration,
59
        availability_iteration=availability_iteration,
60
        subproblem=subproblem,
61
        stage=stage,
62
        which_type="operational_type",
63
    )
64

65
    imported_operational_modules = load_operational_type_modules(
3✔
66
        required_operational_modules
67
    )
68

69
    # Add any components specific to the operational modules
70
    for op_m in required_operational_modules:
3✔
71
        imp_op_m = imported_operational_modules[op_m]
3✔
72
        if hasattr(imp_op_m, "add_model_components"):
3✔
73
            imp_op_m.add_model_components(
3✔
74
                m,
75
                d,
76
                scenario_directory,
77
                weather_iteration,
78
                hydro_iteration,
79
                availability_iteration,
80
                subproblem,
81
                stage,
82
            )
83

84
    # Combined sets from operational type module sets (used to limit cycle select and
85
    # supplemental firing projects)
86
    def gen_commit_binlin_set_init(mod):
3✔
87
        if hasattr(mod, "GEN_COMMIT_BIN") and hasattr(m, "GEN_COMMIT_LIN"):
3✔
88
            return mod.GEN_COMMIT_BIN | mod.GEN_COMMIT_LIN
3✔
89
        elif hasattr(mod, "GEN_COMMIT_BIN"):
3✔
90
            return mod.GEN_COMMIT_BIN
3✔
91
        elif hasattr(mod, "GEN_COMMIT_LIN"):
3✔
92
            return mod.GEN_COMMIT_LIN
3✔
93
        else:
94
            return []
3✔
95

96
    m.GEN_COMMIT_BINLIN = Set(initialize=gen_commit_binlin_set_init)
3✔
97

98

99
def load_model_data(
3✔
100
    m,
101
    d,
102
    data_portal,
103
    scenario_directory,
104
    weather_iteration,
105
    hydro_iteration,
106
    availability_iteration,
107
    subproblem,
108
    stage,
109
):
110
    """
111

112
    :param m:
113
    :param d:
114
    :param data_portal:
115
    :param scenario_directory:
116
    :param subproblem:
117
    :param stage:
118
    :return:
119
    """
120
    # Import needed operational modules
121
    required_operational_modules = get_required_subtype_modules(
3✔
122
        scenario_directory=scenario_directory,
123
        weather_iteration=weather_iteration,
124
        hydro_iteration=hydro_iteration,
125
        availability_iteration=availability_iteration,
126
        subproblem=subproblem,
127
        stage=stage,
128
        which_type="operational_type",
129
    )
130

131
    imported_operational_modules = load_operational_type_modules(
3✔
132
        required_operational_modules
133
    )
134

135
    # Add any components specific to the operational modules
136
    for op_m in required_operational_modules:
3✔
137
        if hasattr(imported_operational_modules[op_m], "load_model_data"):
3✔
138
            imported_operational_modules[op_m].load_model_data(
3✔
139
                m,
140
                d,
141
                data_portal,
142
                scenario_directory,
143
                weather_iteration,
144
                hydro_iteration,
145
                availability_iteration,
146
                subproblem,
147
                stage,
148
            )
149

150

151
def export_results(
3✔
152
    scenario_directory,
153
    weather_iteration,
154
    hydro_iteration,
155
    availability_iteration,
156
    subproblem,
157
    stage,
158
    m,
159
    d,
160
):
161
    """
162
    Export operations results.
163
    :param scenario_directory:
164
    :param subproblem:
165
    :param stage:
166
    :param m:
167
    The Pyomo abstract model
168
    :param d:
169
    Dynamic components
170
    :return:
171
    Nothing
172
    """
173
    # Export module-specific results
174
    # Operational type modules
175
    required_operational_modules = get_required_subtype_modules(
3✔
176
        scenario_directory=scenario_directory,
177
        weather_iteration=weather_iteration,
178
        hydro_iteration=hydro_iteration,
179
        availability_iteration=availability_iteration,
180
        subproblem=subproblem,
181
        stage=stage,
182
        which_type="operational_type",
183
    )
184

185
    imported_operational_modules = load_operational_type_modules(
3✔
186
        required_operational_modules
187
    )
188

189
    # Add any components specific to the operational modules
190
    for op_m in required_operational_modules:
3✔
191
        if hasattr(imported_operational_modules[op_m], "export_results"):
3✔
192
            imported_operational_modules[op_m].export_results(
3✔
193
                m,
194
                d,
195
                scenario_directory,
196
                weather_iteration,
197
                hydro_iteration,
198
                availability_iteration,
199
                subproblem,
200
                stage,
201
            )
202

203

204
def save_duals(
3✔
205
    scenario_directory,
206
    weather_iteration,
207
    hydro_iteration,
208
    availability_iteration,
209
    subproblem,
210
    stage,
211
    instance,
212
    dynamic_components,
213
):
214
    # Save module-specific duals
215
    # Operational type modules
216
    required_operational_modules = get_required_subtype_modules(
3✔
217
        scenario_directory=scenario_directory,
218
        weather_iteration=weather_iteration,
219
        hydro_iteration=hydro_iteration,
220
        availability_iteration=availability_iteration,
221
        subproblem=subproblem,
222
        stage=stage,
223
        which_type="operational_type",
224
    )
225

226
    imported_operational_modules = load_operational_type_modules(
3✔
227
        required_operational_modules
228
    )
229

230
    # Add any components specific to the operational modules
231
    for op_m in required_operational_modules:
3✔
232
        if hasattr(imported_operational_modules[op_m], "save_duals"):
3✔
233
            imported_operational_modules[op_m].save_duals(
3✔
234
                scenario_directory,
235
                weather_iteration,
236
                hydro_iteration,
237
                availability_iteration,
238
                subproblem,
239
                stage,
240
                instance,
241
                dynamic_components,
242
            )
243

244

245
# TODO: move this into SubScenarios class?
246
def get_required_opchar_modules(scenario_id, c):
3✔
247
    """
248
    Get the required operational type submodules based on the database inputs
249
    for the specified scenario_id. Required modules are the unique set of
250
    generator operational types in the scenario's portfolio. Get the list based
251
    on the project_operational_chars_scenario_id of the scenario_id.
252

253
    This list will be used to know for which operational type submodules we
254
    should validate inputs, get inputs from database, or save results to
255
    database.
256

257
    Note: once we have determined the dynamic components, this information
258
    will also be stored in the DynamicComponents class object.
259

260
    :param scenario_id: user-specified scenario ID
261
    :param c: database cursor
262
    :return: List of the required operational type submodules
263
    """
264

265
    project_portfolio_scenario_id = c.execute(
3✔
266
        """SELECT project_portfolio_scenario_id 
267
        FROM scenarios 
268
        WHERE scenario_id = {}""".format(
269
            scenario_id
270
        )
271
    ).fetchone()[0]
272

273
    project_opchars_scenario_id = c.execute(
3✔
274
        """SELECT project_operational_chars_scenario_id 
275
        FROM scenarios 
276
        WHERE scenario_id = {}""".format(
277
            scenario_id
278
        )
279
    ).fetchone()[0]
280

281
    required_opchar_modules = [
3✔
282
        p[0]
283
        for p in c.execute(
284
            """SELECT DISTINCT operational_type 
285
            FROM 
286
            (SELECT project FROM inputs_project_portfolios
287
            WHERE project_portfolio_scenario_id = {}) as prj_tbl
288
            INNER JOIN 
289
            (SELECT project, operational_type
290
            FROM inputs_project_operational_chars
291
            WHERE project_operational_chars_scenario_id = {}) as op_type_tbl
292
            USING (project);""".format(
293
                project_portfolio_scenario_id, project_opchars_scenario_id
294
            )
295
        ).fetchall()
296
    ]
297

298
    return required_opchar_modules
3✔
299

300

301
def validate_inputs(
3✔
302
    scenario_id,
303
    subscenarios,
304
    weather_iteration,
305
    hydro_iteration,
306
    availability_iteration,
307
    subproblem,
308
    stage,
309
    conn,
310
):
311
    """
312
    Get inputs from database and validate the inputs
313
    :param subscenarios: SubScenarios object with all subscenario info
314
    :param subproblem:
315
    :param stage:
316
    :param conn: database connection
317
    :return:
318
    """
319

320
    # Load in the required operational modules
321
    c = conn.cursor()
3✔
322

323
    required_opchar_modules = get_required_opchar_modules(scenario_id, c)
3✔
324
    imported_operational_modules = load_operational_type_modules(
3✔
325
        required_opchar_modules
326
    )
327

328
    # Validate module-specific inputs
329
    for op_m in required_opchar_modules:
3✔
330
        if hasattr(imported_operational_modules[op_m], "validate_inputs"):
3✔
331
            imported_operational_modules[op_m].validate_inputs(
3✔
332
                scenario_id,
333
                subscenarios,
334
                weather_iteration,
335
                hydro_iteration,
336
                availability_iteration,
337
                subproblem,
338
                stage,
339
                conn,
340
            )
341

342

343
def write_model_inputs(
3✔
344
    scenario_directory,
345
    scenario_id,
346
    subscenarios,
347
    weather_iteration,
348
    hydro_iteration,
349
    availability_iteration,
350
    subproblem,
351
    stage,
352
    conn,
353
):
354
    """
355
    Get inputs from database and write out the model input .tab files
356
    :param scenario_directory: string, the scenario directory
357
    :param subscenarios: SubScenarios object with all subscenario info
358
    :param subproblem:
359
    :param stage:
360
    :param conn: database connection
361
    :return:
362
    """
363

364
    # Load in the required operational modules
365
    c = conn.cursor()
3✔
366

367
    required_opchar_modules = get_required_opchar_modules(scenario_id, c)
3✔
368
    imported_operational_modules = load_operational_type_modules(
3✔
369
        required_opchar_modules
370
    )
371

372
    # Write module-specific inputs
373
    for op_m in required_opchar_modules:
3✔
374
        if hasattr(imported_operational_modules[op_m], "write_model_inputs"):
3✔
375
            imported_operational_modules[op_m].write_model_inputs(
3✔
376
                scenario_directory,
377
                scenario_id,
378
                subscenarios,
379
                weather_iteration,
380
                hydro_iteration,
381
                availability_iteration,
382
                subproblem,
383
                stage,
384
                conn,
385
            )
386

387

388
def process_results(db, c, scenario_id, subscenarios, quiet):
3✔
389
    """
390

391
    :param db:
392
    :param c:
393
    :param subscenarios:
394
    :param quiet:
395
    :return:
396
    """
397

398
    # Load in the required operational modules
399

400
    required_opchar_modules = get_required_opchar_modules(scenario_id, c)
3✔
401
    imported_operational_modules = load_operational_type_modules(
3✔
402
        required_opchar_modules
403
    )
404

405
    # Process module-specific results
406
    for op_m in required_opchar_modules:
3✔
407
        if hasattr(imported_operational_modules[op_m], "process_model_results"):
3✔
408
            imported_operational_modules[op_m].process_model_results(
3✔
409
                db, c, scenario_id, subscenarios, quiet
410
            )
411

412

413
# Operational Type Module Method Defaults
414
###############################################################################
415

416

417
def power_provision_rule(mod, prj, tmp):
3✔
418
    """
419
    If no power_provision_rule is specified in an operational type module, the
420
    default power provision for load-balance purposes is 0.
421
    """
422
    return 0
×
423

424

425
def online_capacity_rule(mod, g, tmp):
3✔
426
    """
427
    The default online capacity is the available capacity.
428
    """
429
    return mod.Capacity_MW[g, mod.period[tmp]] * mod.Availability_Derate[g, tmp]
3✔
430

431

432
def variable_om_cost_rule(mod, prj, tmp):
3✔
433
    """
434
    By default the variable cost is the project-level power provision times the
435
    variable cost. Projects of operational type that produce power not used
436
    for load balancing (e.g. curtailed power or auxiliary power) should not
437
    use this default rule.
438
    """
439
    return mod.Project_Power_Provision_MW[prj, tmp] * mod.variable_om_cost_per_mwh[prj]
3✔
440

441

442
def variable_om_by_period_cost_rule(mod, prj, tmp):
3✔
443
    """
444
    By default the variable cost is the project-level power provision times the
445
    variable cost. Projects of operational type that produce power not used
446
    for load balancing (e.g. curtailed power or auxiliary power) should not
447
    use this default rule.
448
    """
449
    return (
3✔
450
        mod.Project_Power_Provision_MW[prj, tmp]
451
        * mod.variable_om_cost_per_mwh_by_period[prj, mod.period[tmp]]
452
    )
453

454

455
def variable_om_by_timepoint_cost_rule(mod, prj, tmp):
3✔
456
    """
457
    By default the variable cost is the power provision (for load balancing
458
    purposes) times the variable cost. Projects of operational type that
459
    produce power not used for load balancing (e.g. curtailed power or
460
    auxiliary power) should not use this default rule.
461
    """
462
    return (
3✔
463
        mod.Project_Power_Provision_MW[prj, tmp]
464
        * mod.variable_om_cost_per_mwh_by_timepoint[prj, tmp]
465
    )
466

467

468
def variable_om_cost_by_ll_rule(mod, prj, tmp, s):
3✔
469
    """
470
    By default the VOM curve cost needs to be greater than or equal to 0.
471
    """
472
    return 0
×
473

474

475
def fuel_burn_rule(mod, prj, tmp):
3✔
476
    """
477
    If no fuel_burn_rule is specified in an operational type module, the
478
    default fuel burn is 0.
479
    """
480
    return 0
3✔
481

482

483
def fuel_burn_by_ll_rule(mod, prj, tmp, s):
3✔
484
    """
485
    If no fuel_burn_by_ll_rule is specified in an operational type module, the
486
    default fuel burn needs to be greater than or equal to 0.
487
    """
488
    return 0
3✔
489

490

491
def startup_cost_simple_rule(mod, prj, tmp):
3✔
492
    """
493
    If no startup_cost_simple_rule is specified in an operational type module,
494
    the default startup cost is 0.
495
    """
496
    return 0
×
497

498

499
def startup_cost_by_st_rule(mod, prj, tmp):
3✔
500
    """
501
    If no startup_cost_rule is specified in an operational type module, the
502
    default startup fuel cost is 0.
503
    """
504
    return 0
×
505

506

507
def shutdown_cost_rule(mod, prj, tmp):
3✔
508
    """
509
    If no shutdown_cost_rule is specified in an operational type module, the
510
    default shutdown fuel cost is 0.
511
    """
512
    return 0
×
513

514

515
def startup_fuel_burn_rule(mod, prj, tmp):
3✔
516
    """
517
    If no startup_fuel_burn_rule is specified in an operational type module, the
518
    default startup fuel burn is 0.
519
    """
520
    return 0
×
521

522

523
def rec_provision_rule(mod, prj, tmp):
3✔
524
    """
525
    If no rec_provision_rule is specified in an operational type module,
526
    the default REC provisions is the project-level power provision.
527
    """
528
    return mod.Project_Power_Provision_MW[prj, tmp]
3✔
529

530

531
def scheduled_curtailment_rule(mod, prj, tmp):
3✔
532
    """
533
    If no scheduled_curtailment_rule is specified in an operational type
534
    module, the default scheduled curtailment is 0.
535
    """
536
    return 0
3✔
537

538

539
def subhourly_curtailment_rule(mod, prj, tmp):
3✔
540
    """
541
    If no subhourly_curtailment_rule is specified in an operational type
542
    module, the default subhourly curtailment is 0.
543
    """
544
    return 0
3✔
545

546

547
def subhourly_energy_delivered_rule(mod, prj, tmp):
3✔
548
    """
549
    If no subhourly_energy_delivered_rule is specified in an operational type
550
    module, the default subhourly energy delivered is 0.
551
    """
552
    return 0
3✔
553

554

555
def operational_violation_cost_rule(mod, prj, tmp):
3✔
556
    """
557
    If no operational_violation_cost_rule is specified, the default
558
    operational violation cost is 0.
559
    """
560
    return 0
×
561

562

563
def curtailment_cost_rule(mod, prj, tmp):
3✔
564
    """
565
    If no curtailment_cost_rule is specified, the default curtailment cost
566
    is 0.
567
    """
568
    return 0
×
569

570

571
def fuel_contribution_rule(mod, prj, tmp):
3✔
572
    """ """
573
    return 0
3✔
574

575

576
def soc_penalty_cost_rule(mod, prj, tmp):
3✔
577
    """
578
    If no soc_penalty_cost_rule is specified, the default SOC penalty cost is 0.
579
    """
580
    return 0
×
581

582

583
def soc_last_tmp_penalty_cost_rule(mod, prj, tmp):
3✔
584
    """
585
    If no soc_last_tmp_penalty_cost_rule is specified, the default last timepoint SOC penalty cost is 0.
586
    """
587
    return 0
×
588

589

590
def peak_deviation_monthly_demand_charge_cost_rule(mod, prj, prd, mnth):
3✔
591
    return 0
3✔
592

593

594
def capacity_providing_inertia_rule(mod, prj, tmp):
3✔
595
    """
596
    If no inertia_provision_rule is specified, the default inertia is 0.
597
    """
NEW
598
    return 0
×
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