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

openmc-dev / openmc / 17929219513

22 Sep 2025 09:36PM UTC coverage: 85.119% (-0.08%) from 85.197%
17929219513

Pull #3576

github

web-flow
Merge a2e2adf20 into ca63da91b
Pull Request #3576: Random Ray Base Source Region Refactor

159 of 166 new or added lines in 6 files covered. (95.78%)

48 existing lines in 5 files now uncovered.

53066 of 62343 relevant lines covered (85.12%)

40618625.22 hits per line

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

78.87
/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_)
596✔
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;
596✔
44
  for (const auto& c : model::cells) {
5,087✔
45
    if (c->type_ != Fill::MATERIAL) {
4,491✔
46
      source_region_offsets_.push_back(-1);
2,160✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
2,331✔
49
      base_source_regions += c->n_instances();
2,331✔
50
    }
51
  }
52

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

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

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

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

79
void FlatSourceDomain::batch_reset()
12,612✔
80
{
81
// Reset scalar fluxes and iteration volume tallies to zero
82
#pragma omp parallel for
6,312✔
83
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
38,692,542✔
84
    source_regions_.volume(sr) = 0.0;
38,686,242✔
85
    source_regions_.volume_sq(sr) = 0.0;
38,686,242✔
86
  }
87

88
#pragma omp parallel for
6,312✔
89
  for (int64_t se = 0; se < n_source_elements(); se++) {
43,766,454✔
90
    source_regions_.scalar_flux_new(se) = 0.0;
43,760,154✔
91
  }
92
}
12,612✔
93

94
void FlatSourceDomain::accumulate_iteration_flux()
5,106✔
95
{
96
#pragma omp parallel for
2,556✔
97
  for (int64_t se = 0; se < n_source_elements(); se++) {
19,909,830✔
98
    source_regions_.scalar_flux_final(se) +=
19,907,280✔
99
      source_regions_.scalar_flux_new(se);
19,907,280✔
100
  }
101
}
5,106✔
102

103
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
47,582,268✔
104
{
105
  // Reset all source regions to zero (important for void regions)
106
  for (int g = 0; g < negroups_; g++) {
100,912,548✔
107
    srh.source(g) = 0.0;
53,330,280✔
108
  }
109

110
  // Add scattering + fission source
111
  int material = srh.material();
47,582,268✔
112
  if (material != MATERIAL_VOID) {
47,582,268✔
113
    double inverse_k_eff = 1.0 / k_eff_;
47,101,500✔
114
    for (int g_out = 0; g_out < negroups_; g_out++) {
99,950,244✔
115
      double sigma_t = sigma_t_[material * negroups_ + g_out];
52,848,744✔
116
      double scatter_source = 0.0;
52,848,744✔
117
      double fission_source = 0.0;
52,848,744✔
118

119
      for (int g_in = 0; g_in < negroups_; g_in++) {
145,926,816✔
120
        double scalar_flux = srh.scalar_flux_old(g_in);
93,078,072✔
121
        double sigma_s =
122
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
93,078,072✔
123
        double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
93,078,072✔
124
        double chi = chi_[material * negroups_ + g_out];
93,078,072✔
125

126
        scatter_source += sigma_s * scalar_flux;
93,078,072✔
127
        fission_source += nu_sigma_f * scalar_flux * chi;
93,078,072✔
128
      }
129
      srh.source(g_out) =
52,848,744✔
130
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
52,848,744✔
131
    }
132
  }
133

134
  // Add external source if in fixed source mode
135
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
47,582,268✔
136
    for (int g = 0; g < negroups_; g++) {
97,303,044✔
137
      srh.source(g) += srh.external_source(g);
50,218,044✔
138
    }
139
  }
140
}
47,582,268✔
141

142
// Compute new estimate of scattering + fission sources in each source region
143
// based on the flux estimate from the previous iteration.
144
void FlatSourceDomain::update_all_neutron_sources()
12,612✔
145
{
146
  simulation::time_update_src.start();
12,612✔
147

148
#pragma omp parallel for
6,312✔
149
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
38,692,542✔
150
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
38,686,242✔
151
    update_single_neutron_source(srh);
38,686,242✔
152
  }
153

154
  simulation::time_update_src.stop();
12,612✔
155
}
12,612✔
156

157
// Normalizes flux and updates simulation-averaged volume estimate
158
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
5,592✔
159
  double total_active_distance_per_iteration)
160
{
161
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
5,592✔
162
  double volume_normalization_factor =
5,592✔
163
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
5,592✔
164

165
// Normalize scalar flux to total distance travelled by all rays this
166
// iteration
167
#pragma omp parallel for
2,802✔
168
  for (int64_t se = 0; se < n_source_elements(); se++) {
26,629,602✔
169
    source_regions_.scalar_flux_new(se) *= normalization_factor;
26,626,812✔
170
  }
171

172
// Accumulate cell-wise ray length tallies collected this iteration, then
173
// update the simulation-averaged cell-wise volume estimates
174
#pragma omp parallel for
2,802✔
175
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
23,756,154✔
176
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
23,753,364✔
177
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
23,753,364✔
178
    source_regions_.volume_naive(sr) =
47,506,728✔
179
      source_regions_.volume(sr) * normalization_factor;
23,753,364✔
180
    source_regions_.volume_sq(sr) =
47,506,728✔
181
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
23,753,364✔
182
    source_regions_.volume(sr) =
23,753,364✔
183
      source_regions_.volume_t(sr) * volume_normalization_factor;
23,753,364✔
184
  }
185
}
5,592✔
186

187
void FlatSourceDomain::set_flux_to_flux_plus_source(
47,151,224✔
188
  int64_t sr, double volume, int g)
189
{
190
  int material = source_regions_.material(sr);
47,151,224✔
191
  if (material == MATERIAL_VOID) {
47,151,224✔
192
    source_regions_.scalar_flux_new(sr, g) /= volume;
961,536✔
193
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
961,536✔
194
      source_regions_.scalar_flux_new(sr, g) +=
961,536✔
195
        0.5f * source_regions_.external_source(sr, g) *
961,536✔
196
        source_regions_.volume_sq(sr);
961,536✔
197
    }
198
  } else {
199
    double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g];
46,189,688✔
200
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
46,189,688✔
201
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
46,189,688✔
202
  }
203
}
47,151,224✔
204

205
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,682✔
206
{
207
  source_regions_.scalar_flux_new(sr, g) =
3,364✔
208
    source_regions_.scalar_flux_old(sr, g);
1,682✔
209
}
1,682✔
210

211
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
9,739,142✔
212
{
213
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
9,739,142✔
214
}
9,739,142✔
215

216
// Combine transport flux contributions and flat source contributions from the
217
// previous iteration to generate this iteration's estimate of scalar flux.
218
int64_t FlatSourceDomain::add_source_to_scalar_flux()
12,612✔
219
{
220
  int64_t n_hits = 0;
12,612✔
221
  double inverse_batch = 1.0 / simulation::current_batch;
12,612✔
222

223
#pragma omp parallel for reduction(+ : n_hits)
6,312✔
224
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
39,903,462✔
225

226
    double volume_simulation_avg = source_regions_.volume(sr);
39,897,162✔
227
    double volume_iteration = source_regions_.volume_naive(sr);
39,897,162✔
228

229
    // Increment the number of hits if cell was hit this iteration
230
    if (volume_iteration) {
39,897,162✔
231
      n_hits++;
35,026,176✔
232
    }
233

234
    // Set the SR to small status if its expected number of hits
235
    // per iteration is less than 1.5
236
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
39,897,162✔
237
      source_regions_.is_small(sr) = 1;
8,175,246✔
238
    } else {
239
      source_regions_.is_small(sr) = 0;
31,721,916✔
240
    }
241

242
    // The volume treatment depends on the volume estimator type
243
    // and whether or not an external source is present in the cell.
244
    double volume;
245
    switch (volume_estimator_) {
39,897,162✔
246
    case RandomRayVolumeEstimator::NAIVE:
933,120✔
247
      volume = volume_iteration;
933,120✔
248
      break;
933,120✔
249
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
518,400✔
250
      volume = volume_simulation_avg;
518,400✔
251
      break;
518,400✔
252
    case RandomRayVolumeEstimator::HYBRID:
38,445,642✔
253
      if (source_regions_.external_source_present(sr) ||
71,866,164✔
254
          source_regions_.is_small(sr)) {
33,420,522✔
255
        volume = volume_iteration;
13,199,910✔
256
      } else {
257
        volume = volume_simulation_avg;
25,245,732✔
258
      }
259
      break;
38,445,642✔
260
    default:
261
      fatal_error("Invalid volume estimator type");
262
    }
263

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

301
  // Return the number of source regions that were hit this iteration
302
  return n_hits;
12,612✔
303
}
304

305
// Generates new estimate of k_eff based on the differences between this
306
// iteration's estimate of the scalar flux and the last iteration's estimate.
307
void FlatSourceDomain::compute_k_eff()
2,280✔
308
{
309
  double fission_rate_old = 0;
2,280✔
310
  double fission_rate_new = 0;
2,280✔
311

312
  // Vector for gathering fission source terms for Shannon entropy calculation
313
  vector<float> p(n_source_regions(), 0.0f);
2,280✔
314

315
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
1,140✔
316
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
370,488✔
317

318
    // If simulation averaged volume is zero, don't include this cell
319
    double volume = source_regions_.volume(sr);
369,348✔
320
    if (volume == 0.0) {
369,348✔
321
      continue;
322
    }
323

324
    int material = source_regions_.material(sr);
369,348✔
325
    if (material == MATERIAL_VOID) {
369,348✔
326
      continue;
327
    }
328

329
    double sr_fission_source_old = 0;
369,348✔
330
    double sr_fission_source_new = 0;
369,348✔
331

332
    for (int g = 0; g < negroups_; g++) {
2,850,384✔
333
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
2,481,036✔
334
      sr_fission_source_old +=
2,481,036✔
335
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,481,036✔
336
      sr_fission_source_new +=
2,481,036✔
337
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,481,036✔
338
    }
339

340
    // Compute total fission rates in FSR
341
    sr_fission_source_old *= volume;
369,348✔
342
    sr_fission_source_new *= volume;
369,348✔
343

344
    // Accumulate totals
345
    fission_rate_old += sr_fission_source_old;
369,348✔
346
    fission_rate_new += sr_fission_source_new;
369,348✔
347

348
    // Store total fission rate in the FSR for Shannon calculation
349
    p[sr] = sr_fission_source_new;
369,348✔
350
  }
351

352
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
2,280✔
353

354
  double H = 0.0;
2,280✔
355
  // defining an inverse sum for better performance
356
  double inverse_sum = 1 / fission_rate_new;
2,280✔
357

358
#pragma omp parallel for reduction(+ : H)
1,140✔
359
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
370,488✔
360
    // Only if FSR has non-negative and non-zero fission source
361
    if (p[sr] > 0.0f) {
369,348✔
362
      // Normalize to total weight of bank sites. p_i for better performance
363
      float p_i = p[sr] * inverse_sum;
157,194✔
364
      // Sum values to obtain Shannon entropy.
365
      H -= p_i * std::log2(p_i);
157,194✔
366
    }
367
  }
368

369
  // Adds entropy value to shared entropy vector in openmc namespace.
370
  simulation::entropy.push_back(H);
2,280✔
371

372
  k_eff_ = k_eff_new;
2,280✔
373
}
2,280✔
374

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

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

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

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

413
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
781✔
414
{
415
  openmc::simulation::time_tallies.start();
781✔
416

417
  // Tracks if we've generated a mapping yet for all source regions.
418
  bool all_source_regions_mapped = true;
781✔
419

420
// Attempt to generate mapping for all source regions
421
#pragma omp parallel for
391✔
422
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,211,310✔
423

424
    // If this source region has not been hit by a ray yet, then
425
    // we aren't going to be able to map it, so skip it.
426
    if (!source_regions_.position_recorded(sr)) {
1,210,920✔
427
      all_source_regions_mapped = false;
428
      continue;
429
    }
430

431
    // A particle located at the recorded midpoint of a ray
432
    // crossing through this source region is used to estabilish
433
    // the spatial location of the source region
434
    Particle p;
1,210,920✔
435
    p.r() = source_regions_.position(sr);
1,210,920✔
436
    p.r_last() = source_regions_.position(sr);
1,210,920✔
437
    p.u() = {1.0, 0.0, 0.0};
1,210,920✔
438
    bool found = exhaustive_find_cell(p);
1,210,920✔
439

440
    // Loop over energy groups (so as to support energy filters)
441
    for (int g = 0; g < negroups_; g++) {
2,591,616✔
442

443
      // Set particle to the current energy
444
      p.g() = g;
1,380,696✔
445
      p.g_last() = g;
1,380,696✔
446
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,380,696✔
447
      p.E_last() = p.E();
1,380,696✔
448

449
      int64_t source_element = sr * negroups_ + g;
1,380,696✔
450

451
      // If this task has already been populated, we don't need to do
452
      // it again.
453
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,380,696✔
454
        continue;
455
      }
456

457
      // Loop over all active tallies. This logic is essentially identical
458
      // to what happens when scanning for applicable tallies during
459
      // MC transport.
460
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
5,289,360✔
461
        Tally& tally {*model::tallies[i_tally]};
3,908,664✔
462

463
        // Initialize an iterator over valid filter bin combinations.
464
        // If there are no valid combinations, use a continue statement
465
        // to ensure we skip the assume_separate break below.
466
        auto filter_iter = FilterBinIter(tally, p);
3,908,664✔
467
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,908,664✔
468
        if (filter_iter == end)
3,908,664✔
469
          continue;
2,360,256✔
470

471
        // Loop over filter bins.
472
        for (; filter_iter != end; ++filter_iter) {
3,096,816✔
473
          auto filter_index = filter_iter.index_;
1,548,408✔
474
          auto filter_weight = filter_iter.weight_;
1,548,408✔
475

476
          // Loop over scores
477
          for (int score = 0; score < tally.scores_.size(); score++) {
3,480,864✔
478
            auto score_bin = tally.scores_[score];
1,932,456✔
479
            // If a valid tally, filter, and score combination has been found,
480
            // then add it to the list of tally tasks for this source element.
481
            TallyTask task(i_tally, filter_index, score, score_bin);
1,932,456✔
482
            source_regions_.tally_task(sr, g).push_back(task);
1,932,456✔
483

484
            // Also add this task to the list of volume tasks for this source
485
            // region.
486
            source_regions_.volume_task(sr).insert(task);
1,932,456✔
487
          }
488
        }
489
      }
490
      // Reset all the filter matches for the next tally event.
491
      for (auto& match : p.filter_matches())
5,833,896✔
492
        match.bins_present_ = false;
4,453,200✔
493
    }
494
  }
1,210,920✔
495
  openmc::simulation::time_tallies.stop();
781✔
496

497
  mapped_all_tallies_ = all_source_regions_mapped;
781✔
498
}
781✔
499

500
// Set the volume accumulators to zero for all tallies
501
void FlatSourceDomain::reset_tally_volumes()
5,106✔
502
{
503
  if (volume_normalized_flux_tallies_) {
5,106✔
504
#pragma omp parallel for
2,070✔
505
    for (int i = 0; i < tally_volumes_.size(); i++) {
7,800✔
506
      auto& tensor = tally_volumes_[i];
5,730✔
507
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
5,730✔
508
    }
509
  }
510
}
5,106✔
511

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

528
  // Step 1 is to sum over all source regions and energy groups to get the
529
  // total external source strength in the simulation.
530
  double simulation_external_source_strength = 0.0;
4,072✔
531
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,050✔
532
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
17,489,286✔
533
    int material = source_regions_.material(sr);
17,487,264✔
534
    double volume = source_regions_.volume(sr) * simulation_volume_;
17,487,264✔
535
    for (int g = 0; g < negroups_; g++) {
35,625,984✔
536
      // For non-void regions, we store the external source pre-divided by
537
      // sigma_t. We need to multiply non-void regions back up by sigma_t
538
      // to get the total source strength in the expected units.
539
      double sigma_t = 1.0;
18,138,720✔
540
      if (material != MATERIAL_VOID) {
18,138,720✔
541
        sigma_t = sigma_t_[material * negroups_ + g];
17,886,720✔
542
      }
543
      simulation_external_source_strength +=
18,138,720✔
544
        source_regions_.external_source(sr, g) * sigma_t * volume;
18,138,720✔
545
    }
546
  }
547

548
  // Step 2 is to determine the total user-specified external source strength
549
  double user_external_source_strength = 0.0;
4,072✔
550
  for (auto& ext_source : model::external_sources) {
8,144✔
551
    user_external_source_strength += ext_source->strength();
4,072✔
552
  }
553

554
  // The correction factor is the ratio of the user-specified external source
555
  // strength to the simulation external source strength.
556
  double source_normalization_factor =
4,072✔
557
    user_external_source_strength / simulation_external_source_strength;
558

559
  return source_normalization_factor;
4,072✔
560
}
561

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

571
void FlatSourceDomain::random_ray_tally()
5,106✔
572
{
573
  openmc::simulation::time_tallies.start();
5,106✔
574

575
  // Reset our tally volumes to zero
576
  reset_tally_volumes();
5,106✔
577

578
  double source_normalization_factor =
579
    compute_fixed_source_normalization_factor();
5,106✔
580

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

597
    double material = source_regions_.material(sr);
18,249,840✔
598
    for (int g = 0; g < negroups_; g++) {
38,157,120✔
599
      double flux =
600
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
19,907,280✔
601

602
      // Determine numerical score value
603
      for (auto& task : source_regions_.tally_task(sr, g)) {
47,000,160✔
604
        double score = 0.0;
27,092,880✔
605
        switch (task.score_type) {
27,092,880✔
606

607
        case SCORE_FLUX:
23,286,000✔
608
          score = flux * volume;
23,286,000✔
609
          break;
23,286,000✔
610

611
        case SCORE_TOTAL:
612
          if (material != MATERIAL_VOID) {
613
            score = flux * volume * sigma_t_[material * negroups_ + g];
614
          }
615
          break;
616

617
        case SCORE_FISSION:
1,903,440✔
618
          if (material != MATERIAL_VOID) {
1,903,440✔
619
            score = flux * volume * sigma_f_[material * negroups_ + g];
1,903,440✔
620
          }
621
          break;
1,903,440✔
622

623
        case SCORE_NU_FISSION:
1,903,440✔
624
          if (material != MATERIAL_VOID) {
1,903,440✔
625
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
1,903,440✔
626
          }
627
          break;
1,903,440✔
628

629
        case SCORE_EVENTS:
630
          score = 1.0;
631
          break;
632

633
        default:
634
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
635
                      "total, fission, nu-fission, and events are supported in "
636
                      "random ray mode.");
637
          break;
638
        }
639
        // Apply score to the appropriate tally bin
640
        Tally& tally {*model::tallies[task.tally_idx]};
27,092,880✔
641
#pragma omp atomic
642
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
27,092,880✔
643
          score;
644
      }
645
    }
646

647
    // For flux tallies, the total volume of the spatial region is needed
648
    // for normalizing the flux. We store this volume in a separate tensor.
649
    // We only contribute to each volume tally bin once per FSR.
650
    if (volume_normalized_flux_tallies_) {
18,249,840✔
651
      for (const auto& task : source_regions_.volume_task(sr)) {
42,925,440✔
652
        if (task.score_type == SCORE_FLUX) {
24,848,640✔
653
#pragma omp atomic
654
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
22,503,360✔
655
            volume;
656
        }
657
      }
658
    }
659
  } // end FSR loop
660

661
  // Normalize any flux scores by the total volume of the FSRs scoring to that
662
  // bin. To do this, we loop over all tallies, and then all filter bins,
663
  // and then scores. For each score, we check the tally data structure to
664
  // see what index that score corresponds to. If that score is a flux score,
665
  // then we divide it by volume.
666
  if (volume_normalized_flux_tallies_) {
5,106✔
667
    for (int i = 0; i < model::tallies.size(); i++) {
15,600✔
668
      Tally& tally {*model::tallies[i]};
11,460✔
669
#pragma omp parallel for
5,730✔
670
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
844,650✔
671
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,699,680✔
672
          auto score_type = tally.scores_[score_idx];
860,760✔
673
          if (score_type == SCORE_FLUX) {
860,760✔
674
            double vol = tally_volumes_[i](bin, score_idx);
838,920✔
675
            if (vol > 0.0) {
838,920✔
676
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
838,920✔
677
            }
678
          }
679
        }
680
      }
681
    }
682
  }
683

684
  openmc::simulation::time_tallies.stop();
5,106✔
685
}
5,106✔
686

687
double FlatSourceDomain::evaluate_flux_at_point(
×
688
  Position r, int64_t sr, int g) const
689
{
690
  return source_regions_.scalar_flux_final(sr, g) /
×
691
         (settings::n_batches - settings::n_inactive);
×
692
}
693

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

708
  // Print header information
709
  print_plot();
×
710

711
  // Outer loop over plots
NEW
712
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
713

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

717
    // Random ray plots only support voxel plots
718
    if (!openmc_plot) {
×
719
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
720
                          "is allowed in random ray mode.",
721
        plt));
722
      continue;
×
723
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
724
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
725
                          "is allowed in random ray mode.",
726
        plt));
727
      continue;
×
728
    }
729

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

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

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

772
          bool found = exhaustive_find_cell(p);
773
          if (!found) {
774
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
775
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
776
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
777
            continue;
778
          }
779

780
          SourceRegionKey sr_key = lookup_source_region_key(p);
781
          int64_t sr = -1;
782
          auto it = source_region_map_.find(sr_key);
783
          if (it != source_region_map_.end()) {
784
            sr = it->second;
785
          }
786

787
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
788
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
789

790
          if (variance_reduction::weight_windows.size() == 1) {
791
            WeightWindow ww =
792
              variance_reduction::weight_windows[0]->get_weight_window(p);
793
            float weight = ww.lower_weight;
794
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
795
            if (weight < min_weight)
796
              min_weight = weight;
797
          }
798
        }
799
      }
800
    }
801

802
    double source_normalization_factor =
803
      compute_fixed_source_normalization_factor();
×
804

805
    // Open file for writing
806
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
807

808
    // Write vtk metadata
809
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
810
    std::fprintf(plot, "Dataset File\n");
×
811
    std::fprintf(plot, "BINARY\n");
×
812
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
813
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
814
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
815
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
816
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
817

818
    int64_t num_neg = 0;
×
819
    int64_t num_samples = 0;
×
820
    float min_flux = 0.0;
×
821
    float max_flux = -1.0e20;
×
822
    // Plot multigroup flux data
823
    for (int g = 0; g < negroups_; g++) {
×
824
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
825
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
826
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
827
        int64_t fsr = voxel_indices[i];
×
828
        int64_t source_element = fsr * negroups_ + g;
×
829
        float flux = 0;
×
830
        if (fsr >= 0) {
×
831
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
832
          if (flux < 0.0)
×
833
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
834
              voxel_positions[i], fsr, g);
×
835
        }
836
        if (flux < 0.0) {
×
837
          num_neg++;
×
838
          if (flux < min_flux) {
×
839
            min_flux = flux;
×
840
          }
841
        }
842
        if (flux > max_flux)
×
843
          max_flux = flux;
×
844
        num_samples++;
×
845
        flux = convert_to_big_endian<float>(flux);
×
846
        std::fwrite(&flux, sizeof(float), 1, plot);
×
847
      }
848
    }
849

850
    // Slightly negative fluxes can be normal when sampling corners of linear
851
    // source regions. However, very common and high magnitude negative fluxes
852
    // may indicate numerical instability.
853
    if (num_neg > 0) {
×
854
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
855
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
856
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
857
    }
858

859
    // Plot FSRs
860
    std::fprintf(plot, "SCALARS FSRs float\n");
×
861
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
862
    for (int fsr : voxel_indices) {
×
863
      float value = future_prn(10, fsr);
×
864
      value = convert_to_big_endian<float>(value);
×
865
      std::fwrite(&value, sizeof(float), 1, plot);
×
866
    }
867

868
    // Plot Materials
869
    std::fprintf(plot, "SCALARS Materials int\n");
×
870
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
871
    for (int fsr : voxel_indices) {
×
872
      int mat = -1;
×
873
      if (fsr >= 0)
×
874
        mat = source_regions_.material(fsr);
×
875
      mat = convert_to_big_endian<int>(mat);
×
876
      std::fwrite(&mat, sizeof(int), 1, plot);
×
877
    }
878

879
    // Plot fission source
880
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
881
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
882
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
883
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
884
        int64_t fsr = voxel_indices[i];
×
885
        float total_fission = 0.0;
×
886
        if (fsr >= 0) {
×
887
          int mat = source_regions_.material(fsr);
×
888
          if (mat != MATERIAL_VOID) {
×
889
            for (int g = 0; g < negroups_; g++) {
×
890
              int64_t source_element = fsr * negroups_ + g;
×
891
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
892
              double sigma_f = sigma_f_[mat * negroups_ + g];
×
893
              total_fission += sigma_f * flux;
×
894
            }
895
          }
896
        }
897
        total_fission = convert_to_big_endian<float>(total_fission);
×
898
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
899
      }
900
    } else {
901
      std::fprintf(plot, "SCALARS external_source float\n");
×
902
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
903
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
904
        int64_t fsr = voxel_indices[i];
×
905
        int mat = source_regions_.material(fsr);
×
906
        float total_external = 0.0f;
×
907
        if (fsr >= 0) {
×
908
          for (int g = 0; g < negroups_; g++) {
×
909
            // External sources are already divided by sigma_t, so we need to
910
            // multiply it back to get the true external source.
911
            double sigma_t = 1.0;
×
912
            if (mat != MATERIAL_VOID) {
×
913
              sigma_t = sigma_t_[mat * negroups_ + g];
×
914
            }
915
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
916
          }
917
        }
918
        total_external = convert_to_big_endian<float>(total_external);
×
919
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
920
      }
921
    }
922

923
    // Plot weight window data
924
    if (variance_reduction::weight_windows.size() == 1) {
×
925
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
926
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
927
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
928
        float weight = weight_windows[i];
×
929
        if (weight == 0.0)
×
930
          weight = min_weight;
×
931
        weight = convert_to_big_endian<float>(weight);
×
932
        std::fwrite(&weight, sizeof(float), 1, plot);
×
933
      }
934
    }
935

936
    std::fclose(plot);
×
937
  }
938
}
939

940
void FlatSourceDomain::apply_external_source_to_source_region(
4,813✔
941
  int src_idx, SourceRegionHandle& srh)
942
{
943
  auto s = model::external_sources[src_idx].get();
4,813✔
944
  auto is = dynamic_cast<IndependentSource*>(s);
4,813✔
945
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,813✔
946
  double strength_factor = is->strength();
4,813✔
947
  const auto& discrete_energies = discrete->x();
4,813✔
948
  const auto& discrete_probs = discrete->prob();
4,813✔
949

950
  srh.external_source_present() = 1;
4,813✔
951

952
  for (int i = 0; i < discrete_energies.size(); i++) {
9,770✔
953
    int g = data::mg.get_group_index(discrete_energies[i]);
4,957✔
954
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,957✔
955
  }
956
}
4,813✔
957

958
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
425✔
959
  int src_idx, int target_material_id, const vector<int32_t>& instances)
960
{
961
  Cell& cell = *model::cells[i_cell];
425✔
962

963
  if (cell.type_ != Fill::MATERIAL)
425✔
964
    return;
×
965

966
  for (int j : instances) {
32,555✔
967
    int cell_material_idx = cell.material(j);
32,130✔
968
    int cell_material_id;
969
    if (cell_material_idx == MATERIAL_VOID) {
32,130✔
970
      cell_material_id = MATERIAL_VOID;
272✔
971
    } else {
972
      cell_material_id = model::materials[cell_material_idx]->id();
31,858✔
973
    }
974
    if (target_material_id == C_NONE ||
32,130✔
975
        cell_material_id == target_material_id) {
976
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,890✔
977
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,890✔
978
    }
979
  }
980
}
981

982
void FlatSourceDomain::apply_external_source_to_cell_and_children(
459✔
983
  int32_t i_cell, int src_idx, int32_t target_material_id)
984
{
985
  Cell& cell = *model::cells[i_cell];
459✔
986

987
  if (cell.type_ == Fill::MATERIAL) {
459✔
988
    vector<int> instances(cell.n_instances());
425✔
989
    std::iota(instances.begin(), instances.end(), 0);
425✔
990
    apply_external_source_to_cell_instances(
425✔
991
      i_cell, src_idx, target_material_id, instances);
992
  } else if (target_material_id == C_NONE) {
459✔
993
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
994
      cell.get_contained_cells(0, nullptr);
×
995
    for (const auto& pair : cell_instance_list) {
×
996
      int32_t i_child_cell = pair.first;
×
NEW
997
      apply_external_source_to_cell_instances(
×
NEW
998
        i_child_cell, src_idx, target_material_id, pair.second);
×
999
    }
1000
  }
1001
}
459✔
1002

1003
void FlatSourceDomain::count_external_source_regions()
1,091✔
1004
{
1005
  n_external_source_regions_ = 0;
1,091✔
1006
#pragma omp parallel for reduction(+ : n_external_source_regions_)
579✔
1007
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,399,520✔
1008
    if (source_regions_.external_source_present(sr)) {
1,399,008✔
1009
      n_external_source_regions_++;
188,604✔
1010
    }
1011
  }
1012
}
1,091✔
1013

1014
void FlatSourceDomain::convert_external_sources()
409✔
1015
{
1016
  // Loop over external sources
1017
  for (int es = 0; es < model::external_sources.size(); es++) {
818✔
1018

1019
    // Extract source information
1020
    Source* s = model::external_sources[es].get();
409✔
1021
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
409✔
1022
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
409✔
1023
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
409✔
1024
    double strength_factor = is->strength();
409✔
1025

1026
    // If there is no domain constraint specified, then this must be a point
1027
    // source. In this case, we need to find the source region that contains the
1028
    // point source and apply or relate it to the external source.
1029
    if (is->domain_ids().size() == 0) {
409✔
1030

1031
      // Extract the point source coordinate and find the base source region at
1032
      // that point
1033
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
18✔
1034
      GeometryState gs;
18✔
1035
      gs.r() = sp->r();
18✔
1036
      gs.r_last() = sp->r();
18✔
1037
      gs.u() = {1.0, 0.0, 0.0};
18✔
1038
      bool found = exhaustive_find_cell(gs);
18✔
1039
      if (!found) {
18✔
1040
        fatal_error(fmt::format("Could not find cell containing external "
×
1041
                                "point source at {}",
1042
          sp->r()));
×
1043
      }
1044
      SourceRegionKey key = lookup_source_region_key(gs);
18✔
1045

1046
      // With the source region and mesh bin known, we can use the
1047
      // accompanying SourceRegionKey as a key into a map that stores the
1048
      // corresponding external source index for the point source. Notably, we
1049
      // do not actually apply the external source to any source regions here,
1050
      // as if mesh subdivision is enabled, they haven't actually been
1051
      // discovered & initilized yet. When discovered, they will read from the
1052
      // external_source_map to determine if there are any external source
1053
      // terms that should be applied.
1054
      external_point_source_map_[key].push_back(es);
18✔
1055

1056
    } else {
18✔
1057
      // If not a point source, then use the volumetric domain constraints to
1058
      // determine which source regions to apply the external source to.
1059
      if (is->domain_type() == Source::DomainType::MATERIAL) {
391✔
1060
        for (int32_t material_id : domain_ids) {
34✔
1061
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
102✔
1062
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
85✔
1063
          }
1064
        }
1065
      } else if (is->domain_type() == Source::DomainType::CELL) {
374✔
1066
        for (int32_t cell_id : domain_ids) {
34✔
1067
          int32_t i_cell = model::cell_map[cell_id];
17✔
1068
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
17✔
1069
        }
1070
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
357✔
1071
        for (int32_t universe_id : domain_ids) {
714✔
1072
          int32_t i_universe = model::universe_map[universe_id];
357✔
1073
          Universe& universe = *model::universes[i_universe];
357✔
1074
          for (int32_t i_cell : universe.cells_) {
714✔
1075
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
357✔
1076
          }
1077
        }
1078
      }
1079
    }
1080
  } // End loop over external sources
1081
}
409✔
1082

1083
void FlatSourceDomain::flux_swap()
12,612✔
1084
{
1085
  source_regions_.flux_swap();
12,612✔
1086
}
12,612✔
1087

1088
void FlatSourceDomain::flatten_xs()
596✔
1089
{
1090
  // Temperature and angle indices, if using multiple temperature
1091
  // data sets and/or anisotropic data sets.
1092
  // TODO: Currently assumes we are only using single temp/single angle data.
1093
  const int t = 0;
596✔
1094
  const int a = 0;
596✔
1095

1096
  n_materials_ = data::mg.macro_xs_.size();
596✔
1097
  for (auto& m : data::mg.macro_xs_) {
2,212✔
1098
    for (int g_out = 0; g_out < negroups_; g_out++) {
8,537✔
1099
      if (m.exists_in_model) {
6,921✔
1100
        double sigma_t =
1101
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
6,853✔
1102
        sigma_t_.push_back(sigma_t);
6,853✔
1103

1104
        double nu_sigma_f =
1105
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
6,853✔
1106
        nu_sigma_f_.push_back(nu_sigma_f);
6,853✔
1107

1108
        double sigma_f =
1109
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
6,853✔
1110
        sigma_f_.push_back(sigma_f);
6,853✔
1111

1112
        double chi =
1113
          m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
6,853✔
1114
        if (!std::isfinite(chi)) {
6,853✔
1115
          // MGXS interface may return NaN in some cases, such as when material
1116
          // is fissionable but has very small sigma_f.
1117
          chi = 0.0;
×
1118
        }
1119
        chi_.push_back(chi);
6,853✔
1120

1121
        for (int g_in = 0; g_in < negroups_; g_in++) {
271,768✔
1122
          double sigma_s =
1123
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
264,915✔
1124
          sigma_s_.push_back(sigma_s);
264,915✔
1125
          // For transport corrected XS data, diagonal elements may be negative.
1126
          // In this case, set a flag to enable transport stabilization for the
1127
          // simulation.
1128
          if (g_out == g_in && sigma_s < 0.0)
264,915✔
1129
            is_transport_stabilization_needed_ = true;
901✔
1130
        }
1131
      } else {
1132
        sigma_t_.push_back(0);
68✔
1133
        nu_sigma_f_.push_back(0);
68✔
1134
        sigma_f_.push_back(0);
68✔
1135
        chi_.push_back(0);
68✔
1136
        for (int g_in = 0; g_in < negroups_; g_in++) {
136✔
1137
          sigma_s_.push_back(0);
68✔
1138
        }
1139
      }
1140
    }
1141
  }
1142
}
596✔
1143

1144
void FlatSourceDomain::set_adjoint_sources()
69✔
1145
{
1146
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1147
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1148
  // as this is likely a very small source region that we don't need to bother
1149
  // trying to vector particles towards. In the case of flux "being extremely
1150
  // close to zero", we define this as being a fixed fraction of the maximum
1151
  // forward flux, below which we assume the flux would be physically
1152
  // undetectable.
1153

1154
  // First, find the maximum forward flux value
1155
  double max_flux = 0.0;
69✔
1156
#pragma omp parallel for reduction(max : max_flux)
37✔
1157
  for (int64_t se = 0; se < n_source_elements(); se++) {
186,656✔
1158
    double flux = source_regions_.scalar_flux_final(se);
186,624✔
1159
    if (flux > max_flux) {
186,624✔
1160
      max_flux = flux;
30✔
1161
    }
1162
  }
1163

1164
  // Then, compute the adjoint source for each source region
1165
#pragma omp parallel for
37✔
1166
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
186,656✔
1167
    for (int g = 0; g < negroups_; g++) {
373,248✔
1168
      double flux = source_regions_.scalar_flux_final(sr, g);
186,624✔
1169
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
186,624✔
1170
        source_regions_.external_source(sr, g) = 0.0;
390✔
1171
      } else {
1172
        source_regions_.external_source(sr, g) = 1.0 / flux;
186,234✔
1173
      }
1174
      if (flux > 0.0) {
186,624✔
1175
        source_regions_.external_source_present(sr) = 1;
186,234✔
1176
      }
1177
      source_regions_.scalar_flux_final(sr, g) = 0.0;
186,624✔
1178
    }
1179
  }
1180

1181
  // "Small" source regions in OpenMC are defined as those that are hit by
1182
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1183
  // very small volumes combined with a low aspect ratio, and are often
1184
  // generated when applying a source region mesh that clips the edge of a
1185
  // curved surface. As perhaps only a few rays will visit these regions over
1186
  // the entire forward simulation, the forward flux estimates are extremely
1187
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1188
  // extremely low, leading to unphysically large adjoint source terms,
1189
  // resulting in weight windows that aggressively try to drive particles
1190
  // towards these regions. To fix this, we simply filter out any "small" source
1191
  // regions from consideration. If a source region is "small", we
1192
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1193
  // flux solution, as the true total adjoint source contribution from small
1194
  // regions is likely to be negligible.
1195
#pragma omp parallel for
37✔
1196
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
186,656✔
1197
    if (source_regions_.is_small(sr)) {
186,624✔
1198
      for (int g = 0; g < negroups_; g++) {
1199
        source_regions_.external_source(sr, g) = 0.0;
1200
      }
1201
      source_regions_.external_source_present(sr) = 0;
1202
    }
1203
  }
1204
  // Divide the fixed source term by sigma t (to save time when applying each
1205
  // iteration)
1206
#pragma omp parallel for
37✔
1207
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
186,656✔
1208
    int material = source_regions_.material(sr);
186,624✔
1209
    if (material == MATERIAL_VOID) {
186,624✔
1210
      continue;
1211
    }
1212
    for (int g = 0; g < negroups_; g++) {
373,248✔
1213
      double sigma_t = sigma_t_[material * negroups_ + g];
186,624✔
1214
      source_regions_.external_source(sr, g) /= sigma_t;
186,624✔
1215
    }
1216
  }
1217
}
69✔
1218

1219
void FlatSourceDomain::transpose_scattering_matrix()
86✔
1220
{
1221
  // Transpose the inner two dimensions for each material
1222
  for (int m = 0; m < n_materials_; ++m) {
325✔
1223
    int material_offset = m * negroups_ * negroups_;
239✔
1224
    for (int i = 0; i < negroups_; ++i) {
683✔
1225
      for (int j = i + 1; j < negroups_; ++j) {
1,159✔
1226
        // Calculate indices of the elements to swap
1227
        int idx1 = material_offset + i * negroups_ + j;
715✔
1228
        int idx2 = material_offset + j * negroups_ + i;
715✔
1229

1230
        // Swap the elements to transpose the matrix
1231
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
715✔
1232
      }
1233
    }
1234
  }
1235
}
86✔
1236

UNCOV
1237
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1238
{
1239
  // Ensure array is correct size
UNCOV
1240
  flux.resize(n_source_regions() * negroups_);
×
1241
// Serialize the final fluxes for output
1242
#pragma omp parallel for
1243
  for (int64_t se = 0; se < n_source_elements(); se++) {
1244
    flux[se] = source_regions_.scalar_flux_final(se);
1245
  }
1246
}
1247

1248
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
648✔
1249
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1250
  bool is_target_void)
1251
{
1252
  Cell& cell = *model::cells[i_cell];
648✔
1253
  if (cell.type_ != Fill::MATERIAL)
648✔
1254
    return;
×
1255
  for (int32_t j : instances) {
185,882✔
1256
    int cell_material_idx = cell.material(j);
185,234✔
1257
    int cell_material_id = (cell_material_idx == C_NONE)
1258
                             ? C_NONE
185,234✔
1259
                             : model::materials[cell_material_idx]->id();
185,233✔
1260

1261
    if ((target_material_id == C_NONE && !is_target_void) ||
185,234✔
1262
        cell_material_id == target_material_id) {
1263
      int64_t sr = source_region_offsets_[i_cell] + j;
151,234✔
1264
      // Check if the key is already present in the mesh_map_
1265
      if (mesh_map_.find(sr) != mesh_map_.end()) {
151,234✔
UNCOV
1266
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1267
                                "applied, but trying to apply mesh idx {}",
NEW
1268
          sr, mesh_map_[sr], mesh_idx));
×
1269
      }
1270
      // If the SR has not already been assigned, then we can write to it
1271
      mesh_map_[sr] = mesh_idx;
151,234✔
1272
    }
1273
  }
1274
}
1275

1276
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
511✔
1277
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1278
{
1279
  Cell& cell = *model::cells[i_cell];
511✔
1280

1281
  if (cell.type_ == Fill::MATERIAL) {
511✔
1282
    vector<int> instances(cell.n_instances());
374✔
1283
    std::iota(instances.begin(), instances.end(), 0);
374✔
1284
    apply_mesh_to_cell_instances(
374✔
1285
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1286
  } else if (target_material_id == C_NONE && !is_target_void) {
511✔
1287
    for (int j = 0; j < cell.n_instances(); j++) {
138✔
1288
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1289
        cell.get_contained_cells(j, nullptr);
69✔
1290
      for (const auto& pair : cell_instance_list) {
343✔
1291
        int32_t i_child_cell = pair.first;
274✔
1292
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
274✔
1293
          pair.second, is_target_void);
274✔
1294
      }
1295
    }
69✔
1296
  }
1297
}
511✔
1298

1299
void FlatSourceDomain::apply_meshes()
596✔
1300
{
1301
  // Skip if there are no mappings between mesh IDs and domains
1302
  if (mesh_domain_map_.empty())
596✔
1303
    return;
425✔
1304

1305
  // Loop over meshes
1306
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
427✔
1307
    Mesh* mesh = model::meshes[mesh_idx].get();
256✔
1308
    int mesh_id = mesh->id();
256✔
1309

1310
    // Skip if mesh id is not present in the map
1311
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
256✔
1312
      continue;
17✔
1313

1314
    // Loop over domains associated with the mesh
1315
    for (auto& domain : mesh_domain_map_[mesh_id]) {
478✔
1316
      Source::DomainType domain_type = domain.first;
239✔
1317
      int domain_id = domain.second;
239✔
1318

1319
      if (domain_type == Source::DomainType::MATERIAL) {
239✔
1320
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
204✔
1321
          if (domain_id == C_NONE) {
170✔
1322
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1323
          } else {
1324
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
170✔
1325
          }
1326
        }
1327
      } else if (domain_type == Source::DomainType::CELL) {
205✔
1328
        int32_t i_cell = model::cell_map[domain_id];
34✔
1329
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
34✔
1330
      } else if (domain_type == Source::DomainType::UNIVERSE) {
171✔
1331
        int32_t i_universe = model::universe_map[domain_id];
171✔
1332
        Universe& universe = *model::universes[i_universe];
171✔
1333
        for (int32_t i_cell : universe.cells_) {
478✔
1334
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
307✔
1335
        }
1336
      }
1337
    }
1338
  }
1339
}
1340

1341
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,326,342,010✔
1342
  SourceRegionKey sr_key, Position r, Direction u)
1343
{
1344
  // Case 1: Check if the source region key is already present in the permanent
1345
  // map. This is the most common condition, as any source region visited in a
1346
  // previous power iteration will already be present in the permanent map. If
1347
  // the source region key is found, we translate the key into a specific 1D
1348
  // source region index and return a handle its position in the
1349
  // source_regions_ vector.
1350
  auto it = source_region_map_.find(sr_key);
1,326,342,010✔
1351
  if (it != source_region_map_.end()) {
1,326,342,010✔
1352
    int64_t sr = it->second;
1,289,119,465✔
1353
    return source_regions_.get_source_region_handle(sr);
1,289,119,465✔
1354
  }
1355

1356
  // Case 2: Check if the source region key is present in the temporary (thread
1357
  // safe) map. This is a common occurrence in the first power iteration when
1358
  // the source region has already been visited already by some other ray. We
1359
  // begin by locking the temporary map before any operations are performed. The
1360
  // lock is not global over the full data structure -- it will be dependent on
1361
  // which key is used.
1362
  discovered_source_regions_.lock(sr_key);
37,222,545✔
1363

1364
  // If the key is found in the temporary map, then we return a handle to the
1365
  // source region that is stored in the temporary map.
1366
  if (discovered_source_regions_.contains(sr_key)) {
37,222,545✔
1367
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
34,689,858✔
1368
    discovered_source_regions_.unlock(sr_key);
34,689,858✔
1369
    return handle;
34,689,858✔
1370
  }
1371

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

1400
  // Sanity check on source region id
1401
  GeometryState gs;
2,532,687✔
1402
  gs.r() = r + TINY_BIT * u;
2,532,687✔
1403
  gs.u() = {1.0, 0.0, 0.0};
2,532,687✔
1404
  exhaustive_find_cell(gs);
2,532,687✔
1405
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,532,687✔
1406
  if (sr_found != sr_key.base_source_region_id) {
2,532,687✔
1407
    discovered_source_regions_.unlock(sr_key);
96✔
1408
    SourceRegionHandle handle;
96✔
1409
    handle.is_numerical_fp_artifact_ = true;
96✔
1410
    return handle;
96✔
1411
  }
1412

1413
  // Sanity check on mesh bin
1414
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,532,591✔
1415
  if (mesh_idx == C_NONE) {
2,532,591✔
1416
    if (sr_key.mesh_bin != 0) {
381,648✔
1417
      discovered_source_regions_.unlock(sr_key);
×
1418
      SourceRegionHandle handle;
×
1419
      handle.is_numerical_fp_artifact_ = true;
×
1420
      return handle;
×
1421
    }
1422
  } else {
1423
    Mesh* mesh = model::meshes[mesh_idx].get();
2,150,943✔
1424
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
2,150,943✔
1425
    if (bin_found != sr_key.mesh_bin) {
2,150,943✔
1426
      discovered_source_regions_.unlock(sr_key);
×
1427
      SourceRegionHandle handle;
×
1428
      handle.is_numerical_fp_artifact_ = true;
×
1429
      return handle;
×
1430
    }
1431
  }
1432

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

1440
  // Call the basic constructor for the source region and store in the parallel
1441
  // map.
1442
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,532,591✔
1443
  SourceRegion* sr_ptr =
1444
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
2,532,591✔
1445
  SourceRegionHandle handle {*sr_ptr};
2,532,591✔
1446

1447
  // Determine the material
1448
  int gs_i_cell = gs.lowest_coord().cell();
2,532,591✔
1449
  Cell& cell = *model::cells[gs_i_cell];
2,532,591✔
1450
  int material = cell.material(gs.cell_instance());
2,532,591✔
1451
  handle.material() = material;
2,532,591✔
1452

1453
  // Store the mesh index (if any) assigned to this source region
1454
  handle.mesh() = mesh_idx;
2,532,591✔
1455

1456
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,532,591✔
1457
    // Determine if there are any volumetric sources, and apply them.
1458
    // Volumetric sources are specifc only to the base SR idx.
1459
    auto it_vol =
1460
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,479,347✔
1461
    if (it_vol != external_volumetric_source_map_.end()) {
2,479,347✔
1462
      const vector<int>& vol_sources = it_vol->second;
4,800✔
1463
      for (int src_idx : vol_sources) {
9,600✔
1464
        apply_external_source_to_source_region(src_idx, handle);
4,800✔
1465
      }
1466
    }
1467

1468
    // Determine if there are any point sources, and apply them.
1469
    // Point sources are specific to the source region key.
1470
    auto it_point = external_point_source_map_.find(sr_key);
2,479,347✔
1471
    if (it_point != external_point_source_map_.end()) {
2,479,347✔
1472
      const vector<int>& point_sources = it_point->second;
13✔
1473
      for (int src_idx : point_sources) {
26✔
1474
        apply_external_source_to_source_region(src_idx, handle);
13✔
1475
      }
1476
    }
1477

1478
    // Divide external source term by sigma_t
1479
    if (material != C_NONE) {
2,479,347✔
1480
      for (int g = 0; g < negroups_; g++) {
4,960,701✔
1481
        double sigma_t = sigma_t_[material * negroups_ + g];
2,505,418✔
1482
        handle.external_source(g) /= sigma_t;
2,505,418✔
1483
      }
1484
    }
1485
  }
1486

1487
  // Compute the combined source term
1488
  update_single_neutron_source(handle);
2,532,591✔
1489

1490
  // Unlock the parallel map. Note: we may be tempted to release
1491
  // this lock earlier, and then just use the source region's lock to protect
1492
  // the flux/source initialization stages above. However, the rest of the code
1493
  // only protects updates to the new flux and volume fields, and assumes that
1494
  // the source is constant for the duration of transport. Thus, using just the
1495
  // source region's lock by itself would result in other threads potentially
1496
  // reading from the source before it is computed, as they won't use the lock
1497
  // when only reading from the SR's source. It would be expensive to protect
1498
  // those operations, whereas generating the SR is only done once, so we just
1499
  // hold the map's bucket lock until the source region is fully initialized.
1500
  discovered_source_regions_.unlock(sr_key);
2,532,591✔
1501

1502
  return handle;
2,532,591✔
1503
}
2,532,687✔
1504

1505
void FlatSourceDomain::finalize_discovered_source_regions()
12,612✔
1506
{
1507
  // Extract keys for entries with a valid volume.
1508
  vector<SourceRegionKey> keys;
12,612✔
1509
  for (const auto& pair : discovered_source_regions_) {
2,545,203✔
1510
    if (pair.second.volume_ > 0.0) {
2,532,591✔
1511
      keys.push_back(pair.first);
2,421,927✔
1512
    }
1513
  }
1514

1515
  if (!keys.empty()) {
12,612✔
1516
    // Sort the keys, so as to ensure reproducible ordering given that source
1517
    // regions may have been added to discovered_source_regions_ in an arbitrary
1518
    // order due to shared memory threading.
1519
    std::sort(keys.begin(), keys.end());
781✔
1520

1521
    // Remember the index of the first new source region
1522
    int64_t start_sr_id = source_regions_.n_source_regions();
781✔
1523

1524
    // Append the source regions in the sorted key order.
1525
    for (const auto& key : keys) {
2,422,708✔
1526
      const SourceRegion& sr = discovered_source_regions_[key];
2,421,927✔
1527
      source_region_map_[key] = source_regions_.n_source_regions();
2,421,927✔
1528
      source_regions_.push_back(sr);
2,421,927✔
1529
    }
1530

1531
    // Map all new source regions to tallies
1532
    convert_source_regions_to_tallies(start_sr_id);
781✔
1533
  }
1534

1535
  discovered_source_regions_.clear();
12,612✔
1536
}
12,612✔
1537

1538
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1539
//
1540
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1541
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1542
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1543
// https://doi.org/10.1016/j.anucene.2018.10.036.
1544
void FlatSourceDomain::apply_transport_stabilization()
12,612✔
1545
{
1546
  // Don't do anything if all in-group scattering
1547
  // cross sections are positive
1548
  if (!is_transport_stabilization_needed_) {
12,612✔
1549
    return;
12,372✔
1550
  }
1551

1552
  // Apply the stabilization factor to all source elements
1553
#pragma omp parallel for
120✔
1554
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,560✔
1555
    int material = source_regions_.material(sr);
1,440✔
1556
    if (material == MATERIAL_VOID) {
1,440✔
1557
      continue;
1558
    }
1559
    for (int g = 0; g < negroups_; g++) {
102,240✔
1560
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1561
      // negative
1562
      double sigma_s =
1563
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g];
100,800✔
1564
      if (sigma_s < 0.0) {
100,800✔
1565
        double sigma_t = sigma_t_[material * negroups_ + g];
25,440✔
1566
        double phi_new = source_regions_.scalar_flux_new(sr, g);
25,440✔
1567
        double phi_old = source_regions_.scalar_flux_old(sr, g);
25,440✔
1568

1569
        // Equation 18 in the above Gunow et al. 2019 paper. For a default
1570
        // rho of 1.0, this ensures there are no negative diagonal elements
1571
        // in the iteration matrix. A lesser rho could be used (or exposed
1572
        // as a user input parameter) to reduce the negative impact on
1573
        // convergence rate though would need to be experimentally tested to see
1574
        // if it doesn't become unstable. rho = 1.0 is good as it gives the
1575
        // highest assurance of stability, and the impacts on convergence rate
1576
        // are pretty mild.
1577
        double D = diagonal_stabilization_rho_ * sigma_s / sigma_t;
25,440✔
1578

1579
        // Equation 16 in the above Gunow et al. 2019 paper
1580
        source_regions_.scalar_flux_new(sr, g) =
25,440✔
1581
          (phi_new - D * phi_old) / (1.0 - D);
25,440✔
1582
      }
1583
    }
1584
  }
1585
}
1586

1587
// Determines the base source region index (i.e., a material filled cell
1588
// instance) that corresponds to a particular location in the geometry. Requires
1589
// that the "gs" object passed in has already been initialized and has called
1590
// find_cell etc.
1591
int64_t FlatSourceDomain::lookup_base_source_region_idx(
843,100,303✔
1592
  const GeometryState& gs) const
1593
{
1594
  int i_cell = gs.lowest_coord().cell();
843,100,303✔
1595
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
843,100,303✔
1596
  return sr;
843,100,303✔
1597
}
1598

1599
// Determines the index of the mesh (if any) that has been applied
1600
// to a particular base source region index.
1601
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
843,100,207✔
1602
{
1603
  int mesh_idx = C_NONE;
843,100,207✔
1604
  auto mesh_it = mesh_map_.find(sr);
843,100,207✔
1605
  if (mesh_it != mesh_map_.end()) {
843,100,207✔
1606
    mesh_idx = mesh_it->second;
467,196,187✔
1607
  }
1608
  return mesh_idx;
843,100,207✔
1609
}
1610

1611
// Determines the source region key that corresponds to a particular location in
1612
// the geometry. This takes into account both the base source region index as
1613
// well as the mesh bin if a mesh is applied to this source region for
1614
// subdivision.
1615
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
2,026,338✔
1616
  const GeometryState& gs) const
1617
{
1618
  int64_t sr = lookup_base_source_region_idx(gs);
2,026,338✔
1619
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
2,026,338✔
1620
  return SourceRegionKey {sr, mesh_bin};
2,026,338✔
1621
}
1622

1623
// Determines the mesh bin that corresponds to a particular base source region
1624
// index and position.
1625
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
2,026,338✔
1626
{
1627
  int mesh_idx = lookup_mesh_idx(sr);
2,026,338✔
1628
  int mesh_bin = 0;
2,026,338✔
1629
  if (mesh_idx != C_NONE) {
2,026,338✔
1630
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
1,249,938✔
1631
  }
1632
  return mesh_bin;
2,026,338✔
1633
}
1634

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