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

openmc-dev / openmc / 22556083755

02 Mar 2026 12:05AM UTC coverage: 81.414% (-0.09%) from 81.508%
22556083755

Pull #3843

github

web-flow
Merge a9263a317 into 83a7b36ad
Pull Request #3843: Implement cell importance variance reduction scheme.

17521 of 25285 branches covered (69.29%)

Branch coverage included in aggregate %.

60 of 125 new or added lines in 7 files covered. (48.0%)

1 existing line in 1 file now uncovered.

57731 of 67146 relevant lines covered (85.98%)

45338148.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.58
/src/geometry.cpp
1
#include "openmc/geometry.h"
2

3
#include <fmt/core.h>
4
#include <fmt/ostream.h>
5

6
#include "openmc/array.h"
7
#include "openmc/cell.h"
8
#include "openmc/constants.h"
9
#include "openmc/error.h"
10
#include "openmc/lattice.h"
11
#include "openmc/settings.h"
12
#include "openmc/simulation.h"
13
#include "openmc/string_utils.h"
14
#include "openmc/surface.h"
15

16
namespace openmc {
17

18
//==============================================================================
19
// Global variables
20
//==============================================================================
21

22
namespace model {
23

24
int root_universe {-1};
25
int n_coord_levels;
26

27
vector<int64_t> overlap_check_count;
28

29
} // namespace model
30

31
//==============================================================================
32
// Non-member functions
33
//==============================================================================
34

35
bool check_cell_overlap(GeometryState& p, bool error)
1,321,100✔
36
{
37
  int n_coord = p.n_coord();
1,321,100✔
38

39
  // Loop through each coordinate level
40
  for (int j = 0; j < n_coord; j++) {
2,267,892✔
41
    Universe& univ = *model::universes[p.coord(j).universe()];
1,321,100✔
42

43
    // Loop through each cell on this level
44
    for (auto index_cell : univ.cells_) {
4,468,992✔
45
      Cell& c = *model::cells[index_cell];
3,522,200✔
46
      if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) {
3,522,200✔
47
        if (index_cell != p.coord(j).cell()) {
1,250,854✔
48
          if (error) {
374,308!
49
            fatal_error(
×
50
              fmt::format("Overlapping cells detected: {}, {} on universe {}",
×
51
                c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_));
×
52
          }
53
          return true;
1,321,100✔
54
        }
55
#pragma omp atomic
478,116✔
56
        ++model::overlap_check_count[index_cell];
876,546✔
57
      }
58
    }
59
  }
60

61
  return false;
62
}
63

64
//==============================================================================
65

66
int cell_instance_at_level(const GeometryState& p, int level)
2,147,483,647✔
67
{
68
  // throw error if the requested level is too deep for the geometry
69
  if (level > model::n_coord_levels) {
2,147,483,647!
70
    fatal_error(fmt::format("Cell instance at level {} requested, but only {} "
×
71
                            "levels exist in the geometry.",
72
      level, p.n_coord()));
73
  }
74

75
  // determine the cell instance
76
  Cell& c {*model::cells[p.coord(level).cell()]};
2,147,483,647!
77

78
  // quick exit if this cell doesn't have distribcell instances
79
  if (c.distribcell_index_ == C_NONE)
2,147,483,647!
80
    return C_NONE;
81

82
  // compute the cell's instance
83
  int instance = 0;
84
  for (int i = 0; i < level; i++) {
2,147,483,647✔
85
    const auto& c_i {*model::cells[p.coord(i).cell()]};
2,147,483,647✔
86
    if (c_i.type_ == Fill::UNIVERSE) {
2,147,483,647✔
87
      instance += c_i.offset_[c.distribcell_index_];
1,060,214,485✔
88
    } else if (c_i.type_ == Fill::LATTICE) {
1,243,507,232!
89
      instance += c_i.offset_[c.distribcell_index_];
1,243,507,232✔
90
      auto& lat {*model::lattices[p.coord(i + 1).lattice()]};
1,243,507,232✔
91
      const auto& i_xyz {p.coord(i + 1).lattice_index()};
1,243,507,232✔
92
      if (lat.are_valid_indices(i_xyz)) {
1,243,507,232✔
93
        instance += lat.offset(c.distribcell_index_, i_xyz);
1,238,623,265✔
94
      }
95
    }
96
  }
97
  return instance;
98
}
99

100
double cell_importance_at_level(
1,718,577,444✔
101
  const GeometryState& p, ParticleType type, int level)
102
{
103
  // throw error if the requested level is too deep for the geometry
104
  if (level > model::n_coord_levels) {
1,718,577,444!
NEW
105
    fatal_error(
×
NEW
106
      fmt::format("Cell importance at level {} requested, but only {} "
×
107
                  "levels exist in the geometry.",
108
        level, p.n_coord()));
109
  }
110
  int j = -1;
1,718,577,444✔
111
  if (type.is_neutron())
1,718,577,444✔
112
    j = 0;
113
  else if (type.is_photon())
13,747,404✔
114
    j = 1;
115
  else
116
    return 1.0;
117

118
  // determine the cell instance
119
  Cell& c {*model::cells[p.coord(level).cell()]};
1,718,467,444✔
120

121
  // compute the cell's instance and importance
122
  int instance = 0.0;
1,718,467,444✔
123
  double importance = 1.0;
1,718,467,444✔
124
  for (int i = 0; i < level; i++) {
2,147,483,647✔
125
    const auto& c_i {*model::cells[p.coord(i).cell()]};
874,543,618✔
126
    if (c_i.type_ == Fill::UNIVERSE) {
874,543,618✔
127
      instance += c_i.offset_[c.distribcell_index_];
481,872,693✔
128
    } else if (c_i.type_ == Fill::LATTICE) {
392,670,925!
129
      instance += c_i.offset_[c.distribcell_index_];
392,670,925✔
130
      auto& lat {*model::lattices[p.coord(i + 1).lattice()]};
392,670,925✔
131
      const auto& i_xyz {p.coord(i + 1).lattice_index()};
392,670,925✔
132
      if (lat.are_valid_indices(i_xyz)) {
392,670,925✔
133
        instance += lat.offset(c.distribcell_index_, i_xyz);
391,565,337✔
134
      }
135
    }
136
    importance *= c_i.importance(j, instance);
874,543,618✔
137
  }
138
  return importance;
139
}
140

141
//==============================================================================
142

143
bool find_cell_inner(
2,147,483,647✔
144
  GeometryState& p, const NeighborList* neighbor_list, bool verbose)
145
{
146
  // Find which cell of this universe the particle is in.  Use the neighbor list
147
  // to shorten the search if one was provided.
148
  bool found = false;
2,147,483,647✔
149
  int32_t i_cell = C_NONE;
2,147,483,647✔
150
  if (neighbor_list) {
2,147,483,647✔
151
    for (auto it = neighbor_list->cbegin(); it != neighbor_list->cend(); ++it) {
2,046,026,838✔
152
      i_cell = *it;
2,044,426,059!
153

154
      // Make sure the search cell is in the same universe.
155
      int i_universe = p.lowest_coord().universe();
2,044,426,059!
156
      if (model::cells[i_cell]->universe_ != i_universe)
2,044,426,059!
157
        continue;
×
158

159
      // Check if this cell contains the particle.
160
      Position r {p.r_local()};
2,044,426,059✔
161
      Direction u {p.u_local()};
2,044,426,059✔
162
      auto surf = p.surface();
2,044,426,059✔
163
      if (model::cells[i_cell]->contains(r, u, surf)) {
2,044,426,059✔
164
        p.lowest_coord().cell() = i_cell;
1,640,041,243✔
165
        found = true;
1,640,041,243✔
166
        break;
1,640,041,243✔
167
      }
168
    }
169

170
    // If we're attempting a neighbor list search and fail, we
171
    // now know we should return false. This will trigger an
172
    // exhaustive search from neighbor_list_find_cell and make
173
    // the result from that be appended to the neighbor list.
174
    if (!found) {
1,641,642,022✔
175
      return found;
176
    }
177
  }
178

179
  // Check successively lower coordinate levels until finding material fill
180
  for (;; ++p.n_coord()) {
2,147,483,647✔
181
    // If we did not attempt to use neighbor lists, i_cell is still C_NONE.  In
182
    // that case, we should now do an exhaustive search to find the right value
183
    // of i_cell.
184
    //
185
    // Alternatively, neighbor list searches could have succeeded, but we found
186
    // that the fill of the neighbor cell was another universe. As such, in the
187
    // code below this conditional, we set i_cell back to C_NONE to indicate
188
    // that.
189
    if (i_cell == C_NONE) {
2,147,483,647✔
190
      int i_universe = p.lowest_coord().universe();
1,657,673,675✔
191
      const auto& univ {model::universes[i_universe]};
1,657,673,675✔
192
      found = univ->find_cell(p);
1,657,673,675✔
193
    }
194

195
    if (!found) {
2,147,483,647✔
196
      return found;
197
    }
198
    i_cell = p.lowest_coord().cell();
2,147,483,647!
199

200
    // Announce the cell that the particle is entering.
201
    if (found && verbose) {
2,147,483,647!
202
      auto msg = fmt::format("    Entering cell {}", model::cells[i_cell]->id_);
×
203
      write_message(msg, 1);
×
204
    }
×
205

206
    Cell& c {*model::cells[i_cell]};
2,147,483,647✔
207
    if (c.type_ == Fill::MATERIAL) {
2,147,483,647✔
208
      // Found a material cell which means this is the lowest coord level.
209

210
      p.cell_instance() = 0;
2,147,483,647✔
211
      // Find the distribcell instance number.
212
      if (c.distribcell_index_ >= 0) {
2,147,483,647✔
213
        p.cell_instance() = cell_instance_at_level(p, p.n_coord() - 1);
2,147,483,647✔
214
      }
215

216
      // Set the material, temperature and density multiplier.
217
      p.material_last() = p.material();
2,147,483,647✔
218
      p.material() = c.material(p.cell_instance());
2,147,483,647✔
219
      p.sqrtkT_last() = p.sqrtkT();
2,147,483,647✔
220
      p.sqrtkT() = c.sqrtkT(p.cell_instance());
2,147,483,647✔
221
      p.density_mult_last() = p.density_mult();
2,147,483,647✔
222
      p.density_mult() = c.density_mult(p.cell_instance());
2,147,483,647✔
223

224
      return true;
2,147,483,647✔
225

226
    } else if (c.type_ == Fill::UNIVERSE) {
396,965,020✔
227
      //========================================================================
228
      //! Found a lower universe, update this coord level then search the next.
229

230
      // Set the lower coordinate level universe.
231
      auto& coord {p.coord(p.n_coord())};
249,733,804✔
232
      coord.universe() = c.fill_;
249,733,804✔
233

234
      // Set the position and direction.
235
      coord.r() = p.r_local();
249,733,804✔
236
      coord.u() = p.u_local();
249,733,804✔
237

238
      // Apply translation.
239
      coord.r() -= c.translation_;
249,733,804✔
240

241
      // Apply rotation.
242
      if (!c.rotation_.empty()) {
249,733,804✔
243
        coord.rotate(c.rotation_);
2,211,858✔
244
      }
245

246
    } else if (c.type_ == Fill::LATTICE) {
147,231,216!
247
      //========================================================================
248
      //! Found a lower lattice, update this coord level then search the next.
249

250
      Lattice& lat {*model::lattices[c.fill_]};
147,231,216✔
251

252
      // Set the position and direction.
253
      auto& coord {p.coord(p.n_coord())};
147,231,216✔
254
      coord.r() = p.r_local();
147,231,216✔
255
      coord.u() = p.u_local();
147,231,216✔
256

257
      // Apply translation.
258
      coord.r() -= c.translation_;
147,231,216✔
259

260
      // Apply rotation.
261
      if (!c.rotation_.empty()) {
147,231,216✔
262
        coord.rotate(c.rotation_);
358,336✔
263
      }
264

265
      // Determine lattice indices.
266
      auto& i_xyz {coord.lattice_index()};
147,231,216✔
267
      lat.get_indices(coord.r(), coord.u(), i_xyz);
147,231,216✔
268

269
      // Get local position in appropriate lattice cell
270
      coord.r() = lat.get_local_position(coord.r(), i_xyz);
147,231,216✔
271

272
      // Set lattice indices.
273
      coord.lattice() = c.fill_;
147,231,216✔
274

275
      // Set the lower coordinate level universe.
276
      if (lat.are_valid_indices(i_xyz)) {
147,231,216✔
277
        coord.universe() = lat[i_xyz];
142,347,249✔
278
      } else {
279
        if (lat.outer_ != NO_OUTER_UNIVERSE) {
4,883,967!
280
          coord.universe() = lat.outer_;
4,883,967✔
281
        } else {
282
          p.mark_as_lost(fmt::format(
×
283
            "Particle {} left lattice {}, but it has no outer definition.",
284
            p.id(), lat.id_));
×
285
        }
286
      }
287
    }
288
    i_cell = C_NONE; // trip non-neighbor cell search at next iteration
396,965,020✔
289
    found = false;
396,965,020✔
290
  }
291

292
  return found;
293
}
294

295
//==============================================================================
296

297
bool neighbor_list_find_cell(GeometryState& p, bool verbose)
1,641,642,022✔
298
{
299

300
  // Reset all the deeper coordinate levels.
301
  for (int i = p.n_coord(); i < model::n_coord_levels; i++) {
2,147,483,647✔
302
    p.coord(i).reset();
752,563,995✔
303
  }
304

305
  // Get the cell this particle was in previously.
306
  auto coord_lvl = p.n_coord() - 1;
1,641,642,022✔
307
  auto i_cell = p.coord(coord_lvl).cell();
1,641,642,022✔
308
  Cell& c {*model::cells[i_cell]};
1,641,642,022✔
309

310
  // Search for the particle in that cell's neighbor list.  Return if we
311
  // found the particle.
312
  bool found = find_cell_inner(p, &c.neighbors_, verbose);
1,641,642,022✔
313
  if (found)
1,641,642,022✔
314
    return found;
315

316
  // The particle could not be found in the neighbor list.  Try searching all
317
  // cells in this universe, and update the neighbor list if we find a new
318
  // neighboring cell.
319
  found = find_cell_inner(p, nullptr, verbose);
1,600,779✔
320
  if (found)
1,600,779✔
321
    c.neighbors_.push_back(p.coord(coord_lvl).cell());
29,108✔
322
  return found;
323
}
324

325
bool exhaustive_find_cell(GeometryState& p, bool verbose)
1,259,107,876✔
326
{
327
  int i_universe = p.lowest_coord().universe();
1,259,107,876✔
328
  if (i_universe == C_NONE) {
1,259,107,876✔
329
    p.coord(0).universe() = model::root_universe;
289,203,012✔
330
    p.n_coord() = 1;
289,203,012✔
331
    i_universe = model::root_universe;
289,203,012✔
332
  }
333
  // Reset all the deeper coordinate levels.
334
  for (int i = p.n_coord(); i < model::n_coord_levels; i++) {
1,337,571,030✔
335
    p.coord(i).reset();
78,463,154✔
336
  }
337
  return find_cell_inner(p, nullptr, verbose);
1,259,107,876✔
338
}
339

340
//==============================================================================
341

342
void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose)
750,250,892✔
343
{
344
  auto& coord {p.lowest_coord()};
750,250,892!
345
  auto& lat {*model::lattices[coord.lattice()]};
750,250,892!
346

347
  if (verbose) {
750,250,892!
348
    write_message(
×
349
      fmt::format("    Crossing lattice {}. Current position ({},{},{}). r={}",
×
350
        lat.id_, coord.lattice_index()[0], coord.lattice_index()[1],
×
351
        coord.lattice_index()[2], p.r()),
×
352
      1);
353
  }
354

355
  // Set the lattice indices.
356
  coord.lattice_index()[0] += boundary.lattice_translation()[0];
750,250,892✔
357
  coord.lattice_index()[1] += boundary.lattice_translation()[1];
750,250,892✔
358
  coord.lattice_index()[2] += boundary.lattice_translation()[2];
750,250,892✔
359

360
  // Set the new coordinate position.
361
  const auto& upper_coord {p.coord(p.n_coord() - 2)};
750,250,892✔
362
  const auto& cell {model::cells[upper_coord.cell()]};
750,250,892✔
363
  Position r = upper_coord.r();
750,250,892✔
364
  r -= cell->translation_;
750,250,892✔
365
  if (!cell->rotation_.empty()) {
750,250,892✔
366
    r = r.rotate(cell->rotation_);
471,647✔
367
  }
368
  p.r_local() = lat.get_local_position(r, coord.lattice_index());
750,250,892✔
369

370
  if (!lat.are_valid_indices(coord.lattice_index())) {
750,250,892✔
371
    // The particle is outside the lattice.  Search for it from the base coords.
372
    p.n_coord() = 1;
3,714,425✔
373
    bool found = exhaustive_find_cell(p);
3,714,425✔
374

375
    if (!found) {
3,714,425!
376
      p.mark_as_lost(fmt::format("Particle {} could not be located after "
×
377
                                 "crossing a boundary of lattice {}",
378
        p.id(), lat.id_));
×
379
    }
380

381
  } else {
382
    // Find cell in next lattice element.
383
    p.lowest_coord().universe() = lat[coord.lattice_index()];
746,536,467✔
384
    bool found = exhaustive_find_cell(p);
746,536,467✔
385

386
    if (!found) {
746,536,467!
387
      // A particle crossing the corner of a lattice tile may not be found.  In
388
      // this case, search for it from the base coords.
389
      p.n_coord() = 1;
×
390
      bool found = exhaustive_find_cell(p);
×
391
      if (!found) {
×
392
        p.mark_as_lost(fmt::format("Particle {} could not be located after "
×
393
                                   "crossing a boundary of lattice {}",
394
          p.id(), lat.id_));
×
395
      }
396
    }
397
  }
398
}
750,250,892✔
399

400
//==============================================================================
401

402
BoundaryInfo distance_to_boundary(GeometryState& p)
2,147,483,647✔
403
{
404
  BoundaryInfo info;
2,147,483,647✔
405
  double d_lat = INFINITY;
2,147,483,647✔
406
  double d_surf = INFINITY;
2,147,483,647✔
407
  int32_t level_surf_cross;
2,147,483,647✔
408
  array<int, 3> level_lat_trans {};
2,147,483,647✔
409

410
  // Loop over each coordinate level.
411
  for (int i = 0; i < p.n_coord(); i++) {
2,147,483,647✔
412
    const auto& coord {p.coord(i)};
2,147,483,647✔
413
    const Position& r {coord.r()};
2,147,483,647✔
414
    const Direction& u {coord.u()};
2,147,483,647✔
415
    Cell& c {*model::cells[coord.cell()]};
2,147,483,647✔
416

417
    // Find the oncoming surface in this cell and the distance to it.
418
    auto surface_distance = c.distance(r, u, p.surface(), &p);
2,147,483,647✔
419
    d_surf = surface_distance.first;
2,147,483,647✔
420
    level_surf_cross = surface_distance.second;
2,147,483,647✔
421

422
    // Find the distance to the next lattice tile crossing.
423
    if (coord.lattice() != C_NONE) {
2,147,483,647✔
424
      auto& lat {*model::lattices[coord.lattice()]};
1,325,733,032!
425
      // TODO: refactor so both lattice use the same position argument (which
426
      // also means the lat.type attribute can be removed)
427
      std::pair<double, array<int, 3>> lattice_distance;
1,325,733,032✔
428
      switch (lat.type_) {
1,325,733,032!
429
      case LatticeType::rect:
1,240,229,735✔
430
        lattice_distance = lat.distance(r, u, coord.lattice_index());
1,240,229,735✔
431
        break;
1,240,229,735✔
432
      case LatticeType::hex:
85,503,297✔
433
        auto& cell_above {model::cells[p.coord(i - 1).cell()]};
85,503,297✔
434
        Position r_hex {p.coord(i - 1).r()};
85,503,297✔
435
        r_hex -= cell_above->translation_;
85,503,297✔
436
        if (coord.rotated()) {
85,503,297✔
437
          r_hex = r_hex.rotate(cell_above->rotation_);
701,954✔
438
        }
439
        r_hex.z = coord.r().z;
85,503,297✔
440
        lattice_distance = lat.distance(r_hex, u, coord.lattice_index());
85,503,297✔
441
        break;
85,503,297✔
442
      }
443
      d_lat = lattice_distance.first;
1,325,733,032✔
444
      level_lat_trans = lattice_distance.second;
1,325,733,032✔
445

446
      if (d_lat < 0) {
1,325,733,032!
447
        p.mark_as_lost(fmt::format("Particle {} had a negative distance "
×
448
                                   "to a lattice boundary.",
449
          p.id()));
×
450
      }
451
    }
452

453
    // If the boundary on this coordinate level is coincident with a boundary on
454
    // a higher level then we need to make sure that the higher level boundary
455
    // is selected.  This logic must consider floating point precision.
456
    double& d = info.distance();
2,147,483,647✔
457
    if (d_surf < d_lat - FP_COINCIDENT) {
2,147,483,647✔
458
      if (d == INFINITY || (d - d_surf) / d >= FP_REL_PRECISION) {
2,147,483,647✔
459
        // Update closest distance
460
        d = d_surf;
2,147,483,647✔
461

462
        // If the cell is not simple, it is possible that both the negative and
463
        // positive half-space were given in the region specification. Thus, we
464
        // have to explicitly check which half-space the particle would be
465
        // traveling into if the surface is crossed
466
        if (c.is_simple() || d == INFTY) {
2,147,483,647✔
467
          info.surface() = level_surf_cross;
2,147,483,647✔
468
        } else {
469
          Position r_hit = r + d_surf * u;
13,190,177✔
470
          Surface& surf {*model::surfaces[std::abs(level_surf_cross) - 1]};
13,190,177✔
471
          Direction norm = surf.normal(r_hit);
13,190,177✔
472
          if (u.dot(norm) > 0) {
13,190,177✔
473
            info.surface() = std::abs(level_surf_cross);
6,582,734✔
474
          } else {
475
            info.surface() = -std::abs(level_surf_cross);
6,607,443✔
476
          }
477
        }
478

479
        info.lattice_translation()[0] = 0;
2,147,483,647✔
480
        info.lattice_translation()[1] = 0;
2,147,483,647✔
481
        info.lattice_translation()[2] = 0;
2,147,483,647✔
482
        info.coord_level() = i + 1;
2,147,483,647✔
483
      }
484
    } else {
485
      if (d == INFINITY || (d - d_lat) / d >= FP_REL_PRECISION) {
1,091,196,941!
486
        d = d_lat;
887,451,710✔
487
        info.surface() = SURFACE_NONE;
887,451,710✔
488
        info.lattice_translation() = level_lat_trans;
887,451,710✔
489
        info.coord_level() = i + 1;
887,451,710✔
490
      }
491
    }
492
  }
493
  return info;
2,147,483,647✔
494
}
495

496
//==============================================================================
497
// C API
498
//==============================================================================
499

500
extern "C" int openmc_find_cell(
600,308✔
501
  const double* xyz, int32_t* index, int32_t* instance)
502
{
503
  GeometryState geom_state;
600,308✔
504

505
  geom_state.r() = Position {xyz};
600,308✔
506
  geom_state.u() = {0.0, 0.0, 1.0};
600,308✔
507

508
  if (!exhaustive_find_cell(geom_state)) {
600,308✔
509
    set_errmsg(
11✔
510
      fmt::format("Could not find cell at position {}.", geom_state.r()));
11✔
511
    return OPENMC_E_GEOMETRY;
11✔
512
  }
513

514
  *index = geom_state.lowest_coord().cell();
600,297✔
515
  *instance = geom_state.cell_instance();
600,297✔
516
  return 0;
600,297✔
517
}
600,308✔
518

519
extern "C" int openmc_global_bounding_box(double* llc, double* urc)
11✔
520
{
521
  auto bbox = model::universes.at(model::root_universe)->bounding_box();
11✔
522

523
  // set lower left corner values
524
  llc[0] = bbox.min.x;
11✔
525
  llc[1] = bbox.min.y;
11✔
526
  llc[2] = bbox.min.z;
11✔
527

528
  // set upper right corner values
529
  urc[0] = bbox.max.x;
11✔
530
  urc[1] = bbox.max.y;
11✔
531
  urc[2] = bbox.max.z;
11✔
532

533
  return 0;
11✔
534
}
535

536
} // namespace openmc
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