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

blue-marble / gridpath / 16325712531

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

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)

517 of 605 new or added lines in 32 files covered. (85.45%)

26 existing lines in 25 files 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

92.0
/gridpath/project/operations/operational_types/load_component_modifier.py
1
# Copyright 2016-2025 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 type is linked to a specific load component that it modifies
17
based on 1) the project capacity relative to the load component peak load and
18
2) a load modification profile (fractional reduction or increase); positive
19
values indicate a load reduction and negative values indicate a load increase.
20
"""
21

22
from pyomo.environ import Param, Set, Reals, Constraint, Var, Any
3✔
23
import warnings
3✔
24

25
from gridpath.auxiliary.auxiliary import (
3✔
26
    subset_init_by_param_value,
27
    subset_init_by_set_membership,
28
)
29
from gridpath.auxiliary.db_interface import directories_to_db_values
3✔
30
from gridpath.auxiliary.validations import (
3✔
31
    write_validation_to_database,
32
    get_projects_by_reserve,
33
    validate_idxs,
34
)
35
from gridpath.auxiliary.dynamic_components import headroom_variables, footroom_variables
3✔
36
from gridpath.project.common_functions import (
3✔
37
    check_if_first_timepoint,
38
    check_boundary_type,
39
)
40
from gridpath.project.operations.operational_types.common_functions import (
3✔
41
    load_var_profile_inputs,
42
    get_prj_temporal_index_opr_inputs_from_db,
43
    write_tab_file_model_inputs,
44
    validate_opchars,
45
    validate_var_profiles,
46
    load_optype_model_data,
47
)
48

49

50
def add_model_components(
3✔
51
    m,
52
    d,
53
    scenario_directory,
54
    weather_iteration,
55
    hydro_iteration,
56
    availability_iteration,
57
    subproblem,
58
    stage,
59
):
60
    """
61
    The following Pyomo model components are defined in this module:
62

63
    +-------------------------------------------------------------------------+
64
    | Sets                                                                    |
65
    +=========================================================================+
66
    | | :code:`LOAD_COMPONENT_MODIFIER_PRJS`                                       |
67
    |                                                                         |
68
    | The set of generators of the :code:`load_component_modifier`            |
69
    | operational type.                                                       |
70
    +-------------------------------------------------------------------------+
71
    | | :code:`LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS`                              |
72
    |                                                                         |
73
    | Two-dimensional set with generators of the                              |
74
    | :code:`load_component_modifier` operational type and their operational  |
75
    | timepoints.                                                             |
76
    +-------------------------------------------------------------------------+
77

78
    |
79

80
    +-------------------------------------------------------------------------+
81
    | Required Input Params                                                   |
82
    +=========================================================================+
83
    | | :code:`load_component_modifier_fraction`                            |
84
    | | *Defined over*: :code:`LOAD_COMPONENT_MODIFIER_PRJS`                       |
85
    | | *Within*: :code:`Reals`                                               |
86
    |                                                                         |
87
    | The project's power output in each operational timepoint as a fraction  |
88
    | of its available capacity (i.e. the capacity factor).                   |
89
    +-------------------------------------------------------------------------+
90

91
    """
92

93
    # Sets
94
    ###########################################################################
95

96
    m.LOAD_COMPONENT_MODIFIER_PRJS = Set(
3✔
97
        within=m.PROJECTS,
98
        initialize=lambda mod: subset_init_by_param_value(
99
            mod, "PROJECTS", "operational_type", "load_component_modifier"
100
        ),
101
    )
102

103
    m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS = Set(
3✔
104
        dimen=2,
105
        within=m.PRJ_OPR_TMPS,
106
        initialize=lambda mod: subset_init_by_set_membership(
107
            mod=mod,
108
            superset="PRJ_OPR_TMPS",
109
            index=0,
110
            membership_set=mod.LOAD_COMPONENT_MODIFIER_PRJS,
111
        ),
112
    )
113

114
    # Derived sets
115
    m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_PRDS = Set(
3✔
116
        within=m.PRJ_OPR_PRDS,
117
        initialize=lambda mod: subset_init_by_set_membership(
118
            mod=mod,
119
            superset="PRJ_OPR_PRDS",
120
            index=0,
121
            membership_set=mod.LOAD_COMPONENT_MODIFIER_PRJS,
122
        ),
123
    )
124

125
    # Required Params
126
    ###########################################################################
127

128
    m.load_component_modifier_linked_load_component = Param(
3✔
129
        m.LOAD_COMPONENT_MODIFIER_PRJS,
130
        within=Any,
131
    )
132

133
    m.load_component_modifier_fraction = Param(
3✔
134
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS, within=Reals
135
    )
136

137
    # Derived params
138
    m.load_component_modifier_load_component_peak_load_in_period = Param(
3✔
139
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_PRDS,
140
        initialize=lambda mod, prj, prd: max(
141
            [
142
                mod.component_static_load_mw[
143
                    mod.load_zone[prj],
144
                    tmp,
145
                    mod.load_component_modifier_linked_load_component[prj],
146
                ]
147
                for tmp in mod.TMPS_IN_PRD[prd]
148
            ]
149
        ),
150
    )
151

152
    m.Load_Component_Modifier_Fraction_Invested = Var(
3✔
153
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_PRDS, bounds=(0, 1), initialize=0
154
    )
155

156
    # Constraints
157
    ###########################################################################
158

159
    def fraction_invested_constraint_rule(mod, prj, prd):
3✔
160
        return (
3✔
161
            mod.Load_Component_Modifier_Fraction_Invested[prj, prd]
162
            == mod.Capacity_MW[prj, prd]
163
            / mod.load_component_modifier_load_component_peak_load_in_period[prj, prd]
164
        )
165

166
    m.Load_Component_Modifier_Fraction_Invested_Constraint = Constraint(
3✔
167
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_PRDS, rule=fraction_invested_constraint_rule
168
    )
169

170
    # TODO: remove this constraint once input validation is in place that
171
    #  does not allow specifying a reserve_zone for 'load_component_modifier'
172
    #  type
173
    def no_upward_reserve_rule(mod, g, tmp):
3✔
174
        """
175
        **Constraint Name**: LoadComponentModifier_No_Upward_Reserves_Constraint
176
        **Enforced Over**: LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS
177

178
        Upward reserves should be zero in every operational timepoint.
179
        """
180
        if getattr(d, headroom_variables)[g]:
3✔
181
            warnings.warn(
×
182
                """project {} is of the 'load_component_modifier' operational 
183
                type and should not be assigned any upward reserve BAs since it 
184
                cannot provide  upward reserves. Please replace the upward 
185
                reserve BA for project {} with '.' (no value) in projects.tab. 
186
                Model will add  constraint to ensure project {} cannot provide 
187
                upward reserves
188
                """.format(
189
                    g, g, g
190
                )
191
            )
192
            return (
×
193
                sum(getattr(mod, c)[g, tmp] for c in getattr(d, headroom_variables)[g])
194
                == 0
195
            )
196
        else:
197
            return Constraint.Skip
3✔
198

199
    m.LoadComponentModifier_No_Upward_Reserves_Constraint = Constraint(
3✔
200
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS, rule=no_upward_reserve_rule
201
    )
202

203
    # TODO: remove this constraint once input validation is in place that
204
    #  does not allow specifying a reserve_zone if 'load_component_modifier' type
205
    def no_downward_reserve_rule(mod, g, tmp):
3✔
206
        """
207
        **Constraint Name**: LoadComponentModifier_No_Downward_Reserves_Constraint
208
        **Enforced Over**: LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS
209

210
        Downward reserves should be zero in every operational timepoint.
211
        """
212
        if getattr(d, footroom_variables)[g]:
3✔
213
            warnings.warn(
×
214
                """project {} is of the 'load_component_modifier' operational 
215
                type and should not be assigned any downward reserve BAs since 
216
                it cannot provide downward reserves. Please replace the
217
                downward reserve BA for project {} with '.' (no value) in 
218
                projects.tab. Model will add constraint to ensure project {} 
219
                cannot provide downward reserves.
220
                """.format(
221
                    g, g, g
222
                )
223
            )
224
            return (
×
225
                sum(getattr(mod, c)[g, tmp] for c in getattr(d, footroom_variables)[g])
226
                == 0
227
            )
228
        else:
229
            return Constraint.Skip
3✔
230

231
    m.LoadComponentModifier_No_Downward_Reserves_Constraint = Constraint(
3✔
232
        m.LOAD_COMPONENT_MODIFIER_PRJS_OPR_TMPS, rule=no_downward_reserve_rule
233
    )
234

235

236
# Operational Type Methods
237
###############################################################################
238

239

240
def power_provision_rule(mod, prj, tmp):
3✔
241
    """
242
    Power provision from variable must-take generators is their capacity times
243
    the capacity factor in each timepoint.
244
    """
245

246
    return (
3✔
247
        mod.Load_Component_Modifier_Fraction_Invested[prj, mod.period[tmp]]
248
        * mod.Availability_Derate[prj, tmp]
249
        * mod.load_component_modifier_fraction[prj, tmp]
250
        * mod.component_static_load_mw[
251
            mod.load_zone[prj],
252
            tmp,
253
            mod.load_component_modifier_linked_load_component[prj],
254
        ]
255
    )
256

257

258
def power_delta_rule(mod, prj, tmp):
3✔
259
    """
260
    This rule is only used in tuning costs, so fine to skip for linked
261
    horizon's first timepoint.
262
    """
263
    if check_if_first_timepoint(
3✔
264
        mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[prj]
265
    ) and (
266
        check_boundary_type(
267
            mod=mod,
268
            tmp=tmp,
269
            balancing_type=mod.balancing_type_project[prj],
270
            boundary_type="linear",
271
        )
272
        or check_boundary_type(
273
            mod=mod,
274
            tmp=tmp,
275
            balancing_type=mod.balancing_type_project[prj],
276
            boundary_type="linked",
277
        )
278
    ):
UNCOV
279
        pass
2✔
280
    else:
281
        return power_provision_rule(mod, prj, tmp) - power_provision_rule(
3✔
282
            mod, prj, mod.prev_tmp[tmp, mod.balancing_type_project[prj]]
283
        )
284

285

286
# Inputs-Outputs
287
###############################################################################
288

289

290
def load_model_data(
3✔
291
    mod,
292
    d,
293
    data_portal,
294
    scenario_directory,
295
    weather_iteration,
296
    hydro_iteration,
297
    availability_iteration,
298
    subproblem,
299
    stage,
300
):
301
    """
302
    :param mod:
303
    :param data_portal:
304
    :param scenario_directory:
305
    :param subproblem:
306
    :param stage:
307
    :return:
308
    """
309

310
    # Load data from projects.tab and get the list of projects of this type
311
    projects = load_optype_model_data(
3✔
312
        mod=mod,
313
        data_portal=data_portal,
314
        scenario_directory=scenario_directory,
315
        weather_iteration=weather_iteration,
316
        hydro_iteration=hydro_iteration,
317
        availability_iteration=availability_iteration,
318
        subproblem=subproblem,
319
        stage=stage,
320
        op_type="load_component_modifier",
321
    )
322

323
    load_var_profile_inputs(
3✔
324
        data_portal=data_portal,
325
        scenario_directory=scenario_directory,
326
        weather_iteration=weather_iteration,
327
        hydro_iteration=hydro_iteration,
328
        availability_iteration=availability_iteration,
329
        subproblem=subproblem,
330
        stage=stage,
331
        op_type="load_component_modifier",
332
        tab_filename="load_component_modifier_fractions.tab",
333
        param_name="fraction",
334
    )
335

336

337
# Database
338
###############################################################################
339

340

341
def get_model_inputs_from_database(
3✔
342
    scenario_id,
343
    subscenarios,
344
    weather_iteration,
345
    hydro_iteration,
346
    availability_iteration,
347
    subproblem,
348
    stage,
349
    conn,
350
):
351
    """
352
    :param subscenarios: SubScenarios object with all subscenario info
353
    :param subproblem:
354
    :param stage:
355
    :param conn: database connection
356
    :return: cursor object with query results
357
    """
358
    (
3✔
359
        db_weather_iteration,
360
        db_hydro_iteration,
361
        db_availability_iteration,
362
        db_subproblem,
363
        db_stage,
364
    ) = directories_to_db_values(
365
        weather_iteration, hydro_iteration, availability_iteration, subproblem, stage
366
    )
367

368
    prj_tmp_data = get_prj_temporal_index_opr_inputs_from_db(
3✔
369
        subscenarios=subscenarios,
370
        weather_iteration=db_weather_iteration,
371
        hydro_iteration=db_hydro_iteration,
372
        availability_iteration=db_availability_iteration,
373
        subproblem=db_subproblem,
374
        stage=db_stage,
375
        conn=conn,
376
        op_type="load_component_modifier",
377
        table="inputs_project_load_modifier_profiles",
378
        subscenario_id_column="load_modifier_profile_scenario_id",
379
        data_column="fraction",
380
    )
381

382
    return prj_tmp_data
3✔
383

384

385
def write_model_inputs(
3✔
386
    scenario_directory,
387
    scenario_id,
388
    subscenarios,
389
    weather_iteration,
390
    hydro_iteration,
391
    availability_iteration,
392
    subproblem,
393
    stage,
394
    conn,
395
):
396
    """
397
    Get inputs from database and write out the model input
398
    variable_generator_profiles.tab file.
399
    :param scenario_directory: string, the scenario directory
400
    :param subscenarios: SubScenarios object with all subscenario info
401
    :param subproblem:
402
    :param stage:
403
    :param conn: database connection
404
    :return:
405
    """
406

407
    data = get_model_inputs_from_database(
3✔
408
        scenario_id,
409
        subscenarios,
410
        weather_iteration,
411
        hydro_iteration,
412
        availability_iteration,
413
        subproblem,
414
        stage,
415
        conn,
416
    )
417
    fname = "load_component_modifier_fractions.tab"
3✔
418

419
    write_tab_file_model_inputs(
3✔
420
        scenario_directory,
421
        weather_iteration,
422
        hydro_iteration,
423
        availability_iteration,
424
        subproblem,
425
        stage,
426
        fname,
427
        data,
428
    )
429

430

431
# Validation
432
###############################################################################
433

434

435
def validate_inputs(
3✔
436
    scenario_id,
437
    subscenarios,
438
    weather_iteration,
439
    hydro_iteration,
440
    availability_iteration,
441
    subproblem,
442
    stage,
443
    conn,
444
):
445
    """
446
    Get inputs from database and validate the inputs
447
    :param subscenarios: SubScenarios object with all subscenario info
448
    :param subproblem:
449
    :param stage:
450
    :param conn: database connection
451
    :return:
452
    """
453

454
    pass
3✔
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