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

openmc-dev / openmc / 30082591782

24 Jul 2026 09:26AM UTC coverage: 81.336% (-0.08%) from 81.413%
30082591782

Pull #4026

github

web-flow
Merge d3b3c7684 into 01790598d
Pull Request #4026: Add domain decomposition for random ray solver

19231 of 27986 branches covered (68.72%)

Branch coverage included in aggregate %.

1186 of 1209 new or added lines in 12 files covered. (98.1%)

415 existing lines in 10 files now uncovered.

61115 of 70797 relevant lines covered (86.32%)

49017206.19 hits per line

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

61.65
/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/decomposition_map.h"
13
#include "openmc/random_ray/random_ray.h"
14
#include "openmc/simulation.h"
15
#include "openmc/tallies/filter.h"
16
#include "openmc/tallies/tally.h"
17
#include "openmc/tallies/tally_scoring.h"
18
#include "openmc/timer.h"
19
#include "openmc/weight_windows.h"
20

21
#include <cstdio>
22
#include <numeric>
23

24
namespace openmc {
25

26
//==============================================================================
27
// FlatSourceDomain implementation
28
//==============================================================================
29

30
// Static Variable Declarations
31
RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ {
32
  RandomRayVolumeEstimator::HYBRID};
33
bool FlatSourceDomain::volume_normalized_flux_tallies_ {false};
34
bool FlatSourceDomain::adjoint_requested_ {false};
35
RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD};
36
bool FlatSourceDomain::fw_cadis_local_ {false};
37
double FlatSourceDomain::diagonal_stabilization_rho_ {1.0};
38
std::unordered_map<int, vector<std::pair<Source::DomainType, int>>>
39
  FlatSourceDomain::mesh_domain_map_;
40
std::vector<size_t> FlatSourceDomain::fw_cadis_local_targets_;
41

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

58
  // Initialize source regions.
59
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
814✔
60
  source_regions_ = SourceRegionContainer(negroups_, is_linear);
814✔
61

62
  // Initialize tally volumes
63
  if (volume_normalized_flux_tallies_) {
814✔
64
    tally_volumes_.resize(model::tallies.size());
506✔
65
    for (int i = 0; i < model::tallies.size(); i++) {
1,837✔
66
      //  Get the shape of the 3D result tensor
67
      auto shape = model::tallies[i]->results().shape();
1,331✔
68

69
      // Create a new 2D tensor with the same size as the first
70
      // two dimensions of the 3D tensor
71
      tally_volumes_[i] = tensor::Tensor<double>({shape[0], shape[1]});
2,662✔
72
    }
1,331✔
73
  }
74

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

83
void FlatSourceDomain::batch_reset()
21,842✔
84
{
85
// Reset scalar fluxes and iteration volume tallies to zero
86
#pragma omp parallel for
12,942✔
87
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
34,252,035✔
88
    source_regions_.centroid_iteration(sr) = {0.0, 0.0, 0.0};
34,243,135✔
89
    source_regions_.volume(sr) = 0.0;
34,243,135✔
90
    source_regions_.volume_sq(sr) = 0.0;
34,243,135✔
91
  }
92

93
#pragma omp parallel for
12,942✔
94
  for (int64_t se = 0; se < n_source_elements(); se++) {
43,082,025✔
95
    source_regions_.scalar_flux_new(se) = 0.0;
43,073,125✔
96
  }
97
}
21,842✔
98

99
void FlatSourceDomain::accumulate_iteration_flux()
9,826✔
100
{
101
#pragma omp parallel for
5,796✔
102
  for (int64_t se = 0; se < n_source_elements(); se++) {
18,664,270✔
103
    source_regions_.scalar_flux_final(se) +=
18,660,240✔
104
      source_regions_.scalar_flux_new(se);
18,660,240✔
105
  }
106
}
9,826✔
107

108
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
48,199,678✔
109
{
110
  // Reset all source regions to zero (important for void regions)
111
  for (int g = 0; g < negroups_; g++) {
112,025,570✔
112
    srh.source(g) = 0.0;
63,825,892✔
113
  }
114

115
  // Add scattering + fission source
116
  int material = srh.material();
48,199,678✔
117
  int temp = srh.temperature_idx();
48,199,678✔
118
  double density_mult = srh.density_mult();
48,199,678✔
119
  if (material != MATERIAL_VOID) {
48,199,678✔
120
    double inverse_k_eff = 1.0 / k_eff_;
46,088,010✔
121
    const int material_offset = (material * ntemperature_ + temp) * negroups_;
46,088,010✔
122
    const int scatter_offset =
46,088,010✔
123
      (material * ntemperature_ + temp) * negroups_ * negroups_;
124
    for (int g_out = 0; g_out < negroups_; g_out++) {
102,791,406✔
125
      double sigma_t = sigma_t_[material_offset + g_out] * density_mult;
56,703,396✔
126
      double scatter_source = 0.0;
56,703,396✔
127
      double fission_source = 0.0;
56,703,396✔
128

129
      for (int g_in = 0; g_in < negroups_; g_in++) {
174,538,986✔
130
        double scalar_flux = srh.scalar_flux_old(g_in);
117,835,590!
131
        double sigma_s =
117,835,590✔
132
          sigma_s_[scatter_offset + g_out * negroups_ + g_in] * density_mult;
117,835,590!
133
        double nu_sigma_f = nu_sigma_f_[material_offset + g_in] * density_mult;
117,835,590✔
134
        double chi = chi_[material_offset + g_out];
117,835,590✔
135

136
        scatter_source += sigma_s * scalar_flux;
117,835,590✔
137
        if (settings::create_fission_neutrons) {
117,835,590!
138
          fission_source += nu_sigma_f * scalar_flux * chi;
117,835,590✔
139
        }
140
      }
141
      srh.source(g_out) =
56,703,396✔
142
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
56,703,396✔
143
    }
144
  }
145

146
  // Add external source if in fixed source mode
147
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
48,199,678✔
148
    for (int g = 0; g < negroups_; g++) {
107,442,618✔
149
      srh.source(g) += srh.external_source(g);
59,858,049✔
150
    }
151
  }
152
}
48,199,678✔
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()
21,842✔
157
{
158
  simulation::time_update_src.start();
21,842✔
159

160
#pragma omp parallel for
12,942✔
161
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
34,252,035✔
162
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
34,243,135✔
163
    update_single_neutron_source(srh);
34,243,135✔
164
  }
165

166
  simulation::time_update_src.stop();
21,842✔
167
}
21,842✔
168

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

177
// Normalize scalar flux to total distance travelled by all rays this
178
// iteration
179
#pragma omp parallel for
6,777✔
180
  for (int64_t se = 0; se < n_source_elements(); se++) {
28,982,620✔
181
    source_regions_.scalar_flux_new(se) *= normalization_factor;
28,977,830✔
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
6,777✔
187
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
21,881,590✔
188
    source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr);
21,876,800✔
189
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
21,876,800✔
190
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
21,876,800✔
191
    source_regions_.volume_naive(sr) =
21,876,800✔
192
      source_regions_.volume(sr) * normalization_factor;
21,876,800✔
193
    source_regions_.volume_sq(sr) =
21,876,800✔
194
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
21,876,800✔
195
    source_regions_.volume(sr) =
21,876,800✔
196
      source_regions_.volume_t(sr) * volume_normalization_factor;
21,876,800✔
197
    if (source_regions_.volume_t(sr) > 0.0) {
21,876,800✔
198
      double inv_volume = 1.0 / source_regions_.volume_t(sr);
21,873,625✔
199
      source_regions_.centroid(sr) = source_regions_.centroid_t(sr);
21,873,625✔
200
      source_regions_.centroid(sr) *= inv_volume;
21,873,625✔
201
    }
202
  }
203
}
11,567✔
204

205
void FlatSourceDomain::set_flux_to_flux_plus_source(
57,850,677✔
206
  int64_t sr, double volume, int g)
207
{
208
  int material = source_regions_.material(sr);
57,850,677✔
209
  int temp = source_regions_.temperature_idx(sr);
57,850,677✔
210
  if (material == MATERIAL_VOID) {
57,850,677✔
211
    source_regions_.scalar_flux_new(sr, g) /= volume;
7,410,872!
212
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
7,410,872!
213
      source_regions_.scalar_flux_new(sr, g) +=
7,410,872✔
214
        0.5f * source_regions_.external_source(sr, g) *
7,410,872✔
215
        source_regions_.volume_sq(sr);
7,410,872✔
216
    }
217
  } else {
218
    double sigma_t =
50,439,805✔
219
      sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
50,439,805✔
220
      source_regions_.density_mult(sr);
50,439,805✔
221
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
50,439,805✔
222
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
50,439,805✔
223
  }
224
}
57,850,677✔
225

226
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
14,742✔
227
{
228
  source_regions_.scalar_flux_new(sr, g) =
14,742✔
229
    source_regions_.scalar_flux_old(sr, g);
14,742✔
230
}
14,742✔
231

232
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
9,193,307✔
233
{
234
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
9,193,307✔
235
}
9,193,307✔
236

237
// Combine transport flux contributions and flat source contributions from the
238
// previous iteration to generate this iteration's estimate of scalar flux.
239
int64_t FlatSourceDomain::add_source_to_scalar_flux()
21,842✔
240
{
241
  int64_t n_hits = 0;
21,842✔
242
  double inverse_batch = 1.0 / simulation::current_batch;
21,842✔
243

244
#pragma omp parallel for reduction(+ : n_hits)
12,942✔
245
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
35,344,865✔
246

247
    double volume_simulation_avg = source_regions_.volume(sr);
35,335,965✔
248
    double volume_iteration = source_regions_.volume_naive(sr);
35,335,965✔
249

250
    // Increment the number of hits if cell was hit this iteration
251
    if (volume_iteration) {
35,335,965✔
252
      n_hits++;
31,241,940✔
253
    }
254

255
    // Set the SR to small status if its expected number of hits
256
    // per iteration is less than 1.5
257
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
35,335,965✔
258
      source_regions_.is_small(sr) = 1;
6,867,975✔
259
    } else {
260
      source_regions_.is_small(sr) = 0;
28,467,990✔
261
    }
262

263
    // The volume treatment depends on the volume estimator type
264
    // and whether or not an external source is present in the cell.
265
    double volume;
35,335,965✔
266
    switch (volume_estimator_) {
35,335,965!
267
    case RandomRayVolumeEstimator::NAIVE:
268
      volume = volume_iteration;
269
      break;
270
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
271
      volume = volume_simulation_avg;
432,000✔
272
      break;
432,000✔
273
    case RandomRayVolumeEstimator::HYBRID:
33,714,765✔
274
      if (source_regions_.external_source_present(sr) ||
33,714,765✔
275
          source_regions_.is_small(sr)) {
28,838,865✔
276
        volume = volume_iteration;
277
      } else {
278
        volume = volume_simulation_avg;
279
      }
280
      break;
281
    default:
282
      fatal_error("Invalid volume estimator type");
283
    }
284

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

322
  // Return the number of source regions that were hit this iteration
323
  return n_hits;
21,842✔
324
}
325

326
// Generates new estimate of k_eff based on the differences between this
327
// iteration's estimate of the scalar flux and the last iteration's estimate.
328
void FlatSourceDomain::compute_k_eff()
6,850✔
329
{
330
  double fission_rate_old = 0;
6,850✔
331
  double fission_rate_new = 0;
6,850✔
332

333
  // Vector for gathering fission source terms for Shannon entropy calculation
334
  vector<float> p(n_source_regions(), 0.0f);
6,850✔
335

336
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
3,990✔
337
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
389,050✔
338

339
    // If simulation averaged volume is zero, don't include this cell
340
    double volume = source_regions_.volume(sr);
386,190!
341
    if (volume == 0.0) {
386,190!
342
      continue;
343
    }
344

345
    int material = source_regions_.material(sr);
386,190!
346
    int temp = source_regions_.temperature_idx(sr);
386,190!
347
    if (material == MATERIAL_VOID) {
386,190!
348
      continue;
349
    }
350

351
    double sr_fission_source_old = 0;
352
    double sr_fission_source_new = 0;
353

354
    for (int g = 0; g < negroups_; g++) {
2,986,920✔
355
      double nu_sigma_f =
2,600,730✔
356
        nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
2,600,730✔
357
        source_regions_.density_mult(sr);
2,600,730✔
358
      sr_fission_source_old +=
2,600,730✔
359
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,600,730✔
360
      sr_fission_source_new +=
2,600,730✔
361
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,600,730✔
362
    }
363

364
    // Compute total fission rates in FSR
365
    sr_fission_source_old *= volume;
386,190✔
366
    sr_fission_source_new *= volume;
386,190✔
367

368
    // Accumulate totals
369
    fission_rate_old += sr_fission_source_old;
386,190✔
370
    fission_rate_new += sr_fission_source_new;
386,190✔
371

372
    // Store total fission rate in the FSR for Shannon calculation
373
    p[sr] = sr_fission_source_new;
386,190✔
374
  }
375

376
  // Sum up fission rates across all ranks
377
#ifdef OPENMC_MPI
378
  if (mpi::n_procs > 1) {
3,280✔
379
    simulation::time_decomposition_handling.start();
2,480✔
380
    MPI_Allreduce(
2,480✔
381
      MPI_IN_PLACE, &fission_rate_old, 1, MPI_DOUBLE, MPI_SUM, mpi::intracomm);
382
    MPI_Allreduce(
2,480✔
383
      MPI_IN_PLACE, &fission_rate_new, 1, MPI_DOUBLE, MPI_SUM, mpi::intracomm);
384
    simulation::time_decomposition_handling.stop();
2,480✔
385
  }
386
#endif
387

388
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
6,850✔
389

390
  double H = 0.0;
6,850✔
391
  // defining an inverse sum for better performance
392
  double inverse_sum = 1 / fission_rate_new;
6,850✔
393

394
#pragma omp parallel for reduction(+ : H)
3,990✔
395
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
389,050✔
396
    // Only if FSR has non-negative and non-zero fission source
397
    if (p[sr] > 0.0f) {
386,190✔
398
      // Normalize to total weight of bank sites. p_i for better performance
399
      float p_i = p[sr] * inverse_sum;
164,195✔
400
      // Sum values to obtain Shannon entropy.
401
      H -= p_i * std::log2(p_i);
164,195✔
402
    }
403
  }
404

405
#ifdef OPENMC_MPI
406
  if (mpi::n_procs > 1) {
3,280✔
407
    simulation::time_decomposition_handling.start();
2,480✔
408
    if (mpi::master) {
2,480✔
409
      MPI_Reduce(MPI_IN_PLACE, &H, 1, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1,240✔
410
    } else {
411
      MPI_Reduce(&H, nullptr, 1, MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1,240✔
412
    }
413
    simulation::time_decomposition_handling.stop();
2,480✔
414
  }
415
#endif
416

417
  // Adds entropy value to shared entropy vector in openmc namespace.
418
  simulation::entropy.push_back(H);
6,850✔
419

420
  fission_rate_ = fission_rate_new;
6,850✔
421
  k_eff_ = k_eff_new;
6,850✔
422
}
6,850✔
423

424
// This function is responsible for generating a mapping between random
425
// ray flat source regions (cell instances) and tally bins. The mapping
426
// takes the form of a "TallyTask" object, which accounts for one single
427
// score being applied to a single tally. Thus, a single source region
428
// may have anywhere from zero to many tally tasks associated with it ---
429
// meaning that the global "tally_task" data structure is in 2D. The outer
430
// dimension corresponds to the source element (i.e., each entry corresponds
431
// to a specific energy group within a specific source region), and the
432
// inner dimension corresponds to the tallying task itself. Mechanically,
433
// the mapping between FSRs and spatial filters is done by considering
434
// the location of a single known ray midpoint that passed through the
435
// FSR. I.e., during transport, the first ray to pass through a given FSR
436
// will write down its midpoint for use with this function. This is a cheap
437
// and easy way of mapping FSRs to spatial tally filters, but comes with
438
// the downside of adding the restriction that spatial tally filters must
439
// share boundaries with the physical geometry of the simulation (so as
440
// not to subdivide any FSR). It is acceptable for a spatial tally region
441
// to contain multiple FSRs, but not the other way around.
442

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

447
// Besides generating the mapping structure, this function also keeps track
448
// of whether or not all flat source regions have been hit yet. This is
449
// required, as there is no guarantee that all flat source regions will
450
// be hit every iteration, such that in the first few iterations some FSRs
451
// may not have a known position within them yet to facilitate mapping to
452
// spatial tally filters. However, after several iterations, if all FSRs
453
// have been hit and have had a tally map generated, then this status will
454
// be passed back to the caller to alert them that this function doesn't
455
// need to be called for the remainder of the simulation.
456

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

462
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
3,808✔
463
{
464
  openmc::simulation::time_tallies.start();
3,808✔
465

466
  // Tracks if we've generated a mapping yet for all source regions.
467
  bool all_source_regions_mapped = true;
3,808✔
468

469
// Attempt to generate mapping for all source regions
470
#pragma omp parallel for
2,615✔
471
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,127,252✔
472

473
    // If this source region has not been hit by a ray yet, then
474
    // we aren't going to be able to map it, so skip it.
475
    if (!source_regions_.position_recorded(sr)) {
1,126,059!
476
      all_source_regions_mapped = false;
477
      continue;
478
    }
479

480
    // A particle located at the recorded midpoint of a ray
481
    // crossing through this source region is used to estabilish
482
    // the spatial location of the source region
483
    Particle p;
1,126,059✔
484
    p.r() = source_regions_.position(sr);
1,126,059✔
485
    p.r_last() = source_regions_.position(sr);
1,126,059✔
486
    p.u() = {1.0, 0.0, 0.0};
1,126,059✔
487
    bool found = exhaustive_find_cell(p);
1,126,059✔
488

489
    // Loop over energy groups (so as to support energy filters)
490
    for (int g = 0; g < negroups_; g++) {
2,523,283✔
491

492
      // Set particle to the current energy
493
      p.g() = g;
1,397,224✔
494
      p.g_last() = g;
1,397,224✔
495
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,397,224!
496
      p.E_last() = p.E();
1,397,224✔
497

498
      int64_t source_element = sr * negroups_ + g;
1,397,224✔
499

500
      // If this task has already been populated, we don't need to do
501
      // it again.
502
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,397,224!
503
        continue;
504
      }
505

506
      // Loop over all active tallies. This logic is essentially identical
507
      // to what happens when scanning for applicable tallies during
508
      // MC transport.
509
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
5,084,713✔
510
        Tally& tally {*model::tallies[i_tally]};
3,687,489✔
511

512
        // Initialize an iterator over valid filter bin combinations.
513
        // If there are no valid combinations, use a continue statement
514
        // to ensure we skip the assume_separate break below.
515
        auto filter_iter = FilterBinIter(tally, p);
3,687,489✔
516
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,687,489✔
517
        if (filter_iter == end)
3,687,489✔
518
          continue;
2,149,074✔
519

520
        // Loop over filter bins.
521
        for (; filter_iter != end; ++filter_iter) {
4,615,245✔
522
          auto filter_index = filter_iter.index_;
523
          auto filter_weight = filter_iter.weight_;
3,444,638✔
524

525
          // Loop over scores
526
          for (int score = 0; score < tally.scores_.size(); score++) {
3,444,638✔
527
            auto score_bin = tally.scores_[score];
1,906,223✔
528
            // If a valid tally, filter, and score combination has been found,
529
            // then add it to the list of tally tasks for this source element.
530
            TallyTask task(i_tally, filter_index, score, score_bin);
1,906,223✔
531
            source_regions_.tally_task(sr, g).push_back(task);
1,906,223✔
532

533
            // Also add this task to the list of volume tasks for this source
534
            // region.
535
            source_regions_.volume_task(sr).insert(task);
1,906,223✔
536
          }
537
        }
538
      }
539
      // Reset all the filter matches for the next tally event.
540
      for (auto& match : p.filter_matches())
5,880,863✔
541
        match.bins_present_ = false;
4,483,639✔
542
    }
543
  }
1,126,059✔
544
  openmc::simulation::time_tallies.stop();
3,808✔
545

546
  mapped_all_tallies_ = all_source_regions_mapped;
3,808✔
547
}
3,808✔
548

549
// Set the volume accumulators to zero for all tallies
550
void FlatSourceDomain::reset_tally_volumes()
9,826✔
551
{
552
  if (volume_normalized_flux_tallies_) {
9,826✔
553
#pragma omp parallel for
4,320✔
554
    for (int i = 0; i < tally_volumes_.size(); i++) {
10,080✔
555
      auto& tensor = tally_volumes_[i];
7,050✔
556
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
14,100✔
557
    }
558
  }
559
}
9,826✔
560

561
// In fixed source mode, due to the way that volumetric fixed sources are
562
// converted and applied as volumetric sources in one or more source regions,
563
// we need to perform an additional normalization step to ensure that the
564
// reported scalar fluxes are in units per source neutron. This allows for
565
// direct comparison of reported tallies to Monte Carlo flux results.
566
// This factor needs to be computed at each iteration, as it is based on the
567
// volume estimate of each FSR, which improves over the course of the
568
// simulation
569
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
10,753✔
570
{
571
  // Eigenvalue mode normalization
572
  if (settings::run_mode == RunMode::EIGENVALUE) {
10,753✔
573
    // Normalize fluxes by total number of fission neutrons produced. This
574
    // ensures consistent scaling of the eigenvector such that its magnitude is
575
    // comparable to the eigenvector produced by the Monte Carlo solver.
576
    // Multiplying by the eigenvalue is unintuitive, but it is necessary.
577
    // If the eigenvalue is 1.2, per starting source neutron, you will
578
    // generate 1.2 neutrons. Thus if we normalize to generating only ONE
579
    // neutron in total for the whole domain, then we don't actually have enough
580
    // flux to generate the required 1.2 neutrons. We only know the flux
581
    // required to generate 1 neutron (which would have required less than one
582
    // starting neutron). Thus, you have to scale the flux up by the eigenvalue
583
    // such that 1.2 neutrons are generated, so as to be consistent with the
584
    // bookkeeping in MC which is all done per starting source neutron (not per
585
    // neutron produced).
586
    return k_eff_ / (fission_rate_ * simulation_volume_);
4,271✔
587
  }
588

589
  // If we are in adjoint mode of a fixed source problem, the external
590
  // source is already normalized, such that all resulting fluxes are
591
  // also normalized.
592
  if (solve_ == RandomRaySolve::ADJOINT) {
6,482✔
593
    return 1.0;
594
  }
595

596
  // Fixed source mode normalization
597

598
  // Step 1 is to sum over all source regions and energy groups to get the
599
  // total external source strength in the simulation.
600
  double simulation_external_source_strength = 0.0;
4,136✔
601
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
3,388✔
602
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,976,078✔
603
    int material = source_regions_.material(sr);
14,973,810✔
604
    int temp = source_regions_.temperature_idx(sr);
14,973,810✔
605
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,973,810✔
606
    for (int g = 0; g < negroups_; g++) {
31,132,620✔
607
      // For non-void regions, we store the external source pre-divided by
608
      // sigma_t. We need to multiply non-void regions back up by sigma_t
609
      // to get the total source strength in the expected units.
610
      double sigma_t = 1.0;
16,158,810✔
611
      if (material != MATERIAL_VOID) {
16,158,810✔
612
        sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
15,492,250✔
613
                  source_regions_.density_mult(sr);
15,492,250✔
614
      }
615
      simulation_external_source_strength +=
16,158,810✔
616
        source_regions_.external_source(sr, g) * sigma_t * volume;
16,158,810✔
617
    }
618
  }
619

620
#ifdef OPENMC_MPI
621
  if (mpi::n_procs > 1) {
2,996✔
622
    MPI_Allreduce(MPI_IN_PLACE, &simulation_external_source_strength, 1,
2,944✔
623
      MPI_DOUBLE, MPI_SUM, mpi::intracomm);
624
  }
625
#endif
626

627
  // Step 2 is to determine the total user-specified external source strength
628
  double user_external_source_strength = 0.0;
5,656✔
629
  for (auto& ext_source : model::external_sources) {
11,312✔
630
    user_external_source_strength += ext_source->strength();
5,656✔
631
  }
632

633
  // The correction factor is the ratio of the user-specified external source
634
  // strength to the simulation external source strength.
635
  double source_normalization_factor =
5,656✔
636
    user_external_source_strength / simulation_external_source_strength;
2,996✔
637

638
  return source_normalization_factor;
5,656✔
639
}
640

641
// Tallying in random ray is not done directly during transport, rather,
642
// it is done only once after each power iteration. This is made possible
643
// by way of a mapping data structure that relates spatial source regions
644
// (FSRs) to tally/filter/score combinations. The mechanism by which the
645
// mapping is done (and the limitations incurred) is documented in the
646
// "convert_source_regions_to_tallies()" function comments above. The present
647
// tally function simply traverses the mapping data structure and executes
648
// the scoring operations to OpenMC's native tally result arrays.
649

650
void FlatSourceDomain::random_ray_tally()
9,826✔
651
{
652
  openmc::simulation::time_tallies.start();
9,826✔
653

654
  // Reset our tally volumes to zero
655
  reset_tally_volumes();
9,826✔
656

657
  double source_normalization_factor =
9,826✔
658
    compute_fixed_source_normalization_factor();
9,826✔
659

660
// We loop over all source regions and energy groups. For each
661
// element, we check if there are any scores needed and apply
662
// them.
663
#pragma omp parallel for
5,796✔
664
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
15,913,390✔
665
    // The fsr.volume_ is the unitless fractional simulation averaged volume
666
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
667
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
668
    // entire global simulation domain (as defined by the ray source box).
669
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
670
    // its fraction of the total volume by the total volume. Not important in
671
    // eigenvalue solves, but useful in fixed source solves for returning the
672
    // flux shape with a magnitude that makes sense relative to the fixed
673
    // source strength.
674
    double volume = source_regions_.volume(sr) * simulation_volume_;
15,909,360✔
675

676
    double material = source_regions_.material(sr);
15,909,360✔
677
    int temp = source_regions_.temperature_idx(sr);
15,909,360✔
678
    double density_mult = source_regions_.density_mult(sr);
15,909,360✔
679
    for (int g = 0; g < negroups_; g++) {
34,569,600✔
680
      double flux =
18,660,240✔
681
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
18,660,240✔
682

683
      // Determine numerical score value
684
      for (auto& task : source_regions_.tally_task(sr, g)) {
43,971,880✔
685
        double score = 0.0;
25,311,640✔
686
        switch (task.score_type) {
25,311,640!
687

688
        case SCORE_FLUX:
21,463,840✔
689
          score = flux * volume;
21,463,840✔
690
          break;
21,463,840✔
691

692
        case SCORE_TOTAL:
693
          if (material != MATERIAL_VOID) {
×
694
            score =
695
              flux * volume *
696
              sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
697
              density_mult;
698
          }
699
          break;
700

701
        case SCORE_FISSION:
1,923,600✔
702
          if (material != MATERIAL_VOID) {
1,923,600!
703
            score =
1,923,600✔
704
              flux * volume *
1,923,600✔
705
              sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,923,600✔
706
              density_mult;
707
          }
708
          break;
709

710
        case SCORE_NU_FISSION:
1,923,600✔
711
          if (material != MATERIAL_VOID) {
1,923,600!
712
            score =
1,923,600✔
713
              flux * volume *
1,923,600✔
714
              nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
1,923,600✔
715
              density_mult;
716
          }
717
          break;
718

719
        case SCORE_EVENTS:
720
          score = 1.0;
721
          break;
722

723
        case SCORE_KAPPA_FISSION:
600✔
724
          score =
600✔
725
            flux * volume *
600✔
726
            kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] *
600✔
727
            density_mult;
728
          break;
600✔
729

730
        default:
731
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
732
                      "total, fission, nu-fission, kappa-fission, and events "
733
                      "are supported in random ray mode.");
734
          break;
25,311,640✔
735
        }
736
        // Apply score to the appropriate tally bin
737
        Tally& tally {*model::tallies[task.tally_idx]};
25,311,640✔
738
#pragma omp atomic
739
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
25,311,640✔
740
          score;
741
      }
742
    }
743

744
    // For flux tallies, the total volume of the spatial region is needed
745
    // for normalizing the flux. We store this volume in a separate tensor.
746
    // We only contribute to each volume tally bin once per FSR.
747
    if (volume_normalized_flux_tallies_) {
15,909,360✔
748
      for (const auto& task : source_regions_.volume_task(sr)) {
36,769,200✔
749
        if (task.score_type == SCORE_FLUX) {
21,364,800✔
750
#pragma omp atomic
751
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
19,167,600✔
752
            volume;
753
        }
754
      }
755
    }
756
  } // end FSR loop
757

758
  // Normalize any flux scores by the total volume of the FSRs scoring to that
759
  // bin. To do this, we loop over all tallies, and then all filter bins,
760
  // and then scores. For each score, we check the tally data structure to
761
  // see what index that score corresponds to. If that score is a flux score,
762
  // then we divide it by volume.
763
  if (volume_normalized_flux_tallies_) {
9,826✔
764
#ifdef OPENMC_MPI
765
    if (mpi::n_procs > 1) {
3,640✔
766
      for (auto& volumes : tally_volumes_) {
11,440✔
767
        MPI_Allreduce(MPI_IN_PLACE, volumes.data(),
8,400✔
768
          static_cast<int>(volumes.size()), MPI_DOUBLE, MPI_SUM,
8,400✔
769
          mpi::intracomm);
770
      }
771
    }
772
#endif
773
    for (int i = 0; i < model::tallies.size(); i++) {
24,750✔
774
      Tally& tally {*model::tallies[i]};
17,400✔
775
#pragma omp parallel for
10,350✔
776
      for (int64_t bin = 0; bin < tally.n_filter_bins(); bin++) {
869,430✔
777
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,751,460✔
778
          auto score_type = tally.scores_[score_idx];
889,080✔
779
          if (score_type == SCORE_FLUX) {
889,080✔
780
            double vol = tally_volumes_[i](bin, score_idx);
862,380!
781
            if (vol > 0.0) {
862,380!
782
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
862,380✔
783
            }
784
          }
785
        }
786
      }
787
    }
788
  }
789

790
  openmc::simulation::time_tallies.stop();
9,826✔
791
}
9,826✔
792

793
double FlatSourceDomain::evaluate_flux_at_point(
×
794
  Position r, int64_t sr, int g) const
795
{
796
  return source_regions_.scalar_flux_final(sr, g) /
×
797
         (settings::n_batches - settings::n_inactive);
×
798
}
799

800
// Outputs all basic material, FSR ID, multigroup flux, and
801
// fission source data to .vtk file that can be directly
802
// loaded and displayed by Paraview. Note that .vtk binary
803
// files require big endian byte ordering, so endianness
804
// is checked and flipped if necessary.
805
void FlatSourceDomain::output_to_vtk() const
×
806
{
807
  // Rename .h5 plot filename(s) to .vtk filenames
808
  for (int p = 0; p < model::plots.size(); p++) {
×
809
    PlottableInterface* plot = model::plots[p].get();
×
810
    plot->path_plot() =
×
811
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
812
  }
813

814
  // Print header information
815
  print_plot();
×
816

817
  // Outer loop over plots
818
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
819

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

823
    // Random ray plots only support voxel plots
824
    if (!openmc_plot) {
×
825
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
826
                          "is allowed in random ray mode.",
827
        plt));
828
      continue;
×
829
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
830
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
831
                          "is allowed in random ray mode.",
832
        plt));
833
      continue;
×
834
    }
835

836
    int Nx = openmc_plot->pixels_[0];
×
837
    int Ny = openmc_plot->pixels_[1];
×
838
    int Nz = openmc_plot->pixels_[2];
×
839
    Position origin = openmc_plot->origin_;
×
840
    Position width = openmc_plot->width_;
×
841
    Position ll = origin - width / 2.0;
×
842
    double x_delta = width.x / Nx;
×
843
    double y_delta = width.y / Ny;
×
844
    double z_delta = width.z / Nz;
×
845
    std::string filename = openmc_plot->path_plot();
×
846

847
    // Tag plots written during the forward solve of an adjoint run
848
    if (solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) {
×
849
      auto dot = filename.find_last_of('.');
×
850
      filename = filename.substr(0, dot) + ".forward" + filename.substr(dot);
×
851
    }
852

853
    // Perform sanity checks on file size
854
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
855
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
856
      openmc_plot->id(), filename, bytes / 1.0e6);
×
857
    if (bytes / 1.0e9 > 1.0) {
×
858
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
859
              "slow.");
860
    } else if (bytes / 1.0e9 > 100.0) {
×
861
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
862
    }
863

864
    // Relate voxel spatial locations to random ray source regions
865
    vector<int> voxel_indices(Nx * Ny * Nz);
×
NEW
866
    vector<SourceRegionKey> voxel_indices_key(Nx * Ny * Nz);
×
867
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
868
    vector<double> weight_windows(Nx * Ny * Nz);
×
869
    float min_weight = 1e20;
×
870
#pragma omp parallel for collapse(3) reduction(min : min_weight)
871
    for (int z = 0; z < Nz; z++) {
×
872
      for (int y = 0; y < Ny; y++) {
×
873
        for (int x = 0; x < Nx; x++) {
×
874
          Position sample;
875
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
876
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
877
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
878
          Particle p;
×
879
          p.r() = sample;
×
880
          p.r_last() = sample;
881
          p.E() = 1.0;
882
          p.E_last() = 1.0;
883
          p.u() = {1.0, 0.0, 0.0};
×
884

885
          bool found = exhaustive_find_cell(p);
×
886
          if (!found) {
×
887
            voxel_indices_key[z * Ny * Nx + y * Nx + x] = {-1, -1};
888
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
889
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
890
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
891
            continue;
892
          }
893

894
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
895
          int64_t sr = -1;
896
          auto it = source_region_map_.find(sr_key);
×
897
          if (it != source_region_map_.end()) {
×
898
            sr = it->second;
899
          }
900

901
          voxel_indices_key[z * Ny * Nx + y * Nx + x] = sr_key;
×
902
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
903
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
904

905
          if (variance_reduction::weight_windows.size() == 1) {
×
906
            auto [ww_found, ww] =
907
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
908
            float weight = ww.lower_weight;
909
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
×
910
            if (weight < min_weight)
×
911
              min_weight = weight;
912
          }
913
        }
914
      }
915
    }
916

917
    double source_normalization_factor =
×
918
      compute_fixed_source_normalization_factor();
×
919

920
    // Open file for writing
921
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
922

923
    // Write vtk metadata
924
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
925
    std::fprintf(plot, "Dataset File\n");
×
926
    std::fprintf(plot, "BINARY\n");
×
927
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
928
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
929
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
930
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
931
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
932

933
    int64_t num_neg = 0;
934
    int64_t num_samples = 0;
935
    float min_flux = 0.0;
936
    float max_flux = -1.0e20;
937
    // Plot multigroup flux data
938
    for (int g = 0; g < negroups_; g++) {
×
939
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
940
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
941
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
942
        int64_t fsr = voxel_indices[i];
×
943
        float flux = 0;
×
944
        if (fsr >= 0) {
×
945
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
946
          if (flux < 0.0)
×
947
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
948
              voxel_positions[i], fsr, g);
×
949
        }
950
        if (flux < 0.0) {
×
951
          num_neg++;
×
952
          if (flux < min_flux) {
×
953
            min_flux = flux;
×
954
          }
955
        }
956
        if (flux > max_flux)
×
957
          max_flux = flux;
×
958
        num_samples++;
×
959
        flux = convert_to_big_endian<float>(flux);
×
960
        std::fwrite(&flux, sizeof(float), 1, plot);
×
961
      }
962
    }
963

964
    // Slightly negative fluxes can be normal when sampling corners of linear
965
    // source regions. However, very common and high magnitude negative fluxes
966
    // may indicate numerical instability.
967
    if (num_neg > 0) {
×
968
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
969
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
970
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
971
    }
972

973
    // Plot FSRs
974
    std::fprintf(plot, "SCALARS FSRs float\n");
×
975
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
976
    for (int fsr : voxel_indices) {
×
977
      float value = future_prn(10, fsr);
×
978
      value = convert_to_big_endian<float>(value);
×
979
      std::fwrite(&value, sizeof(float), 1, plot);
×
980
    }
981

982
    // Plot Materials
983
    std::fprintf(plot, "SCALARS Materials int\n");
×
984
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
985
    for (int fsr : voxel_indices) {
×
986
      int mat = -1;
×
987
      if (fsr >= 0)
×
988
        mat = source_regions_.material(fsr);
×
989
      mat = convert_to_big_endian<int>(mat);
×
990
      std::fwrite(&mat, sizeof(int), 1, plot);
×
991
    }
992

993
    // Plot fission source
994
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
995
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
996
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
997
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
998
        int64_t fsr = voxel_indices[i];
×
999
        float total_fission = 0.0;
×
1000
        if (fsr >= 0) {
×
1001
          int mat = source_regions_.material(fsr);
×
1002
          int temp = source_regions_.temperature_idx(fsr);
×
1003
          if (mat != MATERIAL_VOID) {
×
1004
            for (int g = 0; g < negroups_; g++) {
×
1005
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1006
              double sigma_f =
×
1007
                sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
1008
                source_regions_.density_mult(fsr);
×
1009
              total_fission += sigma_f * flux;
×
1010
            }
1011
          }
1012
        }
1013
        total_fission = convert_to_big_endian<float>(total_fission);
×
1014
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
1015
      }
1016
    } else {
1017
      std::fprintf(plot, "SCALARS external_source float\n");
×
1018
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1019
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1020
        int64_t fsr = voxel_indices[i];
×
1021
        int mat = source_regions_.material(fsr);
×
1022
        int temp = source_regions_.temperature_idx(fsr);
×
1023
        float total_external = 0.0f;
×
1024
        if (fsr >= 0) {
×
1025
          for (int g = 0; g < negroups_; g++) {
×
1026
            // External sources are already divided by sigma_t, so we need to
1027
            // multiply it back to get the true external source.
1028
            double sigma_t = 1.0;
×
1029
            if (mat != MATERIAL_VOID) {
×
1030
              sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
1031
                        source_regions_.density_mult(fsr);
×
1032
            }
1033
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
1034
          }
1035
        }
1036
        total_external = convert_to_big_endian<float>(total_external);
×
1037
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
1038
      }
1039
    }
1040

1041
    // Plot weight window data
1042
    if (variance_reduction::weight_windows.size() == 1) {
×
1043
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
1044
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1045
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1046
        float weight = weight_windows[i];
×
1047
        if (weight == 0.0)
×
1048
          weight = min_weight;
×
1049
        weight = convert_to_big_endian<float>(weight);
×
1050
        std::fwrite(&weight, sizeof(float), 1, plot);
×
1051
      }
1052
    }
1053

1054
    std::fclose(plot);
×
1055
  }
×
1056
}
×
1057

1058
// Variant of output_to_vtk() for domain decomposition case. Every rank
1059
// contributes the data of its own subdomain, reduced onto the master rank for
1060
// file output.
1061
#ifdef OPENMC_MPI
1062
void FlatSourceDomain::output_to_vtk_decomp() const
1063
{
1064

1065
  if (mpi::master) {
×
1066
    // Rename .h5 plot filename(s) to .vtk filenames
1067
    for (int p = 0; p < model::plots.size(); p++) {
×
1068
      PlottableInterface* plot = model::plots[p].get();
1069
      plot->path_plot() =
1070
        plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) +
×
1071
        ".vtk";
1072
    }
1073

1074
    // Print header information
1075
    print_plot();
1076
  }
1077

1078
  // Outer loop over plots
1079
  for (int p = 0; p < model::plots.size(); p++) {
×
1080

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

1084
    // Random ray plots only support voxel plots
1085
    if (!openmc_plot) {
×
1086
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
1087
                          "is allowed in random ray mode.",
1088
        p));
1089
      continue;
1090
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
1091
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
1092
                          "is allowed in random ray mode.",
1093
        p));
1094
      continue;
1095
    }
1096

1097
    int Nx = openmc_plot->pixels_[0];
1098
    int Ny = openmc_plot->pixels_[1];
1099
    int Nz = openmc_plot->pixels_[2];
1100
    Position origin = openmc_plot->origin_;
1101
    Position width = openmc_plot->width_;
1102
    Position ll = origin - width / 2.0;
1103
    double x_delta = width.x / Nx;
1104
    double y_delta = width.y / Ny;
1105
    double z_delta = width.z / Nz;
1106
    std::string filename = openmc_plot->path_plot();
1107

1108
    // Tag plots written during the forward solve of an adjoint run
1109
    if (solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) {
×
1110
      auto dot = filename.find_last_of('.');
1111
      filename = filename.substr(0, dot) + ".forward" + filename.substr(dot);
×
1112
    }
1113

1114
    // Perform sanity checks on file size
1115
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
1116
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
1117
      openmc_plot->id(), filename, bytes / 1.0e6);
×
1118
    if (bytes / 1.0e9 > 1.0) {
×
1119
      if (mpi::master) {
×
1120
        warning(
×
1121
          "Voxel plot specification is very large (>1 GB). Plotting may be "
1122
          "slow.");
1123
      }
1124
    } else if (bytes / 1.0e9 > 100.0) {
×
1125
      if (mpi::master) {
×
1126
        fatal_error(
1127
          "Voxel plot specification is too large (>100 GB). Exiting.");
1128
      }
1129
    }
1130

1131
    // Relate voxel spatial locations to random ray source regions
1132
    vector<int> voxel_indices(Nx * Ny * Nz);
×
1133
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
1134
    vector<double> weight_windows(Nx * Ny * Nz);
×
1135
    vector<int> my_voxel_ids;
×
1136
    float min_weight = 1e20;
1137
#pragma omp parallel for collapse(3) reduction(min : min_weight)
1138
    for (int z = 0; z < Nz; z++) {
×
1139
      for (int y = 0; y < Ny; y++) {
×
1140
        for (int x = 0; x < Nx; x++) {
×
1141
          Position sample;
1142
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
1143
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
1144
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
1145
          Particle p;
×
1146
          p.r() = sample;
×
1147
          p.r_last() = sample;
1148
          p.E() = 1.0;
1149
          p.E_last() = 1.0;
1150
          p.u() = {1.0, 0.0, 0.0};
×
1151

1152
          bool found = exhaustive_find_cell(p);
×
1153
          if (!found) {
×
1154
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
1155
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
1156
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
1157
            continue;
1158
          }
1159

1160
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
1161
          int64_t sr = -1;
1162
          auto it_sr = source_region_map_.find(sr_key);
×
1163
          if (it_sr != source_region_map_.end()) {
×
1164
            sr = it_sr->second;
1165
          }
1166

1167
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
×
1168
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
1169

1170
          // Assumed master rank = 0
1171
          int assigned_rank = 0;
1172
          // Which rank is responsible
1173
          auto it = mpi::decomp_map.subdomain_map_.find(sr_key);
×
1174
          if (it != mpi::decomp_map.subdomain_map_.end()) {
×
1175
            assigned_rank = it->second;
1176
          }
1177
          if (assigned_rank == mpi::rank) {
×
1178
#pragma omp critical(create_my_voxel_ids)
1179
            {
1180
              my_voxel_ids.push_back(z * Ny * Nx + y * Nx + x);
×
1181
            }
1182
          }
1183

1184
          if (variance_reduction::weight_windows.size() == 1) {
×
1185
            auto [ww_found, ww] =
1186
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
1187
            float weight = ww.lower_weight;
1188
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
×
1189
            if (weight < min_weight)
×
1190
              min_weight = weight;
1191
          }
1192
        }
1193
      }
1194
    }
1195

1196
    double source_normalization_factor =
1197
      compute_fixed_source_normalization_factor();
×
1198

1199
    // Open file for writing
1200
    std::FILE* plot = nullptr;
1201
    if (mpi::master) {
×
1202
      plot = std::fopen(filename.c_str(), "wb");
×
1203

1204
      // Write vtk metadata
1205
      std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
1206
      std::fprintf(plot, "Dataset File\n");
×
1207
      std::fprintf(plot, "BINARY\n");
×
1208
      std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
1209
      std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
1210
      std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
1211
      std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
1212
      std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
1213
    }
1214

1215
    int vector_size = Nx * Ny * Nz;
1216
    vector<float> vector_out_float(vector_size, 0.0);
×
1217
    vector<int> vector_out_int(vector_size, 0);
×
1218

1219
    int64_t num_neg = 0;
1220
    int64_t num_samples = 0;
1221
    float min_flux = 0.0;
1222
    float max_flux = -1.0e20;
1223

1224
    // Plot multigroup flux data
1225
    for (int g = 0; g < negroups_; g++) {
×
1226

1227
      for (int voxel_id : my_voxel_ids) {
×
1228
        int64_t fsr = voxel_indices[voxel_id];
×
1229
        float flux = 0;
1230
        if (fsr >= 0) {
×
1231
          flux = evaluate_flux_at_point(voxel_positions[voxel_id], fsr, g);
×
1232
          if (flux < 0.0)
×
1233
            flux = FlatSourceDomain::evaluate_flux_at_point(
1234
              voxel_positions[voxel_id], fsr, g);
×
1235
        }
1236
        if (flux < 0.0) {
×
1237
          num_neg++;
1238
          if (flux < min_flux) {
×
1239
            min_flux = flux;
1240
          }
1241
        }
1242
        if (flux > max_flux)
×
1243
          max_flux = flux;
1244
        num_samples++;
1245
        vector_out_float[voxel_id] = flux;
1246
      }
1247

1248
      if (mpi::master) {
×
1249
        MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size,
×
1250
          MPI_FLOAT, MPI_SUM, 0, mpi::intracomm);
1251
        MPI_Reduce(
×
1252
          MPI_IN_PLACE, &num_neg, 1, MPI_INT64_T, MPI_SUM, 0, mpi::intracomm);
1253
        MPI_Reduce(MPI_IN_PLACE, &num_samples, 1, MPI_INT64_T, MPI_SUM, 0,
×
1254
          mpi::intracomm);
1255
      } else {
1256
        MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1257
          MPI_SUM, 0, mpi::intracomm);
1258
        MPI_Reduce(
×
1259
          &num_neg, nullptr, 1, MPI_INT64_T, MPI_SUM, 0, mpi::intracomm);
1260
        MPI_Reduce(
×
1261
          &num_samples, nullptr, 1, MPI_INT64_T, MPI_SUM, 0, mpi::intracomm);
1262
      }
1263

1264
      if (mpi::master) {
×
1265
        std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
1266
        std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1267
        for (float value : vector_out_float) {
×
1268
          float print_value = convert_to_big_endian<float>(value);
1269
          std::fwrite(&print_value, sizeof(float), 1, plot);
×
1270
        }
1271
      }
1272

1273
      fill(vector_out_float.begin(), vector_out_float.end(), 0.0);
1274
    }
1275

1276
    // Slightly negative fluxes can be normal when sampling corners of linear
1277
    // source regions. However, very common and high magnitude negative fluxes
1278
    // may indicate numerical instability.
1279
    if (mpi::master && num_neg > 0) {
×
1280
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
1281
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
1282
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
1283
    }
1284

1285
    // Plot FSRs
1286
    for (int voxel_id : my_voxel_ids) {
×
1287
      int fsr = voxel_indices[voxel_id];
×
1288
      float value = future_prn(10, fsr);
×
1289
      vector_out_float[voxel_id] = value;
1290
    }
1291

1292
    if (mpi::master) {
×
1293
      MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size, MPI_FLOAT,
×
1294
        MPI_SUM, 0, mpi::intracomm);
1295
    } else {
1296
      MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1297
        MPI_SUM, 0, mpi::intracomm);
1298
    }
1299

1300
    if (mpi::master) {
×
1301
      std::fprintf(plot, "SCALARS FSRs float\n");
×
1302
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1303
      for (float value : vector_out_float) {
×
1304
        float print_value = convert_to_big_endian<float>(value);
1305
        std::fwrite(&print_value, sizeof(float), 1, plot);
×
1306
      }
1307
    }
1308

1309
    fill(vector_out_float.begin(), vector_out_float.end(), 0.0);
1310

1311
    // Plot Materials
1312
    for (int voxel_id : my_voxel_ids) {
×
1313
      int mat = -1;
1314
      int fsr = voxel_indices[voxel_id];
×
1315
      if (fsr >= 0) {
×
1316
        mat = source_regions_.material(fsr);
1317
      }
1318
      vector_out_int[voxel_id] = mat + 1; // To avoid -1 for void (MPI_SUM)
1319
    }
1320

1321
    if (mpi::master) {
×
1322
      MPI_Reduce(MPI_IN_PLACE, vector_out_int.data(), vector_size, MPI_INT,
×
1323
        MPI_SUM, 0, mpi::intracomm);
1324
    } else {
1325
      MPI_Reduce(vector_out_int.data(), nullptr, vector_size, MPI_INT, MPI_SUM,
×
1326
        0, mpi::intracomm);
1327
    }
1328

1329
    if (mpi::master) {
×
1330
      std::fprintf(plot, "SCALARS Materials int\n");
×
1331
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1332

1333
      for (int value : vector_out_int) {
×
1334
        int print_value = convert_to_big_endian<int>(value);
1335
        std::fwrite(&print_value, sizeof(int), 1, plot);
×
1336
      }
1337
    }
1338

1339
    fill(vector_out_int.begin(), vector_out_int.end(), 0);
1340

1341
    // Plot rank subdomains
1342
    for (int voxel_id : my_voxel_ids) {
×
1343
      int rank_id = mpi::rank;
1344
      float value = future_prn(10, rank_id);
×
1345
      vector_out_float[voxel_id] = value;
1346
    }
1347

1348
    if (mpi::master) {
×
1349
      MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size, MPI_FLOAT,
×
1350
        MPI_SUM, 0, mpi::intracomm);
1351
    } else {
1352
      MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1353
        MPI_SUM, 0, mpi::intracomm);
1354
    }
1355

1356
    if (mpi::master) {
×
1357
      std::fprintf(plot, "SCALARS rank_subdomains float\n");
×
1358
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1359

1360
      for (float value : vector_out_float) {
×
1361
        float print_value = convert_to_big_endian<float>(value);
1362
        std::fwrite(&print_value, sizeof(float), 1, plot);
×
1363
      }
1364
    }
1365

1366
    fill(vector_out_float.begin(), vector_out_float.end(), 0.0);
1367

1368
    // Plot measured load based on transport sweep timers
1369
    for (int voxel_id : my_voxel_ids) {
×
1370
      float value = mpi::decomp_map.measured_rank_load_fractions_[mpi::rank];
1371
      vector_out_float[voxel_id] = value;
1372
    }
1373

1374
    if (mpi::master) {
×
1375
      MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size, MPI_FLOAT,
×
1376
        MPI_SUM, 0, mpi::intracomm);
1377
    } else {
1378
      MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1379
        MPI_SUM, 0, mpi::intracomm);
1380
    }
1381

1382
    if (mpi::master) {
×
1383
      std::fprintf(plot, "SCALARS measured_load float\n");
×
1384
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1385

1386
      for (float value : vector_out_float) {
×
1387
        float print_value = convert_to_big_endian<float>(value);
1388
        std::fwrite(&print_value, sizeof(float), 1, plot);
×
1389
      }
1390
    }
1391

1392
    fill(vector_out_float.begin(), vector_out_float.end(), 0.0);
1393

1394
    // Plot fission source
1395
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
1396
      for (int voxel_id : my_voxel_ids) {
×
1397
        int64_t fsr = voxel_indices[voxel_id];
×
1398
        float total_fission = 0.0;
1399
        if (fsr >= 0) {
×
1400
          int mat = source_regions_.material(fsr);
×
1401
          int temp = source_regions_.temperature_idx(fsr);
×
1402
          if (mat != MATERIAL_VOID) {
×
1403
            for (int g = 0; g < negroups_; g++) {
×
1404
              float flux =
1405
                evaluate_flux_at_point(voxel_positions[voxel_id], fsr, g);
×
1406
              double sigma_f =
1407
                sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] *
1408
                source_regions_.density_mult(fsr);
1409
              total_fission += sigma_f * flux;
1410
            }
1411
          }
1412
        }
1413
        vector_out_float[voxel_id] = total_fission;
1414
      }
1415
    } else {
1416
      for (int voxel_id : my_voxel_ids) {
×
1417
        int64_t fsr = voxel_indices[voxel_id];
×
1418
        int mat = source_regions_.material(fsr);
×
1419
        int temp = source_regions_.temperature_idx(fsr);
×
1420
        float total_external = 0.0f;
1421
        if (fsr >= 0) {
×
1422
          for (int g = 0; g < negroups_; g++) {
×
1423
            // External sources are already divided by sigma_t, so we need to
1424
            // multiply it back to get the true external source.
1425
            double sigma_t = 1.0;
1426
            if (mat != MATERIAL_VOID) {
×
1427
              sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] *
1428
                        source_regions_.density_mult(fsr);
1429
            }
1430
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
1431
          }
1432
        }
1433
        vector_out_float[voxel_id] = total_external;
1434
      }
1435
    }
1436

1437
    if (mpi::master) {
×
1438
      MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size, MPI_FLOAT,
×
1439
        MPI_SUM, 0, mpi::intracomm);
1440
    } else {
1441
      MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1442
        MPI_SUM, 0, mpi::intracomm);
1443
    }
1444

1445
    if (mpi::master) {
×
1446
      if (settings::run_mode == RunMode::EIGENVALUE) {
×
1447
        std::fprintf(plot, "SCALARS total_fission_source float\n");
×
1448
      } else {
1449
        std::fprintf(plot, "SCALARS external_source float\n");
×
1450
      }
1451
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1452
      for (float value : vector_out_float) {
×
1453
        float print_value = convert_to_big_endian<float>(value);
1454
        std::fwrite(&print_value, sizeof(float), 1, plot);
×
1455
      }
1456
    }
1457

1458
    fill(vector_out_float.begin(), vector_out_float.end(), 0.0);
1459

1460
    // Plot weight window data
1461
    if (variance_reduction::weight_windows.size() == 1) {
×
1462
      for (int voxel_id : my_voxel_ids) {
×
1463
        float weight = weight_windows[voxel_id];
×
1464
        if (weight == 0.0)
×
1465
          weight = min_weight;
1466
        vector_out_float[voxel_id] = weight;
1467
      }
1468

1469
      if (mpi::master) {
×
1470
        MPI_Reduce(MPI_IN_PLACE, vector_out_float.data(), vector_size,
×
1471
          MPI_FLOAT, MPI_SUM, 0, mpi::intracomm);
1472
      } else {
1473
        MPI_Reduce(vector_out_float.data(), nullptr, vector_size, MPI_FLOAT,
×
1474
          MPI_SUM, 0, mpi::intracomm);
1475
      }
1476

1477
      if (mpi::master) {
×
1478
        std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
1479
        std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1480

1481
        for (float value : vector_out_float) {
×
1482
          float print_value = convert_to_big_endian<float>(value);
1483
          std::fwrite(&print_value, sizeof(float), 1, plot);
×
1484
        }
1485
      }
1486
    }
1487

1488
    if (mpi::master) {
×
1489
      std::fclose(plot);
×
1490
    }
1491
  }
1492
}
1493
#endif // OPENMC_MPI
1494

1495
void FlatSourceDomain::apply_external_source_to_source_region(
5,138✔
1496
  int src_idx, SourceRegionHandle& srh)
1497
{
1498
  auto s =
5,138✔
1499
    (solve_ == RandomRaySolve::ADJOINT && !model::adjoint_sources.empty())
176!
1500
      ? model::adjoint_sources[src_idx].get()
5,314✔
1501
      : model::external_sources[src_idx].get();
4,962✔
1502
  auto is = dynamic_cast<IndependentSource*>(s);
5,138!
1503
  auto discrete = dynamic_cast<Discrete*>(is->energy());
5,138!
1504
  double strength_factor = is->strength();
5,138✔
1505
  const auto& discrete_energies = discrete->x();
5,138✔
1506
  const auto& discrete_probs = discrete->prob();
5,138✔
1507

1508
  srh.external_source_present() = 1;
5,138✔
1509

1510
  for (int i = 0; i < discrete_energies.size(); i++) {
10,408✔
1511
    int g = data::mg.get_group_index(discrete_energies[i]);
5,270✔
1512
    srh.external_source(g) += discrete_probs[i] * strength_factor;
5,270✔
1513
  }
1514
}
5,138✔
1515

1516
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
487✔
1517
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1518
{
1519
  Cell& cell = *model::cells[i_cell];
487!
1520

1521
  if (cell.type_ != Fill::MATERIAL)
487!
1522
    return;
1523

1524
  for (int j : instances) {
29,264✔
1525
    int cell_material_idx = cell.material(j);
28,777!
1526
    int cell_material_id;
28,777✔
1527
    if (cell_material_idx == MATERIAL_VOID) {
28,777✔
1528
      cell_material_id = MATERIAL_VOID;
1529
    } else {
1530
      cell_material_id = model::materials[cell_material_idx]->id();
28,537✔
1531
    }
1532
    if (target_material_id == C_NONE ||
28,777✔
1533
        cell_material_id == target_material_id) {
28,777✔
1534
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,977✔
1535
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,977✔
1536
    }
1537
  }
1538
}
1539

1540
void FlatSourceDomain::apply_external_source_to_cell_and_children(
517✔
1541
  int32_t i_cell, int src_idx, int32_t target_material_id)
1542
{
1543
  Cell& cell = *model::cells[i_cell];
517✔
1544

1545
  if (cell.type_ == Fill::MATERIAL) {
517✔
1546
    vector<int> instances(cell.n_instances());
487✔
1547
    std::iota(instances.begin(), instances.end(), 0);
487✔
1548
    apply_external_source_to_cell_instances(
487✔
1549
      i_cell, src_idx, target_material_id, instances);
1550
  } else if (target_material_id == C_NONE) {
517!
1551
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
×
1552
      cell.get_contained_cells(0, nullptr);
×
1553
    for (const auto& pair : cell_instance_list) {
×
1554
      int32_t i_child_cell = pair.first;
×
1555
      apply_external_source_to_cell_instances(
×
1556
        i_child_cell, src_idx, target_material_id, pair.second);
×
1557
    }
1558
  }
×
1559
}
517✔
1560

1561
void FlatSourceDomain::count_external_source_regions()
1,385✔
1562
{
1563
  n_external_source_regions_ = 0;
1,385✔
1564
#pragma omp parallel for reduction(+ : n_external_source_regions_)
828✔
1565
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,299,527✔
1566
    if (source_regions_.external_source_present(sr)) {
1,298,970✔
1567
      n_external_source_regions_++;
191,800✔
1568
    }
1569
  }
1570
}
1,385✔
1571

1572
void FlatSourceDomain::convert_external_sources(bool use_adjoint_sources)
458✔
1573
{
1574
  // Determine whether forward or (local) adjoint sources are desired
1575
  const auto& sources =
458✔
1576
    use_adjoint_sources ? model::adjoint_sources : model::external_sources;
1577

1578
  // Loop over external sources
1579
  for (int es = 0; es < sources.size(); es++) {
916✔
1580

1581
    // Extract source information
1582
    Source* s = sources[es].get();
458!
1583
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
458!
1584
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
458✔
1585
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
458✔
1586
    double strength_factor = is->strength();
458✔
1587

1588
    // If there is no domain constraint specified, then this must be a point
1589
    // source. In this case, we need to find the source region that contains the
1590
    // point source and apply or relate it to the external source.
1591
    if (is->domain_ids().size() == 0) {
458✔
1592

1593
      // Extract the point source coordinate and find the base source region at
1594
      // that point
1595
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
16!
1596
      GeometryState gs;
16✔
1597
      gs.r() = sp->r();
16✔
1598
      gs.r_last() = sp->r();
16✔
1599
      gs.u() = {1.0, 0.0, 0.0};
16✔
1600
      bool found = exhaustive_find_cell(gs);
16✔
1601
      if (!found) {
16!
1602
        fatal_error(fmt::format("Could not find cell containing external "
×
1603
                                "point source at {}",
1604
          sp->r()));
×
1605
      }
1606
      SourceRegionKey key = lookup_source_region_key(gs);
16✔
1607

1608
      // With the source region and mesh bin known, we can use the
1609
      // accompanying SourceRegionKey as a key into a map that stores the
1610
      // corresponding external source index for the point source. Notably, we
1611
      // do not actually apply the external source to any source regions here,
1612
      // as if mesh subdivision is enabled, they haven't actually been
1613
      // discovered & initilized yet. When discovered, they will read from the
1614
      // external_source_map to determine if there are any external source
1615
      // terms that should be applied.
1616
      external_point_source_map_[key].push_back(es);
16✔
1617

1618
    } else {
16✔
1619
      // If not a point source, then use the volumetric domain constraints to
1620
      // determine which source regions to apply the external source to.
1621
      if (is->domain_type() == Source::DomainType::MATERIAL) {
442✔
1622
        for (int32_t material_id : domain_ids) {
60✔
1623
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
120✔
1624
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
90✔
1625
          }
1626
        }
1627
      } else if (is->domain_type() == Source::DomainType::CELL) {
412✔
1628
        for (int32_t cell_id : domain_ids) {
119✔
1629
          int32_t i_cell = model::cell_map[cell_id];
67✔
1630
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
67✔
1631
        }
1632
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
360!
1633
        for (int32_t universe_id : domain_ids) {
720✔
1634
          int32_t i_universe = model::universe_map[universe_id];
360✔
1635
          Universe& universe = *model::universes[i_universe];
360✔
1636
          for (int32_t i_cell : universe.cells_) {
720✔
1637
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
360✔
1638
          }
1639
        }
1640
      }
1641
    }
1642
  } // End loop over external sources
1643
}
458✔
1644

1645
void FlatSourceDomain::flux_swap()
21,842✔
1646
{
1647
  source_regions_.flux_swap();
21,842✔
1648
}
21,842✔
1649

1650
void FlatSourceDomain::flatten_xs()
814✔
1651
{
1652
  // Temperature and angle indices, if using multiple temperature
1653
  // data sets and/or anisotropic data sets.
1654
  // TODO: Currently assumes we are only using single angle data.
1655
  const int a = 0;
814✔
1656

1657
  n_materials_ = data::mg.macro_xs_.size();
814✔
1658
  ntemperature_ = 1;
814✔
1659
  for (int i = 0; i < n_materials_; i++) {
3,033✔
1660
    ntemperature_ =
4,438✔
1661
      std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points());
2,234✔
1662
  }
1663

1664
  for (int i = 0; i < n_materials_; i++) {
3,033✔
1665
    auto& m = data::mg.macro_xs_[i];
2,219✔
1666
    for (int t = 0; t < ntemperature_; t++) {
4,468✔
1667
      for (int g_out = 0; g_out < negroups_; g_out++) {
11,579✔
1668
        if (m.exists_in_model && t < m.n_temperature_points()) {
9,330✔
1669
          double sigma_t =
9,165✔
1670
            m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
9,165✔
1671
          sigma_t_.push_back(sigma_t);
9,165✔
1672

1673
          if (sigma_t < MINIMUM_MACRO_XS) {
9,165✔
1674
            Material* mat = model::materials[i].get();
26✔
1675
            warning(fmt::format(
36✔
1676
              "Material \"{}\" (id: {}) has a group {} total cross section "
1677
              "({:.3e}) below the minimum threshold "
1678
              "({:.3e}). Material will be treated as pure void.",
1679
              mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
47✔
1680
          }
1681

1682
          double nu_sigma_f =
9,165✔
1683
            m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
9,165✔
1684
          nu_sigma_f_.push_back(nu_sigma_f);
9,165✔
1685

1686
          double sigma_f =
9,165✔
1687
            m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
9,165✔
1688
          sigma_f_.push_back(sigma_f);
9,165✔
1689

1690
          double chi =
9,165✔
1691
            m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
9,165✔
1692
          if (!std::isfinite(chi)) {
9,165✔
1693
            // MGXS interface may return NaN in some cases, such as when
1694
            // material is fissionable but has very small sigma_f.
1695
            chi = 0.0;
660✔
1696
          }
1697
          chi_.push_back(chi);
9,165✔
1698

1699
          double kappa_fission =
9,165✔
1700
            m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a);
9,165✔
1701
          kappa_fission_.push_back(kappa_fission);
9,165✔
1702

1703
          for (int g_in = 0; g_in < negroups_; g_in++) {
261,518✔
1704
            double sigma_s =
252,353✔
1705
              m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
252,353✔
1706
            sigma_s_.push_back(sigma_s);
252,353✔
1707
            // For transport corrected XS data, diagonal elements may be
1708
            // negative. In this case, set a flag to enable transport
1709
            // stabilization for the simulation.
1710
            if (g_out == g_in && sigma_s < 0.0)
252,353✔
1711
              is_transport_stabilization_needed_ = true;
825✔
1712
          }
1713
        } else {
1714
          sigma_t_.push_back(0);
165✔
1715
          nu_sigma_f_.push_back(0);
165✔
1716
          sigma_f_.push_back(0);
165✔
1717
          chi_.push_back(0);
165✔
1718
          kappa_fission_.push_back(0);
165✔
1719
          for (int g_in = 0; g_in < negroups_; g_in++) {
960✔
1720
            sigma_s_.push_back(0);
795✔
1721
          }
1722
        }
1723
      }
1724
    }
1725
  }
1726
}
814✔
1727

1728
void FlatSourceDomain::set_fw_adjoint_sources()
98✔
1729
{
1730
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1731
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1732
  // as this is likely a very small source region that we don't need to bother
1733
  // trying to vector particles towards. In the case of flux "being extremely
1734
  // close to zero", we define this as being a fixed fraction of the maximum
1735
  // forward flux, below which we assume the flux would be physically
1736
  // undetectable.
1737

1738
  // First, find the maximum forward flux value
1739
  double max_flux = 0.0;
98✔
1740
#pragma omp parallel for reduction(max : max_flux)
58✔
1741
  for (int64_t se = 0; se < n_source_elements(); se++) {
312,000✔
1742
    double flux = source_regions_.scalar_flux_final(se);
311,960✔
1743
    if (flux > max_flux) {
311,960✔
1744
      max_flux = flux;
70✔
1745
    }
1746
  }
1747

1748
  // Then, compute the adjoint source for each source region
1749
#pragma omp parallel for
58✔
1750
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
204,960✔
1751
    for (int g = 0; g < negroups_; g++) {
516,880✔
1752
      double flux = source_regions_.scalar_flux_final(sr, g);
311,960✔
1753
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
311,960✔
1754
        source_regions_.external_source(sr, g) = 0.0;
1,565✔
1755
      } else {
1756
        source_regions_.external_source(sr, g) = 1.0 / flux;
310,395!
1757
        if (!std::isfinite(source_regions_.external_source(sr, g))) {
310,395!
1758
          // If the flux is NaN or Inf, set the adjoint source to zero
1759
          source_regions_.external_source(sr, g) = 0.0;
1760
        }
1761
      }
1762
      if (flux > 0.0) {
311,960✔
1763
        source_regions_.external_source_present(sr) = 1;
310,395✔
1764
      }
1765
      source_regions_.scalar_flux_final(sr, g) = 0.0;
311,960✔
1766
    }
1767
  }
1768

1769
  // "Small" source regions in OpenMC are defined as those that are hit by
1770
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1771
  // very small volumes combined with a low aspect ratio, and are often
1772
  // generated when applying a source region mesh that clips the edge of a
1773
  // curved surface. As perhaps only a few rays will visit these regions over
1774
  // the entire forward simulation, the forward flux estimates are extremely
1775
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1776
  // extremely low, leading to unphysically large adjoint source terms,
1777
  // resulting in weight windows that aggressively try to drive particles
1778
  // towards these regions. To fix this, we simply filter out any "small" source
1779
  // regions from consideration. If a source region is "small", we
1780
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1781
  // flux solution, as the true total adjoint source contribution from small
1782
  // regions is likely to be negligible.
1783
#pragma omp parallel for
58✔
1784
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
204,960✔
1785
    if (source_regions_.is_small(sr)) {
204,920✔
1786
      for (int g = 0; g < negroups_; g++) {
7,300✔
1787
        source_regions_.external_source(sr, g) = 0.0;
5,840✔
1788
      }
1789
      source_regions_.external_source_present(sr) = 0;
1,460✔
1790
    }
1791
  }
1792
  // Divide the fixed source term by sigma t (to save time when applying each
1793
  // iteration)
1794
#pragma omp parallel for
58✔
1795
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
204,960✔
1796
    int material = source_regions_.material(sr);
204,920✔
1797
    int temp = source_regions_.temperature_idx(sr);
204,920✔
1798
    if (material == MATERIAL_VOID) {
204,920✔
1799
      continue;
19,020✔
1800
    }
1801
    for (int g = 0; g < negroups_; g++) {
421,780✔
1802
      double sigma_t =
235,880✔
1803
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
235,880!
1804
        source_regions_.density_mult(sr);
235,880!
1805
      source_regions_.external_source(sr, g) /= sigma_t;
235,880!
1806
      if (!std::isfinite(source_regions_.external_source(sr, g))) {
235,880!
1807
        // If the flux is NaN or Inf, set the adjoint source to zero
1808
        source_regions_.external_source(sr, g) = 0.0;
1809
      }
1810
    }
1811
  }
1812

1813
  if (fw_cadis_local_) {
98✔
1814
// Only external sources that have a non-mesh type tally task should remain
1815
// non-zero. Everything else gets zero'd out.
1816
#pragma omp parallel for
9✔
1817
    for (int64_t sr = 0; sr < n_source_regions(); sr++) {
13,726✔
1818

1819
      // If there is already no external source, don't need to do anything
1820
      if (source_regions_.external_source_present(sr) == 0) {
13,720!
1821
        continue;
1822
      }
1823

1824
      // If there is an adjoint source term here, then we need to check it.
1825

1826
      // We will track if ANY group has a valid local FW-CADIS source term
1827
      bool has_any_sources = false;
1828

1829
      // Now, loop over groups
1830
      for (int g = 0; g < negroups_; g++) {
27,440✔
1831

1832
        // If there are no tally tasks associated with this source element
1833
        // then it is not a local FW-CADIS source, so we continue to the next
1834
        // group
1835
        if (source_regions_.tally_task(sr, g).empty()) {
13,720!
1836
          source_regions_.external_source(sr, g) = 0.0;
1837
          continue;
1838
        }
1839

1840
        // If there are tally tasks, we can through them and check if
1841
        // any of them are local FW-CADIS targets.
1842

1843
        // We track if ANY of the tasks are local FW-CADIS target tallies
1844
        bool local_fw_cadis_target_region = false;
27,360✔
1845

1846
        // Now we loop through
1847
        for (const auto& task : source_regions_.tally_task(sr, g)) {
27,360✔
1848
          Tally& tally {*model::tallies[task.tally_idx]};
13,720✔
1849
          const auto t_id = tally.id();
13,720✔
1850

1851
          // Search for target tallies
1852
          if (std::find(fw_cadis_local_targets_.begin(),
13,720✔
1853
                fw_cadis_local_targets_.end(),
1854
                t_id) != fw_cadis_local_targets_.end()) {
13,720✔
1855
            local_fw_cadis_target_region = true;
80✔
1856
            break;
80✔
1857
          }
1858
        }
1859

1860
        // If ANY of the tasks is a local FW-CADIS target,
1861
        // Then we keep the source term and set that this
1862
        // source region has a valid FW-CADIS source term.
1863
        // Otherwise, we zero out the source term.
1864
        if (local_fw_cadis_target_region) {
80✔
1865
          has_any_sources = true;
1866
        } else {
1867
          source_regions_.external_source(sr, g) = 0.0;
13,640✔
1868
        }
1869
      } // End loop over groups
1870

1871
      // If there were any valid FW-CADIS source terms for any
1872
      // of the groups, then the SR as a whole counts as a source
1873
      if (has_any_sources) {
13,720✔
1874
        source_regions_.external_source_present(sr) = 1;
80✔
1875
      } else {
1876
        source_regions_.external_source_present(sr) = 0;
13,640✔
1877
      }
1878
    } // End loop over source regions
1879
  } // End local FW-CADIS logic
1880
}
98✔
1881

1882
void FlatSourceDomain::set_local_adjoint_sources()
15✔
1883
{
1884
  // Set the external source to user-specified adjoint sources.
1885
  convert_external_sources(true);
15✔
1886
}
15✔
1887

1888
void FlatSourceDomain::transpose_scattering_matrix()
128✔
1889
{
1890
  // Transpose the inner two dimensions for each material
1891
#pragma omp parallel for
76✔
1892
  for (int m = 0; m < n_materials_; ++m) {
202✔
1893
    for (int t = 0; t < ntemperature_; t++) {
300✔
1894
      int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_;
150✔
1895
      for (int i = 0; i < negroups_; ++i) {
462✔
1896
        for (int j = i + 1; j < negroups_; ++j) {
744✔
1897
          // Calculate indices of the elements to swap
1898
          int idx1 = material_offset + i * negroups_ + j;
432✔
1899
          int idx2 = material_offset + j * negroups_ + i;
432✔
1900

1901
          // Swap the elements to transpose the matrix
1902
          std::swap(sigma_s_[idx1], sigma_s_[idx2]);
432✔
1903
        }
1904
      }
1905
    }
1906
  }
1907
}
128✔
1908

1909
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1910
{
1911
  // Ensure array is correct size
1912
  flux.resize(n_source_regions() * negroups_);
×
1913
// Serialize the final fluxes for output
1914
#pragma omp parallel for
1915
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1916
    flux[se] = source_regions_.scalar_flux_final(se);
1917
  }
1918
}
×
1919

1920
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
1,305✔
1921
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1922
  bool is_target_void)
1923
{
1924
  Cell& cell = *model::cells[i_cell];
1,305!
1925
  if (cell.type_ != Fill::MATERIAL)
1,305!
1926
    return;
1927
  for (int32_t j : instances) {
217,230✔
1928
    int cell_material_idx = cell.material(j);
215,925!
1929
    int cell_material_id = (cell_material_idx == C_NONE)
215,925✔
1930
                             ? C_NONE
215,925✔
1931
                             : model::materials[cell_material_idx]->id();
215,902✔
1932

1933
    if ((target_material_id == C_NONE && !is_target_void) ||
215,925✔
1934
        cell_material_id == target_material_id) {
1935
      int64_t sr = source_region_offsets_[i_cell] + j;
185,925!
1936
      // Check if the key is already present in the mesh_map_
1937
      if (mesh_map_.find(sr) != mesh_map_.end()) {
185,925!
1938
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1939
                                "applied, but trying to apply mesh idx {}",
1940
          sr, mesh_map_[sr], mesh_idx));
×
1941
      }
1942
      // If the SR has not already been assigned, then we can write to it
1943
      mesh_map_[sr] = mesh_idx;
185,925✔
1944
    }
1945
  }
1946
}
1947

1948
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
1,124✔
1949
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1950
{
1951
  Cell& cell = *model::cells[i_cell];
1,124✔
1952

1953
  if (cell.type_ == Fill::MATERIAL) {
1,124✔
1954
    vector<int> instances(cell.n_instances());
973✔
1955
    std::iota(instances.begin(), instances.end(), 0);
973✔
1956
    apply_mesh_to_cell_instances(
973✔
1957
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1958
  } else if (target_material_id == C_NONE && !is_target_void) {
1,124✔
1959
    for (int j = 0; j < cell.n_instances(); j++) {
182✔
1960
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
91✔
1961
        cell.get_contained_cells(j, nullptr);
91✔
1962
      for (const auto& pair : cell_instance_list) {
423✔
1963
        int32_t i_child_cell = pair.first;
332✔
1964
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
332✔
1965
          pair.second, is_target_void);
332✔
1966
      }
1967
    }
91✔
1968
  }
1969
}
1,124✔
1970

1971
void FlatSourceDomain::apply_meshes()
814✔
1972
{
1973
  // Skip if there are no mappings between mesh IDs and domains
1974
  if (mesh_domain_map_.empty())
814✔
1975
    return;
1976

1977
  // Loop over meshes
1978
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
826✔
1979
    Mesh* mesh = model::meshes[mesh_idx].get();
458✔
1980
    int mesh_id = mesh->id();
458✔
1981

1982
    // Skip if mesh id is not present in the map
1983
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
458✔
1984
      continue;
30✔
1985

1986
    // Loop over domains associated with the mesh
1987
    for (auto& domain : mesh_domain_map_[mesh_id]) {
856✔
1988
      Source::DomainType domain_type = domain.first;
428✔
1989
      int domain_id = domain.second;
428✔
1990

1991
      if (domain_type == Source::DomainType::MATERIAL) {
428✔
1992
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
180✔
1993
          if (domain_id == C_NONE) {
150!
1994
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1995
          } else {
1996
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
150✔
1997
          }
1998
        }
1999
      } else if (domain_type == Source::DomainType::CELL) {
398✔
2000
        int32_t i_cell = model::cell_map[domain_id];
30✔
2001
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
30✔
2002
      } else if (domain_type == Source::DomainType::UNIVERSE) {
368!
2003
        int32_t i_universe = model::universe_map[domain_id];
368✔
2004
        Universe& universe = *model::universes[i_universe];
368✔
2005
        for (int32_t i_cell : universe.cells_) {
1,312✔
2006
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
944✔
2007
        }
2008
      }
2009
    }
2010
  }
2011
}
2012

2013
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,578,308,353✔
2014
  SourceRegionKey sr_key, Position r, Direction u)
2015
{
2016
  // Case 1: Check if the source region key is already present in the permanent
2017
  // map. This is the most common condition, as any source region visited in a
2018
  // previous power iteration will already be present in the permanent map. If
2019
  // the source region key is found, we translate the key into a specific 1D
2020
  // source region index and return a handle its position in the
2021
  // source_regions_ vector.
2022
  auto it = source_region_map_.find(sr_key);
1,578,308,353✔
2023
  if (it != source_region_map_.end()) {
1,578,308,353✔
2024
    int64_t sr = it->second;
1,526,407,451✔
2025
    return source_regions_.get_source_region_handle(sr);
1,526,407,451✔
2026
  }
2027

2028
  // Case 2: Check if the source region key is present in the temporary (thread
2029
  // safe) map. This is a common occurrence in the first power iteration when
2030
  // the source region has already been visited already by some other ray. We
2031
  // begin by locking the temporary map before any operations are performed. The
2032
  // lock is not global over the full data structure -- it will be dependent on
2033
  // which key is used.
2034
  discovered_source_regions_.lock(sr_key);
51,900,902✔
2035

2036
  // If the key is found in the temporary map, then we return a handle to the
2037
  // source region that is stored in the temporary map.
2038
  if (discovered_source_regions_.contains(sr_key)) {
51,900,902✔
2039
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
49,393,418✔
2040
    discovered_source_regions_.unlock(sr_key);
49,393,418✔
2041
    return handle;
49,393,418✔
2042
  }
2043

2044
  // Case 3: The source region key is not present anywhere, but it is only due
2045
  // to floating point artifacts. These artifacts occur when the overlaid mesh
2046
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
2047
  // result in the ray tracer detecting an additional (very short) segment
2048
  // though a mesh bin that is actually past the physical source region
2049
  // boundary. This is a result of the the multi-level ray tracing treatment in
2050
  // OpenMC, which depending on the number of universes in the hierarchy etc can
2051
  // result in the wrong surface being selected as the nearest. This can happen
2052
  // in a lattice when there are two directions that both are very close in
2053
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
2054
  // treated as being equivalent so alternative logic is used. However, when we
2055
  // go and ray trace on this with the mesh tracer we may go past the surface
2056
  // bounding the current source region.
2057
  //
2058
  // To filter out this case, before we create the new source region, we double
2059
  // check that the actual starting point of this segment (r) is still in the
2060
  // same geometry source region that we started in. If an artifact is detected,
2061
  // we discard the segment (and attenuation through it) as it is not really a
2062
  // valid source region and will have only an infinitessimally small cell
2063
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
2064
  // and only triggers for very short ray lengths. It can be fixed by decreasing
2065
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
2066
  // consequences for the general ray tracer, so for now we do the below sanity
2067
  // checks before generating phantom source regions. A significant extra cost
2068
  // is incurred in instantiating the GeometryState object and doing a cell
2069
  // lookup, but again, this is going to be an extremely rare thing to check
2070
  // after the first power iteration has completed.
2071

2072
  // Sanity check on source region id
2073
  GeometryState gs;
2,507,484✔
2074
  gs.r() = r + TINY_BIT * u;
2,507,484✔
2075
  gs.u() = {1.0, 0.0, 0.0};
2,507,484✔
2076
  exhaustive_find_cell(gs);
2,507,484✔
2077
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,507,484✔
2078
  if (sr_found != sr_key.base_source_region_id) {
2,507,484✔
2079
    discovered_source_regions_.unlock(sr_key);
121✔
2080
    SourceRegionHandle handle;
121✔
2081
    handle.is_numerical_fp_artifact_ = true;
121✔
2082
    return handle;
121✔
2083
  }
2084

2085
  // Sanity check on mesh bin
2086
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,507,363✔
2087
  if (mesh_idx == C_NONE) {
2,507,363✔
2088
    if (sr_key.mesh_bin != 0) {
393,756!
2089
      discovered_source_regions_.unlock(sr_key);
×
2090
      SourceRegionHandle handle;
×
2091
      handle.is_numerical_fp_artifact_ = true;
×
2092
      return handle;
×
2093
    }
2094
  } else {
2095
    Mesh* mesh = model::meshes[mesh_idx].get();
2,113,607✔
2096
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
2,113,607✔
2097
    if (bin_found != sr_key.mesh_bin) {
2,113,607!
2098
      discovered_source_regions_.unlock(sr_key);
×
2099
      SourceRegionHandle handle;
×
2100
      handle.is_numerical_fp_artifact_ = true;
×
2101
      return handle;
×
2102
    }
2103
  }
2104

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

2112
  // Call the basic constructor for the source region and store in the parallel
2113
  // map.
2114
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,507,363✔
2115
  SourceRegion* sr_ptr =
2,507,363✔
2116
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
2,507,363✔
2117
  SourceRegionHandle handle {*sr_ptr};
2,507,363✔
2118
  handle.key() = sr_key;
2,507,363!
2119

2120
  // Determine the material
2121
  int gs_i_cell = gs.lowest_coord().cell();
2,507,363!
2122
  Cell& cell = *model::cells[gs_i_cell];
2,507,363!
2123
  int material = cell.material(gs.cell_instance());
2,507,363!
2124
  int temp = 0;
2,507,363✔
2125

2126
  // If material total XS is extremely low, just set it to void to avoid
2127
  // problems with 1/Sigma_t
2128
  if (material != MATERIAL_VOID) {
2,507,363✔
2129
    temp = data::mg.macro_xs_[material].get_temperature_index(
4,886,294✔
2130
      cell.sqrtkT(gs.cell_instance()));
2,443,147✔
2131
    for (int g = 0; g < negroups_; g++) {
5,348,757✔
2132
      double sigma_t =
2,905,830✔
2133
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g];
2,905,830✔
2134
      if (sigma_t < MINIMUM_MACRO_XS) {
2,905,830✔
2135
        material = MATERIAL_VOID;
2136
        temp = 0;
2137
        break;
2138
      }
2139
    }
2140
  }
2141

2142
  handle.material() = material;
2,507,363✔
2143
  handle.temperature_idx() = temp;
2,507,363✔
2144
  handle.density_mult() = cell.density_mult(gs.cell_instance());
2,507,363✔
2145

2146
  // Store the mesh index (if any) assigned to this source region
2147
  handle.mesh() = mesh_idx;
2,507,363✔
2148

2149
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,507,363✔
2150
    // Determine if there are any volumetric sources, and apply them.
2151
    // Volumetric sources are specifc only to the base SR idx.
2152
    auto it_vol =
2,451,340✔
2153
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,451,340✔
2154
    if (it_vol != external_volumetric_source_map_.end()) {
2,451,340✔
2155
      const vector<int>& vol_sources = it_vol->second;
5,126✔
2156
      for (int src_idx : vol_sources) {
10,252✔
2157
        apply_external_source_to_source_region(src_idx, handle);
5,126✔
2158
      }
2159
    }
2160

2161
    // Determine if there are any point sources, and apply them.
2162
    // Point sources are specific to the source region key.
2163
    auto it_point = external_point_source_map_.find(sr_key);
2,451,340✔
2164
    if (it_point != external_point_source_map_.end()) {
2,451,340✔
2165
      const vector<int>& point_sources = it_point->second;
12✔
2166
      for (int src_idx : point_sources) {
24✔
2167
        apply_external_source_to_source_region(src_idx, handle);
12✔
2168
      }
2169
    }
2170

2171
    // Divide external source term by sigma_t
2172
    if (material != C_NONE) {
2,451,340✔
2173
      for (int g = 0; g < negroups_; g++) {
4,931,241✔
2174
        double sigma_t =
2,544,337✔
2175
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,544,337✔
2176
          handle.density_mult();
2,544,337✔
2177
        handle.external_source(g) /= sigma_t;
2,544,337✔
2178
      }
2179
    }
2180
  }
2181

2182
  // Compute the combined source term
2183
  update_single_neutron_source(handle);
2,507,363✔
2184

2185
  // Unlock the parallel map. Note: we may be tempted to release
2186
  // this lock earlier, and then just use the source region's lock to protect
2187
  // the flux/source initialization stages above. However, the rest of the code
2188
  // only protects updates to the new flux and volume fields, and assumes that
2189
  // the source is constant for the duration of transport. Thus, using just the
2190
  // source region's lock by itself would result in other threads potentially
2191
  // reading from the source before it is computed, as they won't use the lock
2192
  // when only reading from the SR's source. It would be expensive to protect
2193
  // those operations, whereas generating the SR is only done once, so we just
2194
  // hold the map's bucket lock until the source region is fully initialized.
2195
  discovered_source_regions_.unlock(sr_key);
2,507,363✔
2196

2197
  return handle;
2,507,363✔
2198
}
2,507,484✔
2199

2200
void FlatSourceDomain::finalize_discovered_source_regions()
21,842✔
2201
{
2202
  // Extract keys for entries with a valid volume.
2203
  vector<SourceRegionKey> keys;
21,842✔
2204
  for (const auto& pair : discovered_source_regions_) {
5,057,320✔
2205
    if (pair.second.scalars_.volume_ > 0.0) {
2,506,818✔
2206
      keys.push_back(pair.first);
2,404,313✔
2207
    }
2208
  }
2209

2210
  if (!keys.empty()) {
21,842✔
2211
    // Sort the keys, so as to ensure reproducible ordering given that source
2212
    // regions may have been added to discovered_source_regions_ in an arbitrary
2213
    // order due to shared memory threading.
2214
    std::sort(keys.begin(), keys.end());
1,542✔
2215

2216
    // Remember the index of the first new source region
2217
    int64_t start_sr_id = source_regions_.n_source_regions();
1,542✔
2218

2219
    // Append the source regions in the sorted key order.
2220
    for (const auto& key : keys) {
2,405,855✔
2221
      const SourceRegion& sr = discovered_source_regions_[key];
2,404,313✔
2222
      source_region_map_[key] = source_regions_.n_source_regions();
2,404,313✔
2223
      source_regions_.push_back(sr);
2,404,313✔
2224
    }
2225

2226
    // Map all new source regions to tallies
2227
    convert_source_regions_to_tallies(start_sr_id);
1,542✔
2228
  }
2229

2230
  discovered_source_regions_.clear();
21,842✔
2231
}
21,842✔
2232

2233
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
2234
//
2235
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
2236
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
2237
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
2238
// https://doi.org/10.1016/j.anucene.2018.10.036.
2239
void FlatSourceDomain::apply_transport_stabilization()
21,842✔
2240
{
2241
  // Don't do anything if all in-group scattering
2242
  // cross sections are positive
2243
  if (!is_transport_stabilization_needed_) {
21,842✔
2244
    return;
2245
  }
2246

2247
  // Apply the stabilization factor to all source elements
2248
#pragma omp parallel for
180✔
2249
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,320✔
2250
    int material = source_regions_.material(sr);
1,200!
2251
    int temp = source_regions_.temperature_idx(sr);
1,200!
2252
    double density_mult = source_regions_.density_mult(sr);
1,200!
2253
    if (material == MATERIAL_VOID) {
1,200!
2254
      continue;
2255
    }
2256
    for (int g = 0; g < negroups_; g++) {
85,200✔
2257
      // Only apply stabilization if the diagonal (in-group) scattering XS is
2258
      // negative
2259
      double sigma_s =
84,000✔
2260
        sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) *
84,000✔
2261
                   negroups_ +
84,000✔
2262
                 g] *
84,000✔
2263
        density_mult;
84,000✔
2264
      if (sigma_s < 0.0) {
84,000✔
2265
        double sigma_t =
22,000✔
2266
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
22,000✔
2267
          density_mult;
22,000✔
2268
        double phi_new = source_regions_.scalar_flux_new(sr, g);
22,000✔
2269
        double phi_old = source_regions_.scalar_flux_old(sr, g);
22,000✔
2270

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

2281
        // Equation 16 in the above Gunow et al. 2019 paper
2282
        source_regions_.scalar_flux_new(sr, g) =
22,000✔
2283
          (phi_new - D * phi_old) / (1.0 - D);
22,000✔
2284
      }
2285
    }
2286
  }
2287
}
2288

2289
// Determines the base source region index (i.e., a material filled cell
2290
// instance) that corresponds to a particular location in the geometry. Requires
2291
// that the "gs" object passed in has already been initialized and has called
2292
// find_cell etc.
2293
int64_t FlatSourceDomain::lookup_base_source_region_idx(
1,394,917,460✔
2294
  const GeometryState& gs) const
2295
{
2296
  int i_cell = gs.lowest_coord().cell();
1,394,917,460✔
2297
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
1,394,917,460✔
2298
  return sr;
1,394,917,460✔
2299
}
2300

2301
// Determines the index of the mesh (if any) that has been applied
2302
// to a particular base source region index.
2303
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
1,050,011,809✔
2304
{
2305
  int mesh_idx = C_NONE;
1,050,011,809✔
2306
  auto mesh_it = mesh_map_.find(sr);
1,050,011,809✔
2307
  if (mesh_it != mesh_map_.end()) {
1,050,011,809✔
2308
    mesh_idx = mesh_it->second;
578,722,501✔
2309
  }
2310
  return mesh_idx;
1,050,011,809✔
2311
}
2312

2313
// Determines the source region key that corresponds to a particular location in
2314
// the geometry. This takes into account both the base source region index as
2315
// well as the mesh bin if a mesh is applied to this source region for
2316
// subdivision.
2317
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
7,458,516✔
2318
  const GeometryState& gs) const
2319
{
2320
  int64_t sr = lookup_base_source_region_idx(gs);
7,458,516✔
2321
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
7,458,516✔
2322
  return SourceRegionKey {sr, mesh_bin};
7,458,516✔
2323
}
2324

2325
// Determines the mesh bin that corresponds to a particular base source region
2326
// index and position.
2327
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
7,458,516✔
2328
{
2329
  int mesh_idx = lookup_mesh_idx(sr);
7,458,516✔
2330
  int mesh_bin = 0;
7,458,516✔
2331
  if (mesh_idx != C_NONE) {
7,458,516✔
2332
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
4,997,609✔
2333
  }
2334
  return mesh_bin;
7,458,516✔
2335
}
2336

2337
bool FlatSourceDomain::is_geometry_3D()
814✔
2338
{
2339
  // Get spatial box of ray_source_
2340
  SpatialBox* sb = dynamic_cast<SpatialBox*>(
1,628!
2341
    dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get())->space());
814!
2342

2343
  double x_length = sb->upper_right().x - sb->lower_left().x;
814✔
2344
  double y_length = sb->upper_right().y - sb->lower_left().y;
814✔
2345
  double z_length = sb->upper_right().z - sb->lower_left().z;
814✔
2346

2347
  int num_xy_points = 100;
814✔
2348
  int num_z_points = 100;
814✔
2349
  uint64_t seed = openmc_get_seed();
814✔
2350

2351
  for (int i = 0; i < num_xy_points; i++) {
39,414✔
2352
    Position sample;
39,028✔
2353
    sample.x = sb->lower_left().x + x_length * prn(&seed);
39,028✔
2354
    sample.y = sb->lower_left().y + y_length * prn(&seed);
39,028✔
2355

2356
    SourceRegionKey sr_key_prev {-1, -1};
39,028✔
2357
    bool check_key = false;
39,028✔
2358

2359
    for (int j = 0; j < num_z_points; j++) {
3,899,457✔
2360
      sample.z = sb->lower_left().z + z_length * prn(&seed);
3,860,857✔
2361

2362
      Particle p;
3,860,857✔
2363
      p.r() = sample;
3,860,857✔
2364
      p.r_last() = sample;
3,860,857✔
2365
      p.E() = 1.0;
3,860,857✔
2366
      p.E_last() = 1.0;
3,860,857✔
2367
      p.u() = {0.0, 0.0, 1.0};
3,860,857✔
2368

2369
      bool found = exhaustive_find_cell(p);
3,860,857✔
2370
      if (!found) {
3,860,857!
NEW
2371
        continue;
×
2372
      }
2373

2374
      SourceRegionKey sr_key = lookup_source_region_key(p);
3,860,857✔
2375

2376
      // Check if sr_key has changed in z-direction
2377
      if (check_key &&
3,860,857✔
2378
          (sr_key.base_source_region_id != sr_key_prev.base_source_region_id ||
3,821,424✔
2379
            sr_key.mesh_bin != sr_key_prev.mesh_bin)) {
2380
        return true;
428✔
2381
      }
2382

2383
      // Set check_key to true after first sr_key has been loaded
2384
      sr_key_prev = sr_key;
3,860,429✔
2385
      check_key = true;
3,860,429✔
2386
    }
3,860,857✔
2387
  }
2388

2389
  return false;
2390
}
2391

2392
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc