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

openmc-dev / openmc / 21075564212

16 Jan 2026 05:45PM UTC coverage: 82.183% (+0.1%) from 82.044%
21075564212

Pull #3737

github

web-flow
Merge 791eac7e9 into 51ea89ccc
Pull Request #3737: Temperature feedback support in the random ray solver.

17249 of 23898 branches covered (72.18%)

Branch coverage included in aggregate %.

331 of 359 new or added lines in 11 files covered. (92.2%)

77 existing lines in 3 files now uncovered.

55865 of 65067 relevant lines covered (85.86%)

45078647.58 hits per line

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

72.23
/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_)
737✔
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;
737✔
44
  for (const auto& c : model::cells) {
6,004✔
45
    if (c->type_ != Fill::MATERIAL) {
5,267✔
46
      source_region_offsets_.push_back(-1);
2,417✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
2,850✔
49
      base_source_regions += c->n_instances();
2,850✔
50
    }
51
  }
52

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

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

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

79
void FlatSourceDomain::batch_reset()
12,772✔
80
{
81
// Reset scalar fluxes and iteration volume tallies to zero
82
#pragma omp parallel for
6,972✔
83
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,425,595✔
84
    source_regions_.volume(sr) = 0.0;
32,419,795✔
85
    source_regions_.volume_sq(sr) = 0.0;
32,419,795✔
86
  }
87

88
#pragma omp parallel for
6,972✔
89
  for (int64_t se = 0; se < n_source_elements(); se++) {
36,802,355✔
90
    source_regions_.scalar_flux_new(se) = 0.0;
36,796,555✔
91
  }
92
}
12,772✔
93

94
void FlatSourceDomain::accumulate_iteration_flux()
5,286✔
95
{
96
#pragma omp parallel for
2,886✔
97
  for (int64_t se = 0; se < n_source_elements(); se++) {
16,775,000✔
98
    source_regions_.scalar_flux_final(se) +=
16,772,600✔
99
      source_regions_.scalar_flux_new(se);
16,772,600✔
100
  }
101
}
5,286✔
102

103
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
44,051,006✔
104
{
105
  // Reset all source regions to zero (important for void regions)
106
  for (int g = 0; g < negroups_; g++) {
93,693,190✔
107
    srh.source(g) = 0.0;
49,642,184✔
108
  }
109

110
  // Add scattering + fission source
111
  int material = srh.material();
44,051,006✔
112
  int temp = srh.temperature_idx();
44,051,006✔
113
  double density_mult = srh.density_mult();
44,051,006✔
114
  if (material != MATERIAL_VOID) {
44,051,006✔
115
    double inverse_k_eff = 1.0 / k_eff_;
43,609,358✔
116
    for (int g_out = 0; g_out < negroups_; g_out++) {
92,809,126✔
117
      double sigma_t =
118
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g_out] *
49,199,768✔
119
        density_mult;
49,199,768✔
120
      double scatter_source = 0.0;
49,199,768✔
121
      double fission_source = 0.0;
49,199,768✔
122

123
      for (int g_in = 0; g_in < negroups_; g_in++) {
137,531,026✔
124
        double scalar_flux = srh.scalar_flux_old(g_in);
88,331,258✔
125
        double sigma_s =
126
          sigma_s_[((material * ntemperature_ + temp) * negroups_ + g_out) *
88,331,258✔
127
                     negroups_ +
88,331,258✔
128
                   g_in] *
88,331,258✔
129
          density_mult;
88,331,258✔
130
        double nu_sigma_f =
131
          nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g_in] *
88,331,258✔
132
          density_mult;
88,331,258✔
133
        double chi =
134
          chi_[(material * ntemperature_ + temp) * negroups_ + g_out];
88,331,258✔
135

136
        scatter_source += sigma_s * scalar_flux;
88,331,258✔
137
        if (settings::create_fission_neutrons) {
88,331,258!
138
          fission_source += nu_sigma_f * scalar_flux * chi;
88,331,258✔
139
        }
140
      }
141
      srh.source(g_out) =
49,199,768✔
142
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
49,199,768✔
143
    }
144
  }
145

146
  // Add external source if in fixed source mode
147
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
44,051,006✔
148
    for (int g = 0; g < negroups_; g++) {
89,955,038✔
149
      srh.source(g) += srh.external_source(g);
46,413,541✔
150
    }
151
  }
152
}
44,051,006✔
153

154
// Compute new estimate of scattering + fission sources in each source region
155
// based on the flux estimate from the previous iteration.
156
void FlatSourceDomain::update_all_neutron_sources()
12,772✔
157
{
158
  simulation::time_update_src.start();
12,772✔
159

160
#pragma omp parallel for
6,972✔
161
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,425,595✔
162
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
32,419,795✔
163
    update_single_neutron_source(srh);
32,419,795✔
164
  }
165

166
  simulation::time_update_src.stop();
12,772✔
167
}
12,772✔
168

169
// Normalizes flux and updates simulation-averaged volume estimate
170
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
5,567✔
171
  double total_active_distance_per_iteration)
172
{
173
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
5,567✔
174
  double volume_normalization_factor =
5,567✔
175
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
5,567✔
176

177
// Normalize scalar flux to total distance travelled by all rays this
178
// iteration
179
#pragma omp parallel for
3,042✔
180
  for (int64_t se = 0; se < n_source_elements(); se++) {
22,535,135✔
181
    source_regions_.scalar_flux_new(se) *= normalization_factor;
22,532,610✔
182
  }
183

184
// Accumulate cell-wise ray length tallies collected this iteration, then
185
// update the simulation-averaged cell-wise volume estimates
186
#pragma omp parallel for
3,042✔
187
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
19,994,195✔
188
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
19,991,670✔
189
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
19,991,670✔
190
    source_regions_.volume_naive(sr) =
39,983,340✔
191
      source_regions_.volume(sr) * normalization_factor;
19,991,670✔
192
    source_regions_.volume_sq(sr) =
39,983,340✔
193
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
19,991,670✔
194
    source_regions_.volume(sr) =
19,991,670✔
195
      source_regions_.volume_t(sr) * volume_normalization_factor;
19,991,670✔
196
  }
197
}
5,567✔
198

199
void FlatSourceDomain::set_flux_to_flux_plus_source(
43,978,049✔
200
  int64_t sr, double volume, int g)
201
{
202
  int material = source_regions_.material(sr);
43,978,049✔
203
  int temp = source_regions_.temperature_idx(sr);
43,978,049✔
204
  if (material == MATERIAL_VOID) {
43,978,049✔
205
    source_regions_.scalar_flux_new(sr, g) /= volume;
882,416✔
206
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
882,416!
207
      source_regions_.scalar_flux_new(sr, g) +=
882,416✔
208
        0.5f * source_regions_.external_source(sr, g) *
882,416✔
209
        source_regions_.volume_sq(sr);
882,416✔
210
    }
211
  } else {
212
    double sigma_t =
213
      sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
43,095,633✔
214
      source_regions_.density_mult(sr);
43,095,633✔
215
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
43,095,633✔
216
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
43,095,633✔
217
  }
218
}
43,978,049✔
219

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

226
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
8,927,547✔
227
{
228
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
8,927,547✔
229
}
8,927,547✔
230

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

238
#pragma omp parallel for reduction(+ : n_hits)
6,972✔
239
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
33,454,835✔
240

241
    double volume_simulation_avg = source_regions_.volume(sr);
33,449,035✔
242
    double volume_iteration = source_regions_.volume_naive(sr);
33,449,035✔
243

244
    // Increment the number of hits if cell was hit this iteration
245
    if (volume_iteration) {
33,449,035✔
246
      n_hits++;
29,389,880✔
247
    }
248

249
    // Set the SR to small status if its expected number of hits
250
    // per iteration is less than 1.5
251
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
33,449,035✔
252
      source_regions_.is_small(sr) = 1;
6,812,705✔
253
    } else {
254
      source_regions_.is_small(sr) = 0;
26,636,330✔
255
    }
256

257
    // The volume treatment depends on the volume estimator type
258
    // and whether or not an external source is present in the cell.
259
    double volume;
260
    switch (volume_estimator_) {
33,449,035!
261
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
262
      volume = volume_iteration;
777,600✔
263
      break;
777,600✔
264
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
265
      volume = volume_simulation_avg;
432,000✔
266
      break;
432,000✔
267
    case RandomRayVolumeEstimator::HYBRID:
32,239,435✔
268
      if (source_regions_.external_source_present(sr) ||
60,290,470✔
269
          source_regions_.is_small(sr)) {
28,051,035✔
270
        volume = volume_iteration;
11,000,725✔
271
      } else {
272
        volume = volume_simulation_avg;
21,238,710✔
273
      }
274
      break;
32,239,435✔
275
    default:
276
      fatal_error("Invalid volume estimator type");
277
    }
278

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

316
  // Return the number of source regions that were hit this iteration
317
  return n_hits;
12,772✔
318
}
319

320
// Generates new estimate of k_eff based on the differences between this
321
// iteration's estimate of the scalar flux and the last iteration's estimate.
322
void FlatSourceDomain::compute_k_eff()
3,080✔
323
{
324
  double fission_rate_old = 0;
3,080✔
325
  double fission_rate_new = 0;
3,080✔
326

327
  // Vector for gathering fission source terms for Shannon entropy calculation
328
  vector<float> p(n_source_regions(), 0.0f);
3,080✔
329

330
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
1,680✔
331
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
337,790✔
332

333
    // If simulation averaged volume is zero, don't include this cell
334
    double volume = source_regions_.volume(sr);
336,390✔
335
    if (volume == 0.0) {
336,390!
336
      continue;
337
    }
338

339
    int material = source_regions_.material(sr);
336,390✔
340
    int temp = source_regions_.temperature_idx(sr);
336,390✔
341
    if (material == MATERIAL_VOID) {
336,390!
342
      continue;
343
    }
344

345
    double sr_fission_source_old = 0;
336,390✔
346
    double sr_fission_source_new = 0;
336,390✔
347

348
    for (int g = 0; g < negroups_; g++) {
2,597,520✔
349
      double nu_sigma_f =
350
        nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
2,261,130✔
351
        source_regions_.density_mult(sr);
2,261,130✔
352
      sr_fission_source_old +=
2,261,130✔
353
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,261,130✔
354
      sr_fission_source_new +=
2,261,130✔
355
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,261,130✔
356
    }
357

358
    // Compute total fission rates in FSR
359
    sr_fission_source_old *= volume;
336,390✔
360
    sr_fission_source_new *= volume;
336,390✔
361

362
    // Accumulate totals
363
    fission_rate_old += sr_fission_source_old;
336,390✔
364
    fission_rate_new += sr_fission_source_new;
336,390✔
365

366
    // Store total fission rate in the FSR for Shannon calculation
367
    p[sr] = sr_fission_source_new;
336,390✔
368
  }
369

370
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
3,080✔
371

372
  double H = 0.0;
3,080✔
373
  // defining an inverse sum for better performance
374
  double inverse_sum = 1 / fission_rate_new;
3,080✔
375

376
#pragma omp parallel for reduction(+ : H)
1,680✔
377
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
337,790✔
378
    // Only if FSR has non-negative and non-zero fission source
379
    if (p[sr] > 0.0f) {
336,390✔
380
      // Normalize to total weight of bank sites. p_i for better performance
381
      float p_i = p[sr] * inverse_sum;
139,595✔
382
      // Sum values to obtain Shannon entropy.
383
      H -= p_i * std::log2(p_i);
139,595✔
384
    }
385
  }
386

387
  // Adds entropy value to shared entropy vector in openmc namespace.
388
  simulation::entropy.push_back(H);
3,080✔
389

390
  fission_rate_ = fission_rate_new;
3,080✔
391
  k_eff_ = k_eff_new;
3,080✔
392
}
3,080✔
393

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

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

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

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

432
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
837✔
433
{
434
  openmc::simulation::time_tallies.start();
837✔
435

436
  // Tracks if we've generated a mapping yet for all source regions.
437
  bool all_source_regions_mapped = true;
837✔
438

439
// Attempt to generate mapping for all source regions
440
#pragma omp parallel for
457✔
441
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,029,620✔
442

443
    // If this source region has not been hit by a ray yet, then
444
    // we aren't going to be able to map it, so skip it.
445
    if (!source_regions_.position_recorded(sr)) {
1,029,240!
446
      all_source_regions_mapped = false;
447
      continue;
448
    }
449

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

459
    // Loop over energy groups (so as to support energy filters)
460
    for (int g = 0; g < negroups_; g++) {
2,216,460✔
461

462
      // Set particle to the current energy
463
      p.g() = g;
1,187,220✔
464
      p.g_last() = g;
1,187,220✔
465
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,187,220✔
466
      p.E_last() = p.E();
1,187,220✔
467

468
      int64_t source_element = sr * negroups_ + g;
1,187,220✔
469

470
      // If this task has already been populated, we don't need to do
471
      // it again.
472
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,187,220!
473
        continue;
474
      }
475

476
      // Loop over all active tallies. This logic is essentially identical
477
      // to what happens when scanning for applicable tallies during
478
      // MC transport.
479
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
4,513,360✔
480
        Tally& tally {*model::tallies[i_tally]};
3,326,140✔
481

482
        // Initialize an iterator over valid filter bin combinations.
483
        // If there are no valid combinations, use a continue statement
484
        // to ensure we skip the assume_separate break below.
485
        auto filter_iter = FilterBinIter(tally, p);
3,326,140✔
486
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,326,140✔
487
        if (filter_iter == end)
3,326,140✔
488
          continue;
2,001,440✔
489

490
        // Loop over filter bins.
491
        for (; filter_iter != end; ++filter_iter) {
2,649,400✔
492
          auto filter_index = filter_iter.index_;
1,324,700✔
493
          auto filter_weight = filter_iter.weight_;
1,324,700✔
494

495
          // Loop over scores
496
          for (int score = 0; score < tally.scores_.size(); score++) {
3,003,600✔
497
            auto score_bin = tally.scores_[score];
1,678,900✔
498
            // If a valid tally, filter, and score combination has been found,
499
            // then add it to the list of tally tasks for this source element.
500
            TallyTask task(i_tally, filter_index, score, score_bin);
1,678,900✔
501
            source_regions_.tally_task(sr, g).push_back(task);
1,678,900✔
502

503
            // Also add this task to the list of volume tasks for this source
504
            // region.
505
            source_regions_.volume_task(sr).insert(task);
1,678,900✔
506
          }
507
        }
508
      }
509
      // Reset all the filter matches for the next tally event.
510
      for (auto& match : p.filter_matches())
4,984,220✔
511
        match.bins_present_ = false;
3,797,000✔
512
    }
513
  }
1,029,240✔
514
  openmc::simulation::time_tallies.stop();
837✔
515

516
  mapped_all_tallies_ = all_source_regions_mapped;
837✔
517
}
837✔
518

519
// Set the volume accumulators to zero for all tallies
520
void FlatSourceDomain::reset_tally_volumes()
5,286✔
521
{
522
  if (volume_normalized_flux_tallies_) {
5,286✔
523
#pragma omp parallel for
2,190✔
524
    for (int i = 0; i < tally_volumes_.size(); i++) {
6,800✔
525
      auto& tensor = tally_volumes_[i];
4,975✔
526
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
4,975✔
527
    }
528
  }
529
}
5,286✔
530

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

559
  // If we are in adjoint mode of a fixed source problem, the external
560
  // source is already normalized, such that all resulting fluxes are
561
  // also normalized.
562
  if (adjoint_) {
4,218✔
563
    return 1.0;
333✔
564
  }
565

566
  // Fixed source mode normalization
567

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

590
  // Step 2 is to determine the total user-specified external source strength
591
  double user_external_source_strength = 0.0;
3,885✔
592
  for (auto& ext_source : model::external_sources) {
7,770✔
593
    user_external_source_strength += ext_source->strength();
3,885✔
594
  }
595

596
  // The correction factor is the ratio of the user-specified external source
597
  // strength to the simulation external source strength.
598
  double source_normalization_factor =
3,885✔
599
    user_external_source_strength / simulation_external_source_strength;
600

601
  return source_normalization_factor;
3,885✔
602
}
603

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

613
void FlatSourceDomain::random_ray_tally()
5,286✔
614
{
615
  openmc::simulation::time_tallies.start();
5,286✔
616

617
  // Reset our tally volumes to zero
618
  reset_tally_volumes();
5,286✔
619

620
  double source_normalization_factor =
621
    compute_fixed_source_normalization_factor();
5,286✔
622

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

639
    double material = source_regions_.material(sr);
15,308,900✔
640
    int temp = source_regions_.temperature_idx(sr);
15,308,900✔
641
    double density_mult = source_regions_.density_mult(sr);
15,308,900✔
642
    for (int g = 0; g < negroups_; g++) {
32,081,500✔
643
      double flux =
644
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
16,772,600✔
645

646
      // Determine numerical score value
647
      for (auto& task : source_regions_.tally_task(sr, g)) {
39,692,600✔
648
        double score = 0.0;
22,920,000✔
649
        switch (task.score_type) {
22,920,000!
650

651
        case SCORE_FLUX:
19,576,800✔
652
          score = flux * volume;
19,576,800✔
653
          break;
19,576,800✔
654

655
        case SCORE_TOTAL:
656
          if (material != MATERIAL_VOID) {
×
657
            score =
658
              flux * volume *
659
              sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
660
              density_mult;
661
          }
662
          break;
663

664
        case SCORE_FISSION:
1,671,600✔
665
          if (material != MATERIAL_VOID) {
1,671,600!
666
            score =
1,671,600✔
667
              flux * volume *
1,671,600✔
668
              sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,671,600✔
669
              density_mult;
670
          }
671
          break;
1,671,600✔
672

673
        case SCORE_NU_FISSION:
1,671,600✔
674
          if (material != MATERIAL_VOID) {
1,671,600!
675
            score =
1,671,600✔
676
              flux * volume *
1,671,600✔
677
              nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,671,600✔
678
              density_mult;
679
          }
680
          break;
1,671,600✔
681

682
        case SCORE_EVENTS:
683
          score = 1.0;
684
          break;
685

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

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

714
  // Normalize any flux scores by the total volume of the FSRs scoring to that
715
  // bin. To do this, we loop over all tallies, and then all filter bins,
716
  // and then scores. For each score, we check the tally data structure to
717
  // see what index that score corresponds to. If that score is a flux score,
718
  // then we divide it by volume.
719
  if (volume_normalized_flux_tallies_) {
5,286✔
720
    for (int i = 0; i < model::tallies.size(); i++) {
14,960✔
721
      Tally& tally {*model::tallies[i]};
10,945✔
722
#pragma omp parallel for
5,970✔
723
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
705,625✔
724
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,422,300✔
725
          auto score_type = tally.scores_[score_idx];
721,650✔
726
          if (score_type == SCORE_FLUX) {
721,650✔
727
            double vol = tally_volumes_[i](bin, score_idx);
700,650✔
728
            if (vol > 0.0) {
700,650!
729
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
700,650✔
730
            }
731
          }
732
        }
733
      }
734
    }
735
  }
736

737
  openmc::simulation::time_tallies.stop();
5,286✔
738
}
5,286✔
739

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

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

761
  // Print header information
762
  print_plot();
×
763

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

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

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

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

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

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

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

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

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

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

855
    double source_normalization_factor =
856
      compute_fixed_source_normalization_factor();
×
857

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

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

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

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

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

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

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

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

994
    std::fclose(plot);
×
995
  }
×
996
}
×
997

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

1008
  srh.external_source_present() = 1;
4,588✔
1009

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

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

1021
  if (cell.type_ != Fill::MATERIAL)
432!
1022
    return;
×
1023

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

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

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

1061
void FlatSourceDomain::count_external_source_regions()
1,235✔
1062
{
1063
  n_external_source_regions_ = 0;
1,235✔
1064
#pragma omp parallel for reduction(+ : n_external_source_regions_)
696✔
1065
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,186,519✔
1066
    if (source_regions_.external_source_present(sr)) {
1,185,980✔
1067
      n_external_source_regions_++;
157,250✔
1068
    }
1069
  }
1070
}
1,235✔
1071

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

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

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

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

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

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

1141
void FlatSourceDomain::flux_swap()
12,772✔
1142
{
1143
  source_regions_.flux_swap();
12,772✔
1144
}
12,772✔
1145

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

1153
  n_materials_ = data::mg.macro_xs_.size();
737✔
1154
  ntemperature_ = 1;
737✔
1155
  for (int i = 0; i < n_materials_; i++) {
2,754✔
1156
    ntemperature_ =
2,017✔
1157
      std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points());
2,017✔
1158
  }
1159

1160
  for (int i = 0; i < n_materials_; i++) {
2,754✔
1161
    auto& m = data::mg.macro_xs_[i];
2,017✔
1162
    for (int t = 0; t < ntemperature_; t++) {
4,066✔
1163
      for (int g_out = 0; g_out < negroups_; g_out++) {
11,155✔
1164
        if (m.exists_in_model && t < m.n_temperature_points()) {
9,106✔
1165
          double sigma_t =
1166
            m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
8,930✔
1167
          sigma_t_.push_back(sigma_t);
8,930✔
1168

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

1178
          double nu_sigma_f =
1179
            m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
8,930✔
1180
          nu_sigma_f_.push_back(nu_sigma_f);
8,930✔
1181

1182
          double sigma_f =
1183
            m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
8,930✔
1184
          sigma_f_.push_back(sigma_f);
8,930✔
1185

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

1195
          for (int g_in = 0; g_in < negroups_; g_in++) {
275,142✔
1196
            double sigma_s =
1197
              m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
266,212✔
1198
            sigma_s_.push_back(sigma_s);
266,212✔
1199
            // For transport corrected XS data, diagonal elements may be
1200
            // negative. In this case, set a flag to enable transport
1201
            // stabilization for the simulation.
1202
            if (g_out == g_in && sigma_s < 0.0)
266,212✔
1203
              is_transport_stabilization_needed_ = true;
880✔
1204
          }
1205
        } else {
1206
          sigma_t_.push_back(0);
176✔
1207
          nu_sigma_f_.push_back(0);
176✔
1208
          sigma_f_.push_back(0);
176✔
1209
          chi_.push_back(0);
176✔
1210
          for (int g_in = 0; g_in < negroups_; g_in++) {
1,024✔
1211
            sigma_s_.push_back(0);
848✔
1212
          }
1213
        }
1214
      }
1215
    }
1216
  }
1217
}
737✔
1218

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

1229
  // First, find the maximum forward flux value
1230
  double max_flux = 0.0;
65✔
1231
#pragma omp parallel for reduction(max : max_flux)
37✔
1232
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,548✔
1233
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1234
    if (flux > max_flux) {
155,520✔
1235
      max_flux = flux;
25✔
1236
    }
1237
  }
1238

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

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

1297
void FlatSourceDomain::transpose_scattering_matrix()
81✔
1298
{
1299
  // Transpose the inner two dimensions for each material
1300
  for (int m = 0; m < n_materials_; ++m) {
306✔
1301
    for (int t = 0; t < ntemperature_; t++) {
450✔
1302
      int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_;
225✔
1303
      for (int i = 0; i < negroups_; ++i) {
643✔
1304
        for (int j = i + 1; j < negroups_; ++j) {
1,091✔
1305
          // Calculate indices of the elements to swap
1306
          int idx1 = material_offset + i * negroups_ + j;
673✔
1307
          int idx2 = material_offset + j * negroups_ + i;
673✔
1308

1309
          // Swap the elements to transpose the matrix
1310
          std::swap(sigma_s_[idx1], sigma_s_[idx2]);
673✔
1311
        }
1312
      }
1313
    }
1314
  }
1315
}
81✔
1316

1317
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1318
{
1319
  // Ensure array is correct size
1320
  flux.resize(n_source_regions() * negroups_);
×
1321
// Serialize the final fluxes for output
1322
#pragma omp parallel for
1323
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1324
    flux[se] = source_regions_.scalar_flux_final(se);
1325
  }
1326
}
×
1327

1328
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
946✔
1329
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1330
  bool is_target_void)
1331
{
1332
  Cell& cell = *model::cells[i_cell];
946✔
1333
  if (cell.type_ != Fill::MATERIAL)
946!
1334
    return;
×
1335
  for (int32_t j : instances) {
175,620✔
1336
    int cell_material_idx = cell.material(j);
174,674✔
1337
    int cell_material_id = (cell_material_idx == C_NONE)
1338
                             ? C_NONE
174,674✔
1339
                             : model::materials[cell_material_idx]->id();
174,673✔
1340

1341
    if ((target_material_id == C_NONE && !is_target_void) ||
174,674!
1342
        cell_material_id == target_material_id) {
1343
      int64_t sr = source_region_offsets_[i_cell] + j;
142,674✔
1344
      // Check if the key is already present in the mesh_map_
1345
      if (mesh_map_.find(sr) != mesh_map_.end()) {
142,674!
1346
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1347
                                "applied, but trying to apply mesh idx {}",
1348
          sr, mesh_map_[sr], mesh_idx));
×
1349
      }
1350
      // If the SR has not already been assigned, then we can write to it
1351
      mesh_map_[sr] = mesh_idx;
142,674✔
1352
    }
1353
  }
1354
}
1355

1356
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
817✔
1357
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1358
{
1359
  Cell& cell = *model::cells[i_cell];
817✔
1360

1361
  if (cell.type_ == Fill::MATERIAL) {
817✔
1362
    vector<int> instances(cell.n_instances());
688✔
1363
    std::iota(instances.begin(), instances.end(), 0);
688✔
1364
    apply_mesh_to_cell_instances(
688✔
1365
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1366
  } else if (target_material_id == C_NONE && !is_target_void) {
817!
1367
    for (int j = 0; j < cell.n_instances(); j++) {
130✔
1368
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1369
        cell.get_contained_cells(j, nullptr);
65✔
1370
      for (const auto& pair : cell_instance_list) {
323✔
1371
        int32_t i_child_cell = pair.first;
258✔
1372
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
258✔
1373
          pair.second, is_target_void);
258✔
1374
      }
1375
    }
65✔
1376
  }
1377
}
817✔
1378

1379
void FlatSourceDomain::apply_meshes()
737✔
1380
{
1381
  // Skip if there are no mappings between mesh IDs and domains
1382
  if (mesh_domain_map_.empty())
737✔
1383
    return;
464✔
1384

1385
  // Loop over meshes
1386
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
626✔
1387
    Mesh* mesh = model::meshes[mesh_idx].get();
353✔
1388
    int mesh_id = mesh->id();
353✔
1389

1390
    // Skip if mesh id is not present in the map
1391
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
353✔
1392
      continue;
16✔
1393

1394
    // Loop over domains associated with the mesh
1395
    for (auto& domain : mesh_domain_map_[mesh_id]) {
674✔
1396
      Source::DomainType domain_type = domain.first;
337✔
1397
      int domain_id = domain.second;
337✔
1398

1399
      if (domain_type == Source::DomainType::MATERIAL) {
337✔
1400
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1401
          if (domain_id == C_NONE) {
160!
1402
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1403
          } else {
1404
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1405
          }
1406
        }
1407
      } else if (domain_type == Source::DomainType::CELL) {
305✔
1408
        int32_t i_cell = model::cell_map[domain_id];
32✔
1409
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1410
      } else if (domain_type == Source::DomainType::UNIVERSE) {
273!
1411
        int32_t i_universe = model::universe_map[domain_id];
273✔
1412
        Universe& universe = *model::universes[i_universe];
273✔
1413
        for (int32_t i_cell : universe.cells_) {
898✔
1414
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
625✔
1415
        }
1416
      }
1417
    }
1418
  }
1419
}
1420

1421
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,287,877,179✔
1422
  SourceRegionKey sr_key, Position r, Direction u)
1423
{
1424
  // Case 1: Check if the source region key is already present in the permanent
1425
  // map. This is the most common condition, as any source region visited in a
1426
  // previous power iteration will already be present in the permanent map. If
1427
  // the source region key is found, we translate the key into a specific 1D
1428
  // source region index and return a handle its position in the
1429
  // source_regions_ vector.
1430
  auto it = source_region_map_.find(sr_key);
1,287,877,179✔
1431
  if (it != source_region_map_.end()) {
1,287,877,179✔
1432
    int64_t sr = it->second;
1,246,548,498✔
1433
    return source_regions_.get_source_region_handle(sr);
1,246,548,498✔
1434
  }
1435

1436
  // Case 2: Check if the source region key is present in the temporary (thread
1437
  // safe) map. This is a common occurrence in the first power iteration when
1438
  // the source region has already been visited already by some other ray. We
1439
  // begin by locking the temporary map before any operations are performed. The
1440
  // lock is not global over the full data structure -- it will be dependent on
1441
  // which key is used.
1442
  discovered_source_regions_.lock(sr_key);
41,328,681✔
1443

1444
  // If the key is found in the temporary map, then we return a handle to the
1445
  // source region that is stored in the temporary map.
1446
  if (discovered_source_regions_.contains(sr_key)) {
41,328,681✔
1447
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
38,962,736✔
1448
    discovered_source_regions_.unlock(sr_key);
38,962,736✔
1449
    return handle;
38,962,736✔
1450
  }
1451

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

1480
  // Sanity check on source region id
1481
  GeometryState gs;
2,365,945✔
1482
  gs.r() = r + TINY_BIT * u;
2,365,945✔
1483
  gs.u() = {1.0, 0.0, 0.0};
2,365,945✔
1484
  exhaustive_find_cell(gs);
2,365,945✔
1485
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,365,945✔
1486
  if (sr_found != sr_key.base_source_region_id) {
2,365,945✔
1487
    discovered_source_regions_.unlock(sr_key);
88✔
1488
    SourceRegionHandle handle;
88✔
1489
    handle.is_numerical_fp_artifact_ = true;
88✔
1490
    return handle;
88✔
1491
  }
1492

1493
  // Sanity check on mesh bin
1494
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,365,857✔
1495
  if (mesh_idx == C_NONE) {
2,365,857✔
1496
    if (sr_key.mesh_bin != 0) {
393,228!
1497
      discovered_source_regions_.unlock(sr_key);
×
1498
      SourceRegionHandle handle;
×
1499
      handle.is_numerical_fp_artifact_ = true;
×
1500
      return handle;
×
1501
    }
1502
  } else {
1503
    Mesh* mesh = model::meshes[mesh_idx].get();
1,972,629✔
1504
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,972,629✔
1505
    if (bin_found != sr_key.mesh_bin) {
1,972,629!
1506
      discovered_source_regions_.unlock(sr_key);
×
1507
      SourceRegionHandle handle;
×
1508
      handle.is_numerical_fp_artifact_ = true;
×
1509
      return handle;
×
1510
    }
1511
  }
1512

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

1520
  // Call the basic constructor for the source region and store in the parallel
1521
  // map.
1522
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,365,857✔
1523
  SourceRegion* sr_ptr =
1524
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
2,365,857✔
1525
  SourceRegionHandle handle {*sr_ptr};
2,365,857✔
1526

1527
  // Determine the material
1528
  int gs_i_cell = gs.lowest_coord().cell();
2,365,857✔
1529
  Cell& cell = *model::cells[gs_i_cell];
2,365,857✔
1530
  int material = cell.material(gs.cell_instance());
2,365,857✔
1531
  int temp = 0;
2,365,857✔
1532

1533
  // If material total XS is extremely low, just set it to void to avoid
1534
  // problems with 1/Sigma_t
1535
  if (material != MATERIAL_VOID) {
2,365,857✔
1536
    temp = data::mg.macro_xs_[material].get_temperature_index(
4,687,586✔
1537
      cell.sqrtkT(gs.cell_instance()));
2,343,793✔
1538
    for (int g = 0; g < negroups_; g++) {
5,035,143✔
1539
      double sigma_t =
1540
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g];
2,691,438✔
1541
      if (sigma_t < MINIMUM_MACRO_XS) {
2,691,438✔
1542
        material = MATERIAL_VOID;
88✔
1543
        temp = 0;
88✔
1544
        break;
88✔
1545
      }
1546
    }
1547
  }
1548

1549
  handle.material() = material;
2,365,857✔
1550
  handle.temperature_idx() = temp;
2,365,857✔
1551

1552
  handle.density_mult() = cell.density_mult(gs.cell_instance());
2,365,857✔
1553

1554
  // Store the mesh index (if any) assigned to this source region
1555
  handle.mesh() = mesh_idx;
2,365,857✔
1556

1557
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,365,857✔
1558
    // Determine if there are any volumetric sources, and apply them.
1559
    // Volumetric sources are specifc only to the base SR idx.
1560
    auto it_vol =
1561
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,310,758✔
1562
    if (it_vol != external_volumetric_source_map_.end()) {
2,310,758✔
1563
      const vector<int>& vol_sources = it_vol->second;
4,576✔
1564
      for (int src_idx : vol_sources) {
9,152✔
1565
        apply_external_source_to_source_region(src_idx, handle);
4,576✔
1566
      }
1567
    }
1568

1569
    // Determine if there are any point sources, and apply them.
1570
    // Point sources are specific to the source region key.
1571
    auto it_point = external_point_source_map_.find(sr_key);
2,310,758✔
1572
    if (it_point != external_point_source_map_.end()) {
2,310,758✔
1573
      const vector<int>& point_sources = it_point->second;
12✔
1574
      for (int src_idx : point_sources) {
24✔
1575
        apply_external_source_to_source_region(src_idx, handle);
12✔
1576
      }
1577
    }
1578

1579
    // Divide external source term by sigma_t
1580
    if (material != C_NONE) {
2,310,758✔
1581
      for (int g = 0; g < negroups_; g++) {
4,623,171✔
1582
        double sigma_t =
1583
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,334,565✔
1584
          handle.density_mult();
2,334,565✔
1585
        handle.external_source(g) /= sigma_t;
2,334,565✔
1586
      }
1587
    }
1588
  }
1589

1590
  // Compute the combined source term
1591
  update_single_neutron_source(handle);
2,365,857✔
1592

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

1605
  return handle;
2,365,857✔
1606
}
2,365,945✔
1607

1608
void FlatSourceDomain::finalize_discovered_source_regions()
12,772✔
1609
{
1610
  // Extract keys for entries with a valid volume.
1611
  vector<SourceRegionKey> keys;
12,772✔
1612
  for (const auto& pair : discovered_source_regions_) {
2,378,629✔
1613
    if (pair.second.volume_ > 0.0) {
2,365,857✔
1614
      keys.push_back(pair.first);
2,264,415✔
1615
    }
1616
  }
1617

1618
  if (!keys.empty()) {
12,772✔
1619
    // Sort the keys, so as to ensure reproducible ordering given that source
1620
    // regions may have been added to discovered_source_regions_ in an arbitrary
1621
    // order due to shared memory threading.
1622
    std::sort(keys.begin(), keys.end());
837✔
1623

1624
    // Remember the index of the first new source region
1625
    int64_t start_sr_id = source_regions_.n_source_regions();
837✔
1626

1627
    // Append the source regions in the sorted key order.
1628
    for (const auto& key : keys) {
2,265,252✔
1629
      const SourceRegion& sr = discovered_source_regions_[key];
2,264,415✔
1630
      source_region_map_[key] = source_regions_.n_source_regions();
2,264,415✔
1631
      source_regions_.push_back(sr);
2,264,415✔
1632
    }
1633

1634
    // Map all new source regions to tallies
1635
    convert_source_regions_to_tallies(start_sr_id);
837✔
1636
  }
1637

1638
  discovered_source_regions_.clear();
12,772✔
1639
}
12,772✔
1640

1641
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1642
//
1643
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1644
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1645
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1646
// https://doi.org/10.1016/j.anucene.2018.10.036.
1647
void FlatSourceDomain::apply_transport_stabilization()
12,772✔
1648
{
1649
  // Don't do anything if all in-group scattering
1650
  // cross sections are positive
1651
  if (!is_transport_stabilization_needed_) {
12,772✔
1652
    return;
12,552✔
1653
  }
1654

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

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

1689
        // Equation 16 in the above Gunow et al. 2019 paper
1690
        source_regions_.scalar_flux_new(sr, g) =
22,000✔
1691
          (phi_new - D * phi_old) / (1.0 - D);
22,000✔
1692
      }
1693
    }
1694
  }
1695
}
1696

1697
// Determines the base source region index (i.e., a material filled cell
1698
// instance) that corresponds to a particular location in the geometry. Requires
1699
// that the "gs" object passed in has already been initialized and has called
1700
// find_cell etc.
1701
int64_t FlatSourceDomain::lookup_base_source_region_idx(
833,979,492✔
1702
  const GeometryState& gs) const
1703
{
1704
  int i_cell = gs.lowest_coord().cell();
833,979,492✔
1705
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
833,979,492✔
1706
  return sr;
833,979,492✔
1707
}
1708

1709
// Determines the index of the mesh (if any) that has been applied
1710
// to a particular base source region index.
1711
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
833,979,404✔
1712
{
1713
  int mesh_idx = C_NONE;
833,979,404✔
1714
  auto mesh_it = mesh_map_.find(sr);
833,979,404✔
1715
  if (mesh_it != mesh_map_.end()) {
833,979,404✔
1716
    mesh_idx = mesh_it->second;
462,742,087✔
1717
  }
1718
  return mesh_idx;
833,979,404✔
1719
}
1720

1721
// Determines the source region key that corresponds to a particular location in
1722
// the geometry. This takes into account both the base source region index as
1723
// well as the mesh bin if a mesh is applied to this source region for
1724
// subdivision.
1725
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
1,976,337✔
1726
  const GeometryState& gs) const
1727
{
1728
  int64_t sr = lookup_base_source_region_idx(gs);
1,976,337✔
1729
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
1,976,337✔
1730
  return SourceRegionKey {sr, mesh_bin};
1,976,337✔
1731
}
1732

1733
// Determines the mesh bin that corresponds to a particular base source region
1734
// index and position.
1735
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
1,976,337✔
1736
{
1737
  int mesh_idx = lookup_mesh_idx(sr);
1,976,337✔
1738
  int mesh_bin = 0;
1,976,337✔
1739
  if (mesh_idx != C_NONE) {
1,976,337✔
1740
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
1,222,837✔
1741
  }
1742
  return mesh_bin;
1,976,337✔
1743
}
1744

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