• 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

92.73
/gridpath/project/operations/operational_types/gen_commit_lin.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 type is the same as the *gen_commit_bin* operational type,
17
but the commitment decisions are declared as continuous (with bounds of 0 to
18
1) instead of binary, so 'partial' generators can be committed. This
19
linear relaxation treatment can be helpful in situations when mixed-integer
20
problem run-times are long and is similar to loosening the MIP gap (but can
21
target specific generators). Please refer to the *gen_commit_bin* module for
22
more information on the formulation.
23
"""
24

25
from gridpath.project.operations.operational_types.common_functions import (
3✔
26
    validate_opchars,
27
)
28
from gridpath.common_functions import create_results_df
3✔
29
import gridpath.project.operations.operational_types.gen_commit_unit_common as gen_commit_unit_common
3✔
30

31

32
def add_model_components(
3✔
33
    m,
34
    d,
35
    scenario_directory,
36
    weather_iteration,
37
    hydro_iteration,
38
    availability_iteration,
39
    subproblem,
40
    stage,
41
):
42
    """
43
    See the formulation documentation in the
44
    gen_commit_unit_common.add_model_components().
45
    """
46

47
    gen_commit_unit_common.add_model_components(
3✔
48
        m=m,
49
        d=d,
50
        scenario_directory=scenario_directory,
51
        weather_iteration=weather_iteration,
52
        hydro_iteration=hydro_iteration,
53
        availability_iteration=availability_iteration,
54
        subproblem=subproblem,
55
        stage=stage,
56
        bin_or_lin_optype="gen_commit_lin",
57
    )
58

59

60
# Operational Type Methods
61
###############################################################################
62

63

64
def power_provision_rule(mod, g, tmp):
3✔
65
    """
66
    Power provision for gen_commit_lin generators is a variable constrained
67
    constrained to be between the generator's minimum stable level and its
68
    capacity if the generator is committed and 0 otherwise.
69
    """
70
    return gen_commit_unit_common.power_provision_rule(mod, g, tmp, "Lin")
3✔
71

72

73
def commitment_rule(mod, g, tmp):
3✔
74
    """
75
    Commitment decision in each timepoint
76
    """
77
    return gen_commit_unit_common.commitment_rule(mod, g, tmp, "Lin")
3✔
78

79

80
def online_capacity_rule(mod, g, tmp):
3✔
81
    """
82
    Capacity online in each timepoint.
83
    """
84
    return gen_commit_unit_common.online_capacity_rule(mod, g, tmp, "Lin")
3✔
85

86

87
def variable_om_cost_rule(mod, g, tmp):
3✔
88
    """
89
    Variable O&M cost has two components which are additive:
90
    1. A fixed variable O&M rate (cost/MWh) that doesn't change with loading
91
       levels: :code:`variable_om_cost_per_mwh`.
92
    2. A fixed variable O&M rate by period (cost/MWh) that doesn't change with
93
       loading levels: :code:`variable_om_cost_per_mwh_by_period`.
94
    3. A variable O&M rate that changes with the loading level,
95
       similar to the heat rates. The idea is to represent higher variable cost
96
       rates at lower loading levels. This is captured in the
97
       :code:`GenCommitLin_Variable_OM_Cost_By_LL` decision variable. If no
98
       variable O&M curve inputs are provided, this component will be zero.
99

100
    Most users will only use the first component, which is specified in the
101
    operational characteristics table.  Only operational types with
102
    commitment decisions can have the second component.
103

104
    We need to explicitly have the op type method here because of auxiliary
105
    consumption. The default method takes Project_Power_Provision_MW multiplied by
106
    the variable cost, and Project_Power_Provision_MW is equal to Provide_Power_MW
107
    minus the auxiliary consumption. The variable cost should be applied to
108
    the gross power.
109
    """
110
    return gen_commit_unit_common.variable_om_cost_rule(mod, g, tmp, "Lin")
3✔
111

112

113
def variable_om_by_period_cost_rule(mod, g, tmp):
3✔
114
    """ """
115
    return gen_commit_unit_common.variable_om_by_period_cost_rule(mod, g, tmp, "Lin")
×
116

117

118
def variable_om_by_timepoint_cost_rule(mod, g, tmp):
3✔
119
    """ """
120
    return gen_commit_unit_common.variable_om_by_timepoint_cost_rule(mod, g, tmp, "Lin")
×
121

122

123
def variable_om_cost_by_ll_rule(mod, g, tmp, s):
3✔
124
    """
125
    Variable O&M cost has two components which are additive:
126
    1. A fixed variable O&M rate (cost/MWh) that doesn't change with loading
127
       levels: :code:`variable_om_cost_per_mwh`.
128
    2. A variable variable O&M rate that changes with the loading level,
129
       similar to the heat rates. The idea is to represent higher variable cost
130
       rates at lower loading levels. This is captured in the
131
       :code:`GenCommitLin_Variable_OM_Cost_By_LL` decision variable. If no
132
       variable O&M curve inputs are provided, this component will be zero.
133

134
    Most users will only use the first component, which is specified in the
135
    operational characteristics table.  Only operational types with
136
    commitment decisions can have the second component.
137
    """
138
    return gen_commit_unit_common.variable_om_cost_by_ll_rule(mod, g, tmp, s, "Lin")
3✔
139

140

141
def startup_cost_simple_rule(mod, g, tmp):
3✔
142
    """
143
    Simple startup costs are applied in each timepoint based on the amount of
144
    capacity (in MW) that is started up in that timepoint and the startup cost
145
    parameter.
146
    """
147
    return gen_commit_unit_common.startup_cost_simple_rule(mod, g, tmp, "Lin")
×
148

149

150
def startup_cost_by_st_rule(mod, g, tmp):
3✔
151
    """
152
    Startup costs are applied in each timepoint based on the amount of capacity
153
    (in MW) that is started up in that timepoint for a given startup type and
154
    the startup cost parameter for that startup type. We take the sum across
155
    all startup types since only one startup type is active at the same time.
156
    """
157
    return gen_commit_unit_common.startup_cost_by_st_rule(mod, g, tmp, "LIN", "Lin")
3✔
158

159

160
def shutdown_cost_rule(mod, g, tmp):
3✔
161
    """
162
    Shutdown costs are applied in each timepoint based on the amount of
163
    capacity (in Mw) that is shut down in that timepoint and the shutdown
164
    cost parameter.
165
    """
166
    return gen_commit_unit_common.shutdown_cost_rule(mod, g, tmp, "Lin")
3✔
167

168

169
def fuel_burn_by_ll_rule(mod, g, tmp, s):
3✔
170
    """ """
171
    return gen_commit_unit_common.fuel_burn_by_ll_rule(mod, g, tmp, s, "Lin")
3✔
172

173

174
def startup_fuel_burn_rule(mod, g, tmp):
3✔
175
    """
176
    Startup fuel burn is applied in each timepoint based on the amount of
177
    capacity (in MW) that is started up in that timepoint and the startup
178
    fuel parameter. This does not vary by startup type.
179
    """
180
    return gen_commit_unit_common.startup_fuel_burn_rule(mod, g, tmp, "Lin")
3✔
181

182

183
def power_delta_rule(mod, g, tmp):
3✔
184
    """
185
    Ramp between this timepoint and the previous timepoint.
186
    Actual ramp rate in MW/hr depends on the duration of the timepoints.
187
    This is only used in tuning costs, so fine to skip for linked horizon's
188
    first timepoint.
189
    """
190
    return gen_commit_unit_common.power_delta_rule(mod, g, tmp, "Lin")
3✔
191

192

193
def fix_commitment(mod, g, tmp):
3✔
194
    """ """
195
    return gen_commit_unit_common.fix_commitment(mod, g, tmp, "Lin")
3✔
196

197

198
def operational_violation_cost_rule(mod, g, tmp):
3✔
199
    """
200

201
    :param mod:
202
    :param g:
203
    :param tmp:
204
    :return:
205
    """
206
    return gen_commit_unit_common.operational_violation_cost_rule(
3✔
207
        mod, g, tmp, "lin", "Lin"
208
    )
209

210

211
def capacity_providing_inertia_rule(mod, g, tmp):
3✔
212
    """
213
    Capacity providing inertia for GEN_VAR project is equal to the online
214
    capacity
215
    """
NEW
216
    return gen_commit_unit_common.capacity_providing_inertia_rule(mod, g, tmp, "Lin")
×
217

218

219
# Input-Output
220
###############################################################################
221

222

223
def load_model_data(
3✔
224
    mod,
225
    d,
226
    data_portal,
227
    scenario_directory,
228
    weather_iteration,
229
    hydro_iteration,
230
    availability_iteration,
231
    subproblem,
232
    stage,
233
):
234
    """
235
    :param mod:
236
    :param data_portal:
237
    :param scenario_directory:
238
    :param subproblem:
239
    :param stage:
240
    :return:
241
    """
242

243
    gen_commit_unit_common.load_model_data(
3✔
244
        mod=mod,
245
        d=d,
246
        data_portal=data_portal,
247
        scenario_directory=scenario_directory,
248
        weather_iteration=weather_iteration,
249
        hydro_iteration=hydro_iteration,
250
        availability_iteration=availability_iteration,
251
        subproblem=subproblem,
252
        stage=stage,
253
        bin_or_lin_optype="gen_commit_lin",
254
        bin_or_lin="lin",
255
        BIN_OR_LIN="LIN",
256
    )
257

258

259
def add_to_prj_tmp_results(mod):
3✔
260
    results_columns, data = gen_commit_unit_common.add_to_prj_tmp_results(
3✔
261
        mod=mod,
262
        BIN_OR_LIN="LIN",
263
        Bin_or_Lin="Lin",
264
        bin_or_lin="lin",
265
    )
266

267
    (
3✔
268
        duals_results_columns,
269
        duals_data,
270
    ) = gen_commit_unit_common.add_duals_to_dispatch_results(
271
        mod=mod,
272
        BIN_OR_LIN="LIN",
273
        Bin_or_Lin="Lin",
274
    )
275

276
    # Create DF
277
    optype_dispatch_df = create_results_df(
3✔
278
        index_columns=["project", "timepoint"],
279
        results_columns=results_columns,
280
        data=data,
281
    )
282

283
    # Get the duals
284
    optype_duals_df = create_results_df(
3✔
285
        index_columns=["project", "timepoint"],
286
        results_columns=duals_results_columns,
287
        data=duals_data,
288
    )
289

290
    # Add duals to dispatch DF
291
    results_columns += duals_results_columns
3✔
292
    for column in duals_results_columns:
3✔
293
        optype_dispatch_df[column] = None
3✔
294
    optype_dispatch_df.update(optype_duals_df)
3✔
295

296
    return results_columns, optype_dispatch_df
3✔
297

298

299
def export_results(
3✔
300
    mod,
301
    d,
302
    scenario_directory,
303
    weather_iteration,
304
    hydro_iteration,
305
    availability_iteration,
306
    subproblem,
307
    stage,
308
):
309
    """
310
    :param scenario_directory:
311
    :param subproblem:
312
    :param stage:
313
    :param mod:
314
    :param d:
315
    :return:
316
    """
317
    gen_commit_unit_common.export_linked_subproblem_inputs(
3✔
318
        mod=mod,
319
        d=d,
320
        scenario_directory=scenario_directory,
321
        weather_iteration=weather_iteration,
322
        hydro_iteration=hydro_iteration,
323
        availability_iteration=availability_iteration,
324
        subproblem=subproblem,
325
        stage=stage,
326
        BIN_OR_LIN="LIN",
327
        Bin_or_Lin="Lin",
328
        bin_or_lin="lin",
329
    )
330

331

332
def save_duals(
3✔
333
    scenario_directory,
334
    weather_iteration,
335
    hydro_iteration,
336
    availability_iteration,
337
    subproblem,
338
    stage,
339
    instance,
340
    dynamic_components,
341
):
342
    gen_commit_unit_common.save_duals(instance, "Lin")
3✔
343

344

345
# Validation
346
###############################################################################
347

348

349
def validate_inputs(
3✔
350
    scenario_id,
351
    subscenarios,
352
    weather_iteration,
353
    hydro_iteration,
354
    availability_iteration,
355
    subproblem,
356
    stage,
357
    conn,
358
):
359
    """
360
    Get inputs from database and validate the inputs
361

362
    :param subscenarios: SubScenarios object with all subscenario info
363
    :param subproblem:
364
    :param stage:
365
    :param conn: database connection
366
    :return:
367
    """
368

369
    # Validate operational chars table inputs
370
    opchar_df = validate_opchars(
3✔
371
        scenario_id,
372
        subscenarios,
373
        weather_iteration,
374
        hydro_iteration,
375
        availability_iteration,
376
        subproblem,
377
        stage,
378
        conn,
379
        "gen_commit_lin",
380
    )
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