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

openmc-dev / openmc / 9966309227

17 Jul 2024 12:53AM UTC coverage: 84.815% (+0.008%) from 84.807%
9966309227

push

github

web-flow
Linear Source Random Ray (#3072)

Co-authored-by: John Tramm <john.tramm@gmail.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>

336 of 369 new or added lines in 9 files covered. (91.06%)

36 existing lines in 4 files now uncovered.

49336 of 58169 relevant lines covered (84.81%)

32020493.4 hits per line

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

80.48
/src/random_ray/flat_source_domain.cpp
1
#include "openmc/random_ray/flat_source_domain.h"
2

3
#include "openmc/cell.h"
4
#include "openmc/eigenvalue.h"
5
#include "openmc/geometry.h"
6
#include "openmc/material.h"
7
#include "openmc/message_passing.h"
8
#include "openmc/mgxs_interface.h"
9
#include "openmc/output.h"
10
#include "openmc/plot.h"
11
#include "openmc/random_ray/random_ray.h"
12
#include "openmc/simulation.h"
13
#include "openmc/tallies/filter.h"
14
#include "openmc/tallies/tally.h"
15
#include "openmc/tallies/tally_scoring.h"
16
#include "openmc/timer.h"
17

18
#include <cstdio>
19

20
namespace openmc {
21

22
//==============================================================================
23
// FlatSourceDomain implementation
24
//==============================================================================
25

26
// Static Variable Declarations
27
bool FlatSourceDomain::volume_normalized_flux_tallies_ {false};
28

29
FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_)
187✔
30
{
31
  // Count the number of source regions, compute the cell offset
32
  // indices, and store the material type The reason for the offsets is that
33
  // some cell types may not have material fills, and therefore do not
34
  // produce FSRs. Thus, we cannot index into the global arrays directly
35
  for (const auto& c : model::cells) {
1,683✔
36
    if (c->type_ != Fill::MATERIAL) {
1,496✔
37
      source_region_offsets_.push_back(-1);
765✔
38
    } else {
39
      source_region_offsets_.push_back(n_source_regions_);
731✔
40
      n_source_regions_ += c->n_instances_;
731✔
41
      n_source_elements_ += c->n_instances_ * negroups_;
731✔
42
    }
43
  }
44

45
  // Initialize cell-wise arrays
46
  lock_.resize(n_source_regions_);
187✔
47
  material_.resize(n_source_regions_);
187✔
48
  position_recorded_.assign(n_source_regions_, 0);
187✔
49
  position_.resize(n_source_regions_);
187✔
50
  volume_.assign(n_source_regions_, 0.0);
187✔
51
  volume_t_.assign(n_source_regions_, 0.0);
187✔
52
  was_hit_.assign(n_source_regions_, 0);
187✔
53

54
  // Initialize element-wise arrays
55
  scalar_flux_new_.assign(n_source_elements_, 0.0);
187✔
56
  scalar_flux_final_.assign(n_source_elements_, 0.0);
187✔
57
  source_.resize(n_source_elements_);
187✔
58
  external_source_.assign(n_source_elements_, 0.0);
187✔
59
  tally_task_.resize(n_source_elements_);
187✔
60
  volume_task_.resize(n_source_regions_);
187✔
61

62
  if (settings::run_mode == RunMode::EIGENVALUE) {
187✔
63
    // If in eigenvalue mode, set starting flux to guess of unity
64
    scalar_flux_old_.assign(n_source_elements_, 1.0);
68✔
65
  } else {
66
    // If in fixed source mode, set starting flux to guess of zero
67
    scalar_flux_old_.assign(n_source_elements_, 0.0);
119✔
68
  }
69

70
  // Initialize material array
71
  int64_t source_region_id = 0;
187✔
72
  for (int i = 0; i < model::cells.size(); i++) {
1,683✔
73
    Cell& cell = *model::cells[i];
1,496✔
74
    if (cell.type_ == Fill::MATERIAL) {
1,496✔
75
      for (int j = 0; j < cell.n_instances_; j++) {
227,511✔
76
        material_[source_region_id++] = cell.material(j);
226,780✔
77
      }
78
    }
79
  }
80

81
  // Sanity check
82
  if (source_region_id != n_source_regions_) {
187✔
83
    fatal_error("Unexpected number of source regions");
×
84
  }
85

86
  // Initialize tally volumes
87
  if (volume_normalized_flux_tallies_) {
187✔
88
    tally_volumes_.resize(model::tallies.size());
153✔
89
    for (int i = 0; i < model::tallies.size(); i++) {
510✔
90
      //  Get the shape of the 3D result tensor
91
      auto shape = model::tallies[i]->results().shape();
357✔
92

93
      // Create a new 2D tensor with the same size as the first
94
      // two dimensions of the 3D tensor
95
      tally_volumes_[i] =
357✔
96
        xt::xtensor<double, 2>::from_shape({shape[0], shape[1]});
714✔
97
    }
98
  }
99

100
  // Compute simulation domain volume based on ray source
101
  auto* is = dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
187✔
102
  SpatialDistribution* space_dist = is->space();
187✔
103
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
187✔
104
  Position dims = sb->upper_right() - sb->lower_left();
187✔
105
  simulation_volume_ = dims.x * dims.y * dims.z;
187✔
106
}
187✔
107

108
void FlatSourceDomain::batch_reset()
1,870✔
109
{
110
  // Reset scalar fluxes, iteration volume tallies, and region hit flags to
111
  // zero
112
  parallel_fill<float>(scalar_flux_new_, 0.0f);
1,870✔
113
  parallel_fill<double>(volume_, 0.0);
1,870✔
114
  parallel_fill<int>(was_hit_, 0);
1,870✔
115
}
1,870✔
116

117
void FlatSourceDomain::accumulate_iteration_flux()
660✔
118
{
119
#pragma omp parallel for
330✔
120
  for (int64_t se = 0; se < n_source_elements_; se++) {
532,290✔
121
    scalar_flux_final_[se] += scalar_flux_new_[se];
531,960✔
122
  }
123
}
660✔
124

125
// Compute new estimate of scattering + fission sources in each source region
126
// based on the flux estimate from the previous iteration.
127
void FlatSourceDomain::update_neutron_source(double k_eff)
1,190✔
128
{
129
  simulation::time_update_src.start();
1,190✔
130

131
  double inverse_k_eff = 1.0 / k_eff;
1,190✔
132

133
  // Temperature and angle indices, if using multiple temperature
134
  // data sets and/or anisotropic data sets.
135
  // TODO: Currently assumes we are only using single temp/single angle data.
136
  const int t = 0;
1,190✔
137
  const int a = 0;
1,190✔
138

139
  // Add scattering source
140
#pragma omp parallel for
630✔
141
  for (int sr = 0; sr < n_source_regions_; sr++) {
752,240✔
142
    int material = material_[sr];
751,680✔
143

144
    for (int e_out = 0; e_out < negroups_; e_out++) {
1,620,480✔
145
      float sigma_t = data::mg.macro_xs_[material].get_xs(
868,800✔
146
        MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a);
868,800✔
147
      float scatter_source = 0.0f;
868,800✔
148

149
      for (int e_in = 0; e_in < negroups_; e_in++) {
2,557,440✔
150
        float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in];
1,688,640✔
151

152
        float sigma_s = data::mg.macro_xs_[material].get_xs(
1,688,640✔
153
          MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a);
1,688,640✔
154
        scatter_source += sigma_s * scalar_flux;
1,688,640✔
155
      }
156

157
      source_[sr * negroups_ + e_out] = scatter_source / sigma_t;
868,800✔
158
    }
159
  }
160

161
  if (settings::run_mode == RunMode::EIGENVALUE) {
1,190✔
162
    // Add fission source if in eigenvalue mode
163
#pragma omp parallel for
180✔
164
    for (int sr = 0; sr < n_source_regions_; sr++) {
60,640✔
165
      int material = material_[sr];
60,480✔
166

167
      for (int e_out = 0; e_out < negroups_; e_out++) {
238,080✔
168
        float sigma_t = data::mg.macro_xs_[material].get_xs(
177,600✔
169
          MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a);
177,600✔
170
        float fission_source = 0.0f;
177,600✔
171

172
        for (int e_in = 0; e_in < negroups_; e_in++) {
1,175,040✔
173
          float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in];
997,440✔
174
          float nu_sigma_f = data::mg.macro_xs_[material].get_xs(
997,440✔
175
            MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a);
997,440✔
176
          float chi = data::mg.macro_xs_[material].get_xs(
997,440✔
177
            MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a);
997,440✔
178
          fission_source += nu_sigma_f * scalar_flux * chi;
997,440✔
179
        }
180
        source_[sr * negroups_ + e_out] +=
177,600✔
181
          fission_source * inverse_k_eff / sigma_t;
177,600✔
182
      }
183
    }
184
  } else {
185
// Add external source if in fixed source mode
186
#pragma omp parallel for
450✔
187
    for (int se = 0; se < n_source_elements_; se++) {
691,600✔
188
      source_[se] += external_source_[se];
691,200✔
189
    }
190
  }
191

192
  simulation::time_update_src.stop();
1,190✔
193
}
1,190✔
194

195
// Normalizes flux and updates simulation-averaged volume estimate
196
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
1,190✔
197
  double total_active_distance_per_iteration)
198
{
199
  float normalization_factor = 1.0 / total_active_distance_per_iteration;
1,190✔
200
  double volume_normalization_factor =
1,190✔
201
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
1,190✔
202

203
// Normalize scalar flux to total distance travelled by all rays this iteration
204
#pragma omp parallel for
630✔
205
  for (int64_t e = 0; e < scalar_flux_new_.size(); e++) {
869,360✔
206
    scalar_flux_new_[e] *= normalization_factor;
868,800✔
207
  }
208

209
// Accumulate cell-wise ray length tallies collected this iteration, then
210
// update the simulation-averaged cell-wise volume estimates
211
#pragma omp parallel for
630✔
212
  for (int64_t sr = 0; sr < n_source_regions_; sr++) {
752,240✔
213
    volume_t_[sr] += volume_[sr];
751,680✔
214
    volume_[sr] = volume_t_[sr] * volume_normalization_factor;
751,680✔
215
  }
216
}
1,190✔
217

218
// Combine transport flux contributions and flat source contributions from the
219
// previous iteration to generate this iteration's estimate of scalar flux.
220
int64_t FlatSourceDomain::add_source_to_scalar_flux()
1,190✔
221
{
222
  int64_t n_hits = 0;
1,190✔
223

224
  // Temperature and angle indices, if using multiple temperature
225
  // data sets and/or anisotropic data sets.
226
  // TODO: Currently assumes we are only using single temp/single
227
  // angle data.
228
  const int t = 0;
1,190✔
229
  const int a = 0;
1,190✔
230

231
#pragma omp parallel for reduction(+ : n_hits)
630✔
232
  for (int sr = 0; sr < n_source_regions_; sr++) {
752,240✔
233

234
    // Check if this cell was hit this iteration
235
    int was_cell_hit = was_hit_[sr];
751,680✔
236
    if (was_cell_hit) {
751,680✔
237
      n_hits++;
751,664✔
238
    }
239

240
    double volume = volume_[sr];
751,680✔
241
    int material = material_[sr];
751,680✔
242
    for (int g = 0; g < negroups_; g++) {
1,620,480✔
243
      int64_t idx = (sr * negroups_) + g;
868,800✔
244

245
      // There are three scenarios we need to consider:
246
      if (was_cell_hit) {
868,800✔
247
        // 1. If the FSR was hit this iteration, then the new flux is equal to
248
        // the flat source from the previous iteration plus the contributions
249
        // from rays passing through the source region (computed during the
250
        // transport sweep)
251
        float sigma_t = data::mg.macro_xs_[material].get_xs(
868,784✔
252
          MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a);
868,784✔
253
        scalar_flux_new_[idx] /= (sigma_t * volume);
868,784✔
254
        scalar_flux_new_[idx] += source_[idx];
868,784✔
255
      } else if (volume > 0.0) {
16✔
256
        // 2. If the FSR was not hit this iteration, but has been hit some
257
        // previous iteration, then we simply set the new scalar flux to be
258
        // equal to the contribution from the flat source alone.
259
        scalar_flux_new_[idx] = source_[idx];
16✔
260
      } else {
261
        // If the FSR was not hit this iteration, and it has never been hit in
262
        // any iteration (i.e., volume is zero), then we want to set this to 0
263
        // to avoid dividing anything by a zero volume.
264
        scalar_flux_new_[idx] = 0.0f;
265
      }
266
    }
267
  }
268

269
  // Return the number of source regions that were hit this iteration
270
  return n_hits;
1,190✔
271
}
272

273
// Generates new estimate of k_eff based on the differences between this
274
// iteration's estimate of the scalar flux and the last iteration's estimate.
275
double FlatSourceDomain::compute_k_eff(double k_eff_old) const
680✔
276
{
277
  double fission_rate_old = 0;
680✔
278
  double fission_rate_new = 0;
680✔
279

280
  // Temperature and angle indices, if using multiple temperature
281
  // data sets and/or anisotropic data sets.
282
  // TODO: Currently assumes we are only using single temp/single
283
  // angle data.
284
  const int t = 0;
680✔
285
  const int a = 0;
680✔
286

287
  // Vector for gathering fission source terms for Shannon entropy calculation
288
  vector<float> p(n_source_regions_, 0.0f);
680✔
289

290
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
360✔
291
  for (int sr = 0; sr < n_source_regions_; sr++) {
99,840✔
292

293
    // If simulation averaged volume is zero, don't include this cell
294
    double volume = volume_[sr];
99,520✔
295
    if (volume == 0.0) {
99,520✔
296
      continue;
297
    }
298

299
    int material = material_[sr];
99,520✔
300

301
    double sr_fission_source_old = 0;
99,520✔
302
    double sr_fission_source_new = 0;
99,520✔
303

304
    for (int g = 0; g < negroups_; g++) {
550,400✔
305
      int64_t idx = (sr * negroups_) + g;
450,880✔
306
      double nu_sigma_f = data::mg.macro_xs_[material].get_xs(
450,880✔
307
        MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a);
308
      sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx];
450,880✔
309
      sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx];
450,880✔
310
    }
311

312
    // Compute total fission rates in FSR
313
    sr_fission_source_old *= volume;
99,520✔
314
    sr_fission_source_new *= volume;
99,520✔
315

316
    // Accumulate totals
317
    fission_rate_old += sr_fission_source_old;
99,520✔
318
    fission_rate_new += sr_fission_source_new;
99,520✔
319

320
    // Store total fission rate in the FSR for Shannon calculation
321
    p[sr] = sr_fission_source_new;
99,520✔
322
  }
323

324
  double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old);
680✔
325

326
  double H = 0.0;
680✔
327
  // defining an inverse sum for better performance
328
  double inverse_sum = 1 / fission_rate_new;
680✔
329

330
#pragma omp parallel for reduction(+ : H)
360✔
331
  for (int sr = 0; sr < n_source_regions_; sr++) {
99,840✔
332
    // Only if FSR has non-negative and non-zero fission source
333
    if (p[sr] > 0.0f) {
99,520✔
334
      // Normalize to total weight of bank sites. p_i for better performance
335
      float p_i = p[sr] * inverse_sum;
58,240✔
336
      // Sum values to obtain Shannon entropy.
337
      H -= p_i * std::log2(p_i);
58,240✔
338
    }
339
  }
340

341
  // Adds entropy value to shared entropy vector in openmc namespace.
342
  simulation::entropy.push_back(H);
680✔
343

344
  return k_eff_new;
680✔
345
}
680✔
346

347
// This function is responsible for generating a mapping between random
348
// ray flat source regions (cell instances) and tally bins. The mapping
349
// takes the form of a "TallyTask" object, which accounts for one single
350
// score being applied to a single tally. Thus, a single source region
351
// may have anywhere from zero to many tally tasks associated with it ---
352
// meaning that the global "tally_task" data structure is in 2D. The outer
353
// dimension corresponds to the source element (i.e., each entry corresponds
354
// to a specific energy group within a specific source region), and the
355
// inner dimension corresponds to the tallying task itself. Mechanically,
356
// the mapping between FSRs and spatial filters is done by considering
357
// the location of a single known ray midpoint that passed through the
358
// FSR. I.e., during transport, the first ray to pass through a given FSR
359
// will write down its midpoint for use with this function. This is a cheap
360
// and easy way of mapping FSRs to spatial tally filters, but comes with
361
// the downside of adding the restriction that spatial tally filters must
362
// share boundaries with the physical geometry of the simulation (so as
363
// not to subdivide any FSR). It is acceptable for a spatial tally region
364
// to contain multiple FSRs, but not the other way around.
365

366
// TODO: In future work, it would be preferable to offer a more general
367
// (but perhaps slightly more expensive) option for handling arbitrary
368
// spatial tallies that would be allowed to subdivide FSRs.
369

370
// Besides generating the mapping structure, this function also keeps track
371
// of whether or not all flat source regions have been hit yet. This is
372
// required, as there is no guarantee that all flat source regions will
373
// be hit every iteration, such that in the first few iterations some FSRs
374
// may not have a known position within them yet to facilitate mapping to
375
// spatial tally filters. However, after several iterations, if all FSRs
376
// have been hit and have had a tally map generated, then this status will
377
// be passed back to the caller to alert them that this function doesn't
378
// need to be called for the remainder of the simulation.
379

380
void FlatSourceDomain::convert_source_regions_to_tallies()
132✔
381
{
382
  openmc::simulation::time_tallies.start();
132✔
383

384
  // Tracks if we've generated a mapping yet for all source regions.
385
  bool all_source_regions_mapped = true;
132✔
386

387
// Attempt to generate mapping for all source regions
388
#pragma omp parallel for
66✔
389
  for (int sr = 0; sr < n_source_regions_; sr++) {
80,106✔
390

391
    // If this source region has not been hit by a ray yet, then
392
    // we aren't going to be able to map it, so skip it.
393
    if (!position_recorded_[sr]) {
80,040✔
394
      all_source_regions_mapped = false;
395
      continue;
396
    }
397

398
    // A particle located at the recorded midpoint of a ray
399
    // crossing through this source region is used to estabilish
400
    // the spatial location of the source region
401
    Particle p;
80,040✔
402
    p.r() = position_[sr];
80,040✔
403
    p.r_last() = position_[sr];
80,040✔
404
    bool found = exhaustive_find_cell(p);
80,040✔
405

406
    // Loop over energy groups (so as to support energy filters)
407
    for (int g = 0; g < negroups_; g++) {
186,432✔
408

409
      // Set particle to the current energy
410
      p.g() = g;
106,392✔
411
      p.g_last() = g;
106,392✔
412
      p.E() = data::mg.energy_bin_avg_[p.g()];
106,392✔
413
      p.E_last() = p.E();
106,392✔
414

415
      int64_t source_element = sr * negroups_ + g;
106,392✔
416

417
      // If this task has already been populated, we don't need to do
418
      // it again.
419
      if (tally_task_[source_element].size() > 0) {
106,392✔
420
        continue;
421
      }
422

423
      // Loop over all active tallies. This logic is essentially identical
424
      // to what happens when scanning for applicable tallies during
425
      // MC transport.
426
      for (auto i_tally : model::active_tallies) {
354,864✔
427
        Tally& tally {*model::tallies[i_tally]};
248,472✔
428

429
        // Initialize an iterator over valid filter bin combinations.
430
        // If there are no valid combinations, use a continue statement
431
        // to ensure we skip the assume_separate break below.
432
        auto filter_iter = FilterBinIter(tally, p);
248,472✔
433
        auto end = FilterBinIter(tally, true, &p.filter_matches());
248,472✔
434
        if (filter_iter == end)
248,472✔
435
          continue;
145,152✔
436

437
        // Loop over filter bins.
438
        for (; filter_iter != end; ++filter_iter) {
206,640✔
439
          auto filter_index = filter_iter.index_;
103,320✔
440
          auto filter_weight = filter_iter.weight_;
103,320✔
441

442
          // Loop over scores
443
          for (auto score_index = 0; score_index < tally.scores_.size();
268,128✔
444
               score_index++) {
445
            auto score_bin = tally.scores_[score_index];
164,808✔
446
            // If a valid tally, filter, and score combination has been found,
447
            // then add it to the list of tally tasks for this source element.
448
            TallyTask task(i_tally, filter_index, score_index, score_bin);
164,808✔
449
            tally_task_[source_element].push_back(task);
164,808✔
450

451
            // Also add this task to the list of volume tasks for this source
452
            // region.
453
            volume_task_[sr].insert(task);
164,808✔
454
          }
455
        }
456
      }
457
      // Reset all the filter matches for the next tally event.
458
      for (auto& match : p.filter_matches())
385,608✔
459
        match.bins_present_ = false;
279,216✔
460
    }
461
  }
80,040✔
462
  openmc::simulation::time_tallies.stop();
132✔
463

464
  mapped_all_tallies_ = all_source_regions_mapped;
132✔
465
}
132✔
466

467
// Set the volume accumulators to zero for all tallies
468
void FlatSourceDomain::reset_tally_volumes()
660✔
469
{
470
  if (volume_normalized_flux_tallies_) {
660✔
471
#pragma omp parallel for
270✔
472
    for (int i = 0; i < tally_volumes_.size(); i++) {
900✔
473
      auto& tensor = tally_volumes_[i];
630✔
474
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
630✔
475
    }
476
  }
477
}
660✔
478

479
// In fixed source mode, due to the way that volumetric fixed sources are
480
// converted and applied as volumetric sources in one or more source regions,
481
// we need to perform an additional normalization step to ensure that the
482
// reported scalar fluxes are in units per source neutron. This allows for
483
// direct comparison of reported tallies to Monte Carlo flux results.
484
// This factor needs to be computed at each iteration, as it is based on the
485
// volume estimate of each FSR, which improves over the course of the simulation
486
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
660✔
487
{
488
  // If we are not in fixed source mode, then there are no external sources
489
  // so no normalization is needed.
490
  if (settings::run_mode != RunMode::FIXED_SOURCE) {
660✔
491
    return 1.0;
240✔
492
  }
493

494
  // Step 1 is to sum over all source regions and energy groups to get the
495
  // total external source strength in the simulation.
496
  double simulation_external_source_strength = 0.0;
420✔
497
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
210✔
498
  for (int sr = 0; sr < n_source_regions_; sr++) {
363,090✔
499
    int material = material_[sr];
362,880✔
500
    double volume = volume_[sr] * simulation_volume_;
362,880✔
501
    for (int e = 0; e < negroups_; e++) {
725,760✔
502
      // Temperature and angle indices, if using multiple temperature
503
      // data sets and/or anisotropic data sets.
504
      // TODO: Currently assumes we are only using single temp/single
505
      // angle data.
506
      const int t = 0;
362,880✔
507
      const int a = 0;
362,880✔
508
      float sigma_t = data::mg.macro_xs_[material].get_xs(
362,880✔
509
        MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a);
362,880✔
510
      simulation_external_source_strength +=
362,880✔
511
        external_source_[sr * negroups_ + e] * sigma_t * volume;
362,880✔
512
    }
513
  }
514

515
  // Step 2 is to determine the total user-specified external source strength
516
  double user_external_source_strength = 0.0;
420✔
517
  for (auto& ext_source : model::external_sources) {
840✔
518
    user_external_source_strength += ext_source->strength();
420✔
519
  }
520

521
  // The correction factor is the ratio of the user-specified external source
522
  // strength to the simulation external source strength.
523
  double source_normalization_factor =
420✔
524
    user_external_source_strength / simulation_external_source_strength;
525

526
  return source_normalization_factor;
420✔
527
}
528

529
// Tallying in random ray is not done directly during transport, rather,
530
// it is done only once after each power iteration. This is made possible
531
// by way of a mapping data structure that relates spatial source regions
532
// (FSRs) to tally/filter/score combinations. The mechanism by which the
533
// mapping is done (and the limitations incurred) is documented in the
534
// "convert_source_regions_to_tallies()" function comments above. The present
535
// tally function simply traverses the mapping data structure and executes
536
// the scoring operations to OpenMC's native tally result arrays.
537

538
void FlatSourceDomain::random_ray_tally()
660✔
539
{
540
  openmc::simulation::time_tallies.start();
660✔
541

542
  // Reset our tally volumes to zero
543
  reset_tally_volumes();
660✔
544

545
  // Temperature and angle indices, if using multiple temperature
546
  // data sets and/or anisotropic data sets.
547
  // TODO: Currently assumes we are only using single temp/single
548
  // angle data.
549
  const int t = 0;
660✔
550
  const int a = 0;
660✔
551

552
  double source_normalization_factor =
553
    compute_fixed_source_normalization_factor();
660✔
554

555
// We loop over all source regions and energy groups. For each
556
// element, we check if there are any scores needed and apply
557
// them.
558
#pragma omp parallel for
330✔
559
  for (int sr = 0; sr < n_source_regions_; sr++) {
400,530✔
560
    // The fsr.volume_ is the unitless fractional simulation averaged volume
561
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
562
    // simulation_volume_ is the total 3D physical volume in cm^3 of the entire
563
    // global simulation domain (as defined by the ray source box). Thus, the
564
    // FSR's true 3D spatial volume in cm^3 is found by multiplying its fraction
565
    // of the total volume by the total volume. Not important in eigenvalue
566
    // solves, but useful in fixed source solves for returning the flux shape
567
    // with a magnitude that makes sense relative to the fixed source strength.
568
    double volume = volume_[sr] * simulation_volume_;
400,200✔
569

570
    double material = material_[sr];
400,200✔
571
    for (int g = 0; g < negroups_; g++) {
932,160✔
572
      int idx = sr * negroups_ + g;
531,960✔
573
      double flux = scalar_flux_new_[idx] * source_normalization_factor;
531,960✔
574

575
      // Determine numerical score value
576
      for (auto& task : tally_task_[idx]) {
1,356,000✔
577
        double score;
578
        switch (task.score_type) {
824,040✔
579

580
        case SCORE_FLUX:
516,600✔
581
          score = flux * volume;
516,600✔
582
          break;
516,600✔
583

584
        case SCORE_TOTAL:
585
          score = flux * volume *
586
                  data::mg.macro_xs_[material].get_xs(
587
                    MgxsType::TOTAL, g, NULL, NULL, NULL, t, a);
588
          break;
589

590
        case SCORE_FISSION:
153,720✔
591
          score = flux * volume *
307,440✔
592
                  data::mg.macro_xs_[material].get_xs(
153,720✔
593
                    MgxsType::FISSION, g, NULL, NULL, NULL, t, a);
594
          break;
153,720✔
595

596
        case SCORE_NU_FISSION:
153,720✔
597
          score = flux * volume *
307,440✔
598
                  data::mg.macro_xs_[material].get_xs(
153,720✔
599
                    MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a);
600
          break;
153,720✔
601

602
        case SCORE_EVENTS:
603
          score = 1.0;
604
          break;
605

606
        default:
607
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
608
                      "total, fission, nu-fission, and events are supported in "
609
                      "random ray mode.");
610
          break;
611
        }
612

613
        // Apply score to the appropriate tally bin
614
        Tally& tally {*model::tallies[task.tally_idx]};
824,040✔
615
#pragma omp atomic
616
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
824,040✔
617
          score;
618
      }
619
    }
620

621
    // For flux tallies, the total volume of the spatial region is needed
622
    // for normalizing the flux. We store this volume in a separate tensor.
623
    // We only contribute to each volume tally bin once per FSR.
624
    if (volume_normalized_flux_tallies_) {
400,200✔
625
      for (const auto& task : volume_task_[sr]) {
1,105,200✔
626
        if (task.score_type == SCORE_FLUX) {
772,200✔
627
#pragma omp atomic
628
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
464,760✔
629
            volume;
630
        }
631
      }
632
    }
633
  } // end FSR loop
634

635
  // Normalize any flux scores by the total volume of the FSRs scoring to that
636
  // bin. To do this, we loop over all tallies, and then all filter bins,
637
  // and then scores. For each score, we check the tally data structure to
638
  // see what index that score corresponds to. If that score is a flux score,
639
  // then we divide it by volume.
640
  if (volume_normalized_flux_tallies_) {
660✔
641
    for (int i = 0; i < model::tallies.size(); i++) {
1,800✔
642
      Tally& tally {*model::tallies[i]};
1,260✔
643
#pragma omp parallel for
630✔
644
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
3,690✔
645
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
11,160✔
646
          auto score_type = tally.scores_[score_idx];
8,100✔
647
          if (score_type == SCORE_FLUX) {
8,100✔
648
            double vol = tally_volumes_[i](bin, score_idx);
3,060✔
649
            if (vol > 0.0) {
3,060✔
650
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
3,060✔
651
            }
652
          }
653
        }
654
      }
655
    }
656
  }
657

658
  openmc::simulation::time_tallies.stop();
660✔
659
}
660✔
660

661
void FlatSourceDomain::all_reduce_replicated_source_regions()
1,590✔
662
{
663
#ifdef OPENMC_MPI
664

665
  // If we only have 1 MPI rank, no need
666
  // to reduce anything.
667
  if (mpi::n_procs <= 1)
1,100✔
668
    return;
669

670
  simulation::time_bank_sendrecv.start();
1,100✔
671

672
  // The "position_recorded" variable needs to be allreduced (and maxed),
673
  // as whether or not a cell was hit will affect some decisions in how the
674
  // source is calculated in the next iteration so as to avoid dividing
675
  // by zero. We take the max rather than the sum as the hit values are
676
  // expected to be zero or 1.
677
  MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_,
1,100✔
678
    MPI_INT, MPI_MAX, mpi::intracomm);
679

680
  // The position variable is more complicated to reduce than the others,
681
  // as we do not want the sum of all positions in each cell, rather, we
682
  // want to just pick any single valid position. Thus, we perform a gather
683
  // and then pick the first valid position we find for all source regions
684
  // that have had a position recorded. This operation does not need to
685
  // be broadcast back to other ranks, as this value is only used for the
686
  // tally conversion operation, which is only performed on the master rank.
687
  // While this is expensive, it only needs to be done for active batches,
688
  // and only if we have not mapped all the tallies yet. Once tallies are
689
  // fully mapped, then the position vector is fully populated, so this
690
  // operation can be skipped.
691

692
  // First, we broadcast the fully mapped tally status variable so that
693
  // all ranks are on the same page
694
  int mapped_all_tallies_i = static_cast<int>(mapped_all_tallies_);
1,100✔
695
  MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm);
1,100✔
696

697
  // Then, we perform the gather of position data, if needed
698
  if (simulation::current_batch > settings::n_inactive &&
1,100✔
699
      !mapped_all_tallies_i) {
550✔
700

701
    // Master rank will gather results and pick valid positions
702
    if (mpi::master) {
110✔
703
      // Initialize temporary vector for receiving positions
704
      vector<vector<Position>> all_position;
55✔
705
      all_position.resize(mpi::n_procs);
55✔
706
      for (int i = 0; i < mpi::n_procs; i++) {
165✔
707
        all_position[i].resize(n_source_regions_);
110✔
708
      }
709

710
      // Copy master rank data into gathered vector for convenience
711
      all_position[0] = position_;
55✔
712

713
      // Receive all data into gather vector
714
      for (int i = 1; i < mpi::n_procs; i++) {
110✔
715
        MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i,
55✔
716
          0, mpi::intracomm, MPI_STATUS_IGNORE);
717
      }
718

719
      // Scan through gathered data and pick first valid cell posiiton
720
      for (int sr = 0; sr < n_source_regions_; sr++) {
66,755✔
721
        if (position_recorded_[sr] == 1) {
66,700✔
722
          for (int i = 0; i < mpi::n_procs; i++) {
66,805✔
723
            if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 ||
66,910✔
724
                all_position[i][sr].z != 0.0) {
105✔
725
              position_[sr] = all_position[i][sr];
66,700✔
726
              break;
66,700✔
727
            }
728
          }
729
        }
730
      }
731
    } else {
55✔
732
      // Other ranks just send in their data
733
      MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0,
55✔
734
        mpi::intracomm);
735
    }
736
  }
737

738
  // For the rest of the source region data, we simply perform an all reduce,
739
  // as these values will be needed on all ranks for transport during the
740
  // next iteration.
741
  MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE,
1,100✔
742
    MPI_SUM, mpi::intracomm);
743

744
  MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT,
1,100✔
745
    MPI_SUM, mpi::intracomm);
746

747
  MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_,
1,100✔
748
    MPI_FLOAT, MPI_SUM, mpi::intracomm);
749

750
  simulation::time_bank_sendrecv.stop();
1,100✔
751
#endif
752
}
490✔
753

NEW
754
double FlatSourceDomain::evaluate_flux_at_point(
×
755
  Position r, int64_t sr, int g) const
756
{
NEW
757
  return scalar_flux_final_[sr * negroups_ + g] /
×
NEW
758
         (settings::n_batches - settings::n_inactive);
×
759
}
760

761
// Outputs all basic material, FSR ID, multigroup flux, and
762
// fission source data to .vtk file that can be directly
763
// loaded and displayed by Paraview. Note that .vtk binary
764
// files require big endian byte ordering, so endianness
765
// is checked and flipped if necessary.
766
void FlatSourceDomain::output_to_vtk() const
×
767
{
768
  // Rename .h5 plot filename(s) to .vtk filenames
769
  for (int p = 0; p < model::plots.size(); p++) {
×
770
    PlottableInterface* plot = model::plots[p].get();
×
771
    plot->path_plot() =
×
772
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
773
  }
774

775
  // Print header information
776
  print_plot();
×
777

778
  // Outer loop over plots
779
  for (int p = 0; p < model::plots.size(); p++) {
×
780

781
    // Get handle to OpenMC plot object and extract params
782
    Plot* openmc_plot = dynamic_cast<Plot*>(model::plots[p].get());
×
783

784
    // Random ray plots only support voxel plots
785
    if (!openmc_plot) {
×
786
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
787
                          "is allowed in random ray mode.",
788
        p));
789
      continue;
×
790
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
791
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
792
                          "is allowed in random ray mode.",
793
        p));
794
      continue;
×
795
    }
796

797
    int Nx = openmc_plot->pixels_[0];
×
798
    int Ny = openmc_plot->pixels_[1];
×
799
    int Nz = openmc_plot->pixels_[2];
×
800
    Position origin = openmc_plot->origin_;
×
801
    Position width = openmc_plot->width_;
×
802
    Position ll = origin - width / 2.0;
×
803
    double x_delta = width.x / Nx;
×
804
    double y_delta = width.y / Ny;
×
805
    double z_delta = width.z / Nz;
×
806
    std::string filename = openmc_plot->path_plot();
×
807

808
    // Perform sanity checks on file size
809
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
810
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
811
      openmc_plot->id(), filename, bytes / 1.0e6);
×
812
    if (bytes / 1.0e9 > 1.0) {
×
813
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
814
              "slow.");
815
    } else if (bytes / 1.0e9 > 100.0) {
×
816
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
817
    }
818

819
    // Relate voxel spatial locations to random ray source regions
820
    vector<int> voxel_indices(Nx * Ny * Nz);
×
NEW
821
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
822

823
#pragma omp parallel for collapse(3)
824
    for (int z = 0; z < Nz; z++) {
825
      for (int y = 0; y < Ny; y++) {
826
        for (int x = 0; x < Nx; x++) {
827
          Position sample;
828
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
829
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
830
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
831
          Particle p;
832
          p.r() = sample;
833
          bool found = exhaustive_find_cell(p);
834
          int i_cell = p.lowest_coord().cell;
835
          int64_t source_region_idx =
836
            source_region_offsets_[i_cell] + p.cell_instance();
837
          voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx;
838
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
839
        }
840
      }
841
    }
842

843
    double source_normalization_factor =
844
      compute_fixed_source_normalization_factor();
×
845

846
    // Open file for writing
847
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
848

849
    // Write vtk metadata
850
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
851
    std::fprintf(plot, "Dataset File\n");
×
852
    std::fprintf(plot, "BINARY\n");
×
853
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
854
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
855
    std::fprintf(plot, "ORIGIN 0 0 0\n");
×
856
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
857
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
858

859
    // Plot multigroup flux data
860
    for (int g = 0; g < negroups_; g++) {
×
861
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
862
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
NEW
863
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
NEW
864
        int64_t fsr = voxel_indices[i];
×
UNCOV
865
        int64_t source_element = fsr * negroups_ + g;
×
NEW
866
        float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
867
        flux = convert_to_big_endian<float>(flux);
×
868
        std::fwrite(&flux, sizeof(float), 1, plot);
×
869
      }
870
    }
871

872
    // Plot FSRs
873
    std::fprintf(plot, "SCALARS FSRs float\n");
×
874
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
875
    for (int fsr : voxel_indices) {
×
876
      float value = future_prn(10, fsr);
×
877
      value = convert_to_big_endian<float>(value);
×
878
      std::fwrite(&value, sizeof(float), 1, plot);
×
879
    }
880

881
    // Plot Materials
882
    std::fprintf(plot, "SCALARS Materials int\n");
×
883
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
884
    for (int fsr : voxel_indices) {
×
885
      int mat = material_[fsr];
×
886
      mat = convert_to_big_endian<int>(mat);
×
887
      std::fwrite(&mat, sizeof(int), 1, plot);
×
888
    }
889

890
    // Plot fission source
891
    std::fprintf(plot, "SCALARS total_fission_source float\n");
×
892
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
NEW
893
    for (int i = 0; i < Nx * Ny * Nz; i++) {
×
NEW
894
      int64_t fsr = voxel_indices[i];
×
895

896
      float total_fission = 0.0;
×
897
      int mat = material_[fsr];
×
898
      for (int g = 0; g < negroups_; g++) {
×
899
        int64_t source_element = fsr * negroups_ + g;
×
NEW
900
        float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
901
        float Sigma_f = data::mg.macro_xs_[mat].get_xs(
×
902
          MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0);
×
903
        total_fission += Sigma_f * flux;
×
904
      }
905
      total_fission = convert_to_big_endian<float>(total_fission);
×
906
      std::fwrite(&total_fission, sizeof(float), 1, plot);
×
907
    }
908

909
    std::fclose(plot);
×
910
  }
911
}
912

913
void FlatSourceDomain::apply_external_source_to_source_region(
952✔
914
  Discrete* discrete, double strength_factor, int64_t source_region)
915
{
916
  const auto& discrete_energies = discrete->x();
952✔
917
  const auto& discrete_probs = discrete->prob();
952✔
918

919
  for (int e = 0; e < discrete_energies.size(); e++) {
1,904✔
920
    int g = data::mg.get_group_index(discrete_energies[e]);
952✔
921
    external_source_[source_region * negroups_ + g] +=
952✔
922
      discrete_probs[e] * strength_factor;
952✔
923
  }
924
}
952✔
925

926
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
153✔
927
  Discrete* discrete, double strength_factor, int target_material_id,
928
  const vector<int32_t>& instances)
929
{
930
  Cell& cell = *model::cells[i_cell];
153✔
931

932
  if (cell.type_ != Fill::MATERIAL)
153✔
933
    return;
×
934

935
  for (int j : instances) {
30,345✔
936
    int cell_material_idx = cell.material(j);
30,192✔
937
    int cell_material_id = model::materials[cell_material_idx]->id();
30,192✔
938
    if (target_material_id == C_NONE ||
30,192✔
939
        cell_material_id == target_material_id) {
940
      int64_t source_region = source_region_offsets_[i_cell] + j;
952✔
941
      apply_external_source_to_source_region(
952✔
942
        discrete, strength_factor, source_region);
943
    }
944
  }
945
}
946

947
void FlatSourceDomain::apply_external_source_to_cell_and_children(
187✔
948
  int32_t i_cell, Discrete* discrete, double strength_factor,
949
  int32_t target_material_id)
950
{
951
  Cell& cell = *model::cells[i_cell];
187✔
952

953
  if (cell.type_ == Fill::MATERIAL) {
187✔
954
    vector<int> instances(cell.n_instances_);
153✔
955
    std::iota(instances.begin(), instances.end(), 0);
153✔
956
    apply_external_source_to_cell_instances(
153✔
957
      i_cell, discrete, strength_factor, target_material_id, instances);
958
  } else if (target_material_id == C_NONE) {
187✔
959
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
960
      cell.get_contained_cells(0, nullptr);
×
961
    for (const auto& pair : cell_instance_list) {
×
962
      int32_t i_child_cell = pair.first;
×
963
      apply_external_source_to_cell_instances(i_child_cell, discrete,
×
964
        strength_factor, target_material_id, pair.second);
×
965
    }
966
  }
967
}
187✔
968

969
void FlatSourceDomain::count_external_source_regions()
119✔
970
{
971
#pragma omp parallel for reduction(+ : n_external_source_regions_)
63✔
972
  for (int sr = 0; sr < n_source_regions_; sr++) {
96,824✔
973
    float total = 0.f;
96,768✔
974
    for (int e = 0; e < negroups_; e++) {
193,536✔
975
      int64_t se = sr * negroups_ + e;
96,768✔
976
      total += external_source_[se];
96,768✔
977
    }
978
    if (total != 0.f) {
96,768✔
979
      n_external_source_regions_++;
448✔
980
    }
981
  }
982
}
119✔
983

984
void FlatSourceDomain::convert_external_sources()
119✔
985
{
986
  // Loop over external sources
987
  for (int es = 0; es < model::external_sources.size(); es++) {
238✔
988
    Source* s = model::external_sources[es].get();
119✔
989
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
119✔
990
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
119✔
991
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
119✔
992

993
    double strength_factor = is->strength();
119✔
994

995
    if (is->domain_type() == Source::DomainType::MATERIAL) {
119✔
996
      for (int32_t material_id : domain_ids) {
34✔
997
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
102✔
998
          apply_external_source_to_cell_and_children(
85✔
999
            i_cell, energy, strength_factor, material_id);
1000
        }
1001
      }
1002
    } else if (is->domain_type() == Source::DomainType::CELL) {
102✔
1003
      for (int32_t cell_id : domain_ids) {
34✔
1004
        int32_t i_cell = model::cell_map[cell_id];
17✔
1005
        apply_external_source_to_cell_and_children(
17✔
1006
          i_cell, energy, strength_factor, C_NONE);
1007
      }
1008
    } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
85✔
1009
      for (int32_t universe_id : domain_ids) {
170✔
1010
        int32_t i_universe = model::universe_map[universe_id];
85✔
1011
        Universe& universe = *model::universes[i_universe];
85✔
1012
        for (int32_t i_cell : universe.cells_) {
170✔
1013
          apply_external_source_to_cell_and_children(
85✔
1014
            i_cell, energy, strength_factor, C_NONE);
1015
        }
1016
      }
1017
    }
1018
  } // End loop over external sources
1019

1020
  // Temperature and angle indices, if using multiple temperature
1021
  // data sets and/or anisotropic data sets.
1022
  // TODO: Currently assumes we are only using single temp/single angle data.
1023
  const int t = 0;
119✔
1024
  const int a = 0;
119✔
1025

1026
// Divide the fixed source term by sigma t (to save time when applying each
1027
// iteration)
1028
#pragma omp parallel for
63✔
1029
  for (int sr = 0; sr < n_source_regions_; sr++) {
96,824✔
1030
    int material = material_[sr];
96,768✔
1031
    for (int e = 0; e < negroups_; e++) {
193,536✔
1032
      float sigma_t = data::mg.macro_xs_[material].get_xs(
96,768✔
1033
        MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a);
96,768✔
1034
      external_source_[sr * negroups_ + e] /= sigma_t;
96,768✔
1035
    }
1036
  }
1037
}
119✔
1038
void FlatSourceDomain::flux_swap()
1,870✔
1039
{
1040
  scalar_flux_old_.swap(scalar_flux_new_);
1,870✔
1041
}
1,870✔
1042

1043
} // 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