• 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

80.9
/gridpath/project/operations/operational_types/gen_simple.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 describes generators that can vary their output
17
between zero and full capacity in every timepoint in which they are available
18
(i.e. they have a power output variable but no commitment variables associated
19
with them).
20

21
The heat rate of these generators does not degrade below full load and they
22
can be allowed to provide upward and/or downward reserves subject to
23
available headroom and footroom. Ramp limits can be optionally specified.
24

25
Costs for this operational type include fuel costs, variable O&M costs, and
26
startup and shutdown costs.
27

28
"""
29

30
import csv
3✔
31
import os
3✔
32
from pyomo.environ import (
3✔
33
    Set,
34
    Var,
35
    Constraint,
36
    NonNegativeReals,
37
    Param,
38
    PercentFraction,
39
    Expression,
40
    value,
41
)
42

43
from gridpath.auxiliary.auxiliary import (
3✔
44
    subset_init_by_param_value,
45
    cursor_to_df,
46
    subset_init_by_set_membership,
47
)
48
from gridpath.auxiliary.validations import (
3✔
49
    write_validation_to_database,
50
    validate_single_input,
51
)
52
from gridpath.auxiliary.dynamic_components import headroom_variables, footroom_variables
3✔
53
from gridpath.project.common_functions import (
3✔
54
    check_if_boundary_type_and_first_timepoint,
55
    check_if_first_timepoint,
56
    check_boundary_type,
57
)
58
from gridpath.project.operations.operational_types.common_functions import (
3✔
59
    load_optype_model_data,
60
    check_for_tmps_to_link,
61
    validate_opchars,
62
)
63

64

65
def add_model_components(
3✔
66
    m,
67
    d,
68
    scenario_directory,
69
    weather_iteration,
70
    hydro_iteration,
71
    availability_iteration,
72
    subproblem,
73
    stage,
74
):
75
    """
76
    The following Pyomo model components are defined in this module:
77

78
    +-------------------------------------------------------------------------+
79
    | Sets                                                                    |
80
    +=========================================================================+
81
    | | :code:`GEN_SIMPLE`                                                    |
82
    |                                                                         |
83
    | The set of generators of the :code:`gen_simple` operational type.       |
84
    +-------------------------------------------------------------------------+
85
    | | :code:`GEN_SIMPLE_OPR_TMPS`                                           |
86
    |                                                                         |
87
    | Two-dimensional set with generators of the :code:`gen_simple`           |
88
    | operational type and their operational timepoints.                      |
89
    +-------------------------------------------------------------------------+
90
    | | :code:`GEN_SIMPLE_LINKED_TMPS`                                        |
91
    |                                                                         |
92
    | Two-dimensional set with generators of the :code:`gen_simple`           |
93
    | operational type and their linked timepoints.                           |
94
    +-------------------------------------------------------------------------+
95

96
    |
97

98
    +-------------------------------------------------------------------------+
99
    | Optional Input Params                                                   |
100
    +=========================================================================+
101
    | | :code:`gen_simple_ramp_up_when_on_rate`                               |
102
    | | *Defined over*: :code:`GEN_SIMPLE`                                    |
103
    | | *Within*: :code:`PercentFraction`                                     |
104
    | | *Default*: :code:`1`                                                  |
105
    |                                                                         |
106
    | The project's upward ramp rate limit during operations, defined as a    |
107
    | fraction of its capacity per minute.                                    |
108
    +-------------------------------------------------------------------------+
109
    | | :code:`gen_simple_ramp_down_when_on_rate`                             |
110
    | | *Defined over*: :code:`GEN_SIMPLE`                                    |
111
    | | *Within*: :code:`PercentFraction`                                     |
112
    | | *Default*: :code:`1`                                                  |
113
    |                                                                         |
114
    | The project's downward ramp rate limit during operations, defined as a  |
115
    | fraction of its capacity per minute.                                    |
116
    +-------------------------------------------------------------------------+
117

118
    |
119

120
    +-------------------------------------------------------------------------+
121
    | Linked Input Params                                                     |
122
    +=========================================================================+
123
    | | :code:`gen_simple_linked_power`                                       |
124
    | | *Defined over*: :code:`GEN_SIMPLE_LINKED_TMPS`                        |
125
    | | *Within*: :code:`NonNegativeReals`                                    |
126
    |                                                                         |
127
    | The project's power provision in the linked timepoints.                 |
128
    +-------------------------------------------------------------------------+
129
    | | :code:`gen_simple_linked_upwards_reserves`                            |
130
    | | *Defined over*: :code:`GEN_SIMPLE_LINKED_TMPS`                        |
131
    | | *Within*: :code:`NonNegativeReals`                                    |
132
    |                                                                         |
133
    | The project's upward reserve provision in the linked timepoints.        |
134
    +-------------------------------------------------------------------------+
135
    | | :code:`gen_simple_linked_downwards_reserves`                          |
136
    | | *Defined over*: :code:`GEN_SIMPLE_LINKED_TMPS`                        |
137
    | | *Within*: :code:`NonNegativeReals`                                    |
138
    |                                                                         |
139
    | The project's downward reserve provision in the linked timepoints.      |
140
    +-------------------------------------------------------------------------+
141

142
    |
143

144
    +-------------------------------------------------------------------------+
145
    | Variables                                                               |
146
    +=========================================================================+
147
    | | :code:`GenSimple_Provide_Power_MW`                                    |
148
    | | *Defined over*: :code:`GEN_SIMPLE_OPR_TMPS`                           |
149
    | | *Within*: :code:`NonNegativeReals`                                    |
150
    |                                                                         |
151
    | Power provision in MW from this project in each timepoint in which the  |
152
    | project is operational (capacity exists and the project is available).  |
153
    +-------------------------------------------------------------------------+
154

155
    |
156

157
    +-------------------------------------------------------------------------+
158
    | Constraints                                                             |
159
    +=========================================================================+
160
    | Power                                                                   |
161
    +-------------------------------------------------------------------------+
162
    | | :code:`GenSimple_Max_Power_Constraint`                                |
163
    | | *Defined over*: :code:`GEN_SIMPLE_OPR_TMPS`                           |
164
    |                                                                         |
165
    | Limits the power plus upward reserves to the available capacity.        |
166
    +-------------------------------------------------------------------------+
167
    | | :code:`GenSimple_Min_Power_Constraint`                                |
168
    | | *Defined over*: :code:`GEN_SIMPLE_OPR_TMPS`                           |
169
    |                                                                         |
170
    | Power provision minus downward reserves should exceed the minimum       |
171
    | stable level for the project.                                           |
172
    +-------------------------------------------------------------------------+
173
    | Ramps                                                                   |
174
    +-------------------------------------------------------------------------+
175
    | | :code:`GenSimple_Ramp_Up_Constraint`                                  |
176
    | | *Defined over*: :code:`GEN_SIMPLE_OPR_TMPS`                           |
177
    |                                                                         |
178
    | Limits the allowed project upward ramp based on the                     |
179
    | :code:`gen_simple_ramp_up_when_on_rate`.                                |
180
    +-------------------------------------------------------------------------+
181
    | | :code:`GenSimple_Ramp_Down_Constraint`                                |
182
    | | *Defined over*: :code:`GEN_SIMPLE_OPR_TMPS`                           |
183
    |                                                                         |
184
    | Limits the allowed project downward ramp based on the                   |
185
    | :code:`gen_simple_ramp_down_when_on_rate`.                              |
186
    +-------------------------------------------------------------------------+
187

188
    """
189

190
    # Sets
191
    ###########################################################################
192

193
    m.GEN_SIMPLE = Set(
3✔
194
        within=m.PROJECTS,
195
        initialize=lambda mod: subset_init_by_param_value(
196
            mod, "PROJECTS", "operational_type", "gen_simple"
197
        ),
198
    )
199

200
    m.GEN_SIMPLE_OPR_TMPS = Set(
3✔
201
        dimen=2,
202
        within=m.PRJ_OPR_TMPS,
203
        initialize=lambda mod: subset_init_by_set_membership(
204
            mod=mod, superset="PRJ_OPR_TMPS", index=0, membership_set=mod.GEN_SIMPLE
205
        ),
206
    )
207

208
    m.GEN_SIMPLE_LINKED_TMPS = Set(dimen=2)
3✔
209

210
    # Optional Params
211
    ###########################################################################
212

213
    m.gen_simple_ramp_up_when_on_rate = Param(
3✔
214
        m.GEN_SIMPLE, within=PercentFraction, default=1
215
    )
216
    m.gen_simple_ramp_down_when_on_rate = Param(
3✔
217
        m.GEN_SIMPLE, within=PercentFraction, default=1
218
    )
219

220
    # Linked Params
221
    ###########################################################################
222

223
    m.gen_simple_linked_power = Param(m.GEN_SIMPLE_LINKED_TMPS, within=NonNegativeReals)
3✔
224

225
    m.gen_simple_linked_upwards_reserves = Param(
3✔
226
        m.GEN_SIMPLE_LINKED_TMPS, within=NonNegativeReals
227
    )
228

229
    m.gen_simple_linked_downwards_reserves = Param(
3✔
230
        m.GEN_SIMPLE_LINKED_TMPS, within=NonNegativeReals
231
    )
232

233
    # Variables
234
    ###########################################################################
235

236
    m.GenSimple_Provide_Power_MW = Var(m.GEN_SIMPLE_OPR_TMPS, within=NonNegativeReals)
3✔
237

238
    # Expressions
239
    ###########################################################################
240

241
    def upwards_reserve_rule(mod, g, tmp):
3✔
242
        return sum(getattr(mod, c)[g, tmp] for c in getattr(d, headroom_variables)[g])
3✔
243

244
    m.GenSimple_Upwards_Reserves_MW = Expression(
3✔
245
        m.GEN_SIMPLE_OPR_TMPS, rule=upwards_reserve_rule
246
    )
247

248
    def downwards_reserve_rule(mod, g, tmp):
3✔
249
        return sum(getattr(mod, c)[g, tmp] for c in getattr(d, footroom_variables)[g])
3✔
250

251
    m.GenSimple_Downwards_Reserves_MW = Expression(
3✔
252
        m.GEN_SIMPLE_OPR_TMPS, rule=downwards_reserve_rule
253
    )
254

255
    # Constraints
256
    ###########################################################################
257

258
    m.GenSimple_Max_Power_Constraint = Constraint(
3✔
259
        m.GEN_SIMPLE_OPR_TMPS, rule=max_power_rule
260
    )
261

262
    m.GenSimple_Min_Power_Constraint = Constraint(
3✔
263
        m.GEN_SIMPLE_OPR_TMPS, rule=min_power_rule
264
    )
265

266
    m.GenSimple_Ramp_Up_Constraint = Constraint(
3✔
267
        m.GEN_SIMPLE_OPR_TMPS, rule=ramp_up_rule
268
    )
269

270
    m.GenSimple_Ramp_Down_Constraint = Constraint(
3✔
271
        m.GEN_SIMPLE_OPR_TMPS, rule=ramp_down_rule
272
    )
273

274

275
# Constraint Formulation Rules
276
###############################################################################
277

278

279
# Power
280
def max_power_rule(mod, g, tmp):
3✔
281
    """
282
    **Constraint Name**: GenSimple_Max_Power_Constraint
283
    **Enforced Over**: GEN_SIMPLE_OPR_TMPS
284

285
    Power plus upward services cannot exceed capacity.
286
    """
287
    return (
3✔
288
        mod.GenSimple_Provide_Power_MW[g, tmp]
289
        + mod.GenSimple_Upwards_Reserves_MW[g, tmp]
290
        <= mod.Capacity_MW[g, mod.period[tmp]] * mod.Availability_Derate[g, tmp]
291
    )
292

293

294
def min_power_rule(mod, g, tmp):
3✔
295
    """
296
    **Constraint Name**: GenSimple_Min_Power_Constraint
297
    **Enforced Over**: GEN_SIMPLE_OPR_TMPS
298

299
    Power minus downward services cannot be below zero.
300
    """
301
    return (
3✔
302
        mod.GenSimple_Provide_Power_MW[g, tmp]
303
        - mod.GenSimple_Downwards_Reserves_MW[g, tmp]
304
        >= 0
305
    )
306

307

308
# Ramps
309
def ramp_up_rule(mod, g, tmp):
3✔
310
    """
311
    **Constraint Name**: GenSimple_Ramp_Up_Constraint
312
    **Enforced Over**: GEN_SIMPLE_OPR_TMPS
313

314
    Difference between power generation of consecutive timepoints, adjusted
315
    for reserve provision in current and previous timepoint, has to obey
316
    ramp up rate limits.
317

318
    We assume that a unit has to reach its setpoint at the start of the
319
    timepoint; as such, the ramping between 2 timepoints is assumed to
320
    take place during the duration of the first timepoint, and the
321
    ramp rate limit is adjusted for the duration of the first timepoint.
322
    """
323
    if check_if_boundary_type_and_first_timepoint(
3✔
324
        mod=mod,
325
        tmp=tmp,
326
        balancing_type=mod.balancing_type_project[g],
327
        boundary_type="linear",
328
    ):
329
        return Constraint.Skip
3✔
330
    else:
331
        if check_if_boundary_type_and_first_timepoint(
3✔
332
            mod=mod,
333
            tmp=tmp,
334
            balancing_type=mod.balancing_type_project[g],
335
            boundary_type="linked",
336
        ):
337
            prev_tmp_hrs_in_tmp = mod.hrs_in_linked_tmp[0]
×
338
            prev_tmp_power = mod.gen_simple_linked_power[g, 0]
×
339
            prev_tmp_downwards_reserves = mod.gen_simple_linked_downwards_reserves[g, 0]
×
340
        else:
341
            prev_tmp_hrs_in_tmp = mod.hrs_in_tmp[
3✔
342
                mod.prev_tmp[tmp, mod.balancing_type_project[g]]
343
            ]
344
            prev_tmp_power = mod.GenSimple_Provide_Power_MW[
3✔
345
                g, mod.prev_tmp[tmp, mod.balancing_type_project[g]]
346
            ]
347
            prev_tmp_downwards_reserves = mod.GenSimple_Downwards_Reserves_MW[
3✔
348
                g, mod.prev_tmp[tmp, mod.balancing_type_project[g]]
349
            ]
350
        # If ramp rate limits, adjusted for timepoint duration, allow you to
351
        # ramp up the full operable range between timepoints, constraint won't
352
        # bind, so skip
353
        if mod.gen_simple_ramp_up_when_on_rate[g] * 60 * prev_tmp_hrs_in_tmp >= 1:
3✔
354
            return Constraint.Skip
3✔
355
        else:
356
            return (
×
357
                mod.GenSimple_Provide_Power_MW[g, tmp]
358
                + mod.GenSimple_Upwards_Reserves_MW[g, tmp]
359
                - (prev_tmp_power - prev_tmp_downwards_reserves)
360
                <= mod.gen_simple_ramp_up_when_on_rate[g]
361
                * 60
362
                * prev_tmp_hrs_in_tmp
363
                * mod.Capacity_MW[g, mod.period[tmp]]
364
                * mod.Availability_Derate[g, tmp]
365
            )
366

367

368
def ramp_down_rule(mod, g, tmp):
3✔
369
    """
370
    **Constraint Name**: GenSimple_Ramp_Down_Constraint
371
    **Enforced Over**: GEN_SIMPLE_OPR_TMPS
372

373
    Difference between power generation of consecutive timepoints, adjusted
374
    for reserve provision in current and previous timepoint, has to obey
375
    ramp down rate limits.
376

377
    We assume that a unit has to reach its setpoint at the start of the
378
    timepoint; as such, the ramping between 2 timepoints is assumed to
379
    take place during the duration of the first timepoint, and the
380
    ramp rate limit is adjusted for the duration of the first timepoint.
381
    """
382
    if check_if_boundary_type_and_first_timepoint(
3✔
383
        mod=mod,
384
        tmp=tmp,
385
        balancing_type=mod.balancing_type_project[g],
386
        boundary_type="linear",
387
    ):
388
        return Constraint.Skip
3✔
389
    else:
390
        if check_if_boundary_type_and_first_timepoint(
3✔
391
            mod=mod,
392
            tmp=tmp,
393
            balancing_type=mod.balancing_type_project[g],
394
            boundary_type="linked",
395
        ):
396
            prev_tmp_hrs_in_tmp = mod.hrs_in_linked_tmp[0]
×
397
            prev_tmp_power = mod.gen_simple_linked_power[g, 0]
×
398
            prev_tmp_upwards_reserves = mod.gen_simple_linked_upwards_reserves[g, 0]
×
399
        else:
400
            prev_tmp_hrs_in_tmp = mod.hrs_in_tmp[
3✔
401
                mod.prev_tmp[tmp, mod.balancing_type_project[g]]
402
            ]
403
            prev_tmp_power = mod.GenSimple_Provide_Power_MW[
3✔
404
                g, mod.prev_tmp[tmp, mod.balancing_type_project[g]]
405
            ]
406
            prev_tmp_upwards_reserves = mod.GenSimple_Upwards_Reserves_MW[
3✔
407
                g, mod.prev_tmp[tmp, mod.balancing_type_project[g]]
408
            ]
409
        # If ramp rate limits, adjusted for timepoint duration, allow you to
410
        # ramp down the full operable range between timepoints, constraint
411
        # won't bind, so skip
412
        if mod.gen_simple_ramp_down_when_on_rate[g] * 60 * prev_tmp_hrs_in_tmp >= 1:
3✔
413
            return Constraint.Skip
3✔
414
        else:
415
            return (
×
416
                mod.GenSimple_Provide_Power_MW[g, tmp]
417
                - mod.GenSimple_Downwards_Reserves_MW[g, tmp]
418
                - (prev_tmp_power + prev_tmp_upwards_reserves)
419
                >= -mod.gen_simple_ramp_down_when_on_rate[g]
420
                * 60
421
                * prev_tmp_hrs_in_tmp
422
                * mod.Capacity_MW[g, mod.period[tmp]]
423
                * mod.Availability_Derate[g, tmp]
424
            )
425

426

427
# Operational Type Methods
428
###############################################################################
429

430

431
def power_provision_rule(mod, g, tmp):
3✔
432
    """
433
    Power provision from simple generators is an endogenous variable.
434
    """
435
    return mod.GenSimple_Provide_Power_MW[g, tmp]
3✔
436

437

438
def fuel_burn_rule(mod, g, tmp):
3✔
439
    """
440
    Fuel burn is the product of the fuel burn slope and the power output. For
441
    simple generators we assume only one average heat rate is specified in
442
    heat_rate_curves.tab, so the fuel burn slope is equal to the specified
443
    heat rate and the intercept is zero.
444
    """
445
    return (
3✔
446
        mod.fuel_burn_slope_mmbtu_per_mwh[g, mod.period[tmp], 0]
447
        * mod.GenSimple_Provide_Power_MW[g, tmp]
448
    )
449

450

451
def power_delta_rule(mod, g, tmp):
3✔
452
    """
453
    This rule is only used in tuning costs, so fine to skip for linked
454
    horizon's first timepoint.
455
    """
456
    if check_if_first_timepoint(
3✔
457
        mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[g]
458
    ) and (
459
        check_boundary_type(
460
            mod=mod,
461
            tmp=tmp,
462
            balancing_type=mod.balancing_type_project[g],
463
            boundary_type="linear",
464
        )
465
        or check_boundary_type(
466
            mod=mod,
467
            tmp=tmp,
468
            balancing_type=mod.balancing_type_project[g],
469
            boundary_type="linked",
470
        )
471
    ):
472
        pass
2✔
473
    else:
474
        return (
3✔
475
            mod.GenSimple_Provide_Power_MW[g, tmp]
476
            - mod.GenSimple_Provide_Power_MW[
477
                g, mod.prev_tmp[tmp, mod.balancing_type_project[g]]
478
            ]
479
        )
480

481

482
def capacity_providing_inertia_rule(mod, g, tmp):
3✔
483
    """
484
    Capacity providing inertia for GEN_MUST_RUN project is equal to the online
485
    capacity (this assumes that the amount of capacity that is operated is
486
    proportional to power output)
487
    """
NEW
488
    return (
×
489
        mod.GenSimple_Provide_Power_MW[g, tmp]
490
        + mod.GenSimple_Upwards_Reserves_MW[g, tmp]
491
    )
492

493

494
# Input-Output
495
###############################################################################
496

497

498
def load_model_data(
3✔
499
    mod,
500
    d,
501
    data_portal,
502
    scenario_directory,
503
    weather_iteration,
504
    hydro_iteration,
505
    availability_iteration,
506
    subproblem,
507
    stage,
508
):
509
    """
510

511
    :param mod:
512
    :param data_portal:
513
    :param scenario_directory:
514
    :param subproblem:
515
    :param stage:
516
    :return:
517
    """
518

519
    projects = load_optype_model_data(
3✔
520
        mod=mod,
521
        data_portal=data_portal,
522
        scenario_directory=scenario_directory,
523
        weather_iteration=weather_iteration,
524
        hydro_iteration=hydro_iteration,
525
        availability_iteration=availability_iteration,
526
        subproblem=subproblem,
527
        stage=stage,
528
        op_type="gen_simple",
529
    )
530

531
    # Linked timepoint params
532
    linked_inputs_filename = os.path.join(
3✔
533
        scenario_directory,
534
        weather_iteration,
535
        hydro_iteration,
536
        availability_iteration,
537
        subproblem,
538
        stage,
539
        "inputs",
540
        "gen_simple_linked_timepoint_params.tab",
541
    )
542
    if os.path.exists(linked_inputs_filename):
3✔
543
        data_portal.load(
×
544
            filename=linked_inputs_filename,
545
            index=mod.GEN_SIMPLE_LINKED_TMPS,
546
            param=(
547
                mod.gen_simple_linked_power,
548
                mod.gen_simple_linked_upwards_reserves,
549
                mod.gen_simple_linked_downwards_reserves,
550
            ),
551
        )
552

553

554
def export_results(
3✔
555
    mod,
556
    d,
557
    scenario_directory,
558
    weather_iteration,
559
    hydro_iteration,
560
    availability_iteration,
561
    subproblem,
562
    stage,
563
):
564
    """
565
    :param scenario_directory:
566
    :param subproblem:
567
    :param stage:
568
    :param mod:
569
    :param d:
570
    :return:
571
    """
572
    # If there's a linked_subproblems_map CSV file, check which of the
573
    # current subproblem TMPS we should export results for to link to the
574
    # next subproblem
575
    tmps_to_link, tmp_linked_tmp_dict = check_for_tmps_to_link(
3✔
576
        scenario_directory=scenario_directory, subproblem=subproblem, stage=stage
577
    )
578

579
    # If the list of timepoints to link is not empty, write the linked
580
    # timepoint results for this module in the next subproblem's input
581
    # directory
582
    if tmps_to_link:
3✔
583
        next_subproblem = str(int(subproblem) + 1)
×
584

585
        # Export params by project and timepoint
586
        with open(
×
587
            os.path.join(
588
                scenario_directory,
589
                next_subproblem,
590
                stage,
591
                "inputs",
592
                "gen_simple_linked_timepoint_params.tab",
593
            ),
594
            "w",
595
            newline="",
596
        ) as f:
597
            writer = csv.writer(f, delimiter="\t", lineterminator="\n")
×
598
            writer.writerow(
×
599
                [
600
                    "project",
601
                    "linked_timepoint",
602
                    "linked_provide_power",
603
                    "linked_upward_reserves",
604
                    "linked_downward_reserves",
605
                ]
606
            )
607
            for p, tmp in sorted(mod.GEN_SIMPLE_OPR_TMPS):
×
608
                if tmp in tmps_to_link:
×
609
                    writer.writerow(
×
610
                        [
611
                            p,
612
                            tmp_linked_tmp_dict[tmp],
613
                            max(value(mod.GenSimple_Provide_Power_MW[p, tmp]), 0),
614
                            max(value(mod.GenSimple_Upwards_Reserves_MW[p, tmp]), 0),
615
                            max(value(mod.GenSimple_Downwards_Reserves_MW[p, tmp]), 0),
616
                        ]
617
                    )
618

619

620
# Validation
621
###############################################################################
622

623

624
def validate_inputs(
3✔
625
    scenario_id,
626
    subscenarios,
627
    weather_iteration,
628
    hydro_iteration,
629
    availability_iteration,
630
    subproblem,
631
    stage,
632
    conn,
633
):
634
    """
635
    Get inputs from database and validate the inputs
636
    :param subscenarios: SubScenarios object with all subscenario info
637
    :param subproblem:
638
    :param stage:
639
    :param conn: database connection
640
    :return:
641
    """
642

643
    # Validate operational chars table inputs
644
    validate_opchars(
3✔
645
        scenario_id,
646
        subscenarios,
647
        weather_iteration,
648
        hydro_iteration,
649
        availability_iteration,
650
        subproblem,
651
        stage,
652
        conn,
653
        "gen_simple",
654
    )
655

656
    # Other module specific validations
657

658
    c = conn.cursor()
3✔
659
    heat_rates = c.execute(
3✔
660
        """
661
        SELECT project, load_point_fraction
662
        FROM inputs_project_portfolios
663
        INNER JOIN
664
        (SELECT project, operational_type, heat_rate_curves_scenario_id
665
        FROM inputs_project_operational_chars
666
        WHERE project_operational_chars_scenario_id = {}
667
        AND operational_type = '{}') AS op_char
668
        USING(project)
669
        INNER JOIN
670
        (SELECT project, heat_rate_curves_scenario_id, load_point_fraction
671
        FROM inputs_project_heat_rate_curves) as heat_rates
672
        USING(project, heat_rate_curves_scenario_id)
673
        WHERE project_portfolio_scenario_id = {}
674
        """.format(
675
            subscenarios.PROJECT_OPERATIONAL_CHARS_SCENARIO_ID,
676
            "gen_simple",
677
            subscenarios.PROJECT_PORTFOLIO_SCENARIO_ID,
678
        )
679
    )
680

681
    # Convert inputs to dataframe
682
    hr_df = cursor_to_df(heat_rates)
3✔
683

684
    # Check that there is only one load point (constant heat rate)
685
    write_validation_to_database(
3✔
686
        conn=conn,
687
        scenario_id=scenario_id,
688
        weather_iteration=weather_iteration,
689
        hydro_iteration=hydro_iteration,
690
        availability_iteration=availability_iteration,
691
        subproblem_id=subproblem,
692
        stage_id=stage,
693
        gridpath_module=__name__,
694
        db_table="inputs_project_heat_rate_curves",
695
        severity="Mid",
696
        errors=validate_single_input(
697
            df=hr_df,
698
            msg="gen_simple can only have one load " "point (constant heat rate).",
699
        ),
700
    )
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