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

openmc-dev / openmc / 15371300071

01 Jun 2025 05:04AM UTC coverage: 85.143% (+0.3%) from 84.827%
15371300071

Pull #3176

github

web-flow
Merge 4f739184a into cb95c784b
Pull Request #3176: MeshFilter rotation - solution to issue #3166

86 of 99 new or added lines in 4 files covered. (86.87%)

3707 existing lines in 117 files now uncovered.

52212 of 61323 relevant lines covered (85.14%)

42831974.38 hits per line

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

78.9
/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_)
880✔
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;
880✔
44
  for (const auto& c : model::cells) {
7,502✔
45
    if (c->type_ != Fill::MATERIAL) {
6,622✔
46
      source_region_offsets_.push_back(-1);
3,190✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
3,432✔
49
      base_source_regions += c->n_instances_;
3,432✔
50
    }
51
  }
52

53
  // Initialize source regions.
54
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
880✔
55
  source_regions_ = SourceRegionContainer(negroups_, is_linear);
880✔
56
  source_regions_.assign(
880✔
57
    base_source_regions, SourceRegion(negroups_, is_linear));
1,760✔
58

59
  // Initialize materials
60
  int64_t source_region_id = 0;
880✔
61
  for (int i = 0; i < model::cells.size(); i++) {
7,502✔
62
    Cell& cell = *model::cells[i];
6,622✔
63
    if (cell.type_ == Fill::MATERIAL) {
6,622✔
64
      for (int j = 0; j < cell.n_instances_; j++) {
1,056,264✔
65
        source_regions_.material(source_region_id++) = cell.material(j);
1,052,832✔
66
      }
67
    }
68
  }
69

70
  // Sanity check
71
  if (source_region_id != base_source_regions) {
880✔
UNCOV
72
    fatal_error("Unexpected number of source regions");
×
73
  }
74

75
  // Initialize tally volumes
76
  if (volume_normalized_flux_tallies_) {
880✔
77
    tally_volumes_.resize(model::tallies.size());
704✔
78
    for (int i = 0; i < model::tallies.size(); i++) {
2,640✔
79
      //  Get the shape of the 3D result tensor
80
      auto shape = model::tallies[i]->results().shape();
1,936✔
81

82
      // Create a new 2D tensor with the same size as the first
83
      // two dimensions of the 3D tensor
84
      tally_volumes_[i] =
1,936✔
85
        xt::xtensor<double, 2>::from_shape({shape[0], shape[1]});
3,872✔
86
    }
87
  }
88

89
  // Compute simulation domain volume based on ray source
90
  auto* is = dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
880✔
91
  SpatialDistribution* space_dist = is->space();
880✔
92
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
880✔
93
  Position dims = sb->upper_right() - sb->lower_left();
880✔
94
  simulation_volume_ = dims.x * dims.y * dims.z;
880✔
95
}
880✔
96

97
void FlatSourceDomain::batch_reset()
15,750✔
98
{
99
// Reset scalar fluxes and iteration volume tallies to zero
100
#pragma omp parallel for
6,300✔
101
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
58,405,113✔
102
    source_regions_.volume(sr) = 0.0;
58,395,663✔
103
    source_regions_.volume_sq(sr) = 0.0;
58,395,663✔
104
  }
105

106
#pragma omp parallel for
6,300✔
107
  for (int64_t se = 0; se < n_source_elements(); se++) {
66,134,565✔
108
    source_regions_.scalar_flux_new(se) = 0.0;
66,125,115✔
109
  }
110
}
15,750✔
111

112
void FlatSourceDomain::accumulate_iteration_flux()
6,375✔
113
{
114
#pragma omp parallel for
2,550✔
115
  for (int64_t se = 0; se < n_source_elements(); se++) {
29,864,745✔
116
    source_regions_.scalar_flux_final(se) +=
29,860,920✔
117
      source_regions_.scalar_flux_new(se);
29,860,920✔
118
  }
119
}
6,375✔
120

121
// Compute new estimate of scattering + fission sources in each source region
122
// based on the flux estimate from the previous iteration.
123
void FlatSourceDomain::update_neutron_source(double k_eff)
6,975✔
124
{
125
  simulation::time_update_src.start();
6,975✔
126

127
  double inverse_k_eff = 1.0 / k_eff;
6,975✔
128

129
// Reset all source regions to zero (important for void regions)
130
#pragma omp parallel for
2,790✔
131
  for (int64_t se = 0; se < n_source_elements(); se++) {
38,876,247✔
132
    source_regions_.source(se) = 0.0;
38,872,062✔
133
  }
134

135
  // Add scattering + fission source
136
#pragma omp parallel for
2,790✔
137
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
34,696,323✔
138
    int material = source_regions_.material(sr);
34,692,138✔
139
    if (material == MATERIAL_VOID) {
34,692,138✔
140
      continue;
360,000✔
141
    }
142
    for (int g_out = 0; g_out < negroups_; g_out++) {
72,844,200✔
143
      double sigma_t = sigma_t_[material * negroups_ + g_out];
38,512,062✔
144
      double scatter_source = 0.0;
38,512,062✔
145
      double fission_source = 0.0;
38,512,062✔
146

147
      for (int g_in = 0; g_in < negroups_; g_in++) {
106,283,592✔
148
        double scalar_flux = source_regions_.scalar_flux_old(sr, g_in);
67,771,530✔
149
        double sigma_s =
150
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
67,771,530✔
151
        double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
67,771,530✔
152
        double chi = chi_[material * negroups_ + g_out];
67,771,530✔
153

154
        scatter_source += sigma_s * scalar_flux;
67,771,530✔
155
        fission_source += nu_sigma_f * scalar_flux * chi;
67,771,530✔
156
      }
157
      source_regions_.source(sr, g_out) =
38,512,062✔
158
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
38,512,062✔
159
    }
160
  }
161

162
  // Add external source if in fixed source mode
163
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
6,975✔
164
#pragma omp parallel for
2,430✔
165
    for (int64_t se = 0; se < n_source_elements(); se++) {
36,693,549✔
166
      source_regions_.source(se) += source_regions_.external_source(se);
36,689,904✔
167
    }
168
  }
169

170
  simulation::time_update_src.stop();
6,975✔
171
}
6,975✔
172

173
// Normalizes flux and updates simulation-averaged volume estimate
174
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
6,975✔
175
  double total_active_distance_per_iteration)
176
{
177
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
6,975✔
178
  double volume_normalization_factor =
6,975✔
179
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
6,975✔
180

181
// Normalize scalar flux to total distance travelled by all rays this
182
// iteration
183
#pragma omp parallel for
2,790✔
184
  for (int64_t se = 0; se < n_source_elements(); se++) {
39,944,403✔
185
    source_regions_.scalar_flux_new(se) *= normalization_factor;
39,940,218✔
186
  }
187

188
// Accumulate cell-wise ray length tallies collected this iteration, then
189
// update the simulation-averaged cell-wise volume estimates
190
#pragma omp parallel for
2,790✔
191
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
35,634,231✔
192
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
35,630,046✔
193
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
35,630,046✔
194
    source_regions_.volume_naive(sr) =
71,260,092✔
195
      source_regions_.volume(sr) * normalization_factor;
35,630,046✔
196
    source_regions_.volume_sq(sr) =
71,260,092✔
197
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
35,630,046✔
198
    source_regions_.volume(sr) =
35,630,046✔
199
      source_regions_.volume_t(sr) * volume_normalization_factor;
35,630,046✔
200
  }
201
}
6,975✔
202

203
void FlatSourceDomain::set_flux_to_flux_plus_source(
58,936,425✔
204
  int64_t sr, double volume, int g)
205
{
206
  int material = source_regions_.material(sr);
58,936,425✔
207
  if (material == MATERIAL_VOID) {
58,936,425✔
208
    source_regions_.scalar_flux_new(sr, g) /= volume;
1,200,000✔
209
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
1,200,000✔
210
      source_regions_.scalar_flux_new(sr, g) +=
1,200,000✔
211
        0.5f * source_regions_.external_source(sr, g) *
1,200,000✔
212
        source_regions_.volume_sq(sr);
1,200,000✔
213
    }
214
  } else {
215
    double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g];
57,736,425✔
216
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
57,736,425✔
217
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
57,736,425✔
218
  }
219
}
58,936,425✔
220

221
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,590✔
222
{
223
  source_regions_.scalar_flux_new(sr, g) =
3,180✔
224
    source_regions_.scalar_flux_old(sr, g);
1,590✔
225
}
1,590✔
226

227
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
12,174,945✔
228
{
229
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
12,174,945✔
230
}
12,174,945✔
231

232
// Combine transport flux contributions and flat source contributions from the
233
// previous iteration to generate this iteration's estimate of scalar flux.
234
int64_t FlatSourceDomain::add_source_to_scalar_flux()
15,750✔
235
{
236
  int64_t n_hits = 0;
15,750✔
237
  double inverse_batch = 1.0 / simulation::current_batch;
15,750✔
238

239
#pragma omp parallel for reduction(+ : n_hits)
6,300✔
240
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
59,855,193✔
241

242
    double volume_simulation_avg = source_regions_.volume(sr);
59,845,743✔
243
    double volume_iteration = source_regions_.volume_naive(sr);
59,845,743✔
244

245
    // Increment the number of hits if cell was hit this iteration
246
    if (volume_iteration) {
59,845,743✔
247
      n_hits++;
52,539,264✔
248
    }
249

250
    // Set the SR to small status if its expected number of hits
251
    // per iteration is less than 1.5
252
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
59,845,743✔
253
      source_regions_.is_small(sr) = 1;
12,262,869✔
254
    } else {
255
      source_regions_.is_small(sr) = 0;
47,582,874✔
256
    }
257

258
    // The volume treatment depends on the volume estimator type
259
    // and whether or not an external source is present in the cell.
260
    double volume;
261
    switch (volume_estimator_) {
59,845,743✔
262
    case RandomRayVolumeEstimator::NAIVE:
1,399,680✔
263
      volume = volume_iteration;
1,399,680✔
264
      break;
1,399,680✔
265
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
777,600✔
266
      volume = volume_simulation_avg;
777,600✔
267
      break;
777,600✔
268
    case RandomRayVolumeEstimator::HYBRID:
57,668,463✔
269
      if (source_regions_.external_source_present(sr) ||
107,799,246✔
270
          source_regions_.is_small(sr)) {
50,130,783✔
271
        volume = volume_iteration;
19,799,865✔
272
      } else {
273
        volume = volume_simulation_avg;
37,868,598✔
274
      }
275
      break;
57,668,463✔
276
    default:
277
      fatal_error("Invalid volume estimator type");
278
    }
279

280
    for (int g = 0; g < negroups_; g++) {
127,557,018✔
281
      // There are three scenarios we need to consider:
282
      if (volume_iteration > 0.0) {
67,711,275✔
283
        // 1. If the FSR was hit this iteration, then the new flux is equal to
284
        // the flat source from the previous iteration plus the contributions
285
        // from rays passing through the source region (computed during the
286
        // transport sweep)
287
        set_flux_to_flux_plus_source(sr, volume, g);
60,404,526✔
288
      } else if (volume_simulation_avg > 0.0) {
7,306,749✔
289
        // 2. If the FSR was not hit this iteration, but has been hit some
290
        // previous iteration, then we need to make a choice about what
291
        // to do. Naively we will usually want to set the flux to be equal
292
        // to the reduced source. However, in fixed source problems where
293
        // there is a strong external source present in the cell, and where
294
        // the cell has a very low cross section, this approximation will
295
        // cause a huge upward bias in the flux estimate of the cell (in these
296
        // conditions, the flux estimate can be orders of magnitude too large).
297
        // Thus, to avoid this bias, if any external source is present
298
        // in the cell we will use the previous iteration's flux estimate. This
299
        // injects a small degree of correlation into the simulation, but this
300
        // is going to be trivial when the miss rate is a few percent or less.
301
        if (source_regions_.external_source_present(sr) && !adjoint_) {
7,306,731✔
302
          set_flux_to_old_flux(sr, g);
1,764✔
303
        } else {
304
          set_flux_to_source(sr, g);
7,304,967✔
305
        }
306
      }
307
    }
308
  }
309

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

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

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

324
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
1,140✔
325
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
555,732✔
326

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

333
    int material = source_regions_.material(sr);
554,022✔
334
    if (material == MATERIAL_VOID) {
554,022✔
335
      continue;
336
    }
337

338
    double sr_fission_source_old = 0;
554,022✔
339
    double sr_fission_source_new = 0;
554,022✔
340

341
    for (int g = 0; g < negroups_; g++) {
4,275,576✔
342
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
3,721,554✔
343
      sr_fission_source_old +=
3,721,554✔
344
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
3,721,554✔
345
      sr_fission_source_new +=
3,721,554✔
346
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
3,721,554✔
347
    }
348

349
    // Compute total fission rates in FSR
350
    sr_fission_source_old *= volume;
554,022✔
351
    sr_fission_source_new *= volume;
554,022✔
352

353
    // Accumulate totals
354
    fission_rate_old += sr_fission_source_old;
554,022✔
355
    fission_rate_new += sr_fission_source_new;
554,022✔
356

357
    // Store total fission rate in the FSR for Shannon calculation
358
    p[sr] = sr_fission_source_new;
554,022✔
359
  }
360

361
  double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old);
2,850✔
362

363
  double H = 0.0;
2,850✔
364
  // defining an inverse sum for better performance
365
  double inverse_sum = 1 / fission_rate_new;
2,850✔
366

367
#pragma omp parallel for reduction(+ : H)
1,140✔
368
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
555,732✔
369
    // Only if FSR has non-negative and non-zero fission source
370
    if (p[sr] > 0.0f) {
554,022✔
371
      // Normalize to total weight of bank sites. p_i for better performance
372
      float p_i = p[sr] * inverse_sum;
235,791✔
373
      // Sum values to obtain Shannon entropy.
374
      H -= p_i * std::log2(p_i);
235,791✔
375
    }
376
  }
377

378
  // Adds entropy value to shared entropy vector in openmc namespace.
379
  simulation::entropy.push_back(H);
2,850✔
380

381
  return k_eff_new;
2,850✔
382
}
2,850✔
383

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

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

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

417
void FlatSourceDomain::convert_source_regions_to_tallies()
600✔
418
{
419
  openmc::simulation::time_tallies.start();
600✔
420

421
  // Tracks if we've generated a mapping yet for all source regions.
422
  bool all_source_regions_mapped = true;
600✔
423

424
// Attempt to generate mapping for all source regions
425
#pragma omp parallel for
240✔
426
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
2,098,872✔
427

428
    // If this source region has not been hit by a ray yet, then
429
    // we aren't going to be able to map it, so skip it.
430
    if (!source_regions_.position_recorded(sr)) {
2,098,512✔
431
      all_source_regions_mapped = false;
432
      continue;
433
    }
434

435
    // A particle located at the recorded midpoint of a ray
436
    // crossing through this source region is used to estabilish
437
    // the spatial location of the source region
438
    Particle p;
2,098,512✔
439
    p.r() = source_regions_.position(sr);
2,098,512✔
440
    p.r_last() = source_regions_.position(sr);
2,098,512✔
441
    p.u() = {1.0, 0.0, 0.0};
2,098,512✔
442
    bool found = exhaustive_find_cell(p);
2,098,512✔
443

444
    // Loop over energy groups (so as to support energy filters)
445
    for (int g = 0; g < negroups_; g++) {
4,464,864✔
446

447
      // Set particle to the current energy
448
      p.g() = g;
2,366,352✔
449
      p.g_last() = g;
2,366,352✔
450
      p.E() = data::mg.energy_bin_avg_[p.g()];
2,366,352✔
451
      p.E_last() = p.E();
2,366,352✔
452

453
      int64_t source_element = sr * negroups_ + g;
2,366,352✔
454

455
      // If this task has already been populated, we don't need to do
456
      // it again.
457
      if (source_regions_.tally_task(sr, g).size() > 0) {
2,366,352✔
458
        continue;
459
      }
460

461
      // Loop over all active tallies. This logic is essentially identical
462
      // to what happens when scanning for applicable tallies during
463
      // MC transport.
464
      for (auto i_tally : model::active_tallies) {
9,348,912✔
465
        Tally& tally {*model::tallies[i_tally]};
6,982,560✔
466

467
        // Initialize an iterator over valid filter bin combinations.
468
        // If there are no valid combinations, use a continue statement
469
        // to ensure we skip the assume_separate break below.
470
        auto filter_iter = FilterBinIter(tally, p);
6,982,560✔
471
        auto end = FilterBinIter(tally, true, &p.filter_matches());
6,982,560✔
472
        if (filter_iter == end)
6,982,560✔
473
          continue;
4,100,256✔
474

475
        // Loop over filter bins.
476
        for (; filter_iter != end; ++filter_iter) {
5,764,608✔
477
          auto filter_index = filter_iter.index_;
2,882,304✔
478
          auto filter_weight = filter_iter.weight_;
2,882,304✔
479

480
          // Loop over scores
481
          for (int score = 0; score < tally.scores_.size(); score++) {
6,371,424✔
482
            auto score_bin = tally.scores_[score];
3,489,120✔
483
            // If a valid tally, filter, and score combination has been found,
484
            // then add it to the list of tally tasks for this source element.
485
            TallyTask task(i_tally, filter_index, score, score_bin);
3,489,120✔
486
            source_regions_.tally_task(sr, g).push_back(task);
3,489,120✔
487

488
            // Also add this task to the list of volume tasks for this source
489
            // region.
490
            source_regions_.volume_task(sr).insert(task);
3,489,120✔
491
          }
492
        }
493
      }
494
      // Reset all the filter matches for the next tally event.
495
      for (auto& match : p.filter_matches())
10,709,856✔
496
        match.bins_present_ = false;
8,343,504✔
497
    }
498
  }
2,098,512✔
499
  openmc::simulation::time_tallies.stop();
600✔
500

501
  mapped_all_tallies_ = all_source_regions_mapped;
600✔
502
}
600✔
503

504
// Set the volume accumulators to zero for all tallies
505
void FlatSourceDomain::reset_tally_volumes()
6,375✔
506
{
507
  if (volume_normalized_flux_tallies_) {
6,375✔
508
#pragma omp parallel for
2,070✔
509
    for (int i = 0; i < tally_volumes_.size(); i++) {
11,700✔
510
      auto& tensor = tally_volumes_[i];
8,595✔
511
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
8,595✔
512
    }
513
  }
514
}
6,375✔
515

516
// In fixed source mode, due to the way that volumetric fixed sources are
517
// converted and applied as volumetric sources in one or more source regions,
518
// we need to perform an additional normalization step to ensure that the
519
// reported scalar fluxes are in units per source neutron. This allows for
520
// direct comparison of reported tallies to Monte Carlo flux results.
521
// This factor needs to be computed at each iteration, as it is based on the
522
// volume estimate of each FSR, which improves over the course of the
523
// simulation
524
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
7,145✔
525
{
526
  // If we are not in fixed source mode, then there are no external sources
527
  // so no normalization is needed.
528
  if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) {
7,145✔
529
    return 1.0;
2,042✔
530
  }
531

532
  // Step 1 is to sum over all source regions and energy groups to get the
533
  // total external source strength in the simulation.
534
  double simulation_external_source_strength = 0.0;
5,103✔
535
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,046✔
536
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
26,388,801✔
537
    int material = source_regions_.material(sr);
26,385,744✔
538
    double volume = source_regions_.volume(sr) * simulation_volume_;
26,385,744✔
539
    for (int g = 0; g < negroups_; g++) {
53,765,376✔
540
      // For non-void regions, we store the external source pre-divided by
541
      // sigma_t. We need to multiply non-void regions back up by sigma_t
542
      // to get the total source strength in the expected units.
543
      double sigma_t = 1.0;
27,379,632✔
544
      if (material != MATERIAL_VOID) {
27,379,632✔
545
        sigma_t = sigma_t_[material * negroups_ + g];
26,993,632✔
546
      }
547
      simulation_external_source_strength +=
27,379,632✔
548
        source_regions_.external_source(sr, g) * sigma_t * volume;
27,379,632✔
549
    }
550
  }
551

552
  // Step 2 is to determine the total user-specified external source strength
553
  double user_external_source_strength = 0.0;
5,103✔
554
  for (auto& ext_source : model::external_sources) {
10,206✔
555
    user_external_source_strength += ext_source->strength();
5,103✔
556
  }
557

558
  // The correction factor is the ratio of the user-specified external source
559
  // strength to the simulation external source strength.
560
  double source_normalization_factor =
5,103✔
561
    user_external_source_strength / simulation_external_source_strength;
562

563
  return source_normalization_factor;
5,103✔
564
}
565

566
// Tallying in random ray is not done directly during transport, rather,
567
// it is done only once after each power iteration. This is made possible
568
// by way of a mapping data structure that relates spatial source regions
569
// (FSRs) to tally/filter/score combinations. The mechanism by which the
570
// mapping is done (and the limitations incurred) is documented in the
571
// "convert_source_regions_to_tallies()" function comments above. The present
572
// tally function simply traverses the mapping data structure and executes
573
// the scoring operations to OpenMC's native tally result arrays.
574

575
void FlatSourceDomain::random_ray_tally()
6,375✔
576
{
577
  openmc::simulation::time_tallies.start();
6,375✔
578

579
  // Reset our tally volumes to zero
580
  reset_tally_volumes();
6,375✔
581

582
  double source_normalization_factor =
583
    compute_fixed_source_normalization_factor();
6,375✔
584

585
// We loop over all source regions and energy groups. For each
586
// element, we check if there are any scores needed and apply
587
// them.
588
#pragma omp parallel for
2,550✔
589
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
27,378,585✔
590
    // The fsr.volume_ is the unitless fractional simulation averaged volume
591
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
592
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
593
    // entire global simulation domain (as defined by the ray source box).
594
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
595
    // its fraction of the total volume by the total volume. Not important in
596
    // eigenvalue solves, but useful in fixed source solves for returning the
597
    // flux shape with a magnitude that makes sense relative to the fixed
598
    // source strength.
599
    double volume = source_regions_.volume(sr) * simulation_volume_;
27,374,760✔
600

601
    double material = source_regions_.material(sr);
27,374,760✔
602
    for (int g = 0; g < negroups_; g++) {
57,235,680✔
603
      double flux =
604
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
29,860,920✔
605

606
      // Determine numerical score value
607
      for (auto& task : source_regions_.tally_task(sr, g)) {
70,500,240✔
608
        double score = 0.0;
40,639,320✔
609
        switch (task.score_type) {
40,639,320✔
610

611
        case SCORE_FLUX:
34,929,000✔
612
          score = flux * volume;
34,929,000✔
613
          break;
34,929,000✔
614

615
        case SCORE_TOTAL:
616
          if (material != MATERIAL_VOID) {
617
            score = flux * volume * sigma_t_[material * negroups_ + g];
618
          }
619
          break;
620

621
        case SCORE_FISSION:
2,855,160✔
622
          if (material != MATERIAL_VOID) {
2,855,160✔
623
            score = flux * volume * sigma_f_[material * negroups_ + g];
2,855,160✔
624
          }
625
          break;
2,855,160✔
626

627
        case SCORE_NU_FISSION:
2,855,160✔
628
          if (material != MATERIAL_VOID) {
2,855,160✔
629
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
2,855,160✔
630
          }
631
          break;
2,855,160✔
632

633
        case SCORE_EVENTS:
634
          score = 1.0;
635
          break;
636

637
        default:
638
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
639
                      "total, fission, nu-fission, and events are supported in "
640
                      "random ray mode.");
641
          break;
642
        }
643

644
        // Apply score to the appropriate tally bin
645
        Tally& tally {*model::tallies[task.tally_idx]};
40,639,320✔
646
#pragma omp atomic
647
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
40,639,320✔
648
          score;
649
      }
650
    }
651

652
    // For flux tallies, the total volume of the spatial region is needed
653
    // for normalizing the flux. We store this volume in a separate tensor.
654
    // We only contribute to each volume tally bin once per FSR.
655
    if (volume_normalized_flux_tallies_) {
27,374,760✔
656
      for (const auto& task : source_regions_.volume_task(sr)) {
64,388,160✔
657
        if (task.score_type == SCORE_FLUX) {
37,272,960✔
658
#pragma omp atomic
659
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
33,755,040✔
660
            volume;
661
        }
662
      }
663
    }
664
  } // end FSR loop
665

666
  // Normalize any flux scores by the total volume of the FSRs scoring to that
667
  // bin. To do this, we loop over all tallies, and then all filter bins,
668
  // and then scores. For each score, we check the tally data structure to
669
  // see what index that score corresponds to. If that score is a flux score,
670
  // then we divide it by volume.
671
  if (volume_normalized_flux_tallies_) {
6,375✔
672
    for (int i = 0; i < model::tallies.size(); i++) {
19,500✔
673
      Tally& tally {*model::tallies[i]};
14,325✔
674
#pragma omp parallel for
5,730✔
675
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
1,266,975✔
676
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
2,549,520✔
677
          auto score_type = tally.scores_[score_idx];
1,291,140✔
678
          if (score_type == SCORE_FLUX) {
1,291,140✔
679
            double vol = tally_volumes_[i](bin, score_idx);
1,258,380✔
680
            if (vol > 0.0) {
1,258,380✔
681
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
1,258,380✔
682
            }
683
          }
684
        }
685
      }
686
    }
687
  }
688

689
  openmc::simulation::time_tallies.stop();
6,375✔
690
}
6,375✔
691

UNCOV
692
double FlatSourceDomain::evaluate_flux_at_point(
×
693
  Position r, int64_t sr, int g) const
694
{
UNCOV
695
  return source_regions_.scalar_flux_final(sr, g) /
×
UNCOV
696
         (settings::n_batches - settings::n_inactive);
×
697
}
698

699
// Outputs all basic material, FSR ID, multigroup flux, and
700
// fission source data to .vtk file that can be directly
701
// loaded and displayed by Paraview. Note that .vtk binary
702
// files require big endian byte ordering, so endianness
703
// is checked and flipped if necessary.
UNCOV
704
void FlatSourceDomain::output_to_vtk() const
×
705
{
706
  // Rename .h5 plot filename(s) to .vtk filenames
UNCOV
707
  for (int p = 0; p < model::plots.size(); p++) {
×
UNCOV
708
    PlottableInterface* plot = model::plots[p].get();
×
UNCOV
709
    plot->path_plot() =
×
UNCOV
710
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
711
  }
712

713
  // Print header information
UNCOV
714
  print_plot();
×
715

716
  // Outer loop over plots
UNCOV
717
  for (int p = 0; p < model::plots.size(); p++) {
×
718

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

722
    // Random ray plots only support voxel plots
UNCOV
723
    if (!openmc_plot) {
×
UNCOV
724
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
725
                          "is allowed in random ray mode.",
726
        p));
UNCOV
727
      continue;
×
UNCOV
728
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
UNCOV
729
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
730
                          "is allowed in random ray mode.",
731
        p));
UNCOV
732
      continue;
×
733
    }
734

UNCOV
735
    int Nx = openmc_plot->pixels_[0];
×
UNCOV
736
    int Ny = openmc_plot->pixels_[1];
×
UNCOV
737
    int Nz = openmc_plot->pixels_[2];
×
UNCOV
738
    Position origin = openmc_plot->origin_;
×
UNCOV
739
    Position width = openmc_plot->width_;
×
UNCOV
740
    Position ll = origin - width / 2.0;
×
UNCOV
741
    double x_delta = width.x / Nx;
×
UNCOV
742
    double y_delta = width.y / Ny;
×
UNCOV
743
    double z_delta = width.z / Nz;
×
UNCOV
744
    std::string filename = openmc_plot->path_plot();
×
745

746
    // Perform sanity checks on file size
UNCOV
747
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
UNCOV
748
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
UNCOV
749
      openmc_plot->id(), filename, bytes / 1.0e6);
×
UNCOV
750
    if (bytes / 1.0e9 > 1.0) {
×
UNCOV
751
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
752
              "slow.");
UNCOV
753
    } else if (bytes / 1.0e9 > 100.0) {
×
UNCOV
754
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
755
    }
756

757
    // Relate voxel spatial locations to random ray source regions
UNCOV
758
    vector<int> voxel_indices(Nx * Ny * Nz);
×
UNCOV
759
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
UNCOV
760
    vector<double> weight_windows(Nx * Ny * Nz);
×
UNCOV
761
    float min_weight = 1e20;
×
762
#pragma omp parallel for collapse(3) reduction(min : min_weight)
763
    for (int z = 0; z < Nz; z++) {
764
      for (int y = 0; y < Ny; y++) {
765
        for (int x = 0; x < Nx; x++) {
766
          Position sample;
767
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
768
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
769
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
770
          Particle p;
771
          p.r() = sample;
772
          p.r_last() = sample;
773
          p.E() = 1.0;
774
          p.E_last() = 1.0;
775
          p.u() = {1.0, 0.0, 0.0};
776

777
          bool found = exhaustive_find_cell(p);
778
          if (!found) {
779
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
780
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
781
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
782
            continue;
783
          }
784

785
          int i_cell = p.lowest_coord().cell;
786
          int64_t sr = source_region_offsets_[i_cell] + p.cell_instance();
787
          if (RandomRay::mesh_subdivision_enabled_) {
788
            int mesh_idx = base_source_regions_.mesh(sr);
789
            int mesh_bin;
790
            if (mesh_idx == C_NONE) {
791
              mesh_bin = 0;
792
            } else {
793
              mesh_bin = model::meshes[mesh_idx]->get_bin(p.r());
794
            }
795
            SourceRegionKey sr_key {sr, mesh_bin};
796
            auto it = source_region_map_.find(sr_key);
797
            if (it != source_region_map_.end()) {
798
              sr = it->second;
799
            } else {
800
              sr = -1;
801
            }
802
          }
803

804
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
805
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
806

807
          if (variance_reduction::weight_windows.size() == 1) {
808
            WeightWindow ww =
809
              variance_reduction::weight_windows[0]->get_weight_window(p);
810
            float weight = ww.lower_weight;
811
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
812
            if (weight < min_weight)
813
              min_weight = weight;
814
          }
815
        }
816
      }
817
    }
818

819
    double source_normalization_factor =
UNCOV
820
      compute_fixed_source_normalization_factor();
×
821

822
    // Open file for writing
UNCOV
823
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
824

825
    // Write vtk metadata
UNCOV
826
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
UNCOV
827
    std::fprintf(plot, "Dataset File\n");
×
828
    std::fprintf(plot, "BINARY\n");
×
829
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
830
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
831
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
UNCOV
832
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
UNCOV
833
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
834

835
    int64_t num_neg = 0;
×
UNCOV
836
    int64_t num_samples = 0;
×
UNCOV
837
    float min_flux = 0.0;
×
838
    float max_flux = -1.0e20;
×
839
    // Plot multigroup flux data
UNCOV
840
    for (int g = 0; g < negroups_; g++) {
×
841
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
UNCOV
842
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
843
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
844
        int64_t fsr = voxel_indices[i];
×
845
        int64_t source_element = fsr * negroups_ + g;
×
UNCOV
846
        float flux = 0;
×
UNCOV
847
        if (fsr >= 0) {
×
848
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
849
          if (flux < 0.0)
×
850
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
UNCOV
851
              voxel_positions[i], fsr, g);
×
852
        }
853
        if (flux < 0.0) {
×
UNCOV
854
          num_neg++;
×
UNCOV
855
          if (flux < min_flux) {
×
856
            min_flux = flux;
×
857
          }
858
        }
859
        if (flux > max_flux)
×
860
          max_flux = flux;
×
861
        num_samples++;
×
862
        flux = convert_to_big_endian<float>(flux);
×
863
        std::fwrite(&flux, sizeof(float), 1, plot);
×
864
      }
865
    }
866

867
    // Slightly negative fluxes can be normal when sampling corners of linear
868
    // source regions. However, very common and high magnitude negative fluxes
869
    // may indicate numerical instability.
870
    if (num_neg > 0) {
×
871
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
872
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
UNCOV
873
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
874
    }
875

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

885
    // Plot Materials
UNCOV
886
    std::fprintf(plot, "SCALARS Materials int\n");
×
UNCOV
887
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
888
    for (int fsr : voxel_indices) {
×
UNCOV
889
      int mat = -1;
×
UNCOV
890
      if (fsr >= 0)
×
UNCOV
891
        mat = source_regions_.material(fsr);
×
UNCOV
892
      mat = convert_to_big_endian<int>(mat);
×
UNCOV
893
      std::fwrite(&mat, sizeof(int), 1, plot);
×
894
    }
895

896
    // Plot fission source
UNCOV
897
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
UNCOV
898
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
UNCOV
899
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
UNCOV
900
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
UNCOV
901
        int64_t fsr = voxel_indices[i];
×
UNCOV
902
        float total_fission = 0.0;
×
903
        if (fsr >= 0) {
×
UNCOV
904
          int mat = source_regions_.material(fsr);
×
UNCOV
905
          if (mat != MATERIAL_VOID) {
×
906
            for (int g = 0; g < negroups_; g++) {
×
UNCOV
907
              int64_t source_element = fsr * negroups_ + g;
×
UNCOV
908
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
909
              double sigma_f = sigma_f_[mat * negroups_ + g];
×
910
              total_fission += sigma_f * flux;
×
911
            }
912
          }
913
        }
914
        total_fission = convert_to_big_endian<float>(total_fission);
×
915
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
916
      }
917
    } else {
UNCOV
918
      std::fprintf(plot, "SCALARS external_source float\n");
×
919
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
920
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
921
        int64_t fsr = voxel_indices[i];
×
922
        float total_external = 0.0f;
×
923
        if (fsr >= 0) {
×
924
          for (int g = 0; g < negroups_; g++) {
×
925
            total_external += source_regions_.external_source(fsr, g);
×
926
          }
927
        }
UNCOV
928
        total_external = convert_to_big_endian<float>(total_external);
×
UNCOV
929
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
930
      }
931
    }
932

933
    // Plot weight window data
934
    if (variance_reduction::weight_windows.size() == 1) {
×
935
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
936
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
937
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
UNCOV
938
        float weight = weight_windows[i];
×
UNCOV
939
        if (weight == 0.0)
×
UNCOV
940
          weight = min_weight;
×
941
        weight = convert_to_big_endian<float>(weight);
×
942
        std::fwrite(&weight, sizeof(float), 1, plot);
×
943
      }
944
    }
945

946
    std::fclose(plot);
×
947
  }
948
}
949

950
void FlatSourceDomain::apply_external_source_to_source_region(
3,755✔
951
  Discrete* discrete, double strength_factor, SourceRegionHandle& srh)
952
{
953
  srh.external_source_present() = 1;
3,755✔
954

955
  const auto& discrete_energies = discrete->x();
3,755✔
956
  const auto& discrete_probs = discrete->prob();
3,755✔
957

958
  for (int i = 0; i < discrete_energies.size(); i++) {
7,774✔
959
    int g = data::mg.get_group_index(discrete_energies[i]);
4,019✔
960
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,019✔
961
  }
962
}
3,755✔
963

964
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
550✔
965
  Discrete* discrete, double strength_factor, int target_material_id,
966
  const vector<int32_t>& instances)
967
{
968
  Cell& cell = *model::cells[i_cell];
550✔
969

970
  if (cell.type_ != Fill::MATERIAL)
550✔
UNCOV
971
    return;
×
972

973
  for (int j : instances) {
42,130✔
974
    int cell_material_idx = cell.material(j);
41,580✔
975
    int cell_material_id;
976
    if (cell_material_idx == MATERIAL_VOID) {
41,580✔
977
      cell_material_id = MATERIAL_VOID;
352✔
978
    } else {
979
      cell_material_id = model::materials[cell_material_idx]->id();
41,228✔
980
    }
981
    if (target_material_id == C_NONE ||
41,580✔
982
        cell_material_id == target_material_id) {
983
      int64_t source_region = source_region_offsets_[i_cell] + j;
3,740✔
984
      SourceRegionHandle srh =
985
        source_regions_.get_source_region_handle(source_region);
3,740✔
986
      apply_external_source_to_source_region(discrete, strength_factor, srh);
3,740✔
987
    }
988
  }
989
}
990

991
void FlatSourceDomain::apply_external_source_to_cell_and_children(
594✔
992
  int32_t i_cell, Discrete* discrete, double strength_factor,
993
  int32_t target_material_id)
994
{
995
  Cell& cell = *model::cells[i_cell];
594✔
996

997
  if (cell.type_ == Fill::MATERIAL) {
594✔
998
    vector<int> instances(cell.n_instances_);
550✔
999
    std::iota(instances.begin(), instances.end(), 0);
550✔
1000
    apply_external_source_to_cell_instances(
550✔
1001
      i_cell, discrete, strength_factor, target_material_id, instances);
1002
  } else if (target_material_id == C_NONE) {
594✔
1003
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
UNCOV
1004
      cell.get_contained_cells(0, nullptr);
×
UNCOV
1005
    for (const auto& pair : cell_instance_list) {
×
UNCOV
1006
      int32_t i_child_cell = pair.first;
×
UNCOV
1007
      apply_external_source_to_cell_instances(i_child_cell, discrete,
×
UNCOV
1008
        strength_factor, target_material_id, pair.second);
×
1009
    }
1010
  }
1011
}
594✔
1012

1013
void FlatSourceDomain::count_external_source_regions()
1,408✔
1014
{
1015
  n_external_source_regions_ = 0;
1,408✔
1016
#pragma omp parallel for reduction(+ : n_external_source_regions_)
576✔
1017
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
2,794,024✔
1018
    if (source_regions_.external_source_present(sr)) {
2,793,192✔
1019
      n_external_source_regions_++;
285,796✔
1020
    }
1021
  }
1022
}
1,408✔
1023

1024
void FlatSourceDomain::convert_external_sources()
528✔
1025
{
1026
  // Loop over external sources
1027
  for (int es = 0; es < model::external_sources.size(); es++) {
1,056✔
1028

1029
    // Extract source information
1030
    Source* s = model::external_sources[es].get();
528✔
1031
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
528✔
1032
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
528✔
1033
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
528✔
1034
    double strength_factor = is->strength();
528✔
1035

1036
    // If there is no domain constraint specified, then this must be a point
1037
    // source. In this case, we need to find the source region that contains the
1038
    // point source and apply or relate it to the external source.
1039
    if (is->domain_ids().size() == 0) {
528✔
1040

1041
      // Extract the point source coordinate and find the base source region at
1042
      // that point
1043
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
22✔
1044
      GeometryState gs;
22✔
1045
      gs.r() = sp->r();
22✔
1046
      gs.r_last() = sp->r();
22✔
1047
      gs.u() = {1.0, 0.0, 0.0};
22✔
1048
      bool found = exhaustive_find_cell(gs);
22✔
1049
      if (!found) {
22✔
UNCOV
1050
        fatal_error(fmt::format("Could not find cell containing external "
×
1051
                                "point source at {}",
UNCOV
1052
          sp->r()));
×
1053
      }
1054
      int i_cell = gs.lowest_coord().cell;
22✔
1055
      int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
22✔
1056

1057
      if (RandomRay::mesh_subdivision_enabled_) {
22✔
1058
        // If mesh subdivision is enabled, we need to determine which subdivided
1059
        // mesh bin the point source coordinate is in as well
1060
        int mesh_idx = source_regions_.mesh(sr);
22✔
1061
        int mesh_bin;
1062
        if (mesh_idx == C_NONE) {
22✔
UNCOV
1063
          mesh_bin = 0;
×
1064
        } else {
1065
          mesh_bin = model::meshes[mesh_idx]->get_bin(gs.r());
22✔
1066
        }
1067
        // With the source region and mesh bin known, we can use the
1068
        // accompanying SourceRegionKey as a key into a map that stores the
1069
        // corresponding external source index for the point source. Notably, we
1070
        // do not actually apply the external source to any source regions here,
1071
        // as if mesh subdivision is enabled, they haven't actually been
1072
        // discovered & initilized yet. When discovered, they will read from the
1073
        // point_source_map to determine if there are any point source terms
1074
        // that should be applied.
1075
        SourceRegionKey key {sr, mesh_bin};
22✔
1076
        point_source_map_[key] = es;
22✔
1077
      } else {
1078
        // If we are not using mesh subdivision, we can apply the external
1079
        // source directly to the source region as we do for volumetric domain
1080
        // constraint sources.
UNCOV
1081
        SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
×
UNCOV
1082
        apply_external_source_to_source_region(energy, strength_factor, srh);
×
1083
      }
1084

1085
    } else {
22✔
1086
      // If not a point source, then use the volumetric domain constraints to
1087
      // determine which source regions to apply the external source to.
1088
      if (is->domain_type() == Source::DomainType::MATERIAL) {
506✔
1089
        for (int32_t material_id : domain_ids) {
44✔
1090
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
132✔
1091
            apply_external_source_to_cell_and_children(
110✔
1092
              i_cell, energy, strength_factor, material_id);
1093
          }
1094
        }
1095
      } else if (is->domain_type() == Source::DomainType::CELL) {
484✔
1096
        for (int32_t cell_id : domain_ids) {
44✔
1097
          int32_t i_cell = model::cell_map[cell_id];
22✔
1098
          apply_external_source_to_cell_and_children(
22✔
1099
            i_cell, energy, strength_factor, C_NONE);
1100
        }
1101
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
462✔
1102
        for (int32_t universe_id : domain_ids) {
924✔
1103
          int32_t i_universe = model::universe_map[universe_id];
462✔
1104
          Universe& universe = *model::universes[i_universe];
462✔
1105
          for (int32_t i_cell : universe.cells_) {
924✔
1106
            apply_external_source_to_cell_and_children(
462✔
1107
              i_cell, energy, strength_factor, C_NONE);
1108
          }
1109
        }
1110
      }
1111
    }
1112
  } // End loop over external sources
1113

1114
// Divide the fixed source term by sigma t (to save time when applying each
1115
// iteration)
1116
#pragma omp parallel for
216✔
1117
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
503,568✔
1118
    int material = source_regions_.material(sr);
503,256✔
1119
    if (material == MATERIAL_VOID) {
503,256✔
1120
      continue;
26,000✔
1121
    }
1122
    for (int g = 0; g < negroups_; g++) {
1,008,800✔
1123
      double sigma_t = sigma_t_[material * negroups_ + g];
531,544✔
1124
      source_regions_.external_source(sr, g) /= sigma_t;
531,544✔
1125
    }
1126
  }
1127
}
528✔
1128

1129
void FlatSourceDomain::flux_swap()
15,750✔
1130
{
1131
  source_regions_.flux_swap();
15,750✔
1132
}
15,750✔
1133

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

1142
  n_materials_ = data::mg.macro_xs_.size();
880✔
1143
  for (auto& m : data::mg.macro_xs_) {
3,278✔
1144
    for (int g_out = 0; g_out < negroups_; g_out++) {
11,924✔
1145
      if (m.exists_in_model) {
9,526✔
1146
        double sigma_t =
1147
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
9,438✔
1148
        sigma_t_.push_back(sigma_t);
9,438✔
1149

1150
        double nu_Sigma_f =
1151
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
9,438✔
1152
        nu_sigma_f_.push_back(nu_Sigma_f);
9,438✔
1153

1154
        double sigma_f =
1155
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
9,438✔
1156
        sigma_f_.push_back(sigma_f);
9,438✔
1157

1158
        double chi =
1159
          m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
9,438✔
1160
        chi_.push_back(chi);
9,438✔
1161

1162
        for (int g_in = 0; g_in < negroups_; g_in++) {
354,684✔
1163
          double sigma_s =
1164
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
345,246✔
1165
          sigma_s_.push_back(sigma_s);
345,246✔
1166
          // For transport corrected XS data, diagonal elements may be negative.
1167
          // In this case, set a flag to enable transport stabilization for the
1168
          // simulation.
1169
          if (g_out == g_in && sigma_s < 0.0)
345,246✔
1170
            is_transport_stabilization_needed_ = true;
1,210✔
1171
        }
1172
      } else {
1173
        sigma_t_.push_back(0);
88✔
1174
        nu_sigma_f_.push_back(0);
88✔
1175
        sigma_f_.push_back(0);
88✔
1176
        chi_.push_back(0);
88✔
1177
        for (int g_in = 0; g_in < negroups_; g_in++) {
176✔
1178
          sigma_s_.push_back(0);
88✔
1179
        }
1180
      }
1181
    }
1182
  }
1183
}
880✔
1184

1185
void FlatSourceDomain::set_adjoint_sources(const vector<double>& forward_flux)
88✔
1186
{
1187
  // Set the external source to 1/forward_flux. If the forward flux is negative
1188
  // or zero, set the adjoint source to zero, as this is likely a very small
1189
  // source region that we don't need to bother trying to vector particles
1190
  // towards. Flux negativity in random ray is not related to the flux being
1191
  // small in magnitude, but rather due to the source region being physically
1192
  // small in volume and thus having a noisy flux estimate.
1193
#pragma omp parallel for
36✔
1194
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
307,636✔
1195
    for (int g = 0; g < negroups_; g++) {
615,168✔
1196
      double flux = forward_flux[sr * negroups_ + g];
307,584✔
1197
      if (flux <= 0.0) {
307,584✔
1198
        source_regions_.external_source(sr, g) = 0.0;
585✔
1199
      } else {
1200
        source_regions_.external_source(sr, g) = 1.0 / flux;
306,999✔
1201
      }
1202
      if (flux > 0.0) {
307,584✔
1203
        source_regions_.external_source_present(sr) = 1;
279,351✔
1204
      }
1205
    }
1206
  }
1207

1208
  // Divide the fixed source term by sigma t (to save time when applying each
1209
  // iteration)
1210
#pragma omp parallel for
36✔
1211
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
307,636✔
1212
    int material = source_regions_.material(sr);
307,584✔
1213
    if (material == MATERIAL_VOID) {
307,584✔
1214
      continue;
1215
    }
1216
    for (int g = 0; g < negroups_; g++) {
615,168✔
1217
      double sigma_t = sigma_t_[material * negroups_ + g];
307,584✔
1218
      source_regions_.external_source(sr, g) /= sigma_t;
307,584✔
1219
    }
1220
  }
1221
}
88✔
1222

1223
void FlatSourceDomain::transpose_scattering_matrix()
110✔
1224
{
1225
  // Transpose the inner two dimensions for each material
1226
  for (int m = 0; m < n_materials_; ++m) {
418✔
1227
    int material_offset = m * negroups_ * negroups_;
308✔
1228
    for (int i = 0; i < negroups_; ++i) {
880✔
1229
      for (int j = i + 1; j < negroups_; ++j) {
1,496✔
1230
        // Calculate indices of the elements to swap
1231
        int idx1 = material_offset + i * negroups_ + j;
924✔
1232
        int idx2 = material_offset + j * negroups_ + i;
924✔
1233

1234
        // Swap the elements to transpose the matrix
1235
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
924✔
1236
      }
1237
    }
1238
  }
1239
}
110✔
1240

1241
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
770✔
1242
{
1243
  // Ensure array is correct size
1244
  flux.resize(n_source_regions() * negroups_);
770✔
1245
// Serialize the final fluxes for output
1246
#pragma omp parallel for
315✔
1247
  for (int64_t se = 0; se < n_source_elements(); se++) {
2,287,003✔
1248
    flux[se] = source_regions_.scalar_flux_final(se);
2,286,548✔
1249
  }
1250
}
770✔
1251

1252
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
836✔
1253
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1254
  bool is_target_void)
1255
{
1256
  Cell& cell = *model::cells[i_cell];
836✔
1257
  if (cell.type_ != Fill::MATERIAL)
836✔
UNCOV
1258
    return;
×
1259
  for (int32_t j : instances) {
240,548✔
1260
    int cell_material_idx = cell.material(j);
239,712✔
1261
    int cell_material_id = (cell_material_idx == C_NONE)
1262
                             ? C_NONE
239,712✔
1263
                             : model::materials[cell_material_idx]->id();
239,712✔
1264

1265
    if ((target_material_id == C_NONE && !is_target_void) ||
239,712✔
1266
        cell_material_id == target_material_id) {
1267
      int64_t sr = source_region_offsets_[i_cell] + j;
195,712✔
1268
      if (source_regions_.mesh(sr) != C_NONE) {
195,712✔
1269
        // print out the source region that is broken:
UNCOV
1270
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1271
                                "applied, but trying to apply mesh idx {}",
1272
          sr, source_regions_.mesh(sr), mesh_idx));
1273
      }
1274
      source_regions_.mesh(sr) = mesh_idx;
195,712✔
1275
    }
1276
  }
1277
}
1278

1279
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
660✔
1280
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1281
{
1282
  Cell& cell = *model::cells[i_cell];
660✔
1283

1284
  if (cell.type_ == Fill::MATERIAL) {
660✔
1285
    vector<int> instances(cell.n_instances_);
484✔
1286
    std::iota(instances.begin(), instances.end(), 0);
484✔
1287
    apply_mesh_to_cell_instances(
484✔
1288
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1289
  } else if (target_material_id == C_NONE && !is_target_void) {
660✔
1290
    for (int j = 0; j < cell.n_instances_; j++) {
176✔
1291
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1292
        cell.get_contained_cells(j, nullptr);
88✔
1293
      for (const auto& pair : cell_instance_list) {
440✔
1294
        int32_t i_child_cell = pair.first;
352✔
1295
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
352✔
1296
          pair.second, is_target_void);
352✔
1297
      }
1298
    }
88✔
1299
  }
1300
}
660✔
1301

1302
void FlatSourceDomain::apply_meshes()
770✔
1303
{
1304
  // Skip if there are no mappings between mesh IDs and domains
1305
  if (mesh_domain_map_.empty())
770✔
1306
    return;
550✔
1307

1308
  // Loop over meshes
1309
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
550✔
1310
    Mesh* mesh = model::meshes[mesh_idx].get();
330✔
1311
    int mesh_id = mesh->id();
330✔
1312

1313
    // Skip if mesh id is not present in the map
1314
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
330✔
1315
      continue;
22✔
1316

1317
    // Loop over domains associated with the mesh
1318
    for (auto& domain : mesh_domain_map_[mesh_id]) {
616✔
1319
      Source::DomainType domain_type = domain.first;
308✔
1320
      int domain_id = domain.second;
308✔
1321

1322
      if (domain_type == Source::DomainType::MATERIAL) {
308✔
1323
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
264✔
1324
          if (domain_id == C_NONE) {
220✔
UNCOV
1325
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1326
          } else {
1327
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
220✔
1328
          }
1329
        }
1330
      } else if (domain_type == Source::DomainType::CELL) {
264✔
1331
        int32_t i_cell = model::cell_map[domain_id];
44✔
1332
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
44✔
1333
      } else if (domain_type == Source::DomainType::UNIVERSE) {
220✔
1334
        int32_t i_universe = model::universe_map[domain_id];
220✔
1335
        Universe& universe = *model::universes[i_universe];
220✔
1336
        for (int32_t i_cell : universe.cells_) {
616✔
1337
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
396✔
1338
        }
1339
      }
1340
    }
1341
  }
1342
}
1343

1344
void FlatSourceDomain::prepare_base_source_regions()
150✔
1345
{
1346
  std::swap(source_regions_, base_source_regions_);
150✔
1347
  source_regions_.negroups() = base_source_regions_.negroups();
150✔
1348
  source_regions_.is_linear() = base_source_regions_.is_linear();
150✔
1349
}
150✔
1350

1351
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,188,430,710✔
1352
  int64_t sr, int mesh_bin, Position r, double dist, Direction u)
1353
{
1354
  SourceRegionKey sr_key {sr, mesh_bin};
1,188,430,710✔
1355

1356
  // Case 1: Check if the source region key is already present in the permanent
1357
  // map. This is the most common condition, as any source region visited in a
1358
  // previous power iteration will already be present in the permanent map. If
1359
  // the source region key is found, we translate the key into a specific 1D
1360
  // source region index and return a handle its position in the
1361
  // source_regions_ vector.
1362
  auto it = source_region_map_.find(sr_key);
1,188,430,710✔
1363
  if (it != source_region_map_.end()) {
1,188,430,710✔
1364
    int64_t sr = it->second;
1,159,697,925✔
1365
    return source_regions_.get_source_region_handle(sr);
1,159,697,925✔
1366
  }
1367

1368
  // Case 2: Check if the source region key is present in the temporary (thread
1369
  // safe) map. This is a common occurrence in the first power iteration when
1370
  // the source region has already been visited already by some other ray. We
1371
  // begin by locking the temporary map before any operations are performed. The
1372
  // lock is not global over the full data structure -- it will be dependent on
1373
  // which key is used.
1374
  discovered_source_regions_.lock(sr_key);
28,732,785✔
1375

1376
  // If the key is found in the temporary map, then we return a handle to the
1377
  // source region that is stored in the temporary map.
1378
  if (discovered_source_regions_.contains(sr_key)) {
28,732,785✔
1379
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
26,044,095✔
1380
    discovered_source_regions_.unlock(sr_key);
26,044,095✔
1381
    return handle;
26,044,095✔
1382
  }
1383

1384
  // Case 3: The source region key is not present anywhere, but it is only due
1385
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1386
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1387
  // result in the ray tracer detecting an additional (very short) segment
1388
  // though a mesh bin that is actually past the physical source region
1389
  // boundary. This is a result of the the multi-level ray tracing treatment in
1390
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1391
  // result in the wrong surface being selected as the nearest. This can happen
1392
  // in a lattice when there are two directions that both are very close in
1393
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1394
  // treated as being equivalent so alternative logic is used. However, when we
1395
  // go and ray trace on this with the mesh tracer we may go past the surface
1396
  // bounding the current source region.
1397
  //
1398
  // To filter out this case, before we create the new source region, we double
1399
  // check that the actual starting point of this segment (r) is still in the
1400
  // same geometry source region that we started in. If an artifact is detected,
1401
  // we discard the segment (and attenuation through it) as it is not really a
1402
  // valid source region and will have only an infinitessimally small cell
1403
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1404
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1405
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1406
  // consequences for the general ray tracer, so for now we do the below sanity
1407
  // checks before generating phantom source regions. A significant extra cost
1408
  // is incurred in instantiating the GeometryState object and doing a cell
1409
  // lookup, but again, this is going to be an extremely rare thing to check
1410
  // after the first power iteration has completed.
1411

1412
  // Sanity check on source region id
1413
  GeometryState gs;
2,688,690✔
1414
  gs.r() = r + TINY_BIT * u;
2,688,690✔
1415
  gs.u() = {1.0, 0.0, 0.0};
2,688,690✔
1416
  exhaustive_find_cell(gs);
2,688,690✔
1417
  int gs_i_cell = gs.lowest_coord().cell;
2,688,690✔
1418
  int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance();
2,688,690✔
1419
  if (sr_found != sr) {
2,688,690✔
1420
    discovered_source_regions_.unlock(sr_key);
120✔
1421
    SourceRegionHandle handle;
120✔
1422
    handle.is_numerical_fp_artifact_ = true;
120✔
1423
    return handle;
120✔
1424
  }
1425

1426
  // Sanity check on mesh bin
1427
  int mesh_idx = base_source_regions_.mesh(sr);
2,688,570✔
1428
  if (mesh_idx == C_NONE) {
2,688,570✔
UNCOV
1429
    if (mesh_bin != 0) {
×
UNCOV
1430
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1431
      SourceRegionHandle handle;
×
UNCOV
1432
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1433
      return handle;
×
1434
    }
1435
  } else {
1436
    Mesh* mesh = model::meshes[mesh_idx].get();
2,688,570✔
1437
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
2,688,570✔
1438
    if (bin_found != mesh_bin) {
2,688,570✔
UNCOV
1439
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1440
      SourceRegionHandle handle;
×
UNCOV
1441
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1442
      return handle;
×
1443
    }
1444
  }
1445

1446
  // Case 4: The source region key is valid, but is not present anywhere. This
1447
  // condition only occurs the first time the source region is discovered
1448
  // (typically in the first power iteration). In this case, we need to handle
1449
  // creation of the new source region and its storage into the parallel map.
1450
  // The new source region is created by copying the base source region, so as
1451
  // to inherit material, external source, and some flux properties etc. We
1452
  // also pass the base source region id to allow the new source region to
1453
  // know which base source region it is derived from.
1454
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
2,688,570✔
1455
    sr_key, {base_source_regions_.get_source_region_handle(sr), sr});
2,688,570✔
1456
  discovered_source_regions_.unlock(sr_key);
2,688,570✔
1457
  SourceRegionHandle handle {*sr_ptr};
2,688,570✔
1458

1459
  // Check if the new source region contains a point source and apply it if so
1460
  auto it2 = point_source_map_.find(sr_key);
2,688,570✔
1461
  if (it2 != point_source_map_.end()) {
2,688,570✔
1462
    int es = it2->second;
15✔
1463
    auto s = model::external_sources[es].get();
15✔
1464
    auto is = dynamic_cast<IndependentSource*>(s);
15✔
1465
    auto energy = dynamic_cast<Discrete*>(is->energy());
15✔
1466
    double strength_factor = is->strength();
15✔
1467
    apply_external_source_to_source_region(energy, strength_factor, handle);
15✔
1468
    int material = handle.material();
15✔
1469
    if (material != MATERIAL_VOID) {
15✔
1470
      for (int g = 0; g < negroups_; g++) {
30✔
1471
        double sigma_t = sigma_t_[material * negroups_ + g];
15✔
1472
        handle.external_source(g) /= sigma_t;
15✔
1473
      }
1474
    }
1475
  }
1476

1477
  return handle;
2,688,570✔
1478
}
2,688,690✔
1479

1480
void FlatSourceDomain::finalize_discovered_source_regions()
4,050✔
1481
{
1482
  // Extract keys for entries with a valid volume.
1483
  vector<SourceRegionKey> keys;
4,050✔
1484
  for (const auto& pair : discovered_source_regions_) {
2,692,620✔
1485
    if (pair.second.volume_ > 0.0) {
2,688,570✔
1486
      keys.push_back(pair.first);
2,550,240✔
1487
    }
1488
  }
1489

1490
  if (!keys.empty()) {
4,050✔
1491
    // Sort the keys, so as to ensure reproducible ordering given that source
1492
    // regions may have been added to discovered_source_regions_ in an arbitrary
1493
    // order due to shared memory threading.
1494
    std::sort(keys.begin(), keys.end());
600✔
1495

1496
    // Append the source regions in the sorted key order.
1497
    for (const auto& key : keys) {
2,550,840✔
1498
      const SourceRegion& sr = discovered_source_regions_[key];
2,550,240✔
1499
      source_region_map_[key] = source_regions_.n_source_regions();
2,550,240✔
1500
      source_regions_.push_back(sr);
2,550,240✔
1501
    }
1502

1503
    // If any new source regions were discovered, we need to update the
1504
    // tally mapping between source regions and tally bins.
1505
    mapped_all_tallies_ = false;
600✔
1506
  }
1507

1508
  discovered_source_regions_.clear();
4,050✔
1509
}
4,050✔
1510

1511
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1512
//
1513
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1514
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1515
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1516
// https://doi.org/10.1016/j.anucene.2018.10.036.
1517
void FlatSourceDomain::apply_transport_stabilization()
15,750✔
1518
{
1519
  // Don't do anything if all in-group scattering
1520
  // cross sections are positive
1521
  if (!is_transport_stabilization_needed_) {
15,750✔
1522
    return;
15,450✔
1523
  }
1524

1525
  // Apply the stabilization factor to all source elements
1526
#pragma omp parallel for
120✔
1527
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
2,340✔
1528
    int material = source_regions_.material(sr);
2,160✔
1529
    if (material == MATERIAL_VOID) {
2,160✔
1530
      continue;
1531
    }
1532
    for (int g = 0; g < negroups_; g++) {
153,360✔
1533
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1534
      // negative
1535
      double sigma_s =
1536
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g];
151,200✔
1537
      if (sigma_s < 0.0) {
151,200✔
1538
        double sigma_t = sigma_t_[material * negroups_ + g];
39,600✔
1539
        double phi_new = source_regions_.scalar_flux_new(sr, g);
39,600✔
1540
        double phi_old = source_regions_.scalar_flux_old(sr, g);
39,600✔
1541

1542
        // Equation 18 in the above Gunow et al. 2019 paper. For a default
1543
        // rho of 1.0, this ensures there are no negative diagonal elements
1544
        // in the iteration matrix. A lesser rho could be used (or exposed
1545
        // as a user input parameter) to reduce the negative impact on
1546
        // convergence rate though would need to be experimentally tested to see
1547
        // if it doesn't become unstable. rho = 1.0 is good as it gives the
1548
        // highest assurance of stability, and the impacts on convergence rate
1549
        // are pretty mild.
1550
        double D = diagonal_stabilization_rho_ * sigma_s / sigma_t;
39,600✔
1551

1552
        // Equation 16 in the above Gunow et al. 2019 paper
1553
        source_regions_.scalar_flux_new(sr, g) =
39,600✔
1554
          (phi_new - D * phi_old) / (1.0 - D);
39,600✔
1555
      }
1556
    }
1557
  }
1558
}
1559

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