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

openmc-dev / openmc / 21879063231

10 Feb 2026 07:18PM UTC coverage: 81.744% (-0.07%) from 81.817%
21879063231

Pull #3785

github

web-flow
Merge 4fcb5ec98 into 3f20a5e22
Pull Request #3785: Coincident source

17383 of 24440 branches covered (71.13%)

Branch coverage included in aggregate %.

127 of 206 new or added lines in 5 files covered. (61.65%)

508 existing lines in 13 files now uncovered.

56323 of 65727 relevant lines covered (85.69%)

46749711.38 hits per line

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

72.43
/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/constants.h"
5
#include "openmc/eigenvalue.h"
6
#include "openmc/geometry.h"
7
#include "openmc/material.h"
8
#include "openmc/message_passing.h"
9
#include "openmc/mgxs_interface.h"
10
#include "openmc/output.h"
11
#include "openmc/plot.h"
12
#include "openmc/random_ray/random_ray.h"
13
#include "openmc/simulation.h"
14
#include "openmc/tallies/filter.h"
15
#include "openmc/tallies/tally.h"
16
#include "openmc/tallies/tally_scoring.h"
17
#include "openmc/timer.h"
18
#include "openmc/weight_windows.h"
19

20
#include <cstdio>
21

22
namespace openmc {
23

24
//==============================================================================
25
// FlatSourceDomain implementation
26
//==============================================================================
27

28
// Static Variable Declarations
29
RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ {
30
  RandomRayVolumeEstimator::HYBRID};
31
bool FlatSourceDomain::volume_normalized_flux_tallies_ {false};
32
bool FlatSourceDomain::adjoint_ {false};
33
double FlatSourceDomain::diagonal_stabilization_rho_ {1.0};
34
std::unordered_map<int, vector<std::pair<Source::DomainType, int>>>
35
  FlatSourceDomain::mesh_domain_map_;
36

37
FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_)
796✔
38
{
39
  // Count the number of source regions, compute the cell offset
40
  // indices, and store the material type The reason for the offsets is that
41
  // some cell types may not have material fills, and therefore do not
42
  // produce FSRs. Thus, we cannot index into the global arrays directly
43
  int base_source_regions = 0;
796✔
44
  for (const auto& c : model::cells) {
6,372✔
45
    if (c->type_ != Fill::MATERIAL) {
5,576✔
46
      source_region_offsets_.push_back(-1);
2,516✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
3,060✔
49
      base_source_regions += c->n_instances();
3,060✔
50
    }
51
  }
52

53
  // Initialize source regions.
54
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
796✔
55
  source_regions_ = SourceRegionContainer(negroups_, is_linear);
796✔
56

57
  // Initialize tally volumes
58
  if (volume_normalized_flux_tallies_) {
796✔
59
    tally_volumes_.resize(model::tallies.size());
507✔
60
    for (int i = 0; i < model::tallies.size(); i++) {
1,798✔
61
      //  Get the shape of the 3D result tensor
62
      auto shape = model::tallies[i]->results().shape();
1,291✔
63

64
      // Create a new 2D tensor with the same size as the first
65
      // two dimensions of the 3D tensor
66
      tally_volumes_[i] =
1,291✔
67
        xt::xtensor<double, 2>::from_shape({shape[0], shape[1]});
2,582✔
68
    }
69
  }
70

71
  // Compute simulation domain volume based on ray source
72
  auto* is = dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
796!
73
  SpatialDistribution* space_dist = is->space();
796✔
74
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
796!
75
  Position dims = sb->upper_right() - sb->lower_left();
796✔
76
  simulation_volume_ = dims.x * dims.y * dims.z;
796✔
77
}
796✔
78

79
void FlatSourceDomain::batch_reset()
15,302✔
80
{
81
// Reset scalar fluxes and iteration volume tallies to zero
82
#pragma omp parallel for
8,352✔
83
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,476,125✔
84
    source_regions_.volume(sr) = 0.0;
32,469,175✔
85
    source_regions_.volume_sq(sr) = 0.0;
32,469,175✔
86
  }
87

88
#pragma omp parallel for
8,352✔
89
  for (int64_t se = 0; se < n_source_elements(); se++) {
37,141,065✔
90
    source_regions_.scalar_flux_new(se) = 0.0;
37,134,115✔
91
  }
92
}
15,302✔
93

94
void FlatSourceDomain::accumulate_iteration_flux()
7,101✔
95
{
96
#pragma omp parallel for
3,876✔
97
  for (int64_t se = 0; se < n_source_elements(); se++) {
17,029,625✔
98
    source_regions_.scalar_flux_final(se) +=
17,026,400✔
99
      source_regions_.scalar_flux_new(se);
17,026,400✔
100
  }
101
}
7,101✔
102

103
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
44,156,606✔
104
{
105
  // Reset all source regions to zero (important for void regions)
106
  for (int g = 0; g < negroups_; g++) {
94,537,990✔
107
    srh.source(g) = 0.0;
50,381,384✔
108
  }
109

110
  // Add scattering + fission source
111
  int material = srh.material();
44,156,606✔
112
  int temp = srh.temperature_idx();
44,156,606✔
113
  double density_mult = srh.density_mult();
44,156,606✔
114
  if (material != MATERIAL_VOID) {
44,156,606✔
115
    double inverse_k_eff = 1.0 / k_eff_;
43,714,958✔
116
    const int material_offset = (material * ntemperature_ + temp) * negroups_;
43,714,958✔
117
    const int scatter_offset =
43,714,958✔
118
      (material * ntemperature_ + temp) * negroups_ * negroups_;
43,714,958✔
119
    for (int g_out = 0; g_out < negroups_; g_out++) {
93,653,926✔
120
      double sigma_t = sigma_t_[material_offset + g_out] * density_mult;
49,938,968✔
121
      double scatter_source = 0.0;
49,938,968✔
122
      double fission_source = 0.0;
49,938,968✔
123

124
      for (int g_in = 0; g_in < negroups_; g_in++) {
143,444,626✔
125
        double scalar_flux = srh.scalar_flux_old(g_in);
93,505,658✔
126
        double sigma_s =
127
          sigma_s_[scatter_offset + g_out * negroups_ + g_in] * density_mult;
93,505,658✔
128
        double nu_sigma_f = nu_sigma_f_[material_offset + g_in] * density_mult;
93,505,658✔
129
        double chi = chi_[material_offset + g_out];
93,505,658✔
130

131
        scatter_source += sigma_s * scalar_flux;
93,505,658✔
132
        if (settings::create_fission_neutrons) {
93,505,658!
133
          fission_source += nu_sigma_f * scalar_flux * chi;
93,505,658✔
134
        }
135
      }
136
      srh.source(g_out) =
49,938,968✔
137
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
49,938,968✔
138
    }
139
  }
140

141
  // Add external source if in fixed source mode
142
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
44,156,606✔
143
    for (int g = 0; g < negroups_; g++) {
89,955,038✔
144
      srh.source(g) += srh.external_source(g);
46,413,541✔
145
    }
146
  }
147
}
44,156,606✔
148

149
// Compute new estimate of scattering + fission sources in each source region
150
// based on the flux estimate from the previous iteration.
151
void FlatSourceDomain::update_all_neutron_sources()
15,302✔
152
{
153
  simulation::time_update_src.start();
15,302✔
154

155
#pragma omp parallel for
8,352✔
156
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,476,125✔
157
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
32,469,175✔
158
    update_single_neutron_source(srh);
32,469,175✔
159
  }
160

161
  simulation::time_update_src.stop();
15,302✔
162
}
15,302✔
163

164
// Normalizes flux and updates simulation-averaged volume estimate
165
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
7,767✔
166
  double total_active_distance_per_iteration)
167
{
168
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
7,767✔
169
  double volume_normalization_factor =
7,767✔
170
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
7,767✔
171

172
// Normalize scalar flux to total distance travelled by all rays this
173
// iteration
174
#pragma omp parallel for
4,242✔
175
  for (int64_t se = 0; se < n_source_elements(); se++) {
22,872,135✔
176
    source_regions_.scalar_flux_new(se) *= normalization_factor;
22,868,610✔
177
  }
178

179
// Accumulate cell-wise ray length tallies collected this iteration, then
180
// update the simulation-averaged cell-wise volume estimates
181
#pragma omp parallel for
4,242✔
182
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
20,043,195✔
183
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
20,039,670✔
184
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
20,039,670✔
185
    source_regions_.volume_naive(sr) =
40,079,340✔
186
      source_regions_.volume(sr) * normalization_factor;
20,039,670✔
187
    source_regions_.volume_sq(sr) =
40,079,340✔
188
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
20,039,670✔
189
    source_regions_.volume(sr) =
20,039,670✔
190
      source_regions_.volume_t(sr) * volume_normalization_factor;
20,039,670✔
191
  }
192
}
7,767✔
193

194
void FlatSourceDomain::set_flux_to_flux_plus_source(
44,717,249✔
195
  int64_t sr, double volume, int g)
196
{
197
  int material = source_regions_.material(sr);
44,717,249✔
198
  int temp = source_regions_.temperature_idx(sr);
44,717,249✔
199
  if (material == MATERIAL_VOID) {
44,717,249✔
200
    source_regions_.scalar_flux_new(sr, g) /= volume;
882,416✔
201
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
882,416!
202
      source_regions_.scalar_flux_new(sr, g) +=
882,416✔
203
        0.5f * source_regions_.external_source(sr, g) *
882,416✔
204
        source_regions_.volume_sq(sr);
882,416✔
205
    }
206
  } else {
207
    double sigma_t =
208
      sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
43,834,833✔
209
      source_regions_.density_mult(sr);
43,834,833✔
210
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
43,834,833✔
211
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
43,834,833✔
212
  }
213
}
44,717,249✔
214

215
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,542✔
216
{
217
  source_regions_.scalar_flux_new(sr, g) =
3,084✔
218
    source_regions_.scalar_flux_old(sr, g);
1,542✔
219
}
1,542✔
220

221
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
8,927,547✔
222
{
223
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
8,927,547✔
224
}
8,927,547✔
225

226
// Combine transport flux contributions and flat source contributions from the
227
// previous iteration to generate this iteration's estimate of scalar flux.
228
int64_t FlatSourceDomain::add_source_to_scalar_flux()
15,302✔
229
{
230
  int64_t n_hits = 0;
15,302✔
231
  double inverse_batch = 1.0 / simulation::current_batch;
15,302✔
232

233
#pragma omp parallel for reduction(+ : n_hits)
8,352✔
234
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
33,505,785✔
235

236
    double volume_simulation_avg = source_regions_.volume(sr);
33,498,835✔
237
    double volume_iteration = source_regions_.volume_naive(sr);
33,498,835✔
238

239
    // Increment the number of hits if cell was hit this iteration
240
    if (volume_iteration) {
33,498,835✔
241
      n_hits++;
29,439,680✔
242
    }
243

244
    // Set the SR to small status if its expected number of hits
245
    // per iteration is less than 1.5
246
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
33,498,835✔
247
      source_regions_.is_small(sr) = 1;
6,812,705✔
248
    } else {
249
      source_regions_.is_small(sr) = 0;
26,686,130✔
250
    }
251

252
    // The volume treatment depends on the volume estimator type
253
    // and whether or not an external source is present in the cell.
254
    double volume;
255
    switch (volume_estimator_) {
33,498,835!
256
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
257
      volume = volume_iteration;
777,600✔
258
      break;
777,600✔
259
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
260
      volume = volume_simulation_avg;
432,000✔
261
      break;
432,000✔
262
    case RandomRayVolumeEstimator::HYBRID:
32,289,235✔
263
      if (source_regions_.external_source_present(sr) ||
60,390,070✔
264
          source_regions_.is_small(sr)) {
28,100,835✔
265
        volume = volume_iteration;
11,000,725✔
266
      } else {
267
        volume = volume_simulation_avg;
21,288,510✔
268
      }
269
      break;
32,289,235✔
270
    default:
271
      fatal_error("Invalid volume estimator type");
272
    }
273

274
    for (int g = 0; g < negroups_; g++) {
71,822,210✔
275
      // There are three scenarios we need to consider:
276
      if (volume_iteration > 0.0) {
38,323,375✔
277
        // 1. If the FSR was hit this iteration, then the new flux is equal to
278
        // the flat source from the previous iteration plus the contributions
279
        // from rays passing through the source region (computed during the
280
        // transport sweep)
281
        set_flux_to_flux_plus_source(sr, volume, g);
34,264,070✔
282
      } else if (volume_simulation_avg > 0.0) {
4,059,305✔
283
        // 2. If the FSR was not hit this iteration, but has been hit some
284
        // previous iteration, then we need to make a choice about what
285
        // to do. Naively we will usually want to set the flux to be equal
286
        // to the reduced source. However, in fixed source problems where
287
        // there is a strong external source present in the cell, and where
288
        // the cell has a very low cross section, this approximation will
289
        // cause a huge upward bias in the flux estimate of the cell (in these
290
        // conditions, the flux estimate can be orders of magnitude too large).
291
        // Thus, to avoid this bias, if any external source is present
292
        // in the cell we will use the previous iteration's flux estimate. This
293
        // injects a small degree of correlation into the simulation, but this
294
        // is going to be trivial when the miss rate is a few percent or less.
295
        if (source_regions_.external_source_present(sr)) {
4,059,295✔
296
          set_flux_to_old_flux(sr, g);
1,320✔
297
        } else {
298
          set_flux_to_source(sr, g);
4,057,975✔
299
        }
300
      }
301
      // Halt if NaN implosion is detected
302
      if (!std::isfinite(source_regions_.scalar_flux_new(sr, g))) {
38,323,375!
303
        fatal_error("A source region scalar flux is not finite. "
304
                    "This indicates a numerical instability in the "
305
                    "simulation. Consider increasing ray density or adjusting "
306
                    "the source region mesh.");
307
      }
308
    }
309
  }
310

311
  // Return the number of source regions that were hit this iteration
312
  return n_hits;
15,302✔
313
}
314

315
// Generates new estimate of k_eff based on the differences between this
316
// iteration's estimate of the scalar flux and the last iteration's estimate.
317
void FlatSourceDomain::compute_k_eff()
5,610✔
318
{
319
  double fission_rate_old = 0;
5,610✔
320
  double fission_rate_new = 0;
5,610✔
321

322
  // Vector for gathering fission source terms for Shannon entropy calculation
323
  vector<float> p(n_source_regions(), 0.0f);
5,610✔
324

325
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
3,060✔
326
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
388,740✔
327

328
    // If simulation averaged volume is zero, don't include this cell
329
    double volume = source_regions_.volume(sr);
386,190✔
330
    if (volume == 0.0) {
386,190!
331
      continue;
332
    }
333

334
    int material = source_regions_.material(sr);
386,190✔
335
    int temp = source_regions_.temperature_idx(sr);
386,190✔
336
    if (material == MATERIAL_VOID) {
386,190!
337
      continue;
338
    }
339

340
    double sr_fission_source_old = 0;
386,190✔
341
    double sr_fission_source_new = 0;
386,190✔
342

343
    for (int g = 0; g < negroups_; g++) {
2,986,920✔
344
      double nu_sigma_f =
345
        nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
2,600,730✔
346
        source_regions_.density_mult(sr);
2,600,730✔
347
      sr_fission_source_old +=
2,600,730✔
348
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,600,730✔
349
      sr_fission_source_new +=
2,600,730✔
350
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,600,730✔
351
    }
352

353
    // Compute total fission rates in FSR
354
    sr_fission_source_old *= volume;
386,190✔
355
    sr_fission_source_new *= volume;
386,190✔
356

357
    // Accumulate totals
358
    fission_rate_old += sr_fission_source_old;
386,190✔
359
    fission_rate_new += sr_fission_source_new;
386,190✔
360

361
    // Store total fission rate in the FSR for Shannon calculation
362
    p[sr] = sr_fission_source_new;
386,190✔
363
  }
364

365
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
5,610✔
366

367
  double H = 0.0;
5,610✔
368
  // defining an inverse sum for better performance
369
  double inverse_sum = 1 / fission_rate_new;
5,610✔
370

371
#pragma omp parallel for reduction(+ : H)
3,060✔
372
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
388,740✔
373
    // Only if FSR has non-negative and non-zero fission source
374
    if (p[sr] > 0.0f) {
386,190✔
375
      // Normalize to total weight of bank sites. p_i for better performance
376
      float p_i = p[sr] * inverse_sum;
164,195✔
377
      // Sum values to obtain Shannon entropy.
378
      H -= p_i * std::log2(p_i);
164,195✔
379
    }
380
  }
381

382
  // Adds entropy value to shared entropy vector in openmc namespace.
383
  simulation::entropy.push_back(H);
5,610✔
384

385
  fission_rate_ = fission_rate_new;
5,610✔
386
  k_eff_ = k_eff_new;
5,610✔
387
}
5,610✔
388

389
// This function is responsible for generating a mapping between random
390
// ray flat source regions (cell instances) and tally bins. The mapping
391
// takes the form of a "TallyTask" object, which accounts for one single
392
// score being applied to a single tally. Thus, a single source region
393
// may have anywhere from zero to many tally tasks associated with it ---
394
// meaning that the global "tally_task" data structure is in 2D. The outer
395
// dimension corresponds to the source element (i.e., each entry corresponds
396
// to a specific energy group within a specific source region), and the
397
// inner dimension corresponds to the tallying task itself. Mechanically,
398
// the mapping between FSRs and spatial filters is done by considering
399
// the location of a single known ray midpoint that passed through the
400
// FSR. I.e., during transport, the first ray to pass through a given FSR
401
// will write down its midpoint for use with this function. This is a cheap
402
// and easy way of mapping FSRs to spatial tally filters, but comes with
403
// the downside of adding the restriction that spatial tally filters must
404
// share boundaries with the physical geometry of the simulation (so as
405
// not to subdivide any FSR). It is acceptable for a spatial tally region
406
// to contain multiple FSRs, but not the other way around.
407

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

412
// Besides generating the mapping structure, this function also keeps track
413
// of whether or not all flat source regions have been hit yet. This is
414
// required, as there is no guarantee that all flat source regions will
415
// be hit every iteration, such that in the first few iterations some FSRs
416
// may not have a known position within them yet to facilitate mapping to
417
// spatial tally filters. However, after several iterations, if all FSRs
418
// have been hit and have had a tally map generated, then this status will
419
// be passed back to the caller to alert them that this function doesn't
420
// need to be called for the remainder of the simulation.
421

422
// It takes as an argument the starting index in the source region array,
423
// and it will operate from that index until the end of the array. This
424
// is useful as it can be called for both explicit user source regions or
425
// when a source region mesh is overlaid.
426

427
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
881✔
428
{
429
  openmc::simulation::time_tallies.start();
881✔
430

431
  // Tracks if we've generated a mapping yet for all source regions.
432
  bool all_source_regions_mapped = true;
881✔
433

434
// Attempt to generate mapping for all source regions
435
#pragma omp parallel for
481✔
436
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,030,060✔
437

438
    // If this source region has not been hit by a ray yet, then
439
    // we aren't going to be able to map it, so skip it.
440
    if (!source_regions_.position_recorded(sr)) {
1,029,660!
441
      all_source_regions_mapped = false;
442
      continue;
443
    }
444

445
    // A particle located at the recorded midpoint of a ray
446
    // crossing through this source region is used to estabilish
447
    // the spatial location of the source region
448
    Particle p;
1,029,660✔
449
    p.r() = source_regions_.position(sr);
1,029,660✔
450
    p.r_last() = source_regions_.position(sr);
1,029,660✔
451
    p.u() = {1.0, 0.0, 0.0};
1,029,660✔
452
    bool found = exhaustive_find_cell(p);
1,029,660✔
453

454
    // Loop over energy groups (so as to support energy filters)
455
    for (int g = 0; g < negroups_; g++) {
2,218,920✔
456

457
      // Set particle to the current energy
458
      p.g() = g;
1,189,260✔
459
      p.g_last() = g;
1,189,260✔
460
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,189,260✔
461
      p.E_last() = p.E();
1,189,260✔
462

463
      int64_t source_element = sr * negroups_ + g;
1,189,260✔
464

465
      // If this task has already been populated, we don't need to do
466
      // it again.
467
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,189,260!
468
        continue;
469
      }
470

471
      // Loop over all active tallies. This logic is essentially identical
472
      // to what happens when scanning for applicable tallies during
473
      // MC transport.
474
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
4,517,440✔
475
        Tally& tally {*model::tallies[i_tally]};
3,328,180✔
476

477
        // Initialize an iterator over valid filter bin combinations.
478
        // If there are no valid combinations, use a continue statement
479
        // to ensure we skip the assume_separate break below.
480
        auto filter_iter = FilterBinIter(tally, p);
3,328,180✔
481
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,328,180✔
482
        if (filter_iter == end)
3,328,180✔
483
          continue;
2,001,680✔
484

485
        // Loop over filter bins.
486
        for (; filter_iter != end; ++filter_iter) {
2,653,000✔
487
          auto filter_index = filter_iter.index_;
1,326,500✔
488
          auto filter_weight = filter_iter.weight_;
1,326,500✔
489

490
          // Loop over scores
491
          for (int score = 0; score < tally.scores_.size(); score++) {
3,010,560✔
492
            auto score_bin = tally.scores_[score];
1,684,060✔
493
            // If a valid tally, filter, and score combination has been found,
494
            // then add it to the list of tally tasks for this source element.
495
            TallyTask task(i_tally, filter_index, score, score_bin);
1,684,060✔
496
            source_regions_.tally_task(sr, g).push_back(task);
1,684,060✔
497

498
            // Also add this task to the list of volume tasks for this source
499
            // region.
500
            source_regions_.volume_task(sr).insert(task);
1,684,060✔
501
          }
502
        }
503
      }
504
      // Reset all the filter matches for the next tally event.
505
      for (auto& match : p.filter_matches())
4,986,620✔
506
        match.bins_present_ = false;
3,797,360✔
507
    }
508
  }
1,029,660✔
509
  openmc::simulation::time_tallies.stop();
881✔
510

511
  mapped_all_tallies_ = all_source_regions_mapped;
881✔
512
}
881✔
513

514
// Set the volume accumulators to zero for all tallies
515
void FlatSourceDomain::reset_tally_volumes()
7,101✔
516
{
517
  if (volume_normalized_flux_tallies_) {
7,101✔
518
#pragma omp parallel for
3,090✔
519
    for (int i = 0; i < tally_volumes_.size(); i++) {
8,300✔
520
      auto& tensor = tally_volumes_[i];
5,725✔
521
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
5,725✔
522
    }
523
  }
524
}
7,101✔
525

526
// In fixed source mode, due to the way that volumetric fixed sources are
527
// converted and applied as volumetric sources in one or more source regions,
528
// we need to perform an additional normalization step to ensure that the
529
// reported scalar fluxes are in units per source neutron. This allows for
530
// direct comparison of reported tallies to Monte Carlo flux results.
531
// This factor needs to be computed at each iteration, as it is based on the
532
// volume estimate of each FSR, which improves over the course of the
533
// simulation
534
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
7,978✔
535
{
536
  // Eigenvalue mode normalization
537
  if (settings::run_mode == RunMode::EIGENVALUE) {
7,978✔
538
    // Normalize fluxes by total number of fission neutrons produced. This
539
    // ensures consistent scaling of the eigenvector such that its magnitude is
540
    // comparable to the eigenvector produced by the Monte Carlo solver.
541
    // Multiplying by the eigenvalue is unintuitive, but it is necessary.
542
    // If the eigenvalue is 1.2, per starting source neutron, you will
543
    // generate 1.2 neutrons. Thus if we normalize to generating only ONE
544
    // neutron in total for the whole domain, then we don't actually have enough
545
    // flux to generate the required 1.2 neutrons. We only know the flux
546
    // required to generate 1 neutron (which would have required less than one
547
    // starting neutron). Thus, you have to scale the flux up by the eigenvalue
548
    // such that 1.2 neutrons are generated, so as to be consistent with the
549
    // bookkeeping in MC which is all done per starting source neutron (not per
550
    // neutron produced).
551
    return k_eff_ / (fission_rate_ * simulation_volume_);
3,695✔
552
  }
553

554
  // If we are in adjoint mode of a fixed source problem, the external
555
  // source is already normalized, such that all resulting fluxes are
556
  // also normalized.
557
  if (adjoint_) {
4,283✔
558
    return 1.0;
398✔
559
  }
560

561
  // Fixed source mode normalization
562

563
  // Step 1 is to sum over all source regions and energy groups to get the
564
  // total external source strength in the simulation.
565
  double simulation_external_source_strength = 0.0;
3,885✔
566
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,128✔
567
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,678,157✔
568
    int material = source_regions_.material(sr);
14,676,400✔
569
    int temp = source_regions_.temperature_idx(sr);
14,676,400✔
570
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,676,400✔
571
    for (int g = 0; g < negroups_; g++) {
29,895,680✔
572
      // For non-void regions, we store the external source pre-divided by
573
      // sigma_t. We need to multiply non-void regions back up by sigma_t
574
      // to get the total source strength in the expected units.
575
      double sigma_t = 1.0;
15,219,280✔
576
      if (material != MATERIAL_VOID) {
15,219,280✔
577
        sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
30,018,080✔
578
                  source_regions_.density_mult(sr);
15,009,040✔
579
      }
580
      simulation_external_source_strength +=
15,219,280✔
581
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,219,280✔
582
    }
583
  }
584

585
  // Step 2 is to determine the total user-specified external source strength
586
  double user_external_source_strength = 0.0;
3,885✔
587
  for (auto& ext_source : model::external_sources) {
7,770✔
588
    user_external_source_strength += ext_source->strength();
3,885✔
589
  }
590

591
  // The correction factor is the ratio of the user-specified external source
592
  // strength to the simulation external source strength.
593
  double source_normalization_factor =
3,885✔
594
    user_external_source_strength / simulation_external_source_strength;
595

596
  return source_normalization_factor;
3,885✔
597
}
598

599
// Tallying in random ray is not done directly during transport, rather,
600
// it is done only once after each power iteration. This is made possible
601
// by way of a mapping data structure that relates spatial source regions
602
// (FSRs) to tally/filter/score combinations. The mechanism by which the
603
// mapping is done (and the limitations incurred) is documented in the
604
// "convert_source_regions_to_tallies()" function comments above. The present
605
// tally function simply traverses the mapping data structure and executes
606
// the scoring operations to OpenMC's native tally result arrays.
607

608
void FlatSourceDomain::random_ray_tally()
7,101✔
609
{
610
  openmc::simulation::time_tallies.start();
7,101✔
611

612
  // Reset our tally volumes to zero
613
  reset_tally_volumes();
7,101✔
614

615
  double source_normalization_factor =
616
    compute_fixed_source_normalization_factor();
7,101✔
617

618
// We loop over all source regions and energy groups. For each
619
// element, we check if there are any scores needed and apply
620
// them.
621
#pragma omp parallel for
3,876✔
622
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
15,349,025✔
623
    // The fsr.volume_ is the unitless fractional simulation averaged volume
624
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
625
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
626
    // entire global simulation domain (as defined by the ray source box).
627
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
628
    // its fraction of the total volume by the total volume. Not important in
629
    // eigenvalue solves, but useful in fixed source solves for returning the
630
    // flux shape with a magnitude that makes sense relative to the fixed
631
    // source strength.
632
    double volume = source_regions_.volume(sr) * simulation_volume_;
15,345,800✔
633

634
    double material = source_regions_.material(sr);
15,345,800✔
635
    int temp = source_regions_.temperature_idx(sr);
15,345,800✔
636
    double density_mult = source_regions_.density_mult(sr);
15,345,800✔
637
    for (int g = 0; g < negroups_; g++) {
32,372,200✔
638
      double flux =
639
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
17,026,400✔
640

641
      // Determine numerical score value
642
      for (auto& task : source_regions_.tally_task(sr, g)) {
40,703,000✔
643
        double score = 0.0;
23,676,600✔
644
        switch (task.score_type) {
23,676,600!
645

646
        case SCORE_FLUX:
19,828,800✔
647
          score = flux * volume;
19,828,800✔
648
          break;
19,828,800✔
649

650
        case SCORE_TOTAL:
651
          if (material != MATERIAL_VOID) {
×
652
            score =
653
              flux * volume *
654
              sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
655
              density_mult;
656
          }
657
          break;
658

659
        case SCORE_FISSION:
1,923,600✔
660
          if (material != MATERIAL_VOID) {
1,923,600!
661
            score =
1,923,600✔
662
              flux * volume *
1,923,600✔
663
              sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,923,600✔
664
              density_mult;
665
          }
666
          break;
1,923,600✔
667

668
        case SCORE_NU_FISSION:
1,923,600✔
669
          if (material != MATERIAL_VOID) {
1,923,600!
670
            score =
1,923,600✔
671
              flux * volume *
1,923,600✔
672
              nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,923,600✔
673
              density_mult;
674
          }
675
          break;
1,923,600✔
676

677
        case SCORE_EVENTS:
678
          score = 1.0;
679
          break;
680

681
        case SCORE_KAPPA_FISSION:
600✔
682
          score =
600✔
683
            flux * volume *
1,200✔
684
            kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] *
600✔
685
            density_mult;
686
          break;
600✔
687

688
        default:
689
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
690
                      "total, fission, nu-fission, kappa-fission, and events "
691
                      "are supported in random ray mode.");
692
          break;
693
        }
694
        // Apply score to the appropriate tally bin
695
        Tally& tally {*model::tallies[task.tally_idx]};
23,676,600✔
696
#pragma omp atomic
697
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
23,676,600✔
698
          score;
699
      }
700
    }
701

702
    // For flux tallies, the total volume of the spatial region is needed
703
    // for normalizing the flux. We store this volume in a separate tensor.
704
    // We only contribute to each volume tally bin once per FSR.
705
    if (volume_normalized_flux_tallies_) {
15,345,800✔
706
      for (const auto& task : source_regions_.volume_task(sr)) {
36,356,400✔
707
        if (task.score_type == SCORE_FLUX) {
21,157,800✔
708
#pragma omp atomic
709
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
18,960,600✔
710
            volume;
711
        }
712
      }
713
    }
714
  } // end FSR loop
715

716
  // Normalize any flux scores by the total volume of the FSRs scoring to that
717
  // bin. To do this, we loop over all tallies, and then all filter bins,
718
  // and then scores. For each score, we check the tally data structure to
719
  // see what index that score corresponds to. If that score is a flux score,
720
  // then we divide it by volume.
721
  if (volume_normalized_flux_tallies_) {
7,101✔
722
    for (int i = 0; i < model::tallies.size(); i++) {
18,260✔
723
      Tally& tally {*model::tallies[i]};
12,595✔
724
#pragma omp parallel for
6,870✔
725
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
707,125✔
726
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,425,300✔
727
          auto score_type = tally.scores_[score_idx];
723,900✔
728
          if (score_type == SCORE_FLUX) {
723,900✔
729
            double vol = tally_volumes_[i](bin, score_idx);
701,400✔
730
            if (vol > 0.0) {
701,400!
731
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
701,400✔
732
            }
733
          }
734
        }
735
      }
736
    }
737
  }
738

739
  openmc::simulation::time_tallies.stop();
7,101✔
740
}
7,101✔
741

742
double FlatSourceDomain::evaluate_flux_at_point(
×
743
  Position r, int64_t sr, int g) const
744
{
745
  return source_regions_.scalar_flux_final(sr, g) /
×
UNCOV
746
         (settings::n_batches - settings::n_inactive);
×
747
}
748

749
// Outputs all basic material, FSR ID, multigroup flux, and
750
// fission source data to .vtk file that can be directly
751
// loaded and displayed by Paraview. Note that .vtk binary
752
// files require big endian byte ordering, so endianness
753
// is checked and flipped if necessary.
UNCOV
754
void FlatSourceDomain::output_to_vtk() const
×
755
{
756
  // Rename .h5 plot filename(s) to .vtk filenames
UNCOV
757
  for (int p = 0; p < model::plots.size(); p++) {
×
758
    PlottableInterface* plot = model::plots[p].get();
×
759
    plot->path_plot() =
×
UNCOV
760
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
761
  }
762

763
  // Print header information
764
  print_plot();
×
765

766
  // Outer loop over plots
767
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
768

769
    // Get handle to OpenMC plot object and extract params
770
    Plot* openmc_plot = dynamic_cast<Plot*>(model::plots[plt].get());
×
771

772
    // Random ray plots only support voxel plots
773
    if (!openmc_plot) {
×
774
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
775
                          "is allowed in random ray mode.",
776
        plt));
777
      continue;
×
778
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
779
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
780
                          "is allowed in random ray mode.",
781
        plt));
782
      continue;
×
783
    }
784

785
    int Nx = openmc_plot->pixels_[0];
×
786
    int Ny = openmc_plot->pixels_[1];
×
UNCOV
787
    int Nz = openmc_plot->pixels_[2];
×
788
    Position origin = openmc_plot->origin_;
×
789
    Position width = openmc_plot->width_;
×
UNCOV
790
    Position ll = origin - width / 2.0;
×
UNCOV
791
    double x_delta = width.x / Nx;
×
UNCOV
792
    double y_delta = width.y / Ny;
×
793
    double z_delta = width.z / Nz;
×
794
    std::string filename = openmc_plot->path_plot();
×
795

796
    // Perform sanity checks on file size
UNCOV
797
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
UNCOV
798
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
UNCOV
799
      openmc_plot->id(), filename, bytes / 1.0e6);
×
UNCOV
800
    if (bytes / 1.0e9 > 1.0) {
×
UNCOV
801
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
802
              "slow.");
UNCOV
803
    } else if (bytes / 1.0e9 > 100.0) {
×
UNCOV
804
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
805
    }
806

807
    // Relate voxel spatial locations to random ray source regions
UNCOV
808
    vector<int> voxel_indices(Nx * Ny * Nz);
×
UNCOV
809
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
UNCOV
810
    vector<double> weight_windows(Nx * Ny * Nz);
×
UNCOV
811
    float min_weight = 1e20;
×
812
#pragma omp parallel for collapse(3) reduction(min : min_weight)
813
    for (int z = 0; z < Nz; z++) {
×
814
      for (int y = 0; y < Ny; y++) {
×
815
        for (int x = 0; x < Nx; x++) {
×
816
          Position sample;
817
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
818
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
819
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
820
          Particle p;
×
821
          p.r() = sample;
822
          p.r_last() = sample;
823
          p.E() = 1.0;
824
          p.E_last() = 1.0;
825
          p.u() = {1.0, 0.0, 0.0};
826

827
          bool found = exhaustive_find_cell(p);
×
828
          if (!found) {
×
829
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
830
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
831
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
832
            continue;
833
          }
834

835
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
836
          int64_t sr = -1;
837
          auto it = source_region_map_.find(sr_key);
×
838
          if (it != source_region_map_.end()) {
×
839
            sr = it->second;
840
          }
841

842
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
843
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
844

845
          if (variance_reduction::weight_windows.size() == 1) {
×
846
            WeightWindow ww =
847
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
848
            float weight = ww.lower_weight;
849
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
850
            if (weight < min_weight)
×
851
              min_weight = weight;
852
          }
853
        }
×
854
      }
855
    }
856

857
    double source_normalization_factor =
858
      compute_fixed_source_normalization_factor();
×
859

860
    // Open file for writing
861
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
862

863
    // Write vtk metadata
864
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
865
    std::fprintf(plot, "Dataset File\n");
×
866
    std::fprintf(plot, "BINARY\n");
×
867
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
868
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
869
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
870
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
871
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
872

873
    int64_t num_neg = 0;
×
874
    int64_t num_samples = 0;
×
UNCOV
875
    float min_flux = 0.0;
×
876
    float max_flux = -1.0e20;
×
877
    // Plot multigroup flux data
878
    for (int g = 0; g < negroups_; g++) {
×
879
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
UNCOV
880
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
881
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
882
        int64_t fsr = voxel_indices[i];
×
883
        int64_t source_element = fsr * negroups_ + g;
×
884
        float flux = 0;
×
885
        if (fsr >= 0) {
×
886
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
UNCOV
887
          if (flux < 0.0)
×
UNCOV
888
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
UNCOV
889
              voxel_positions[i], fsr, g);
×
890
        }
UNCOV
891
        if (flux < 0.0) {
×
UNCOV
892
          num_neg++;
×
893
          if (flux < min_flux) {
×
894
            min_flux = flux;
×
895
          }
896
        }
UNCOV
897
        if (flux > max_flux)
×
UNCOV
898
          max_flux = flux;
×
UNCOV
899
        num_samples++;
×
900
        flux = convert_to_big_endian<float>(flux);
×
901
        std::fwrite(&flux, sizeof(float), 1, plot);
×
902
      }
903
    }
904

905
    // Slightly negative fluxes can be normal when sampling corners of linear
906
    // source regions. However, very common and high magnitude negative fluxes
907
    // may indicate numerical instability.
UNCOV
908
    if (num_neg > 0) {
×
909
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
910
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
911
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
912
    }
913

914
    // Plot FSRs
915
    std::fprintf(plot, "SCALARS FSRs float\n");
×
916
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
917
    for (int fsr : voxel_indices) {
×
UNCOV
918
      float value = future_prn(10, fsr);
×
UNCOV
919
      value = convert_to_big_endian<float>(value);
×
920
      std::fwrite(&value, sizeof(float), 1, plot);
×
921
    }
922

923
    // Plot Materials
924
    std::fprintf(plot, "SCALARS Materials int\n");
×
925
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
926
    for (int fsr : voxel_indices) {
×
927
      int mat = -1;
×
928
      if (fsr >= 0)
×
929
        mat = source_regions_.material(fsr);
×
930
      mat = convert_to_big_endian<int>(mat);
×
931
      std::fwrite(&mat, sizeof(int), 1, plot);
×
932
    }
933

934
    // Plot fission source
UNCOV
935
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
UNCOV
936
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
UNCOV
937
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
938
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
939
        int64_t fsr = voxel_indices[i];
×
UNCOV
940
        float total_fission = 0.0;
×
UNCOV
941
        if (fsr >= 0) {
×
942
          int mat = source_regions_.material(fsr);
×
943
          int temp = source_regions_.temperature_idx(fsr);
×
944
          if (mat != MATERIAL_VOID) {
×
945
            for (int g = 0; g < negroups_; g++) {
×
946
              int64_t source_element = fsr * negroups_ + g;
×
947
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
948
              double sigma_f =
949
                sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
UNCOV
950
                source_regions_.density_mult(fsr);
×
UNCOV
951
              total_fission += sigma_f * flux;
×
952
            }
953
          }
954
        }
955
        total_fission = convert_to_big_endian<float>(total_fission);
×
UNCOV
956
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
957
      }
958
    } else {
UNCOV
959
      std::fprintf(plot, "SCALARS external_source float\n");
×
960
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
961
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
UNCOV
962
        int64_t fsr = voxel_indices[i];
×
UNCOV
963
        int mat = source_regions_.material(fsr);
×
UNCOV
964
        int temp = source_regions_.temperature_idx(fsr);
×
UNCOV
965
        float total_external = 0.0f;
×
966
        if (fsr >= 0) {
×
967
          for (int g = 0; g < negroups_; g++) {
×
968
            // External sources are already divided by sigma_t, so we need to
969
            // multiply it back to get the true external source.
970
            double sigma_t = 1.0;
×
971
            if (mat != MATERIAL_VOID) {
×
972
              sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
973
                        source_regions_.density_mult(fsr);
×
974
            }
UNCOV
975
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
976
          }
977
        }
978
        total_external = convert_to_big_endian<float>(total_external);
×
979
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
980
      }
981
    }
982

983
    // Plot weight window data
UNCOV
984
    if (variance_reduction::weight_windows.size() == 1) {
×
UNCOV
985
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
UNCOV
986
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
987
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
UNCOV
988
        float weight = weight_windows[i];
×
UNCOV
989
        if (weight == 0.0)
×
UNCOV
990
          weight = min_weight;
×
UNCOV
991
        weight = convert_to_big_endian<float>(weight);
×
UNCOV
992
        std::fwrite(&weight, sizeof(float), 1, plot);
×
993
      }
994
    }
995

UNCOV
996
    std::fclose(plot);
×
UNCOV
997
  }
×
UNCOV
998
}
×
999

1000
void FlatSourceDomain::apply_external_source_to_source_region(
4,588✔
1001
  int src_idx, SourceRegionHandle& srh)
1002
{
1003
  auto s = model::external_sources[src_idx].get();
4,588✔
1004
  auto is = dynamic_cast<IndependentSource*>(s);
4,588!
1005
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,588!
1006
  double strength_factor = is->strength();
4,588✔
1007
  const auto& discrete_energies = discrete->x();
4,588✔
1008
  const auto& discrete_probs = discrete->prob();
4,588✔
1009

1010
  srh.external_source_present() = 1;
4,588✔
1011

1012
  for (int i = 0; i < discrete_energies.size(); i++) {
9,308✔
1013
    int g = data::mg.get_group_index(discrete_energies[i]);
4,720✔
1014
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,720✔
1015
  }
1016
}
4,588✔
1017

1018
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
432✔
1019
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1020
{
1021
  Cell& cell = *model::cells[i_cell];
432✔
1022

1023
  if (cell.type_ != Fill::MATERIAL)
432!
UNCOV
1024
    return;
×
1025

1026
  for (int j : instances) {
30,928✔
1027
    int cell_material_idx = cell.material(j);
30,496✔
1028
    int cell_material_id;
1029
    if (cell_material_idx == MATERIAL_VOID) {
30,496✔
1030
      cell_material_id = MATERIAL_VOID;
256✔
1031
    } else {
1032
      cell_material_id = model::materials[cell_material_idx]->id();
30,240✔
1033
    }
1034
    if (target_material_id == C_NONE ||
30,496✔
1035
        cell_material_id == target_material_id) {
1036
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,976✔
1037
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,976✔
1038
    }
1039
  }
1040
}
1041

1042
void FlatSourceDomain::apply_external_source_to_cell_and_children(
464✔
1043
  int32_t i_cell, int src_idx, int32_t target_material_id)
1044
{
1045
  Cell& cell = *model::cells[i_cell];
464✔
1046

1047
  if (cell.type_ == Fill::MATERIAL) {
464✔
1048
    vector<int> instances(cell.n_instances());
432✔
1049
    std::iota(instances.begin(), instances.end(), 0);
432✔
1050
    apply_external_source_to_cell_instances(
432✔
1051
      i_cell, src_idx, target_material_id, instances);
1052
  } else if (target_material_id == C_NONE) {
464!
1053
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
UNCOV
1054
      cell.get_contained_cells(0, nullptr);
×
UNCOV
1055
    for (const auto& pair : cell_instance_list) {
×
UNCOV
1056
      int32_t i_child_cell = pair.first;
×
UNCOV
1057
      apply_external_source_to_cell_instances(
×
UNCOV
1058
        i_child_cell, src_idx, target_material_id, pair.second);
×
1059
    }
UNCOV
1060
  }
×
1061
}
464✔
1062

1063
void FlatSourceDomain::count_external_source_regions()
1,294✔
1064
{
1065
  n_external_source_regions_ = 0;
1,294✔
1066
#pragma omp parallel for reduction(+ : n_external_source_regions_)
729✔
1067
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,186,965✔
1068
    if (source_regions_.external_source_present(sr)) {
1,186,400✔
1069
      n_external_source_regions_++;
157,250✔
1070
    }
1071
  }
1072
}
1,294✔
1073

1074
void FlatSourceDomain::convert_external_sources()
417✔
1075
{
1076
  // Loop over external sources
1077
  for (int es = 0; es < model::external_sources.size(); es++) {
834✔
1078

1079
    // Extract source information
1080
    Source* s = model::external_sources[es].get();
417✔
1081
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
417!
1082
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
417!
1083
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
417✔
1084
    double strength_factor = is->strength();
417✔
1085

1086
    // If there is no domain constraint specified, then this must be a point
1087
    // source. In this case, we need to find the source region that contains the
1088
    // point source and apply or relate it to the external source.
1089
    if (is->domain_ids().size() == 0) {
417✔
1090

1091
      // Extract the point source coordinate and find the base source region at
1092
      // that point
1093
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
17!
1094
      GeometryState gs;
17✔
1095
      gs.r() = sp->r();
17✔
1096
      gs.r_last() = sp->r();
17✔
1097
      gs.u() = {1.0, 0.0, 0.0};
17✔
1098
      bool found = exhaustive_find_cell(gs);
17✔
1099
      if (!found) {
17!
UNCOV
1100
        fatal_error(fmt::format("Could not find cell containing external "
×
1101
                                "point source at {}",
UNCOV
1102
          sp->r()));
×
1103
      }
1104
      SourceRegionKey key = lookup_source_region_key(gs);
17✔
1105

1106
      // With the source region and mesh bin known, we can use the
1107
      // accompanying SourceRegionKey as a key into a map that stores the
1108
      // corresponding external source index for the point source. Notably, we
1109
      // do not actually apply the external source to any source regions here,
1110
      // as if mesh subdivision is enabled, they haven't actually been
1111
      // discovered & initilized yet. When discovered, they will read from the
1112
      // external_source_map to determine if there are any external source
1113
      // terms that should be applied.
1114
      external_point_source_map_[key].push_back(es);
17✔
1115

1116
    } else {
17✔
1117
      // If not a point source, then use the volumetric domain constraints to
1118
      // determine which source regions to apply the external source to.
1119
      if (is->domain_type() == Source::DomainType::MATERIAL) {
400✔
1120
        for (int32_t material_id : domain_ids) {
32✔
1121
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
96✔
1122
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
80✔
1123
          }
1124
        }
1125
      } else if (is->domain_type() == Source::DomainType::CELL) {
384✔
1126
        for (int32_t cell_id : domain_ids) {
32✔
1127
          int32_t i_cell = model::cell_map[cell_id];
16✔
1128
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
16✔
1129
        }
1130
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
368!
1131
        for (int32_t universe_id : domain_ids) {
736✔
1132
          int32_t i_universe = model::universe_map[universe_id];
368✔
1133
          Universe& universe = *model::universes[i_universe];
368✔
1134
          for (int32_t i_cell : universe.cells_) {
736✔
1135
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
368✔
1136
          }
1137
        }
1138
      }
1139
    }
1140
  } // End loop over external sources
1141
}
417✔
1142

1143
void FlatSourceDomain::flux_swap()
15,302✔
1144
{
1145
  source_regions_.flux_swap();
15,302✔
1146
}
15,302✔
1147

1148
void FlatSourceDomain::flatten_xs()
796✔
1149
{
1150
  // Temperature and angle indices, if using multiple temperature
1151
  // data sets and/or anisotropic data sets.
1152
  // TODO: Currently assumes we are only using single angle data.
1153
  const int a = 0;
796✔
1154

1155
  n_materials_ = data::mg.macro_xs_.size();
796✔
1156
  ntemperature_ = 1;
796✔
1157
  for (int i = 0; i < n_materials_; i++) {
2,979✔
1158
    ntemperature_ =
2,183✔
1159
      std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points());
2,183✔
1160
  }
1161

1162
  for (int i = 0; i < n_materials_; i++) {
2,979✔
1163
    auto& m = data::mg.macro_xs_[i];
2,183✔
1164
    for (int t = 0; t < ntemperature_; t++) {
4,398✔
1165
      for (int g_out = 0; g_out < negroups_; g_out++) {
11,763✔
1166
        if (m.exists_in_model && t < m.n_temperature_points()) {
9,548✔
1167
          double sigma_t =
1168
            m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
9,372✔
1169
          sigma_t_.push_back(sigma_t);
9,372✔
1170

1171
          if (sigma_t < MINIMUM_MACRO_XS) {
9,372✔
1172
            Material* mat = model::materials[i].get();
16✔
1173
            warning(fmt::format(
16✔
1174
              "Material \"{}\" (id: {}) has a group {} total cross section "
1175
              "({:.3e}) below the minimum threshold "
1176
              "({:.3e}). Material will be treated as pure void.",
1177
              mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
32✔
1178
          }
1179

1180
          double nu_sigma_f =
1181
            m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
9,372✔
1182
          nu_sigma_f_.push_back(nu_sigma_f);
9,372✔
1183

1184
          double sigma_f =
1185
            m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
9,372✔
1186
          sigma_f_.push_back(sigma_f);
9,372✔
1187

1188
          double chi =
1189
            m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
9,372✔
1190
          if (!std::isfinite(chi)) {
9,372✔
1191
            // MGXS interface may return NaN in some cases, such as when
1192
            // material is fissionable but has very small sigma_f.
1193
            chi = 0.0;
704✔
1194
          }
1195
          chi_.push_back(chi);
9,372✔
1196

1197
          double kappa_fission =
1198
            m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a);
9,372✔
1199
          kappa_fission_.push_back(kappa_fission);
9,372✔
1200

1201
          for (int g_in = 0; g_in < negroups_; g_in++) {
277,238✔
1202
            double sigma_s =
1203
              m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
267,866✔
1204
            sigma_s_.push_back(sigma_s);
267,866✔
1205
            // For transport corrected XS data, diagonal elements may be
1206
            // negative. In this case, set a flag to enable transport
1207
            // stabilization for the simulation.
1208
            if (g_out == g_in && sigma_s < 0.0)
267,866✔
1209
              is_transport_stabilization_needed_ = true;
880✔
1210
          }
1211
        } else {
1212
          sigma_t_.push_back(0);
176✔
1213
          nu_sigma_f_.push_back(0);
176✔
1214
          sigma_f_.push_back(0);
176✔
1215
          chi_.push_back(0);
176✔
1216
          kappa_fission_.push_back(0);
176✔
1217
          for (int g_in = 0; g_in < negroups_; g_in++) {
1,024✔
1218
            sigma_s_.push_back(0);
848✔
1219
          }
1220
        }
1221
      }
1222
    }
1223
  }
1224
}
796✔
1225

1226
void FlatSourceDomain::set_adjoint_sources()
65✔
1227
{
1228
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1229
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1230
  // as this is likely a very small source region that we don't need to bother
1231
  // trying to vector particles towards. In the case of flux "being extremely
1232
  // close to zero", we define this as being a fixed fraction of the maximum
1233
  // forward flux, below which we assume the flux would be physically
1234
  // undetectable.
1235

1236
  // First, find the maximum forward flux value
1237
  double max_flux = 0.0;
65✔
1238
#pragma omp parallel for reduction(max : max_flux)
37✔
1239
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,548✔
1240
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1241
    if (flux > max_flux) {
155,520✔
1242
      max_flux = flux;
25✔
1243
    }
1244
  }
1245

1246
  // Then, compute the adjoint source for each source region
1247
#pragma omp parallel for
37✔
1248
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1249
    for (int g = 0; g < negroups_; g++) {
311,040✔
1250
      double flux = source_regions_.scalar_flux_final(sr, g);
155,520✔
1251
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
155,520✔
1252
        source_regions_.external_source(sr, g) = 0.0;
325✔
1253
      } else {
1254
        source_regions_.external_source(sr, g) = 1.0 / flux;
155,195✔
1255
      }
1256
      if (flux > 0.0) {
155,520✔
1257
        source_regions_.external_source_present(sr) = 1;
155,195✔
1258
      }
1259
      source_regions_.scalar_flux_final(sr, g) = 0.0;
155,520✔
1260
    }
1261
  }
1262

1263
  // "Small" source regions in OpenMC are defined as those that are hit by
1264
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1265
  // very small volumes combined with a low aspect ratio, and are often
1266
  // generated when applying a source region mesh that clips the edge of a
1267
  // curved surface. As perhaps only a few rays will visit these regions over
1268
  // the entire forward simulation, the forward flux estimates are extremely
1269
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1270
  // extremely low, leading to unphysically large adjoint source terms,
1271
  // resulting in weight windows that aggressively try to drive particles
1272
  // towards these regions. To fix this, we simply filter out any "small" source
1273
  // regions from consideration. If a source region is "small", we
1274
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1275
  // flux solution, as the true total adjoint source contribution from small
1276
  // regions is likely to be negligible.
1277
#pragma omp parallel for
37✔
1278
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1279
    if (source_regions_.is_small(sr)) {
155,520!
1280
      for (int g = 0; g < negroups_; g++) {
×
1281
        source_regions_.external_source(sr, g) = 0.0;
1282
      }
1283
      source_regions_.external_source_present(sr) = 0;
1284
    }
1285
  }
1286
  // Divide the fixed source term by sigma t (to save time when applying each
1287
  // iteration)
1288
#pragma omp parallel for
37✔
1289
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1290
    int material = source_regions_.material(sr);
155,520✔
1291
    int temp = source_regions_.temperature_idx(sr);
155,520✔
1292
    if (material == MATERIAL_VOID) {
155,520!
1293
      continue;
1294
    }
1295
    for (int g = 0; g < negroups_; g++) {
311,040✔
1296
      double sigma_t =
1297
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
155,520✔
1298
        source_regions_.density_mult(sr);
155,520✔
1299
      source_regions_.external_source(sr, g) /= sigma_t;
155,520✔
1300
    }
1301
  }
1302
}
65✔
1303

1304
void FlatSourceDomain::transpose_scattering_matrix()
81✔
1305
{
1306
  // Transpose the inner two dimensions for each material
1307
#pragma omp parallel for
46✔
1308
  for (int m = 0; m < n_materials_; ++m) {
133✔
1309
    for (int t = 0; t < ntemperature_; t++) {
196✔
1310
      int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_;
98✔
1311
      for (int i = 0; i < negroups_; ++i) {
280✔
1312
        for (int j = i + 1; j < negroups_; ++j) {
476✔
1313
          // Calculate indices of the elements to swap
1314
          int idx1 = material_offset + i * negroups_ + j;
294✔
1315
          int idx2 = material_offset + j * negroups_ + i;
294✔
1316

1317
          // Swap the elements to transpose the matrix
1318
          std::swap(sigma_s_[idx1], sigma_s_[idx2]);
294✔
1319
        }
1320
      }
1321
    }
1322
  }
1323
}
81✔
1324

1325
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1326
{
1327
  // Ensure array is correct size
UNCOV
1328
  flux.resize(n_source_regions() * negroups_);
×
1329
// Serialize the final fluxes for output
1330
#pragma omp parallel for
1331
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1332
    flux[se] = source_regions_.scalar_flux_final(se);
1333
  }
UNCOV
1334
}
×
1335

1336
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
1,090✔
1337
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1338
  bool is_target_void)
1339
{
1340
  Cell& cell = *model::cells[i_cell];
1,090✔
1341
  if (cell.type_ != Fill::MATERIAL)
1,090!
UNCOV
1342
    return;
×
1343
  for (int32_t j : instances) {
175,908✔
1344
    int cell_material_idx = cell.material(j);
174,818✔
1345
    int cell_material_id = (cell_material_idx == C_NONE)
1346
                             ? C_NONE
174,818✔
1347
                             : model::materials[cell_material_idx]->id();
174,817✔
1348

1349
    if ((target_material_id == C_NONE && !is_target_void) ||
174,818!
1350
        cell_material_id == target_material_id) {
1351
      int64_t sr = source_region_offsets_[i_cell] + j;
142,818✔
1352
      // Check if the key is already present in the mesh_map_
1353
      if (mesh_map_.find(sr) != mesh_map_.end()) {
142,818!
UNCOV
1354
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1355
                                "applied, but trying to apply mesh idx {}",
UNCOV
1356
          sr, mesh_map_[sr], mesh_idx));
×
1357
      }
1358
      // If the SR has not already been assigned, then we can write to it
1359
      mesh_map_[sr] = mesh_idx;
142,818✔
1360
    }
1361
  }
1362
}
1363

1364
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
961✔
1365
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1366
{
1367
  Cell& cell = *model::cells[i_cell];
961✔
1368

1369
  if (cell.type_ == Fill::MATERIAL) {
961✔
1370
    vector<int> instances(cell.n_instances());
832✔
1371
    std::iota(instances.begin(), instances.end(), 0);
832✔
1372
    apply_mesh_to_cell_instances(
832✔
1373
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1374
  } else if (target_material_id == C_NONE && !is_target_void) {
961!
1375
    for (int j = 0; j < cell.n_instances(); j++) {
130✔
1376
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1377
        cell.get_contained_cells(j, nullptr);
65✔
1378
      for (const auto& pair : cell_instance_list) {
323✔
1379
        int32_t i_child_cell = pair.first;
258✔
1380
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
258✔
1381
          pair.second, is_target_void);
258✔
1382
      }
1383
    }
65✔
1384
  }
1385
}
961✔
1386

1387
void FlatSourceDomain::apply_meshes()
796✔
1388
{
1389
  // Skip if there are no mappings between mesh IDs and domains
1390
  if (mesh_domain_map_.empty())
796✔
1391
    return;
475✔
1392

1393
  // Loop over meshes
1394
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
722✔
1395
    Mesh* mesh = model::meshes[mesh_idx].get();
401✔
1396
    int mesh_id = mesh->id();
401✔
1397

1398
    // Skip if mesh id is not present in the map
1399
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
401✔
1400
      continue;
16✔
1401

1402
    // Loop over domains associated with the mesh
1403
    for (auto& domain : mesh_domain_map_[mesh_id]) {
770✔
1404
      Source::DomainType domain_type = domain.first;
385✔
1405
      int domain_id = domain.second;
385✔
1406

1407
      if (domain_type == Source::DomainType::MATERIAL) {
385✔
1408
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1409
          if (domain_id == C_NONE) {
160!
UNCOV
1410
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1411
          } else {
1412
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1413
          }
1414
        }
1415
      } else if (domain_type == Source::DomainType::CELL) {
353✔
1416
        int32_t i_cell = model::cell_map[domain_id];
32✔
1417
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1418
      } else if (domain_type == Source::DomainType::UNIVERSE) {
321!
1419
        int32_t i_universe = model::universe_map[domain_id];
321✔
1420
        Universe& universe = *model::universes[i_universe];
321✔
1421
        for (int32_t i_cell : universe.cells_) {
1,090✔
1422
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
769✔
1423
        }
1424
      }
1425
    }
1426
  }
1427
}
1428

1429
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,401,572,013✔
1430
  SourceRegionKey sr_key, Position r, Direction u)
1431
{
1432
  // Case 1: Check if the source region key is already present in the permanent
1433
  // map. This is the most common condition, as any source region visited in a
1434
  // previous power iteration will already be present in the permanent map. If
1435
  // the source region key is found, we translate the key into a specific 1D
1436
  // source region index and return a handle its position in the
1437
  // source_regions_ vector.
1438
  auto it = source_region_map_.find(sr_key);
1,401,572,013✔
1439
  if (it != source_region_map_.end()) {
1,401,572,013✔
1440
    int64_t sr = it->second;
1,357,824,344✔
1441
    return source_regions_.get_source_region_handle(sr);
1,357,824,344✔
1442
  }
1443

1444
  // Case 2: Check if the source region key is present in the temporary (thread
1445
  // safe) map. This is a common occurrence in the first power iteration when
1446
  // the source region has already been visited already by some other ray. We
1447
  // begin by locking the temporary map before any operations are performed. The
1448
  // lock is not global over the full data structure -- it will be dependent on
1449
  // which key is used.
1450
  discovered_source_regions_.lock(sr_key);
43,747,669✔
1451

1452
  // If the key is found in the temporary map, then we return a handle to the
1453
  // source region that is stored in the temporary map.
1454
  if (discovered_source_regions_.contains(sr_key)) {
43,747,669✔
1455
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
41,380,800✔
1456
    discovered_source_regions_.unlock(sr_key);
41,380,800✔
1457
    return handle;
41,380,800✔
1458
  }
1459

1460
  // Case 3: The source region key is not present anywhere, but it is only due
1461
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1462
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1463
  // result in the ray tracer detecting an additional (very short) segment
1464
  // though a mesh bin that is actually past the physical source region
1465
  // boundary. This is a result of the the multi-level ray tracing treatment in
1466
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1467
  // result in the wrong surface being selected as the nearest. This can happen
1468
  // in a lattice when there are two directions that both are very close in
1469
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1470
  // treated as being equivalent so alternative logic is used. However, when we
1471
  // go and ray trace on this with the mesh tracer we may go past the surface
1472
  // bounding the current source region.
1473
  //
1474
  // To filter out this case, before we create the new source region, we double
1475
  // check that the actual starting point of this segment (r) is still in the
1476
  // same geometry source region that we started in. If an artifact is detected,
1477
  // we discard the segment (and attenuation through it) as it is not really a
1478
  // valid source region and will have only an infinitessimally small cell
1479
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1480
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1481
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1482
  // consequences for the general ray tracer, so for now we do the below sanity
1483
  // checks before generating phantom source regions. A significant extra cost
1484
  // is incurred in instantiating the GeometryState object and doing a cell
1485
  // lookup, but again, this is going to be an extremely rare thing to check
1486
  // after the first power iteration has completed.
1487

1488
  // Sanity check on source region id
1489
  GeometryState gs;
2,366,869✔
1490
  gs.r() = r + TINY_BIT * u;
2,366,869✔
1491
  gs.u() = {1.0, 0.0, 0.0};
2,366,869✔
1492
  exhaustive_find_cell(gs);
2,366,869✔
1493
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,366,869✔
1494
  if (sr_found != sr_key.base_source_region_id) {
2,366,869✔
1495
    discovered_source_regions_.unlock(sr_key);
88✔
1496
    SourceRegionHandle handle;
88✔
1497
    handle.is_numerical_fp_artifact_ = true;
88✔
1498
    return handle;
88✔
1499
  }
1500

1501
  // Sanity check on mesh bin
1502
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,366,781✔
1503
  if (mesh_idx == C_NONE) {
2,366,781✔
1504
    if (sr_key.mesh_bin != 0) {
393,756!
UNCOV
1505
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1506
      SourceRegionHandle handle;
×
UNCOV
1507
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1508
      return handle;
×
1509
    }
1510
  } else {
1511
    Mesh* mesh = model::meshes[mesh_idx].get();
1,973,025✔
1512
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,973,025✔
1513
    if (bin_found != sr_key.mesh_bin) {
1,973,025!
UNCOV
1514
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1515
      SourceRegionHandle handle;
×
UNCOV
1516
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1517
      return handle;
×
1518
    }
1519
  }
1520

1521
  // Case 4: The source region key is valid, but is not present anywhere. This
1522
  // condition only occurs the first time the source region is discovered
1523
  // (typically in the first power iteration). In this case, we need to handle
1524
  // creation of the new source region and its storage into the parallel map.
1525
  // Additionally, we need to determine the source region's material, initialize
1526
  // the starting scalar flux guess, and apply any known external sources.
1527

1528
  // Call the basic constructor for the source region and store in the parallel
1529
  // map.
1530
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,366,781✔
1531
  SourceRegion* sr_ptr =
1532
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
2,366,781✔
1533
  SourceRegionHandle handle {*sr_ptr};
2,366,781✔
1534

1535
  // Determine the material
1536
  int gs_i_cell = gs.lowest_coord().cell();
2,366,781✔
1537
  Cell& cell = *model::cells[gs_i_cell];
2,366,781✔
1538
  int material = cell.material(gs.cell_instance());
2,366,781✔
1539
  int temp = 0;
2,366,781✔
1540

1541
  // If material total XS is extremely low, just set it to void to avoid
1542
  // problems with 1/Sigma_t
1543
  if (material != MATERIAL_VOID) {
2,366,781✔
1544
    temp = data::mg.macro_xs_[material].get_temperature_index(
4,689,434✔
1545
      cell.sqrtkT(gs.cell_instance()));
2,344,717✔
1546
    for (int g = 0; g < negroups_; g++) {
5,040,555✔
1547
      double sigma_t =
1548
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g];
2,695,926✔
1549
      if (sigma_t < MINIMUM_MACRO_XS) {
2,695,926✔
1550
        material = MATERIAL_VOID;
88✔
1551
        temp = 0;
88✔
1552
        break;
88✔
1553
      }
1554
    }
1555
  }
1556

1557
  handle.material() = material;
2,366,781✔
1558
  handle.temperature_idx() = temp;
2,366,781✔
1559

1560
  handle.density_mult() = cell.density_mult(gs.cell_instance());
2,366,781✔
1561

1562
  // Store the mesh index (if any) assigned to this source region
1563
  handle.mesh() = mesh_idx;
2,366,781✔
1564

1565
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,366,781✔
1566
    // Determine if there are any volumetric sources, and apply them.
1567
    // Volumetric sources are specifc only to the base SR idx.
1568
    auto it_vol =
1569
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,310,758✔
1570
    if (it_vol != external_volumetric_source_map_.end()) {
2,310,758✔
1571
      const vector<int>& vol_sources = it_vol->second;
4,576✔
1572
      for (int src_idx : vol_sources) {
9,152✔
1573
        apply_external_source_to_source_region(src_idx, handle);
4,576✔
1574
      }
1575
    }
1576

1577
    // Determine if there are any point sources, and apply them.
1578
    // Point sources are specific to the source region key.
1579
    auto it_point = external_point_source_map_.find(sr_key);
2,310,758✔
1580
    if (it_point != external_point_source_map_.end()) {
2,310,758✔
1581
      const vector<int>& point_sources = it_point->second;
12✔
1582
      for (int src_idx : point_sources) {
24✔
1583
        apply_external_source_to_source_region(src_idx, handle);
12✔
1584
      }
1585
    }
1586

1587
    // Divide external source term by sigma_t
1588
    if (material != C_NONE) {
2,310,758✔
1589
      for (int g = 0; g < negroups_; g++) {
4,623,171✔
1590
        double sigma_t =
1591
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,334,565✔
1592
          handle.density_mult();
2,334,565✔
1593
        handle.external_source(g) /= sigma_t;
2,334,565✔
1594
      }
1595
    }
1596
  }
1597

1598
  // Compute the combined source term
1599
  update_single_neutron_source(handle);
2,366,781✔
1600

1601
  // Unlock the parallel map. Note: we may be tempted to release
1602
  // this lock earlier, and then just use the source region's lock to protect
1603
  // the flux/source initialization stages above. However, the rest of the code
1604
  // only protects updates to the new flux and volume fields, and assumes that
1605
  // the source is constant for the duration of transport. Thus, using just the
1606
  // source region's lock by itself would result in other threads potentially
1607
  // reading from the source before it is computed, as they won't use the lock
1608
  // when only reading from the SR's source. It would be expensive to protect
1609
  // those operations, whereas generating the SR is only done once, so we just
1610
  // hold the map's bucket lock until the source region is fully initialized.
1611
  discovered_source_regions_.unlock(sr_key);
2,366,781✔
1612

1613
  return handle;
2,366,781✔
1614
}
2,366,869✔
1615

1616
void FlatSourceDomain::finalize_discovered_source_regions()
15,302✔
1617
{
1618
  // Extract keys for entries with a valid volume.
1619
  vector<SourceRegionKey> keys;
15,302✔
1620
  for (const auto& pair : discovered_source_regions_) {
2,382,083✔
1621
    if (pair.second.volume_ > 0.0) {
2,366,781✔
1622
      keys.push_back(pair.first);
2,265,339✔
1623
    }
1624
  }
1625

1626
  if (!keys.empty()) {
15,302✔
1627
    // Sort the keys, so as to ensure reproducible ordering given that source
1628
    // regions may have been added to discovered_source_regions_ in an arbitrary
1629
    // order due to shared memory threading.
1630
    std::sort(keys.begin(), keys.end());
881✔
1631

1632
    // Remember the index of the first new source region
1633
    int64_t start_sr_id = source_regions_.n_source_regions();
881✔
1634

1635
    // Append the source regions in the sorted key order.
1636
    for (const auto& key : keys) {
2,266,220✔
1637
      const SourceRegion& sr = discovered_source_regions_[key];
2,265,339✔
1638
      source_region_map_[key] = source_regions_.n_source_regions();
2,265,339✔
1639
      source_regions_.push_back(sr);
2,265,339✔
1640
    }
1641

1642
    // Map all new source regions to tallies
1643
    convert_source_regions_to_tallies(start_sr_id);
881✔
1644
  }
1645

1646
  discovered_source_regions_.clear();
15,302✔
1647
}
15,302✔
1648

1649
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1650
//
1651
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1652
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1653
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1654
// https://doi.org/10.1016/j.anucene.2018.10.036.
1655
void FlatSourceDomain::apply_transport_stabilization()
15,302✔
1656
{
1657
  // Don't do anything if all in-group scattering
1658
  // cross sections are positive
1659
  if (!is_transport_stabilization_needed_) {
15,302✔
1660
    return;
15,082✔
1661
  }
1662

1663
  // Apply the stabilization factor to all source elements
1664
#pragma omp parallel for
120✔
1665
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,300✔
1666
    int material = source_regions_.material(sr);
1,200✔
1667
    int temp = source_regions_.temperature_idx(sr);
1,200✔
1668
    double density_mult = source_regions_.density_mult(sr);
1,200✔
1669
    if (material == MATERIAL_VOID) {
1,200!
1670
      continue;
1671
    }
1672
    for (int g = 0; g < negroups_; g++) {
85,200✔
1673
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1674
      // negative
1675
      double sigma_s =
1676
        sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) *
84,000✔
1677
                   negroups_ +
84,000✔
1678
                 g] *
84,000✔
1679
        density_mult;
84,000✔
1680
      if (sigma_s < 0.0) {
84,000✔
1681
        double sigma_t =
1682
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
22,000✔
1683
          density_mult;
22,000✔
1684
        double phi_new = source_regions_.scalar_flux_new(sr, g);
22,000✔
1685
        double phi_old = source_regions_.scalar_flux_old(sr, g);
22,000✔
1686

1687
        // Equation 18 in the above Gunow et al. 2019 paper. For a default
1688
        // rho of 1.0, this ensures there are no negative diagonal elements
1689
        // in the iteration matrix. A lesser rho could be used (or exposed
1690
        // as a user input parameter) to reduce the negative impact on
1691
        // convergence rate though would need to be experimentally tested to see
1692
        // if it doesn't become unstable. rho = 1.0 is good as it gives the
1693
        // highest assurance of stability, and the impacts on convergence rate
1694
        // are pretty mild.
1695
        double D = diagonal_stabilization_rho_ * sigma_s / sigma_t;
22,000✔
1696

1697
        // Equation 16 in the above Gunow et al. 2019 paper
1698
        source_regions_.scalar_flux_new(sr, g) =
22,000✔
1699
          (phi_new - D * phi_old) / (1.0 - D);
22,000✔
1700
      }
1701
    }
1702
  }
1703
}
1704

1705
// Determines the base source region index (i.e., a material filled cell
1706
// instance) that corresponds to a particular location in the geometry. Requires
1707
// that the "gs" object passed in has already been initialized and has called
1708
// find_cell etc.
1709
int64_t FlatSourceDomain::lookup_base_source_region_idx(
942,975,357✔
1710
  const GeometryState& gs) const
1711
{
1712
  int i_cell = gs.lowest_coord().cell();
942,975,357✔
1713
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
942,975,357✔
1714
  return sr;
942,975,357✔
1715
}
1716

1717
// Determines the index of the mesh (if any) that has been applied
1718
// to a particular base source region index.
1719
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
942,975,269✔
1720
{
1721
  int mesh_idx = C_NONE;
942,975,269✔
1722
  auto mesh_it = mesh_map_.find(sr);
942,975,269✔
1723
  if (mesh_it != mesh_map_.end()) {
942,975,269✔
1724
    mesh_idx = mesh_it->second;
477,517,804✔
1725
  }
1726
  return mesh_idx;
942,975,269✔
1727
}
1728

1729
// Determines the source region key that corresponds to a particular location in
1730
// the geometry. This takes into account both the base source region index as
1731
// well as the mesh bin if a mesh is applied to this source region for
1732
// subdivision.
1733
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
2,119,337✔
1734
  const GeometryState& gs) const
1735
{
1736
  int64_t sr = lookup_base_source_region_idx(gs);
2,119,337✔
1737
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
2,119,337✔
1738
  return SourceRegionKey {sr, mesh_bin};
2,119,337✔
1739
}
1740

1741
// Determines the mesh bin that corresponds to a particular base source region
1742
// index and position.
1743
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
2,119,337✔
1744
{
1745
  int mesh_idx = lookup_mesh_idx(sr);
2,119,337✔
1746
  int mesh_bin = 0;
2,119,337✔
1747
  if (mesh_idx != C_NONE) {
2,119,337✔
1748
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
1,255,837✔
1749
  }
1750
  return mesh_bin;
2,119,337✔
1751
}
1752

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