• 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.05
/gridpath/project/operations/operational_types/load_component_shift.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
by removing the static load profile for this load component from the load
18
balance constraint and adding a modified load profile to the load balance
19
constraint that is determined as follows:
20
1)
21
"""
22

23
import os.path
3✔
24
from pyomo.environ import Param, Set, Reals, Constraint, Var, Any, NonNegativeReals
3✔
25
import warnings
3✔
26

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

52

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

66
    +-------------------------------------------------------------------------+
67
    | Sets                                                                    |
68
    +=========================================================================+
69
    | | :code:`LOAD_COMPONENT_SHIFT_PRJS`                                     |
70
    |                                                                         |
71
    | The set of generators of the :code:`load_component_shift`               |
72
    | operational type.                                                       |
73
    +-------------------------------------------------------------------------+
74
    | | :code:`LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS`                            |
75
    |                                                                         |
76
    | Two-dimensional set with generators of the                              |
77
    | :code:`load_component_shift` operational type and their operational     |
78
    | timepoints.                                                             |
79
    +-------------------------------------------------------------------------+
80

81
    |
82

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

94
    """
95

96
    # Sets
97
    ###########################################################################
98

99
    m.LOAD_COMPONENT_SHIFT_PRJS = Set(
3✔
100
        within=m.PROJECTS,
101
        initialize=lambda mod: subset_init_by_param_value(
102
            mod, "PROJECTS", "operational_type", "load_component_shift"
103
        ),
104
    )
105

106
    m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS = Set(
3✔
107
        dimen=2,
108
        within=m.PRJ_OPR_TMPS,
109
        initialize=lambda mod: subset_init_by_set_membership(
110
            mod=mod,
111
            superset="PRJ_OPR_TMPS",
112
            index=0,
113
            membership_set=mod.LOAD_COMPONENT_SHIFT_PRJS,
114
        ),
115
    )
116

117
    # Derived sets
118
    m.LOAD_COMPONENT_SHIFT_PRJS_OPR_PRDS = Set(
3✔
119
        within=m.PRJ_OPR_PRDS,
120
        initialize=lambda mod: subset_init_by_set_membership(
121
            mod=mod,
122
            superset="PRJ_OPR_PRDS",
123
            index=0,
124
            membership_set=mod.LOAD_COMPONENT_SHIFT_PRJS,
125
        ),
126
    )
127

128
    m.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS = Set(
3✔
129
        dimen=3, within=m.LOAD_COMPONENT_SHIFT_PRJS * m.BLN_TYPE_HRZS
130
    )
131

132
    # Required Params
133
    ###########################################################################
134

135
    m.load_component_shift_linked_load_component = Param(
3✔
136
        m.LOAD_COMPONENT_SHIFT_PRJS,
137
        within=Any,
138
    )
139

140
    m.load_component_shift_min_load_mw = Param(
3✔
141
        m.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS, within=NonNegativeReals
142
    )
143

144
    m.load_component_shift_max_load_mw = Param(
3✔
145
        m.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS, within=NonNegativeReals
146
    )
147

148
    # Derived params
149
    m.load_component_shift_load_component_peak_load_in_period = Param(
3✔
150
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_PRDS,
151
        initialize=lambda mod, prj, prd: max(
152
            [
153
                mod.component_static_load_mw[
154
                    mod.load_zone[prj],
155
                    tmp,
156
                    mod.load_component_shift_linked_load_component[prj],
157
                ]
158
                for tmp in mod.TMPS_IN_PRD[prd]
159
            ]
160
        ),
161
    )
162

163
    def load_bounds_by_tmp_init(mod, prj, tmp):
3✔
164
        min_vals = []
3✔
165
        max_vals = []
3✔
166

167
        for _prj, bt, hrz in mod.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS:
3✔
168
            if _prj == prj and tmp in mod.TMPS_BY_BLN_TYPE_HRZ[bt, hrz]:
3✔
169
                min_vals.append(mod.load_component_shift_min_load_mw[_prj, bt, hrz])
3✔
170
                max_vals.append(mod.load_component_shift_max_load_mw[_prj, bt, hrz])
3✔
171

172
        if len(min_vals) > 1 or len(max_vals) > 1:
3✔
173
            raise ValueError(
×
174
                f"""More than one value per timepoints specified
175
                 for bounds for load_component_shift project {prj}, 
176
                 timepoint {tmp}. Please ensure you don't have 
177
                 overlapping horizons."""
178
            )
179

180
        # Assuming single value in lists after errors caught above
181
        # Check if the list contains a value; if not, set the min and max to
182
        # the static load (no shifting)
183
        if min_vals:
3✔
184
            tmp_val_min = min_vals[0]
3✔
185
        else:
186
            tmp_val_min = mod.component_static_load_mw[
3✔
187
                mod.load_zone[prj],
188
                tmp,
189
                mod.load_component_shift_linked_load_component[prj],
190
            ]
191
        if max_vals:
3✔
192
            tmp_val_max = max_vals[0]
3✔
193
        else:
194
            tmp_val_max = mod.component_static_load_mw[
3✔
195
                mod.load_zone[prj],
196
                tmp,
197
                mod.load_component_shift_linked_load_component[prj],
198
            ]
199

200
        return tmp_val_min, tmp_val_max
3✔
201

202
    m.load_component_shift_min_load_mw_by_tmp = Param(
3✔
203
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS,
204
        initialize=lambda mod, prj, tmp: load_bounds_by_tmp_init(mod, prj, tmp)[0],
205
    )
206

207
    m.load_component_shift_max_load_mw_by_tmp = Param(
3✔
208
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS,
209
        initialize=lambda mod, prj, tmp: load_bounds_by_tmp_init(mod, prj, tmp)[1],
210
    )
211

212
    # Optional params
213
    ###########################################################################
214
    m.load_component_shift_efficiency_factor = Param(
3✔
215
        m.LOAD_COMPONENT_SHIFT_PRJS, within=NonNegativeReals, default=1
216
    )
217

218
    # Variables
219
    ###########################################################################
220
    m.Load_Component_Shift_Fraction_Invested = Var(
3✔
221
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_PRDS, bounds=(0, 1), initialize=0
222
    )
223

224
    m.Load_Component_Shift_Add_Load_MW = Var(
3✔
225
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS, within=NonNegativeReals
226
    )
227

228
    # Constraints
229
    ###########################################################################
230

231
    def fraction_invested_constraint_rule(mod, prj, prd):
3✔
232
        """
233
        Limits the capacity of this project to the peak load because
234
        Load_Component_Shift_Fraction_Invested is bounded to (0,1).
235
        """
236
        return (
3✔
237
            mod.Load_Component_Shift_Fraction_Invested[prj, prd]
238
            == mod.Capacity_MW[prj, prd]
239
            / mod.load_component_shift_load_component_peak_load_in_period[prj, prd]
240
        )
241

242
    m.Load_Component_Shift_Fraction_Invested_Constraint = Constraint(
3✔
243
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_PRDS, rule=fraction_invested_constraint_rule
244
    )
245

246
    def energy_budget_rule(mod, prj, bt, hrz):
3✔
247
        """
248
        Sets the total energy consumption to equal the static load energy
249
        consumption for each horizon.
250
        """
251
        return (
3✔
252
            sum(
253
                mod.component_static_load_mw[
254
                    mod.load_zone[prj],
255
                    tmp,
256
                    mod.load_component_shift_linked_load_component[prj],
257
                ]
258
                * mod.hrs_in_tmp[tmp]
259
                * mod.tmp_weight[tmp]
260
                for tmp in mod.TMPS_BY_BLN_TYPE_HRZ[bt, hrz]
261
            )
262
            == sum(
263
                mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
264
                * mod.hrs_in_tmp[tmp]
265
                * mod.tmp_weight[tmp]
266
                for tmp in mod.TMPS_BY_BLN_TYPE_HRZ[bt, hrz]
267
            )
268
            * mod.load_component_shift_efficiency_factor[prj]
269
        )
270

271
    m.Load_Component_Shift_Energy_Balance_Constraint = Constraint(
3✔
272
        m.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS, rule=energy_budget_rule
273
    )
274

275
    def min_demand_rule(mod, prj, tmp):
3✔
276
        return (
3✔
277
            mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
278
            >= mod.load_component_shift_min_load_mw_by_tmp[prj, tmp]
279
        )
280

281
    m.Load_Component_Shift_Min_Demand_Constraint = Constraint(
3✔
282
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS, rule=min_demand_rule
283
    )
284

285
    def max_demand_rule(mod, prj, tmp):
3✔
286
        return (
3✔
287
            mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
288
            <= mod.load_component_shift_max_load_mw_by_tmp[prj, tmp]
289
        )
290

291
    m.Load_Component_Shift_Max_Demand_Constraint = Constraint(
3✔
292
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS, rule=max_demand_rule
293
    )
294

295
    # TODO: remove this constraint once input validation is in place that
296
    #  does not allow specifying a reserve_zone for 'load_component_shift'
297
    #  type
298
    def no_upward_reserve_rule(mod, g, tmp):
3✔
299
        """
300
        **Constraint Name**: LoadComponentShift_No_Upward_Reserves_Constraint
301
        **Enforced Over**: LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS
302

303
        Upward reserves should be zero in every operational timepoint.
304
        """
305
        if getattr(d, headroom_variables)[g]:
3✔
306
            warnings.warn(
×
307
                """project {} is of the 'load_component_shift' operational 
308
                type and should not be assigned any upward reserve BAs since it 
309
                cannot provide  upward reserves. Please replace the upward 
310
                reserve BA for project {} with '.' (no value) in projects.tab. 
311
                Model will add  constraint to ensure project {} cannot provide 
312
                upward reserves
313
                """.format(
314
                    g, g, g
315
                )
316
            )
317
            return (
×
318
                sum(getattr(mod, c)[g, tmp] for c in getattr(d, headroom_variables)[g])
319
                == 0
320
            )
321
        else:
322
            return Constraint.Skip
3✔
323

324
    m.LoadComponentShift_No_Upward_Reserves_Constraint = Constraint(
3✔
325
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS, rule=no_upward_reserve_rule
326
    )
327

328
    # TODO: remove this constraint once input validation is in place that
329
    #  does not allow specifying a reserve_zone if 'load_component_shift' type
330
    def no_downward_reserve_rule(mod, g, tmp):
3✔
331
        """
332
        **Constraint Name**: LoadComponentShift_No_Downward_Reserves_Constraint
333
        **Enforced Over**: LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS
334

335
        Downward reserves should be zero in every operational timepoint.
336
        """
337
        if getattr(d, footroom_variables)[g]:
3✔
338
            warnings.warn(
×
339
                """project {} is of the 'load_component_shift' operational 
340
                type and should not be assigned any downward reserve BAs since 
341
                it cannot provide downward reserves. Please replace the
342
                downward reserve BA for project {} with '.' (no value) in 
343
                projects.tab. Model will add constraint to ensure project {} 
344
                cannot provide downward reserves.
345
                """.format(
346
                    g, g, g
347
                )
348
            )
349
            return (
×
350
                sum(getattr(mod, c)[g, tmp] for c in getattr(d, footroom_variables)[g])
351
                == 0
352
            )
353
        else:
354
            return Constraint.Skip
3✔
355

356
    m.LoadComponentShift_No_Downward_Reserves_Constraint = Constraint(
3✔
357
        m.LOAD_COMPONENT_SHIFT_PRJS_OPR_TMPS, rule=no_downward_reserve_rule
358
    )
359

360

361
# Operational Type Methods
362
###############################################################################
363

364

365
def power_provision_rule(mod, prj, tmp):
3✔
366
    """
367
    Add static load to power production (remove from load) and subtract the
368
    shifted load (add to load).
369
    """
370

371
    return (
3✔
372
        -mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
373
        + mod.component_static_load_mw[
374
            mod.load_zone[prj],
375
            tmp,
376
            mod.load_component_shift_linked_load_component[prj],
377
        ]
378
    )
379

380

381
def variable_om_cost_rule(mod, prj, tmp):
3✔
382
    """
383
    Must be defined rather than take the default, as Project_Power_Provision_MW
384
    for this operational type is negative downstream.
385
    """
386
    return (
3✔
387
        mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
388
        * mod.variable_om_cost_per_mwh[prj]
389
    )
390

391

392
def variable_om_by_period_cost_rule(mod, prj, tmp):
3✔
393
    """
394
    Must be defined rather than take the default, as Project_Power_Provision_MW
395
    for this operational type is negative downstream.
396
    """
397
    return (
×
398
        mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
399
        * mod.variable_om_cost_per_mwh_by_period[prj, mod.period[tmp]]
400
    )
401

402

403
def variable_om_by_timepoint_cost_rule(mod, prj, tmp):
3✔
404
    """
405
    Must be defined rather than take the default, as Project_Power_Provision_MW
406
    for this operational type is negative downstream.
407
    """
408
    return (
×
409
        mod.Load_Component_Shift_Add_Load_MW[prj, tmp]
410
        * mod.variable_om_cost_per_mwh_by_timepoint[prj, tmp]
411
    )
412

413

414
def power_delta_rule(mod, prj, tmp):
3✔
415
    """
416
    This rule is only used in tuning costs, so fine to skip for linked
417
    horizon's first timepoint.
418
    """
419
    if check_if_first_timepoint(
3✔
420
        mod=mod, tmp=tmp, balancing_type=mod.balancing_type_project[prj]
421
    ) and (
422
        check_boundary_type(
423
            mod=mod,
424
            tmp=tmp,
425
            balancing_type=mod.balancing_type_project[prj],
426
            boundary_type="linear",
427
        )
428
        or check_boundary_type(
429
            mod=mod,
430
            tmp=tmp,
431
            balancing_type=mod.balancing_type_project[prj],
432
            boundary_type="linked",
433
        )
434
    ):
UNCOV
435
        pass
2✔
436
    else:
437
        return power_provision_rule(mod, prj, tmp) - power_provision_rule(
3✔
438
            mod, prj, mod.prev_tmp[tmp, mod.balancing_type_project[prj]]
439
        )
440

441

442
# Inputs-Outputs
443
###############################################################################
444

445

446
def load_model_data(
3✔
447
    mod,
448
    d,
449
    data_portal,
450
    scenario_directory,
451
    weather_iteration,
452
    hydro_iteration,
453
    availability_iteration,
454
    subproblem,
455
    stage,
456
):
457
    """
458
    :param mod:
459
    :param data_portal:
460
    :param scenario_directory:
461
    :param subproblem:
462
    :param stage:
463
    :return:
464
    """
465

466
    # Load data from projects.tab and get the list of projects of this type
467
    projects = load_optype_model_data(
3✔
468
        mod=mod,
469
        data_portal=data_portal,
470
        scenario_directory=scenario_directory,
471
        weather_iteration=weather_iteration,
472
        hydro_iteration=hydro_iteration,
473
        availability_iteration=availability_iteration,
474
        subproblem=subproblem,
475
        stage=stage,
476
        op_type="load_component_shift",
477
    )
478

479
    # Load data
480
    data_portal.load(
3✔
481
        filename=os.path.join(
482
            scenario_directory,
483
            weather_iteration,
484
            hydro_iteration,
485
            availability_iteration,
486
            subproblem,
487
            stage,
488
            "inputs",
489
            "load_component_shift_bounds.tab",
490
        ),
491
        index=mod.LOAD_COMPONENT_SHIFT_PRJS_BT_HRZS,
492
        param=(
493
            mod.load_component_shift_min_load_mw,
494
            mod.load_component_shift_max_load_mw,
495
        ),
496
    )
497

498

499
# Database
500
###############################################################################
501

502

503
def get_model_inputs_from_database(
3✔
504
    scenario_id,
505
    subscenarios,
506
    weather_iteration,
507
    hydro_iteration,
508
    availability_iteration,
509
    subproblem,
510
    stage,
511
    conn,
512
):
513
    """
514
    :param subscenarios: SubScenarios object with all subscenario info
515
    :param subproblem:
516
    :param stage:
517
    :param conn: database connection
518
    :return: cursor object with query results
519
    """
520
    (
3✔
521
        db_weather_iteration,
522
        db_hydro_iteration,
523
        db_availability_iteration,
524
        db_subproblem,
525
        db_stage,
526
    ) = directories_to_db_values(
527
        weather_iteration, hydro_iteration, availability_iteration, subproblem, stage
528
    )
529

530
    prj_bt_hrz_data = get_prj_temporal_index_opr_inputs_from_db(
3✔
531
        subscenarios=subscenarios,
532
        weather_iteration=db_weather_iteration,
533
        hydro_iteration=db_hydro_iteration,
534
        availability_iteration=db_availability_iteration,
535
        subproblem=db_subproblem,
536
        stage=db_stage,
537
        conn=conn,
538
        op_type="load_component_shift",
539
        table="inputs_project_load_component_shift_bounds",
540
        subscenario_id_column="load_component_shift_bounds_scenario_id",
541
        data_column="min_load_mw, max_load_mw",
542
        opr_index_dict=BT_HRZ_INDEX_QUERY_PARAMS,
543
    )
544

545
    return prj_bt_hrz_data
3✔
546

547

548
def write_model_inputs(
3✔
549
    scenario_directory,
550
    scenario_id,
551
    subscenarios,
552
    weather_iteration,
553
    hydro_iteration,
554
    availability_iteration,
555
    subproblem,
556
    stage,
557
    conn,
558
):
559
    """
560
    Get inputs from database and write out the model input
561
    variable_generator_profiles.tab file.
562
    :param scenario_directory: string, the scenario directory
563
    :param subscenarios: SubScenarios object with all subscenario info
564
    :param subproblem:
565
    :param stage:
566
    :param conn: database connection
567
    :return:
568
    """
569

570
    data = get_model_inputs_from_database(
3✔
571
        scenario_id,
572
        subscenarios,
573
        weather_iteration,
574
        hydro_iteration,
575
        availability_iteration,
576
        subproblem,
577
        stage,
578
        conn,
579
    )
580
    fname = "load_component_shift_bounds.tab"
3✔
581

582
    write_tab_file_model_inputs(
3✔
583
        scenario_directory,
584
        weather_iteration,
585
        hydro_iteration,
586
        availability_iteration,
587
        subproblem,
588
        stage,
589
        fname,
590
        data,
591
    )
592

593

594
# Validation
595
###############################################################################
596

597

598
def validate_inputs(
3✔
599
    scenario_id,
600
    subscenarios,
601
    weather_iteration,
602
    hydro_iteration,
603
    availability_iteration,
604
    subproblem,
605
    stage,
606
    conn,
607
):
608
    """
609
    Get inputs from database and validate the inputs
610
    :param subscenarios: SubScenarios object with all subscenario info
611
    :param subproblem:
612
    :param stage:
613
    :param conn: database connection
614
    :return:
615
    """
616

617
    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