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

openmc-dev / openmc / 21686031975

04 Feb 2026 07:50PM UTC coverage: 81.024% (-0.7%) from 81.763%
21686031975

Pull #3755

github

web-flow
Merge 27d6053a4 into b41e22f68
Pull Request #3755: Warn users that tally heating score with photon bin but without electron and positron bins.

16378 of 22828 branches covered (71.75%)

Branch coverage included in aggregate %.

22 of 23 new or added lines in 1 file covered. (95.65%)

862 existing lines in 51 files now uncovered.

54491 of 64639 relevant lines covered (84.3%)

8259986.93 hits per line

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

68.37
/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_)
90✔
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;
90✔
44
  for (const auto& c : model::cells) {
714✔
45
    if (c->type_ != Fill::MATERIAL) {
624✔
46
      source_region_offsets_.push_back(-1);
282✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
342✔
49
      base_source_regions += c->n_instances();
342✔
50
    }
51
  }
52

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

57
  // Initialize tally volumes
58
  if (volume_normalized_flux_tallies_) {
90✔
59
    tally_volumes_.resize(model::tallies.size());
60✔
60
    for (int i = 0; i < model::tallies.size(); i++) {
218✔
61
      //  Get the shape of the 3D result tensor
62
      auto shape = model::tallies[i]->results().shape();
158✔
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] =
158✔
67
        xt::xtensor<double, 2>::from_shape({shape[0], shape[1]});
316✔
68
    }
69
  }
70

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

79
void FlatSourceDomain::batch_reset()
1,150✔
80
{
81
// Reset scalar fluxes and iteration volume tallies to zero
82
#pragma omp parallel for
83
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
6,482,913✔
84
    source_regions_.volume(sr) = 0.0;
6,481,763✔
85
    source_regions_.volume_sq(sr) = 0.0;
6,481,763✔
86
  }
87

88
#pragma omp parallel for
89
  for (int64_t se = 0; se < n_source_elements(); se++) {
7,345,089✔
90
    source_regions_.scalar_flux_new(se) = 0.0;
7,343,939✔
91
  }
92
}
1,150✔
93

94
void FlatSourceDomain::accumulate_iteration_flux()
475✔
95
{
96
#pragma omp parallel for
97
  for (int64_t se = 0; se < n_source_elements(); se++) {
3,346,455✔
98
    source_regions_.scalar_flux_final(se) +=
3,345,980✔
99
      source_regions_.scalar_flux_new(se);
3,345,980✔
100
  }
101
}
475✔
102

103
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
4,002,102✔
104
{
105
  // Reset all source regions to zero (important for void regions)
106
  for (int g = 0; g < negroups_; g++) {
8,497,758✔
107
    srh.source(g) = 0.0;
4,495,656✔
108
  }
109

110
  // Add scattering + fission source
111
  int material = srh.material();
4,002,102✔
112
  double density_mult = srh.density_mult();
4,002,102✔
113
  if (material != MATERIAL_VOID) {
4,002,102✔
114
    double inverse_k_eff = 1.0 / k_eff_;
3,962,022✔
115
    for (int g_out = 0; g_out < negroups_; g_out++) {
8,417,598✔
116
      double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult;
4,455,576✔
117
      double scatter_source = 0.0;
4,455,576✔
118
      double fission_source = 0.0;
4,455,576✔
119

120
      for (int g_in = 0; g_in < negroups_; g_in++) {
12,366,030✔
121
        double scalar_flux = srh.scalar_flux_old(g_in);
7,910,454✔
122
        double sigma_s = sigma_s_[material * negroups_ * negroups_ +
7,910,454✔
123
                                  g_out * negroups_ + g_in] *
7,910,454✔
124
                         density_mult;
7,910,454✔
125
        double nu_sigma_f =
126
          nu_sigma_f_[material * negroups_ + g_in] * density_mult;
7,910,454✔
127
        double chi = chi_[material * negroups_ + g_out];
7,910,454✔
128

129
        scatter_source += sigma_s * scalar_flux;
7,910,454✔
130
        if (settings::create_fission_neutrons) {
7,910,454!
131
          fission_source += nu_sigma_f * scalar_flux * chi;
7,910,454✔
132
        }
133
      }
134
      srh.source(g_out) =
4,455,576✔
135
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
4,455,576✔
136
    }
137
  }
138

139
  // Add external source if in fixed source mode
140
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
4,002,102✔
141
    for (int g = 0; g < negroups_; g++) {
8,177,446✔
142
      srh.source(g) += srh.external_source(g);
4,219,223✔
143
    }
144
  }
145
}
4,002,102✔
146

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

153
#pragma omp parallel for
154
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
6,482,913✔
155
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
6,481,763✔
156
    update_single_neutron_source(srh);
6,481,763✔
157
  }
158

159
  simulation::time_update_src.stop();
1,150✔
160
}
1,150✔
161

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

170
// Normalize scalar flux to total distance travelled by all rays this
171
// iteration
172
#pragma omp parallel for
173
  for (int64_t se = 0; se < n_source_elements(); se++) {
4,489,937✔
174
    source_regions_.scalar_flux_new(se) *= normalization_factor;
4,489,442✔
175
  }
176

177
// Accumulate cell-wise ray length tallies collected this iteration, then
178
// update the simulation-averaged cell-wise volume estimates
179
#pragma omp parallel for
180
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
3,996,389✔
181
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
3,995,894✔
182
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
3,995,894✔
183
    source_regions_.volume_naive(sr) =
7,991,788✔
184
      source_regions_.volume(sr) * normalization_factor;
3,995,894✔
185
    source_regions_.volume_sq(sr) =
7,991,788✔
186
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
3,995,894✔
187
    source_regions_.volume(sr) =
3,995,894✔
188
      source_regions_.volume_t(sr) * volume_normalization_factor;
3,995,894✔
189
  }
190
}
495✔
191

192
void FlatSourceDomain::set_flux_to_flux_plus_source(
3,980,735✔
193
  int64_t sr, double volume, int g)
194
{
195
  int material = source_regions_.material(sr);
3,980,735✔
196
  if (material == MATERIAL_VOID) {
3,980,735✔
197
    source_regions_.scalar_flux_new(sr, g) /= volume;
80,080✔
198
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
80,080!
199
      source_regions_.scalar_flux_new(sr, g) +=
80,080✔
200
        0.5f * source_regions_.external_source(sr, g) *
80,080✔
201
        source_regions_.volume_sq(sr);
80,080✔
202
    }
203
  } else {
204
    double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g] *
3,900,655✔
205
                     source_regions_.density_mult(sr);
3,900,655✔
206
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
3,900,655✔
207
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
3,900,655✔
208
  }
209
}
3,980,735✔
210

211
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
140✔
212
{
213
  source_regions_.scalar_flux_new(sr, g) =
280✔
214
    source_regions_.scalar_flux_old(sr, g);
140✔
215
}
140✔
216

217
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
811,595✔
218
{
219
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
811,595✔
220
}
811,595✔
221

222
// Combine transport flux contributions and flat source contributions from the
223
// previous iteration to generate this iteration's estimate of scalar flux.
224
int64_t FlatSourceDomain::add_source_to_scalar_flux()
1,150✔
225
{
226
  int64_t n_hits = 0;
1,150✔
227
  double inverse_batch = 1.0 / simulation::current_batch;
1,150✔
228

229
#pragma omp parallel for reduction(+ : n_hits)
230
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
6,688,517✔
231

232
    double volume_simulation_avg = source_regions_.volume(sr);
6,687,367✔
233
    double volume_iteration = source_regions_.volume_naive(sr);
6,687,367✔
234

235
    // Increment the number of hits if cell was hit this iteration
236
    if (volume_iteration) {
6,687,367✔
237
      n_hits++;
5,875,536✔
238
    }
239

240
    // Set the SR to small status if its expected number of hits
241
    // per iteration is less than 1.5
242
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
6,687,367✔
243
      source_regions_.is_small(sr) = 1;
1,362,541✔
244
    } else {
245
      source_regions_.is_small(sr) = 0;
5,324,826✔
246
    }
247

248
    // The volume treatment depends on the volume estimator type
249
    // and whether or not an external source is present in the cell.
250
    double volume;
251
    switch (volume_estimator_) {
6,687,367!
252
    case RandomRayVolumeEstimator::NAIVE:
155,520✔
253
      volume = volume_iteration;
155,520✔
254
      break;
155,520✔
255
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
86,400✔
256
      volume = volume_simulation_avg;
86,400✔
257
      break;
86,400✔
258
    case RandomRayVolumeEstimator::HYBRID:
6,445,447✔
259
      if (source_regions_.external_source_present(sr) ||
12,053,214✔
260
          source_regions_.is_small(sr)) {
5,607,767✔
261
        volume = volume_iteration;
2,200,145✔
262
      } else {
263
        volume = volume_simulation_avg;
4,245,302✔
264
      }
265
      break;
6,445,447✔
UNCOV
266
    default:
×
UNCOV
267
      fatal_error("Invalid volume estimator type");
×
268
    }
269

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

307
  // Return the number of source regions that were hit this iteration
308
  return n_hits;
1,150✔
309
}
310

311
// Generates new estimate of k_eff based on the differences between this
312
// iteration's estimate of the scalar flux and the last iteration's estimate.
313
void FlatSourceDomain::compute_k_eff()
270✔
314
{
315
  double fission_rate_old = 0;
270✔
316
  double fission_rate_new = 0;
270✔
317

318
  // Vector for gathering fission source terms for Shannon entropy calculation
319
  vector<float> p(n_source_regions(), 0.0f);
270✔
320

321
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
322
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
65,108✔
323

324
    // If simulation averaged volume is zero, don't include this cell
325
    double volume = source_regions_.volume(sr);
64,838✔
326
    if (volume == 0.0) {
64,838!
UNCOV
327
      continue;
×
328
    }
329

330
    int material = source_regions_.material(sr);
64,838✔
331
    if (material == MATERIAL_VOID) {
64,838!
UNCOV
332
      continue;
×
333
    }
334

335
    double sr_fission_source_old = 0;
64,838✔
336
    double sr_fission_source_new = 0;
64,838✔
337

338
    for (int g = 0; g < negroups_; g++) {
499,984✔
339
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g] *
435,146✔
340
                          source_regions_.density_mult(sr);
435,146✔
341
      sr_fission_source_old +=
435,146✔
342
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
435,146✔
343
      sr_fission_source_new +=
435,146✔
344
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
435,146✔
345
    }
346

347
    // Compute total fission rates in FSR
348
    sr_fission_source_old *= volume;
64,838✔
349
    sr_fission_source_new *= volume;
64,838✔
350

351
    // Accumulate totals
352
    fission_rate_old += sr_fission_source_old;
64,838✔
353
    fission_rate_new += sr_fission_source_new;
64,838✔
354

355
    // Store total fission rate in the FSR for Shannon calculation
356
    p[sr] = sr_fission_source_new;
64,838✔
357
  }
358

359
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
270✔
360

361
  double H = 0.0;
270✔
362
  // defining an inverse sum for better performance
363
  double inverse_sum = 1 / fission_rate_new;
270✔
364

365
#pragma omp parallel for reduction(+ : H)
366
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
65,108✔
367
    // Only if FSR has non-negative and non-zero fission source
368
    if (p[sr] > 0.0f) {
64,838✔
369
      // Normalize to total weight of bank sites. p_i for better performance
370
      float p_i = p[sr] * inverse_sum;
27,199✔
371
      // Sum values to obtain Shannon entropy.
372
      H -= p_i * std::log2(p_i);
27,199✔
373
    }
374
  }
375

376
  // Adds entropy value to shared entropy vector in openmc namespace.
377
  simulation::entropy.push_back(H);
270✔
378

379
  fission_rate_ = fission_rate_new;
270✔
380
  k_eff_ = k_eff_new;
270✔
381
}
270✔
382

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

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

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

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

421
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
75✔
422
{
423
  openmc::simulation::time_tallies.start();
75✔
424

425
  // Tracks if we've generated a mapping yet for all source regions.
426
  bool all_source_regions_mapped = true;
75✔
427

428
// Attempt to generate mapping for all source regions
429
#pragma omp parallel for
430
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
205,679✔
431

432
    // If this source region has not been hit by a ray yet, then
433
    // we aren't going to be able to map it, so skip it.
434
    if (!source_regions_.position_recorded(sr)) {
205,604!
UNCOV
435
      all_source_regions_mapped = false;
×
UNCOV
436
      continue;
×
437
    }
438

439
    // A particle located at the recorded midpoint of a ray
440
    // crossing through this source region is used to estabilish
441
    // the spatial location of the source region
442
    Particle p;
205,604✔
443
    p.r() = source_regions_.position(sr);
205,604✔
444
    p.r_last() = source_regions_.position(sr);
205,604✔
445
    p.u() = {1.0, 0.0, 0.0};
205,604✔
446
    bool found = exhaustive_find_cell(p);
205,604✔
447

448
    // Loop over energy groups (so as to support energy filters)
449
    for (int g = 0; g < negroups_; g++) {
441,340✔
450

451
      // Set particle to the current energy
452
      p.g() = g;
235,736✔
453
      p.g_last() = g;
235,736✔
454
      p.E() = data::mg.energy_bin_avg_[p.g()];
235,736✔
455
      p.E_last() = p.E();
235,736✔
456

457
      int64_t source_element = sr * negroups_ + g;
235,736✔
458

459
      // If this task has already been populated, we don't need to do
460
      // it again.
461
      if (source_regions_.tally_task(sr, g).size() > 0) {
235,736!
UNCOV
462
        continue;
×
463
      }
464

465
      // Loop over all active tallies. This logic is essentially identical
466
      // to what happens when scanning for applicable tallies during
467
      // MC transport.
468
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
899,328✔
469
        Tally& tally {*model::tallies[i_tally]};
663,592✔
470

471
        // Initialize an iterator over valid filter bin combinations.
472
        // If there are no valid combinations, use a continue statement
473
        // to ensure we skip the assume_separate break below.
474
        auto filter_iter = FilterBinIter(tally, p);
663,592✔
475
        auto end = FilterBinIter(tally, true, &p.filter_matches());
663,592✔
476
        if (filter_iter == end)
663,592✔
477
          continue;
400,336✔
478

479
        // Loop over filter bins.
480
        for (; filter_iter != end; ++filter_iter) {
526,512✔
481
          auto filter_index = filter_iter.index_;
263,256✔
482
          auto filter_weight = filter_iter.weight_;
263,256✔
483

484
          // Loop over scores
485
          for (int score = 0; score < tally.scores_.size(); score++) {
593,936✔
486
            auto score_bin = tally.scores_[score];
330,680✔
487
            // If a valid tally, filter, and score combination has been found,
488
            // then add it to the list of tally tasks for this source element.
489
            TallyTask task(i_tally, filter_index, score, score_bin);
330,680✔
490
            source_regions_.tally_task(sr, g).push_back(task);
330,680✔
491

492
            // Also add this task to the list of volume tasks for this source
493
            // region.
494
            source_regions_.volume_task(sr).insert(task);
330,680✔
495
          }
496
        }
497
      }
498
      // Reset all the filter matches for the next tally event.
499
      for (auto& match : p.filter_matches())
991,792✔
500
        match.bins_present_ = false;
756,056✔
501
    }
502
  }
205,604✔
503
  openmc::simulation::time_tallies.stop();
75✔
504

505
  mapped_all_tallies_ = all_source_regions_mapped;
75✔
506
}
75✔
507

508
// Set the volume accumulators to zero for all tallies
509
void FlatSourceDomain::reset_tally_volumes()
475✔
510
{
511
  if (volume_normalized_flux_tallies_) {
475✔
512
#pragma omp parallel for
513
    for (int i = 0; i < tally_volumes_.size(); i++) {
1,350✔
514
      auto& tensor = tally_volumes_[i];
990✔
515
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
990✔
516
    }
517
  }
518
}
475✔
519

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

548
  // If we are in adjoint mode of a fixed source problem, the external
549
  // source is already normalized, such that all resulting fluxes are
550
  // also normalized.
551
  if (adjoint_) {
405✔
552
    return 1.0;
38✔
553
  }
554

555
  // Fixed source mode normalization
556

557
  // Step 1 is to sum over all source regions and energy groups to get the
558
  // total external source strength in the simulation.
559
  double simulation_external_source_strength = 0.0;
367✔
560
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
561
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
2,935,647✔
562
    int material = source_regions_.material(sr);
2,935,280✔
563
    double volume = source_regions_.volume(sr) * simulation_volume_;
2,935,280✔
564
    for (int g = 0; g < negroups_; g++) {
5,979,136✔
565
      // For non-void regions, we store the external source pre-divided by
566
      // sigma_t. We need to multiply non-void regions back up by sigma_t
567
      // to get the total source strength in the expected units.
568
      double sigma_t = 1.0;
3,043,856✔
569
      if (material != MATERIAL_VOID) {
3,043,856✔
570
        sigma_t =
3,001,808✔
571
          sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr);
3,001,808✔
572
      }
573
      simulation_external_source_strength +=
3,043,856✔
574
        source_regions_.external_source(sr, g) * sigma_t * volume;
3,043,856✔
575
    }
576
  }
577

578
  // Step 2 is to determine the total user-specified external source strength
579
  double user_external_source_strength = 0.0;
367✔
580
  for (auto& ext_source : model::external_sources) {
734✔
581
    user_external_source_strength += ext_source->strength();
367✔
582
  }
583

584
  // The correction factor is the ratio of the user-specified external source
585
  // strength to the simulation external source strength.
586
  double source_normalization_factor =
367✔
587
    user_external_source_strength / simulation_external_source_strength;
588

589
  return source_normalization_factor;
367✔
590
}
591

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

601
void FlatSourceDomain::random_ray_tally()
475✔
602
{
603
  openmc::simulation::time_tallies.start();
475✔
604

605
  // Reset our tally volumes to zero
606
  reset_tally_volumes();
475✔
607

608
  double source_normalization_factor =
609
    compute_fixed_source_normalization_factor();
475✔
610

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

627
    int material = source_regions_.material(sr);
3,060,560✔
628
    double density_mult = source_regions_.density_mult(sr);
3,060,560✔
629

630
    for (int g = 0; g < negroups_; g++) {
6,406,540✔
631
      double flux =
632
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
3,345,980✔
633

634
      // Determine numerical score value
635
      for (auto& task : source_regions_.tally_task(sr, g)) {
7,904,480✔
636
        double score = 0.0;
4,558,500✔
637
        switch (task.score_type) {
4,558,500!
638

639
        case SCORE_FLUX:
3,906,820✔
640
          score = flux * volume;
3,906,820✔
641
          break;
3,906,820✔
642

UNCOV
643
        case SCORE_TOTAL:
×
UNCOV
644
          if (material != MATERIAL_VOID) {
×
UNCOV
645
            score =
×
UNCOV
646
              flux * volume * sigma_t_[material * negroups_ + g] * density_mult;
×
647
          }
UNCOV
648
          break;
×
649

650
        case SCORE_FISSION:
325,780✔
651
          if (material != MATERIAL_VOID) {
325,780!
652
            score =
325,780✔
653
              flux * volume * sigma_f_[material * negroups_ + g] * density_mult;
325,780✔
654
          }
655
          break;
325,780✔
656

657
        case SCORE_NU_FISSION:
325,780✔
658
          if (material != MATERIAL_VOID) {
325,780!
659
            score = flux * volume * nu_sigma_f_[material * negroups_ + g] *
325,780✔
660
                    density_mult;
661
          }
662
          break;
325,780✔
663

UNCOV
664
        case SCORE_EVENTS:
×
UNCOV
665
          score = 1.0;
×
UNCOV
666
          break;
×
667

668
        case SCORE_KAPPA_FISSION:
120✔
669
          score = flux * volume * kappa_fission_[material * negroups_ + g] *
120✔
670
                  density_mult;
671
          break;
120✔
672

UNCOV
673
        default:
×
UNCOV
674
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
×
675
                      "total, fission, nu-fission, kappa-fission, and events "
676
                      "are supported in random ray mode.");
677
          break;
678
        }
679
        // Apply score to the appropriate tally bin
680
        Tally& tally {*model::tallies[task.tally_idx]};
4,558,500✔
681
#pragma omp atomic
682
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
4,558,500✔
683
          score;
684
      }
685
    }
686

687
    // For flux tallies, the total volume of the spatial region is needed
688
    // for normalizing the flux. We store this volume in a separate tensor.
689
    // We only contribute to each volume tally bin once per FSR.
690
    if (volume_normalized_flux_tallies_) {
3,060,560✔
691
      for (const auto& task : source_regions_.volume_task(sr)) {
7,215,640✔
692
        if (task.score_type == SCORE_FLUX) {
4,184,340✔
693
#pragma omp atomic
694
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
3,776,380✔
695
            volume;
696
        }
697
      }
698
    }
699
  } // end FSR loop
700

701
  // Normalize any flux scores by the total volume of the FSRs scoring to that
702
  // bin. To do this, we loop over all tallies, and then all filter bins,
703
  // and then scores. For each score, we check the tally data structure to
704
  // see what index that score corresponds to. If that score is a flux score,
705
  // then we divide it by volume.
706
  if (volume_normalized_flux_tallies_) {
475✔
707
    for (int i = 0; i < model::tallies.size(); i++) {
1,350✔
708
      Tally& tally {*model::tallies[i]};
990✔
709
#pragma omp parallel for
710
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
140,980✔
711
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
283,900✔
712
          auto score_type = tally.scores_[score_idx];
143,910✔
713
          if (score_type == SCORE_FLUX) {
143,910✔
714
            double vol = tally_volumes_[i](bin, score_idx);
139,990✔
715
            if (vol > 0.0) {
139,990!
716
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
139,990✔
717
            }
718
          }
719
        }
720
      }
721
    }
722
  }
723

724
  openmc::simulation::time_tallies.stop();
475✔
725
}
475✔
726

727
double FlatSourceDomain::evaluate_flux_at_point(
×
728
  Position r, int64_t sr, int g) const
729
{
730
  return source_regions_.scalar_flux_final(sr, g) /
×
731
         (settings::n_batches - settings::n_inactive);
×
732
}
733

734
// Outputs all basic material, FSR ID, multigroup flux, and
735
// fission source data to .vtk file that can be directly
736
// loaded and displayed by Paraview. Note that .vtk binary
737
// files require big endian byte ordering, so endianness
738
// is checked and flipped if necessary.
739
void FlatSourceDomain::output_to_vtk() const
×
740
{
741
  // Rename .h5 plot filename(s) to .vtk filenames
742
  for (int p = 0; p < model::plots.size(); p++) {
×
743
    PlottableInterface* plot = model::plots[p].get();
×
744
    plot->path_plot() =
×
745
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
746
  }
747

748
  // Print header information
749
  print_plot();
×
750

751
  // Outer loop over plots
752
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
753

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

757
    // Random ray plots only support voxel plots
758
    if (!openmc_plot) {
×
759
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
760
                          "is allowed in random ray mode.",
761
        plt));
762
      continue;
×
763
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
764
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
765
                          "is allowed in random ray mode.",
766
        plt));
767
      continue;
×
768
    }
769

770
    int Nx = openmc_plot->pixels_[0];
×
771
    int Ny = openmc_plot->pixels_[1];
×
772
    int Nz = openmc_plot->pixels_[2];
×
773
    Position origin = openmc_plot->origin_;
×
774
    Position width = openmc_plot->width_;
×
775
    Position ll = origin - width / 2.0;
×
776
    double x_delta = width.x / Nx;
×
777
    double y_delta = width.y / Ny;
×
778
    double z_delta = width.z / Nz;
×
779
    std::string filename = openmc_plot->path_plot();
×
780

781
    // Perform sanity checks on file size
782
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
783
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
784
      openmc_plot->id(), filename, bytes / 1.0e6);
×
785
    if (bytes / 1.0e9 > 1.0) {
×
786
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
787
              "slow.");
788
    } else if (bytes / 1.0e9 > 100.0) {
×
789
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
790
    }
791

792
    // Relate voxel spatial locations to random ray source regions
793
    vector<int> voxel_indices(Nx * Ny * Nz);
×
794
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
795
    vector<double> weight_windows(Nx * Ny * Nz);
×
796
    float min_weight = 1e20;
×
797
#pragma omp parallel for collapse(3) reduction(min : min_weight)
UNCOV
798
    for (int z = 0; z < Nz; z++) {
×
UNCOV
799
      for (int y = 0; y < Ny; y++) {
×
UNCOV
800
        for (int x = 0; x < Nx; x++) {
×
UNCOV
801
          Position sample;
×
UNCOV
802
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
×
UNCOV
803
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
×
UNCOV
804
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
×
UNCOV
805
          Particle p;
×
UNCOV
806
          p.r() = sample;
×
UNCOV
807
          p.r_last() = sample;
×
UNCOV
808
          p.E() = 1.0;
×
UNCOV
809
          p.E_last() = 1.0;
×
UNCOV
810
          p.u() = {1.0, 0.0, 0.0};
×
811

UNCOV
812
          bool found = exhaustive_find_cell(p);
×
UNCOV
813
          if (!found) {
×
UNCOV
814
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
×
UNCOV
815
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
×
UNCOV
816
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
×
UNCOV
817
            continue;
×
818
          }
819

UNCOV
820
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
UNCOV
821
          int64_t sr = -1;
×
UNCOV
822
          auto it = source_region_map_.find(sr_key);
×
UNCOV
823
          if (it != source_region_map_.end()) {
×
UNCOV
824
            sr = it->second;
×
825
          }
826

UNCOV
827
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
×
UNCOV
828
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
×
829

UNCOV
830
          if (variance_reduction::weight_windows.size() == 1) {
×
831
            WeightWindow ww =
UNCOV
832
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
UNCOV
833
            float weight = ww.lower_weight;
×
UNCOV
834
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
×
UNCOV
835
            if (weight < min_weight)
×
UNCOV
836
              min_weight = weight;
×
837
          }
UNCOV
838
        }
×
839
      }
840
    }
841

842
    double source_normalization_factor =
843
      compute_fixed_source_normalization_factor();
×
844

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

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

858
    int64_t num_neg = 0;
×
859
    int64_t num_samples = 0;
×
860
    float min_flux = 0.0;
×
861
    float max_flux = -1.0e20;
×
862
    // Plot multigroup flux data
863
    for (int g = 0; g < negroups_; g++) {
×
864
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
865
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
866
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
867
        int64_t fsr = voxel_indices[i];
×
868
        int64_t source_element = fsr * negroups_ + g;
×
869
        float flux = 0;
×
870
        if (fsr >= 0) {
×
871
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
872
          if (flux < 0.0)
×
873
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
874
              voxel_positions[i], fsr, g);
×
875
        }
876
        if (flux < 0.0) {
×
877
          num_neg++;
×
878
          if (flux < min_flux) {
×
879
            min_flux = flux;
×
880
          }
881
        }
882
        if (flux > max_flux)
×
883
          max_flux = flux;
×
884
        num_samples++;
×
885
        flux = convert_to_big_endian<float>(flux);
×
886
        std::fwrite(&flux, sizeof(float), 1, plot);
×
887
      }
888
    }
889

890
    // Slightly negative fluxes can be normal when sampling corners of linear
891
    // source regions. However, very common and high magnitude negative fluxes
892
    // may indicate numerical instability.
893
    if (num_neg > 0) {
×
894
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
895
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
896
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
897
    }
898

899
    // Plot FSRs
900
    std::fprintf(plot, "SCALARS FSRs float\n");
×
901
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
902
    for (int fsr : voxel_indices) {
×
903
      float value = future_prn(10, fsr);
×
904
      value = convert_to_big_endian<float>(value);
×
905
      std::fwrite(&value, sizeof(float), 1, plot);
×
906
    }
907

908
    // Plot Materials
909
    std::fprintf(plot, "SCALARS Materials int\n");
×
910
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
911
    for (int fsr : voxel_indices) {
×
912
      int mat = -1;
×
913
      if (fsr >= 0)
×
914
        mat = source_regions_.material(fsr);
×
915
      mat = convert_to_big_endian<int>(mat);
×
916
      std::fwrite(&mat, sizeof(int), 1, plot);
×
917
    }
918

919
    // Plot fission source
920
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
921
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
922
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
923
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
924
        int64_t fsr = voxel_indices[i];
×
925
        float total_fission = 0.0;
×
926
        if (fsr >= 0) {
×
927
          int mat = source_regions_.material(fsr);
×
928
          if (mat != MATERIAL_VOID) {
×
929
            for (int g = 0; g < negroups_; g++) {
×
930
              int64_t source_element = fsr * negroups_ + g;
×
931
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
932
              double sigma_f = sigma_f_[mat * negroups_ + g] *
×
933
                               source_regions_.density_mult(fsr);
×
934
              total_fission += sigma_f * flux;
×
935
            }
936
          }
937
        }
938
        total_fission = convert_to_big_endian<float>(total_fission);
×
939
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
940
      }
941
    } else {
942
      std::fprintf(plot, "SCALARS external_source float\n");
×
943
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
944
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
945
        int64_t fsr = voxel_indices[i];
×
946
        int mat = source_regions_.material(fsr);
×
947
        float total_external = 0.0f;
×
948
        if (fsr >= 0) {
×
949
          for (int g = 0; g < negroups_; g++) {
×
950
            // External sources are already divided by sigma_t, so we need to
951
            // multiply it back to get the true external source.
952
            double sigma_t = 1.0;
×
953
            if (mat != MATERIAL_VOID) {
×
954
              sigma_t = sigma_t_[mat * negroups_ + g] *
×
955
                        source_regions_.density_mult(fsr);
×
956
            }
957
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
958
          }
959
        }
960
        total_external = convert_to_big_endian<float>(total_external);
×
961
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
962
      }
963
    }
964

965
    // Plot weight window data
966
    if (variance_reduction::weight_windows.size() == 1) {
×
967
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
968
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
969
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
970
        float weight = weight_windows[i];
×
971
        if (weight == 0.0)
×
972
          weight = min_weight;
×
973
        weight = convert_to_big_endian<float>(weight);
×
974
        std::fwrite(&weight, sizeof(float), 1, plot);
×
975
      }
976
    }
977

978
    std::fclose(plot);
×
979
  }
×
980
}
×
981

982
void FlatSourceDomain::apply_external_source_to_source_region(
417✔
983
  int src_idx, SourceRegionHandle& srh)
984
{
985
  auto s = model::external_sources[src_idx].get();
417✔
986
  auto is = dynamic_cast<IndependentSource*>(s);
417!
987
  auto discrete = dynamic_cast<Discrete*>(is->energy());
417!
988
  double strength_factor = is->strength();
417✔
989
  const auto& discrete_energies = discrete->x();
417✔
990
  const auto& discrete_probs = discrete->prob();
417✔
991

992
  srh.external_source_present() = 1;
417✔
993

994
  for (int i = 0; i < discrete_energies.size(); i++) {
846✔
995
    int g = data::mg.get_group_index(discrete_energies[i]);
429✔
996
    srh.external_source(g) += discrete_probs[i] * strength_factor;
429✔
997
  }
998
}
417✔
999

1000
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
54✔
1001
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1002
{
1003
  Cell& cell = *model::cells[i_cell];
54✔
1004

1005
  if (cell.type_ != Fill::MATERIAL)
54!
1006
    return;
×
1007

1008
  for (int j : instances) {
3,866✔
1009
    int cell_material_idx = cell.material(j);
3,812✔
1010
    int cell_material_id;
1011
    if (cell_material_idx == MATERIAL_VOID) {
3,812✔
1012
      cell_material_id = MATERIAL_VOID;
32✔
1013
    } else {
1014
      cell_material_id = model::materials[cell_material_idx]->id();
3,780✔
1015
    }
1016
    if (target_material_id == C_NONE ||
3,812✔
1017
        cell_material_id == target_material_id) {
1018
      int64_t source_region = source_region_offsets_[i_cell] + j;
372✔
1019
      external_volumetric_source_map_[source_region].push_back(src_idx);
372✔
1020
    }
1021
  }
1022
}
1023

1024
void FlatSourceDomain::apply_external_source_to_cell_and_children(
58✔
1025
  int32_t i_cell, int src_idx, int32_t target_material_id)
1026
{
1027
  Cell& cell = *model::cells[i_cell];
58✔
1028

1029
  if (cell.type_ == Fill::MATERIAL) {
58✔
1030
    vector<int> instances(cell.n_instances());
54✔
1031
    std::iota(instances.begin(), instances.end(), 0);
54✔
1032
    apply_external_source_to_cell_instances(
54✔
1033
      i_cell, src_idx, target_material_id, instances);
1034
  } else if (target_material_id == C_NONE) {
58!
1035
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1036
      cell.get_contained_cells(0, nullptr);
×
1037
    for (const auto& pair : cell_instance_list) {
×
1038
      int32_t i_child_cell = pair.first;
×
1039
      apply_external_source_to_cell_instances(
×
1040
        i_child_cell, src_idx, target_material_id, pair.second);
×
1041
    }
1042
  }
×
1043
}
58✔
1044

1045
void FlatSourceDomain::count_external_source_regions()
152✔
1046
{
1047
  n_external_source_regions_ = 0;
152✔
1048
#pragma omp parallel for reduction(+ : n_external_source_regions_)
1049
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
237,104✔
1050
    if (source_regions_.external_source_present(sr)) {
236,952✔
1051
      n_external_source_regions_++;
31,450✔
1052
    }
1053
  }
1054
}
152✔
1055

1056
void FlatSourceDomain::convert_external_sources()
52✔
1057
{
1058
  // Loop over external sources
1059
  for (int es = 0; es < model::external_sources.size(); es++) {
104✔
1060

1061
    // Extract source information
1062
    Source* s = model::external_sources[es].get();
52✔
1063
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
52!
1064
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
52!
1065
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
52✔
1066
    double strength_factor = is->strength();
52✔
1067

1068
    // If there is no domain constraint specified, then this must be a point
1069
    // source. In this case, we need to find the source region that contains the
1070
    // point source and apply or relate it to the external source.
1071
    if (is->domain_ids().size() == 0) {
52✔
1072

1073
      // Extract the point source coordinate and find the base source region at
1074
      // that point
1075
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
2!
1076
      GeometryState gs;
2✔
1077
      gs.r() = sp->r();
2✔
1078
      gs.r_last() = sp->r();
2✔
1079
      gs.u() = {1.0, 0.0, 0.0};
2✔
1080
      bool found = exhaustive_find_cell(gs);
2✔
1081
      if (!found) {
2!
1082
        fatal_error(fmt::format("Could not find cell containing external "
×
1083
                                "point source at {}",
1084
          sp->r()));
×
1085
      }
1086
      SourceRegionKey key = lookup_source_region_key(gs);
2✔
1087

1088
      // With the source region and mesh bin known, we can use the
1089
      // accompanying SourceRegionKey as a key into a map that stores the
1090
      // corresponding external source index for the point source. Notably, we
1091
      // do not actually apply the external source to any source regions here,
1092
      // as if mesh subdivision is enabled, they haven't actually been
1093
      // discovered & initilized yet. When discovered, they will read from the
1094
      // external_source_map to determine if there are any external source
1095
      // terms that should be applied.
1096
      external_point_source_map_[key].push_back(es);
2✔
1097

1098
    } else {
2✔
1099
      // If not a point source, then use the volumetric domain constraints to
1100
      // determine which source regions to apply the external source to.
1101
      if (is->domain_type() == Source::DomainType::MATERIAL) {
50✔
1102
        for (int32_t material_id : domain_ids) {
4✔
1103
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
12✔
1104
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
10✔
1105
          }
1106
        }
1107
      } else if (is->domain_type() == Source::DomainType::CELL) {
48✔
1108
        for (int32_t cell_id : domain_ids) {
4✔
1109
          int32_t i_cell = model::cell_map[cell_id];
2✔
1110
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
2✔
1111
        }
1112
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
46!
1113
        for (int32_t universe_id : domain_ids) {
92✔
1114
          int32_t i_universe = model::universe_map[universe_id];
46✔
1115
          Universe& universe = *model::universes[i_universe];
46✔
1116
          for (int32_t i_cell : universe.cells_) {
92✔
1117
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
46✔
1118
          }
1119
        }
1120
      }
1121
    }
1122
  } // End loop over external sources
1123
}
52✔
1124

1125
void FlatSourceDomain::flux_swap()
1,150✔
1126
{
1127
  source_regions_.flux_swap();
1,150✔
1128
}
1,150✔
1129

1130
void FlatSourceDomain::flatten_xs()
90✔
1131
{
1132
  // Temperature and angle indices, if using multiple temperature
1133
  // data sets and/or anisotropic data sets.
1134
  // TODO: Currently assumes we are only using single temp/single angle data.
1135
  const int t = 0;
90✔
1136
  const int a = 0;
90✔
1137

1138
  n_materials_ = data::mg.macro_xs_.size();
90✔
1139
  for (int i = 0; i < n_materials_; i++) {
338✔
1140
    auto& m = data::mg.macro_xs_[i];
248✔
1141
    for (int g_out = 0; g_out < negroups_; g_out++) {
1,330✔
1142
      if (m.exists_in_model) {
1,082✔
1143
        double sigma_t =
1144
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
1,074✔
1145
        sigma_t_.push_back(sigma_t);
1,074✔
1146

1147
        if (sigma_t < MINIMUM_MACRO_XS) {
1,074✔
1148
          Material* mat = model::materials[i].get();
2✔
1149
          warning(fmt::format(
2✔
1150
            "Material \"{}\" (id: {}) has a group {} total cross section "
1151
            "({:.3e}) below the minimum threshold "
1152
            "({:.3e}). Material will be treated as pure void.",
1153
            mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
4✔
1154
        }
1155

1156
        double nu_sigma_f =
1157
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
1,074✔
1158
        nu_sigma_f_.push_back(nu_sigma_f);
1,074✔
1159

1160
        double sigma_f =
1161
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
1,074✔
1162
        sigma_f_.push_back(sigma_f);
1,074✔
1163

1164
        double chi =
1165
          m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
1,074✔
1166
        if (!std::isfinite(chi)) {
1,074✔
1167
          // MGXS interface may return NaN in some cases, such as when material
1168
          // is fissionable but has very small sigma_f.
1169
          chi = 0.0;
80✔
1170
        }
1171
        chi_.push_back(chi);
1,074✔
1172

1173
        double kappa_fission =
1174
          m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a);
1,074✔
1175
        kappa_fission_.push_back(kappa_fission);
1,074✔
1176

1177
        for (int g_in = 0; g_in < negroups_; g_in++) {
34,056✔
1178
          double sigma_s =
1179
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
32,982✔
1180
          sigma_s_.push_back(sigma_s);
32,982✔
1181
          // For transport corrected XS data, diagonal elements may be negative.
1182
          // In this case, set a flag to enable transport stabilization for the
1183
          // simulation.
1184
          if (g_out == g_in && sigma_s < 0.0)
32,982✔
1185
            is_transport_stabilization_needed_ = true;
110✔
1186
        }
1187
      } else {
1188
        sigma_t_.push_back(0);
8✔
1189
        nu_sigma_f_.push_back(0);
8✔
1190
        sigma_f_.push_back(0);
8✔
1191
        chi_.push_back(0);
8✔
1192
        kappa_fission_.push_back(0);
8✔
1193
        for (int g_in = 0; g_in < negroups_; g_in++) {
16✔
1194
          sigma_s_.push_back(0);
8✔
1195
        }
1196
      }
1197
    }
1198
  }
1199
}
90✔
1200

1201
void FlatSourceDomain::set_adjoint_sources()
8✔
1202
{
1203
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1204
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1205
  // as this is likely a very small source region that we don't need to bother
1206
  // trying to vector particles towards. In the case of flux "being extremely
1207
  // close to zero", we define this as being a fixed fraction of the maximum
1208
  // forward flux, below which we assume the flux would be physically
1209
  // undetectable.
1210

1211
  // First, find the maximum forward flux value
1212
  double max_flux = 0.0;
8✔
1213
#pragma omp parallel for reduction(max : max_flux)
1214
  for (int64_t se = 0; se < n_source_elements(); se++) {
31,112✔
1215
    double flux = source_regions_.scalar_flux_final(se);
31,104✔
1216
    if (flux > max_flux) {
31,104✔
1217
      max_flux = flux;
5✔
1218
    }
1219
  }
1220

1221
  // Then, compute the adjoint source for each source region
1222
#pragma omp parallel for
1223
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
31,112✔
1224
    for (int g = 0; g < negroups_; g++) {
62,208✔
1225
      double flux = source_regions_.scalar_flux_final(sr, g);
31,104✔
1226
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
31,104✔
1227
        source_regions_.external_source(sr, g) = 0.0;
65✔
1228
      } else {
1229
        source_regions_.external_source(sr, g) = 1.0 / flux;
31,039✔
1230
      }
1231
      if (flux > 0.0) {
31,104✔
1232
        source_regions_.external_source_present(sr) = 1;
31,039✔
1233
      }
1234
      source_regions_.scalar_flux_final(sr, g) = 0.0;
31,104✔
1235
    }
1236
  }
1237

1238
  // "Small" source regions in OpenMC are defined as those that are hit by
1239
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1240
  // very small volumes combined with a low aspect ratio, and are often
1241
  // generated when applying a source region mesh that clips the edge of a
1242
  // curved surface. As perhaps only a few rays will visit these regions over
1243
  // the entire forward simulation, the forward flux estimates are extremely
1244
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1245
  // extremely low, leading to unphysically large adjoint source terms,
1246
  // resulting in weight windows that aggressively try to drive particles
1247
  // towards these regions. To fix this, we simply filter out any "small" source
1248
  // regions from consideration. If a source region is "small", we
1249
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1250
  // flux solution, as the true total adjoint source contribution from small
1251
  // regions is likely to be negligible.
1252
#pragma omp parallel for
1253
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
31,112✔
1254
    if (source_regions_.is_small(sr)) {
31,104!
UNCOV
1255
      for (int g = 0; g < negroups_; g++) {
×
UNCOV
1256
        source_regions_.external_source(sr, g) = 0.0;
×
1257
      }
UNCOV
1258
      source_regions_.external_source_present(sr) = 0;
×
1259
    }
1260
  }
1261
  // Divide the fixed source term by sigma t (to save time when applying each
1262
  // iteration)
1263
#pragma omp parallel for
1264
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
31,112✔
1265
    int material = source_regions_.material(sr);
31,104✔
1266
    if (material == MATERIAL_VOID) {
31,104!
UNCOV
1267
      continue;
×
1268
    }
1269
    for (int g = 0; g < negroups_; g++) {
62,208✔
1270
      double sigma_t =
1271
        sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr);
31,104✔
1272
      source_regions_.external_source(sr, g) /= sigma_t;
31,104✔
1273
    }
1274
  }
1275
}
8✔
1276

1277
void FlatSourceDomain::transpose_scattering_matrix()
10✔
1278
{
1279
  // Transpose the inner two dimensions for each material
1280
  for (int m = 0; m < n_materials_; ++m) {
38✔
1281
    int material_offset = m * negroups_ * negroups_;
28✔
1282
    for (int i = 0; i < negroups_; ++i) {
80✔
1283
      for (int j = i + 1; j < negroups_; ++j) {
136✔
1284
        // Calculate indices of the elements to swap
1285
        int idx1 = material_offset + i * negroups_ + j;
84✔
1286
        int idx2 = material_offset + j * negroups_ + i;
84✔
1287

1288
        // Swap the elements to transpose the matrix
1289
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
84✔
1290
      }
1291
    }
1292
  }
1293
}
10✔
1294

1295
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1296
{
1297
  // Ensure array is correct size
1298
  flux.resize(n_source_regions() * negroups_);
×
1299
// Serialize the final fluxes for output
1300
#pragma omp parallel for
UNCOV
1301
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
UNCOV
1302
    flux[se] = source_regions_.scalar_flux_final(se);
×
1303
  }
1304
}
×
1305

1306
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
118✔
1307
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1308
  bool is_target_void)
1309
{
1310
  Cell& cell = *model::cells[i_cell];
118✔
1311
  if (cell.type_ != Fill::MATERIAL)
118!
1312
    return;
×
1313
  for (int32_t j : instances) {
21,952✔
1314
    int cell_material_idx = cell.material(j);
21,834✔
1315
    int cell_material_id = (cell_material_idx == C_NONE)
1316
                             ? C_NONE
21,834!
1317
                             : model::materials[cell_material_idx]->id();
21,834✔
1318

1319
    if ((target_material_id == C_NONE && !is_target_void) ||
21,834!
1320
        cell_material_id == target_material_id) {
1321
      int64_t sr = source_region_offsets_[i_cell] + j;
17,834✔
1322
      // Check if the key is already present in the mesh_map_
1323
      if (mesh_map_.find(sr) != mesh_map_.end()) {
17,834!
1324
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1325
                                "applied, but trying to apply mesh idx {}",
1326
          sr, mesh_map_[sr], mesh_idx));
×
1327
      }
1328
      // If the SR has not already been assigned, then we can write to it
1329
      mesh_map_[sr] = mesh_idx;
17,834✔
1330
    }
1331
  }
1332
}
1333

1334
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
102✔
1335
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1336
{
1337
  Cell& cell = *model::cells[i_cell];
102✔
1338

1339
  if (cell.type_ == Fill::MATERIAL) {
102✔
1340
    vector<int> instances(cell.n_instances());
86✔
1341
    std::iota(instances.begin(), instances.end(), 0);
86✔
1342
    apply_mesh_to_cell_instances(
86✔
1343
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1344
  } else if (target_material_id == C_NONE && !is_target_void) {
102!
1345
    for (int j = 0; j < cell.n_instances(); j++) {
16✔
1346
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1347
        cell.get_contained_cells(j, nullptr);
8✔
1348
      for (const auto& pair : cell_instance_list) {
40✔
1349
        int32_t i_child_cell = pair.first;
32✔
1350
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
32✔
1351
          pair.second, is_target_void);
32✔
1352
      }
1353
    }
8✔
1354
  }
1355
}
102✔
1356

1357
void FlatSourceDomain::apply_meshes()
90✔
1358
{
1359
  // Skip if there are no mappings between mesh IDs and domains
1360
  if (mesh_domain_map_.empty())
90✔
1361
    return;
56✔
1362

1363
  // Loop over meshes
1364
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
78✔
1365
    Mesh* mesh = model::meshes[mesh_idx].get();
44✔
1366
    int mesh_id = mesh->id();
44✔
1367

1368
    // Skip if mesh id is not present in the map
1369
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
44✔
1370
      continue;
2✔
1371

1372
    // Loop over domains associated with the mesh
1373
    for (auto& domain : mesh_domain_map_[mesh_id]) {
84✔
1374
      Source::DomainType domain_type = domain.first;
42✔
1375
      int domain_id = domain.second;
42✔
1376

1377
      if (domain_type == Source::DomainType::MATERIAL) {
42✔
1378
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
24✔
1379
          if (domain_id == C_NONE) {
20!
1380
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1381
          } else {
1382
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
20✔
1383
          }
1384
        }
1385
      } else if (domain_type == Source::DomainType::CELL) {
38✔
1386
        int32_t i_cell = model::cell_map[domain_id];
4✔
1387
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
4✔
1388
      } else if (domain_type == Source::DomainType::UNIVERSE) {
34!
1389
        int32_t i_universe = model::universe_map[domain_id];
34✔
1390
        Universe& universe = *model::universes[i_universe];
34✔
1391
        for (int32_t i_cell : universe.cells_) {
112✔
1392
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
78✔
1393
        }
1394
      }
1395
    }
1396
  }
1397
}
1398

1399
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
116,189,123✔
1400
  SourceRegionKey sr_key, Position r, Direction u)
1401
{
1402
  // Case 1: Check if the source region key is already present in the permanent
1403
  // map. This is the most common condition, as any source region visited in a
1404
  // previous power iteration will already be present in the permanent map. If
1405
  // the source region key is found, we translate the key into a specific 1D
1406
  // source region index and return a handle its position in the
1407
  // source_regions_ vector.
1408
  auto it = source_region_map_.find(sr_key);
116,189,123✔
1409
  if (it != source_region_map_.end()) {
116,189,123✔
1410
    int64_t sr = it->second;
112,521,311✔
1411
    return source_regions_.get_source_region_handle(sr);
112,521,311✔
1412
  }
1413

1414
  // Case 2: Check if the source region key is present in the temporary (thread
1415
  // safe) map. This is a common occurrence in the first power iteration when
1416
  // the source region has already been visited already by some other ray. We
1417
  // begin by locking the temporary map before any operations are performed. The
1418
  // lock is not global over the full data structure -- it will be dependent on
1419
  // which key is used.
1420
  discovered_source_regions_.lock(sr_key);
3,667,812✔
1421

1422
  // If the key is found in the temporary map, then we return a handle to the
1423
  // source region that is stored in the temporary map.
1424
  if (discovered_source_regions_.contains(sr_key)) {
3,667,812✔
1425
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
3,452,978✔
1426
    discovered_source_regions_.unlock(sr_key);
3,452,978✔
1427
    return handle;
3,452,978✔
1428
  }
1429

1430
  // Case 3: The source region key is not present anywhere, but it is only due
1431
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1432
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1433
  // result in the ray tracer detecting an additional (very short) segment
1434
  // though a mesh bin that is actually past the physical source region
1435
  // boundary. This is a result of the the multi-level ray tracing treatment in
1436
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1437
  // result in the wrong surface being selected as the nearest. This can happen
1438
  // in a lattice when there are two directions that both are very close in
1439
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1440
  // treated as being equivalent so alternative logic is used. However, when we
1441
  // go and ray trace on this with the mesh tracer we may go past the surface
1442
  // bounding the current source region.
1443
  //
1444
  // To filter out this case, before we create the new source region, we double
1445
  // check that the actual starting point of this segment (r) is still in the
1446
  // same geometry source region that we started in. If an artifact is detected,
1447
  // we discard the segment (and attenuation through it) as it is not really a
1448
  // valid source region and will have only an infinitessimally small cell
1449
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1450
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1451
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1452
  // consequences for the general ray tracer, so for now we do the below sanity
1453
  // checks before generating phantom source regions. A significant extra cost
1454
  // is incurred in instantiating the GeometryState object and doing a cell
1455
  // lookup, but again, this is going to be an extremely rare thing to check
1456
  // after the first power iteration has completed.
1457

1458
  // Sanity check on source region id
1459
  GeometryState gs;
214,834✔
1460
  gs.r() = r + TINY_BIT * u;
214,834✔
1461
  gs.u() = {1.0, 0.0, 0.0};
214,834✔
1462
  exhaustive_find_cell(gs);
214,834✔
1463
  int64_t sr_found = lookup_base_source_region_idx(gs);
214,834✔
1464
  if (sr_found != sr_key.base_source_region_id) {
214,834✔
1465
    discovered_source_regions_.unlock(sr_key);
8✔
1466
    SourceRegionHandle handle;
8✔
1467
    handle.is_numerical_fp_artifact_ = true;
8✔
1468
    return handle;
8✔
1469
  }
1470

1471
  // Sanity check on mesh bin
1472
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
214,826✔
1473
  if (mesh_idx == C_NONE) {
214,826✔
1474
    if (sr_key.mesh_bin != 0) {
35,504!
1475
      discovered_source_regions_.unlock(sr_key);
×
1476
      SourceRegionHandle handle;
×
1477
      handle.is_numerical_fp_artifact_ = true;
×
1478
      return handle;
×
1479
    }
1480
  } else {
1481
    Mesh* mesh = model::meshes[mesh_idx].get();
179,322✔
1482
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
179,322✔
1483
    if (bin_found != sr_key.mesh_bin) {
179,322!
1484
      discovered_source_regions_.unlock(sr_key);
×
1485
      SourceRegionHandle handle;
×
1486
      handle.is_numerical_fp_artifact_ = true;
×
1487
      return handle;
×
1488
    }
1489
  }
1490

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

1498
  // Call the basic constructor for the source region and store in the parallel
1499
  // map.
1500
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
214,826✔
1501
  SourceRegion* sr_ptr =
1502
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
214,826✔
1503
  SourceRegionHandle handle {*sr_ptr};
214,826✔
1504

1505
  // Determine the material
1506
  int gs_i_cell = gs.lowest_coord().cell();
214,826✔
1507
  Cell& cell = *model::cells[gs_i_cell];
214,826✔
1508
  int material = cell.material(gs.cell_instance());
214,826✔
1509

1510
  // If material total XS is extremely low, just set it to void to avoid
1511
  // problems with 1/Sigma_t
1512
  for (int g = 0; g < negroups_; g++) {
457,782✔
1513
    double sigma_t = sigma_t_[material * negroups_ + g];
244,964✔
1514
    if (sigma_t < MINIMUM_MACRO_XS) {
244,964✔
1515
      material = MATERIAL_VOID;
2,008✔
1516
      break;
2,008✔
1517
    }
1518
  }
1519

1520
  handle.material() = material;
214,826✔
1521

1522
  handle.density_mult() = cell.density_mult(gs.cell_instance());
214,826✔
1523

1524
  // Store the mesh index (if any) assigned to this source region
1525
  handle.mesh() = mesh_idx;
214,826✔
1526

1527
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
214,826✔
1528
    // Determine if there are any volumetric sources, and apply them.
1529
    // Volumetric sources are specifc only to the base SR idx.
1530
    auto it_vol =
1531
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
210,061✔
1532
    if (it_vol != external_volumetric_source_map_.end()) {
210,061✔
1533
      const vector<int>& vol_sources = it_vol->second;
416✔
1534
      for (int src_idx : vol_sources) {
832✔
1535
        apply_external_source_to_source_region(src_idx, handle);
416✔
1536
      }
1537
    }
1538

1539
    // Determine if there are any point sources, and apply them.
1540
    // Point sources are specific to the source region key.
1541
    auto it_point = external_point_source_map_.find(sr_key);
210,061✔
1542
    if (it_point != external_point_source_map_.end()) {
210,061✔
1543
      const vector<int>& point_sources = it_point->second;
1✔
1544
      for (int src_idx : point_sources) {
2✔
1545
        apply_external_source_to_source_region(src_idx, handle);
1✔
1546
      }
1547
    }
1548

1549
    // Divide external source term by sigma_t
1550
    if (material != C_NONE) {
210,061✔
1551
      for (int g = 0; g < negroups_; g++) {
420,282✔
1552
        double sigma_t =
1553
          sigma_t_[material * negroups_ + g] * handle.density_mult();
212,229✔
1554
        handle.external_source(g) /= sigma_t;
212,229✔
1555
      }
1556
    }
1557
  }
1558

1559
  // Compute the combined source term
1560
  update_single_neutron_source(handle);
214,826✔
1561

1562
  // Unlock the parallel map. Note: we may be tempted to release
1563
  // this lock earlier, and then just use the source region's lock to protect
1564
  // the flux/source initialization stages above. However, the rest of the code
1565
  // only protects updates to the new flux and volume fields, and assumes that
1566
  // the source is constant for the duration of transport. Thus, using just the
1567
  // source region's lock by itself would result in other threads potentially
1568
  // reading from the source before it is computed, as they won't use the lock
1569
  // when only reading from the SR's source. It would be expensive to protect
1570
  // those operations, whereas generating the SR is only done once, so we just
1571
  // hold the map's bucket lock until the source region is fully initialized.
1572
  discovered_source_regions_.unlock(sr_key);
214,826✔
1573

1574
  return handle;
214,826✔
1575
}
214,834✔
1576

1577
void FlatSourceDomain::finalize_discovered_source_regions()
1,150✔
1578
{
1579
  // Extract keys for entries with a valid volume.
1580
  vector<SourceRegionKey> keys;
1,150✔
1581
  for (const auto& pair : discovered_source_regions_) {
215,976✔
1582
    if (pair.second.volume_ > 0.0) {
214,826✔
1583
      keys.push_back(pair.first);
205,604✔
1584
    }
1585
  }
1586

1587
  if (!keys.empty()) {
1,150✔
1588
    // Sort the keys, so as to ensure reproducible ordering given that source
1589
    // regions may have been added to discovered_source_regions_ in an arbitrary
1590
    // order due to shared memory threading.
1591
    std::sort(keys.begin(), keys.end());
75✔
1592

1593
    // Remember the index of the first new source region
1594
    int64_t start_sr_id = source_regions_.n_source_regions();
75✔
1595

1596
    // Append the source regions in the sorted key order.
1597
    for (const auto& key : keys) {
205,679✔
1598
      const SourceRegion& sr = discovered_source_regions_[key];
205,604✔
1599
      source_region_map_[key] = source_regions_.n_source_regions();
205,604✔
1600
      source_regions_.push_back(sr);
205,604✔
1601
    }
1602

1603
    // Map all new source regions to tallies
1604
    convert_source_regions_to_tallies(start_sr_id);
75✔
1605
  }
1606

1607
  discovered_source_regions_.clear();
1,150✔
1608
}
1,150✔
1609

1610
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1611
//
1612
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1613
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1614
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1615
// https://doi.org/10.1016/j.anucene.2018.10.036.
1616
void FlatSourceDomain::apply_transport_stabilization()
1,150✔
1617
{
1618
  // Don't do anything if all in-group scattering
1619
  // cross sections are positive
1620
  if (!is_transport_stabilization_needed_) {
1,150✔
1621
    return;
1,130✔
1622
  }
1623

1624
  // Apply the stabilization factor to all source elements
1625
#pragma omp parallel for
1626
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
260✔
1627
    int material = source_regions_.material(sr);
240✔
1628
    double density_mult = source_regions_.density_mult(sr);
240✔
1629
    if (material == MATERIAL_VOID) {
240!
UNCOV
1630
      continue;
×
1631
    }
1632
    for (int g = 0; g < negroups_; g++) {
17,040✔
1633
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1634
      // negative
1635
      double sigma_s =
1636
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g] *
16,800✔
1637
        density_mult;
16,800✔
1638
      if (sigma_s < 0.0) {
16,800✔
1639
        double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
4,400✔
1640
        double phi_new = source_regions_.scalar_flux_new(sr, g);
4,400✔
1641
        double phi_old = source_regions_.scalar_flux_old(sr, g);
4,400✔
1642

1643
        // Equation 18 in the above Gunow et al. 2019 paper. For a default
1644
        // rho of 1.0, this ensures there are no negative diagonal elements
1645
        // in the iteration matrix. A lesser rho could be used (or exposed
1646
        // as a user input parameter) to reduce the negative impact on
1647
        // convergence rate though would need to be experimentally tested to see
1648
        // if it doesn't become unstable. rho = 1.0 is good as it gives the
1649
        // highest assurance of stability, and the impacts on convergence rate
1650
        // are pretty mild.
1651
        double D = diagonal_stabilization_rho_ * sigma_s / sigma_t;
4,400✔
1652

1653
        // Equation 16 in the above Gunow et al. 2019 paper
1654
        source_regions_.scalar_flux_new(sr, g) =
4,400✔
1655
          (phi_new - D * phi_old) / (1.0 - D);
4,400✔
1656
      }
1657
    }
1658
  }
1659
}
1660

1661
// Determines the base source region index (i.e., a material filled cell
1662
// instance) that corresponds to a particular location in the geometry. Requires
1663
// that the "gs" object passed in has already been initialized and has called
1664
// find_cell etc.
1665
int64_t FlatSourceDomain::lookup_base_source_region_idx(
74,929,832✔
1666
  const GeometryState& gs) const
1667
{
1668
  int i_cell = gs.lowest_coord().cell();
74,929,832✔
1669
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
74,929,832✔
1670
  return sr;
74,929,832✔
1671
}
1672

1673
// Determines the index of the mesh (if any) that has been applied
1674
// to a particular base source region index.
1675
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
74,929,824✔
1676
{
1677
  int mesh_idx = C_NONE;
74,929,824✔
1678
  auto mesh_it = mesh_map_.find(sr);
74,929,824✔
1679
  if (mesh_it != mesh_map_.end()) {
74,929,824✔
1680
    mesh_idx = mesh_it->second;
42,065,017✔
1681
  }
1682
  return mesh_idx;
74,929,824✔
1683
}
1684

1685
// Determines the source region key that corresponds to a particular location in
1686
// the geometry. This takes into account both the base source region index as
1687
// well as the mesh bin if a mesh is applied to this source region for
1688
// subdivision.
1689
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
178,602✔
1690
  const GeometryState& gs) const
1691
{
1692
  int64_t sr = lookup_base_source_region_idx(gs);
178,602✔
1693
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
178,602✔
1694
  return SourceRegionKey {sr, mesh_bin};
178,602✔
1695
}
1696

1697
// Determines the mesh bin that corresponds to a particular base source region
1698
// index and position.
1699
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
178,602✔
1700
{
1701
  int mesh_idx = lookup_mesh_idx(sr);
178,602✔
1702
  int mesh_bin = 0;
178,602✔
1703
  if (mesh_idx != C_NONE) {
178,602✔
1704
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
111,102✔
1705
  }
1706
  return mesh_bin;
178,602✔
1707
}
1708

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