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

openmc-dev / openmc / 15900894277

26 Jun 2025 11:42AM UTC coverage: 85.214% (-0.04%) from 85.252%
15900894277

Pull #3452

github

web-flow
Merge a81fd95f9 into 5c1021446
Pull Request #3452: relaxed random ray constraints

237 of 306 new or added lines in 8 files covered. (77.45%)

2 existing lines in 1 file now uncovered.

52791 of 61951 relevant lines covered (85.21%)

36510182.52 hits per line

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

78.88
/src/random_ray/flat_source_domain.cpp
1
#include "openmc/random_ray/flat_source_domain.h"
2

3
#include "openmc/cell.h"
4
#include "openmc/constants.h"
5
#include "openmc/eigenvalue.h"
6
#include "openmc/geometry.h"
7
#include "openmc/material.h"
8
#include "openmc/message_passing.h"
9
#include "openmc/mgxs_interface.h"
10
#include "openmc/output.h"
11
#include "openmc/plot.h"
12
#include "openmc/random_ray/random_ray.h"
13
#include "openmc/simulation.h"
14
#include "openmc/tallies/filter.h"
15
#include "openmc/tallies/tally.h"
16
#include "openmc/tallies/tally_scoring.h"
17
#include "openmc/timer.h"
18
#include "openmc/weight_windows.h"
19

20
#include <cstdio>
21

22
namespace openmc {
23

24
//==============================================================================
25
// FlatSourceDomain implementation
26
//==============================================================================
27

28
// Static Variable Declarations
29
RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ {
30
  RandomRayVolumeEstimator::HYBRID};
31
bool FlatSourceDomain::volume_normalized_flux_tallies_ {false};
32
bool FlatSourceDomain::adjoint_ {false};
33
double FlatSourceDomain::diagonal_stabilization_rho_ {1.0};
34
std::unordered_map<int, vector<std::pair<Source::DomainType, int>>>
35
  FlatSourceDomain::mesh_domain_map_;
36

37
FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_)
656✔
38
{
39
  // Count the number of source regions, compute the cell offset
40
  // indices, and store the material type The reason for the offsets is that
41
  // some cell types may not have material fills, and therefore do not
42
  // produce FSRs. Thus, we cannot index into the global arrays directly
43
  int base_source_regions = 0;
656✔
44
  for (const auto& c : model::cells) {
5,520✔
45
    if (c->type_ != Fill::MATERIAL) {
4,864✔
46
      source_region_offsets_.push_back(-1);
2,320✔
47
    } else {
48
      source_region_offsets_.push_back(base_source_regions);
2,544✔
49
      base_source_regions += c->n_instances_;
2,544✔
50
    }
51
  }
52

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

59
  // Initialize materials
60
  int64_t source_region_id = 0;
656✔
61
  for (int i = 0; i < model::cells.size(); i++) {
5,520✔
62
    Cell& cell = *model::cells[i];
4,864✔
63
    if (cell.type_ == Fill::MATERIAL) {
4,864✔
64
      for (int j = 0; j < cell.n_instances_; j++) {
768,288✔
65
        source_regions_.material(source_region_id++) = cell.material(j);
765,744✔
66
      }
67
    }
68
  }
69

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

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

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

89
  // Compute simulation domain volume based on ray source
90
  auto* is = dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
656✔
91
  SpatialDistribution* space_dist = is->space();
656✔
92
  simulation_volume_ = space_dist->volume();
656✔
93
}
656✔
94

95
void FlatSourceDomain::batch_reset()
11,660✔
96
{
97
// Reset scalar fluxes and iteration volume tallies to zero
98
#pragma omp parallel for
6,360✔
99
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,447,485✔
100
    source_regions_.volume(sr) = 0.0;
32,442,185✔
101
    source_regions_.volume_sq(sr) = 0.0;
32,442,185✔
102
  }
103

104
#pragma omp parallel for
6,360✔
105
  for (int64_t se = 0; se < n_source_elements(); se++) {
36,741,625✔
106
    source_regions_.scalar_flux_new(se) = 0.0;
36,736,325✔
107
  }
108
}
11,660✔
109

110
void FlatSourceDomain::accumulate_iteration_flux()
4,730✔
111
{
112
#pragma omp parallel for
2,580✔
113
  for (int64_t se = 0; se < n_source_elements(); se++) {
16,591,625✔
114
    source_regions_.scalar_flux_final(se) +=
16,589,475✔
115
      source_regions_.scalar_flux_new(se);
16,589,475✔
116
  }
117
}
4,730✔
118

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

125
  double inverse_k_eff = 1.0 / k_eff;
5,225✔
126

127
// Reset all source regions to zero (important for void regions)
128
#pragma omp parallel for
2,850✔
129
  for (int64_t se = 0; se < n_source_elements(); se++) {
21,598,115✔
130
    source_regions_.source(se) = 0.0;
21,595,740✔
131
  }
132

133
  // Add scattering + fission source
134
#pragma omp parallel for
2,850✔
135
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
19,275,935✔
136
    int material = source_regions_.material(sr);
19,273,560✔
137
    if (material == MATERIAL_VOID) {
19,273,560✔
138
      continue;
200,000✔
139
    }
140
    for (int g_out = 0; g_out < negroups_; g_out++) {
40,469,300✔
141
      double sigma_t = sigma_t_[material * negroups_ + g_out];
21,395,740✔
142
      double scatter_source = 0.0;
21,395,740✔
143
      double fission_source = 0.0;
21,395,740✔
144

145
      for (int g_in = 0; g_in < negroups_; g_in++) {
59,046,740✔
146
        double scalar_flux = source_regions_.scalar_flux_old(sr, g_in);
37,651,000✔
147
        double sigma_s =
148
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
37,651,000✔
149
        double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
37,651,000✔
150
        double chi = chi_[material * negroups_ + g_out];
37,651,000✔
151

152
        scatter_source += sigma_s * scalar_flux;
37,651,000✔
153
        fission_source += nu_sigma_f * scalar_flux * chi;
37,651,000✔
154
      }
155
      source_regions_.source(sr, g_out) =
21,395,740✔
156
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
21,395,740✔
157
    }
158
  }
159

160
  // Add external source if in fixed source mode
161
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
5,225✔
162
#pragma omp parallel for
2,490✔
163
    for (int64_t se = 0; se < n_source_elements(); se++) {
20,385,505✔
164
      source_regions_.source(se) += source_regions_.external_source(se);
20,383,430✔
165
    }
166
  }
167

168
  simulation::time_update_src.stop();
5,225✔
169
}
5,225✔
170

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

179
// Normalize scalar flux to total distance travelled by all rays this
180
// iteration
181
#pragma omp parallel for
2,850✔
182
  for (int64_t se = 0; se < n_source_elements(); se++) {
22,191,535✔
183
    source_regions_.scalar_flux_new(se) *= normalization_factor;
22,189,160✔
184
  }
185

186
// Accumulate cell-wise ray length tallies collected this iteration, then
187
// update the simulation-averaged cell-wise volume estimates
188
#pragma omp parallel for
2,850✔
189
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
19,796,995✔
190
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
19,794,620✔
191
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
19,794,620✔
192
    source_regions_.volume_naive(sr) =
39,589,240✔
193
      source_regions_.volume(sr) * normalization_factor;
19,794,620✔
194
    source_regions_.volume_sq(sr) =
39,589,240✔
195
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
19,794,620✔
196
    source_regions_.volume(sr) =
19,794,620✔
197
      source_regions_.volume_t(sr) * volume_normalization_factor;
19,794,620✔
198
  }
199
}
5,225✔
200

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

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

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

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

237
#pragma omp parallel for reduction(+ : n_hits)
6,360✔
238
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
33,253,085✔
239

240
    double volume_simulation_avg = source_regions_.volume(sr);
33,247,785✔
241
    double volume_iteration = source_regions_.volume_naive(sr);
33,247,785✔
242

243
    // Increment the number of hits if cell was hit this iteration
244
    if (volume_iteration) {
33,247,785✔
245
      n_hits++;
29,188,630✔
246
    }
247

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

256
    // The volume treatment depends on the volume estimator type
257
    // and whether or not an external source is present in the cell.
258
    double volume;
259
    switch (volume_estimator_) {
33,247,785✔
260
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
261
      volume = volume_iteration;
777,600✔
262
      break;
777,600✔
263
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
264
      volume = volume_simulation_avg;
432,000✔
265
      break;
432,000✔
266
    case RandomRayVolumeEstimator::HYBRID:
32,038,185✔
267
      if (source_regions_.external_source_present(sr) ||
59,888,720✔
268
          source_regions_.is_small(sr)) {
27,850,535✔
269
        volume = volume_iteration;
10,999,975✔
270
      } else {
271
        volume = volume_simulation_avg;
21,038,210✔
272
      }
273
      break;
32,038,185✔
274
    default:
275
      fatal_error("Invalid volume estimator type");
276
    }
277

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

315
  // Return the number of source regions that were hit this iteration
316
  return n_hits;
11,660✔
317
}
318

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

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

329
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
1,140✔
330
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
308,740✔
331

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

338
    int material = source_regions_.material(sr);
307,790✔
339
    if (material == MATERIAL_VOID) {
307,790✔
340
      continue;
341
    }
342

343
    double sr_fission_source_old = 0;
307,790✔
344
    double sr_fission_source_new = 0;
307,790✔
345

346
    for (int g = 0; g < negroups_; g++) {
2,375,320✔
347
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
2,067,530✔
348
      sr_fission_source_old +=
2,067,530✔
349
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,067,530✔
350
      sr_fission_source_new +=
2,067,530✔
351
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,067,530✔
352
    }
353

354
    // Compute total fission rates in FSR
355
    sr_fission_source_old *= volume;
307,790✔
356
    sr_fission_source_new *= volume;
307,790✔
357

358
    // Accumulate totals
359
    fission_rate_old += sr_fission_source_old;
307,790✔
360
    fission_rate_new += sr_fission_source_new;
307,790✔
361

362
    // Store total fission rate in the FSR for Shannon calculation
363
    p[sr] = sr_fission_source_new;
307,790✔
364
  }
365

366
  double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old);
2,090✔
367

368
  double H = 0.0;
2,090✔
369
  // defining an inverse sum for better performance
370
  double inverse_sum = 1 / fission_rate_new;
2,090✔
371

372
#pragma omp parallel for reduction(+ : H)
1,140✔
373
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
308,740✔
374
    // Only if FSR has non-negative and non-zero fission source
375
    if (p[sr] > 0.0f) {
307,790✔
376
      // Normalize to total weight of bank sites. p_i for better performance
377
      float p_i = p[sr] * inverse_sum;
130,995✔
378
      // Sum values to obtain Shannon entropy.
379
      H -= p_i * std::log2(p_i);
130,995✔
380
    }
381
  }
382

383
  // Adds entropy value to shared entropy vector in openmc namespace.
384
  simulation::entropy.push_back(H);
2,090✔
385

386
  return k_eff_new;
2,090✔
387
}
2,090✔
388

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

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

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

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

427
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
759✔
428
{
429
  openmc::simulation::time_tallies.start();
759✔
430

431
  // Tracks if we've generated a mapping yet for all source regions.
432
  bool all_source_regions_mapped = true;
759✔
433

434
// Attempt to generate mapping for all source regions
435
#pragma omp parallel for
414✔
436
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,027,960✔
437

438
    // If this source region has not been hit by a ray yet, then
439
    // we aren't going to be able to map it, so skip it.
440
    if (!source_regions_.position_recorded(sr)) {
1,027,615✔
441
      all_source_regions_mapped = false;
442
      continue;
443
    }
444

445
    // A particle located at the recorded midpoint of a ray
446
    // crossing through this source region is used to estabilish
447
    // the spatial location of the source region
448
    Particle p;
1,027,615✔
449
    p.r() = source_regions_.position(sr);
1,027,615✔
450
    p.r_last() = source_regions_.position(sr);
1,027,615✔
451
    p.u() = {1.0, 0.0, 0.0};
1,027,615✔
452
    bool found = exhaustive_find_cell(p);
1,027,615✔
453

454
    // Loop over energy groups (so as to support energy filters)
455
    for (int g = 0; g < negroups_; g++) {
2,204,030✔
456

457
      // Set particle to the current energy
458
      p.g() = g;
1,176,415✔
459
      p.g_last() = g;
1,176,415✔
460
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,176,415✔
461
      p.E_last() = p.E();
1,176,415✔
462

463
      int64_t source_element = sr * negroups_ + g;
1,176,415✔
464

465
      // If this task has already been populated, we don't need to do
466
      // it again.
467
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,176,415✔
468
        continue;
469
      }
470

471
      // Loop over all active tallies. This logic is essentially identical
472
      // to what happens when scanning for applicable tallies during
473
      // MC transport.
474
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
4,502,700✔
475
        Tally& tally {*model::tallies[i_tally]};
3,326,285✔
476

477
        // Initialize an iterator over valid filter bin combinations.
478
        // If there are no valid combinations, use a continue statement
479
        // to ensure we skip the assume_separate break below.
480
        auto filter_iter = FilterBinIter(tally, p);
3,326,285✔
481
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,326,285✔
482
        if (filter_iter == end)
3,326,285✔
483
          continue;
2,001,470✔
484

485
        // Loop over filter bins.
486
        for (; filter_iter != end; ++filter_iter) {
2,649,630✔
487
          auto filter_index = filter_iter.index_;
1,324,815✔
488
          auto filter_weight = filter_iter.weight_;
1,324,815✔
489

490
          // Loop over scores
491
          for (int score = 0; score < tally.scores_.size(); score++) {
2,986,750✔
492
            auto score_bin = tally.scores_[score];
1,661,935✔
493
            // If a valid tally, filter, and score combination has been found,
494
            // then add it to the list of tally tasks for this source element.
495
            TallyTask task(i_tally, filter_index, score, score_bin);
1,661,935✔
496
            source_regions_.tally_task(sr, g).push_back(task);
1,661,935✔
497

498
            // Also add this task to the list of volume tasks for this source
499
            // region.
500
            source_regions_.volume_task(sr).insert(task);
1,661,935✔
501
          }
502
        }
503
      }
504
      // Reset all the filter matches for the next tally event.
505
      for (auto& match : p.filter_matches())
4,982,300✔
506
        match.bins_present_ = false;
3,805,885✔
507
    }
508
  }
1,027,615✔
509
  openmc::simulation::time_tallies.stop();
759✔
510

511
  mapped_all_tallies_ = all_source_regions_mapped;
759✔
512
}
759✔
513

514
// Set the volume accumulators to zero for all tallies
515
void FlatSourceDomain::reset_tally_volumes()
4,730✔
516
{
517
  if (volume_normalized_flux_tallies_) {
4,730✔
518
#pragma omp parallel for
2,100✔
519
    for (int i = 0; i < tally_volumes_.size(); i++) {
6,600✔
520
      auto& tensor = tally_volumes_[i];
4,850✔
521
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
4,850✔
522
    }
523
  }
524
}
4,730✔
525

526
// In fixed source mode, due to the way that volumetric fixed sources are
527
// converted and applied as volumetric sources in one or more source regions,
528
// we need to perform an additional normalization step to ensure that the
529
// reported scalar fluxes are in units per source neutron. This allows for
530
// direct comparison of reported tallies to Monte Carlo flux results.
531
// This factor needs to be computed at each iteration, as it is based on the
532
// volume estimate of each FSR, which improves over the course of the
533
// simulation
534
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
5,306✔
535
{
536
  // If we are not in fixed source mode, then there are no external sources
537
  // so no normalization is needed.
538
  if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) {
5,306✔
539
    return 1.0;
1,496✔
540
  }
541

542
  // Step 1 is to sum over all source regions and energy groups to get the
543
  // total external source strength in the simulation.
544
  double simulation_external_source_strength = 0.0;
3,810✔
545
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,085✔
546
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,651,965✔
547
    int material = source_regions_.material(sr);
14,650,240✔
548
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,650,240✔
549
    for (int g = 0; g < negroups_; g++) {
29,851,712✔
550
      // For non-void regions, we store the external source pre-divided by
551
      // sigma_t. We need to multiply non-void regions back up by sigma_t
552
      // to get the total source strength in the expected units.
553
      double sigma_t = 1.0;
15,201,472✔
554
      if (material != MATERIAL_VOID) {
15,201,472✔
555
        sigma_t = sigma_t_[material * negroups_ + g];
14,987,472✔
556
      }
557
      simulation_external_source_strength +=
15,201,472✔
558
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,201,472✔
559
    }
560
  }
561

562
  // Step 2 is to determine the total user-specified external source strength
563
  double user_external_source_strength = 0.0;
3,810✔
564
  for (auto& ext_source : model::external_sources) {
7,620✔
565
    user_external_source_strength += ext_source->strength();
3,810✔
566
  }
567

568
  // The correction factor is the ratio of the user-specified external source
569
  // strength to the simulation external source strength.
570
  double source_normalization_factor =
3,810✔
571
    user_external_source_strength / simulation_external_source_strength;
572

573
  return source_normalization_factor;
3,810✔
574
}
575

576
// Tallying in random ray is not done directly during transport, rather,
577
// it is done only once after each power iteration. This is made possible
578
// by way of a mapping data structure that relates spatial source regions
579
// (FSRs) to tally/filter/score combinations. The mechanism by which the
580
// mapping is done (and the limitations incurred) is documented in the
581
// "convert_source_regions_to_tallies()" function comments above. The present
582
// tally function simply traverses the mapping data structure and executes
583
// the scoring operations to OpenMC's native tally result arrays.
584

585
void FlatSourceDomain::random_ray_tally()
4,730✔
586
{
587
  openmc::simulation::time_tallies.start();
4,730✔
588

589
  // Reset our tally volumes to zero
590
  reset_tally_volumes();
4,730✔
591

592
  double source_normalization_factor =
593
    compute_fixed_source_normalization_factor();
4,730✔
594

595
// We loop over all source regions and energy groups. For each
596
// element, we check if there are any scores needed and apply
597
// them.
598
#pragma omp parallel for
2,580✔
599
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
15,210,425✔
600
    // The fsr.volume_ is the unitless fractional simulation averaged volume
601
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
602
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
603
    // entire global simulation domain (as defined by the ray source box).
604
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
605
    // its fraction of the total volume by the total volume. Not important in
606
    // eigenvalue solves, but useful in fixed source solves for returning the
607
    // flux shape with a magnitude that makes sense relative to the fixed
608
    // source strength.
609
    double volume = source_regions_.volume(sr) * simulation_volume_;
15,208,275✔
610

611
    double material = source_regions_.material(sr);
15,208,275✔
612
    for (int g = 0; g < negroups_; g++) {
31,797,750✔
613
      double flux =
614
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
16,589,475✔
615

616
      // Determine numerical score value
617
      for (auto& task : source_regions_.tally_task(sr, g)) {
39,166,950✔
618
        double score = 0.0;
22,577,475✔
619
        switch (task.score_type) {
22,577,475✔
620

621
        case SCORE_FLUX:
19,405,075✔
622
          score = flux * volume;
19,405,075✔
623
          break;
19,405,075✔
624

625
        case SCORE_TOTAL:
626
          if (material != MATERIAL_VOID) {
627
            score = flux * volume * sigma_t_[material * negroups_ + g];
628
          }
629
          break;
630

631
        case SCORE_FISSION:
1,586,200✔
632
          if (material != MATERIAL_VOID) {
1,586,200✔
633
            score = flux * volume * sigma_f_[material * negroups_ + g];
1,586,200✔
634
          }
635
          break;
1,586,200✔
636

637
        case SCORE_NU_FISSION:
1,586,200✔
638
          if (material != MATERIAL_VOID) {
1,586,200✔
639
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
1,586,200✔
640
          }
641
          break;
1,586,200✔
642

643
        case SCORE_EVENTS:
644
          score = 1.0;
645
          break;
646

647
        default:
648
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
649
                      "total, fission, nu-fission, and events are supported in "
650
                      "random ray mode.");
651
          break;
652
        }
653

654
        // Apply score to the appropriate tally bin
655
        Tally& tally {*model::tallies[task.tally_idx]};
22,577,475✔
656
#pragma omp atomic
657
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
22,577,475✔
658
          score;
659
      }
660
    }
661

662
    // For flux tallies, the total volume of the spatial region is needed
663
    // for normalizing the flux. We store this volume in a separate tensor.
664
    // We only contribute to each volume tally bin once per FSR.
665
    if (volume_normalized_flux_tallies_) {
15,208,275✔
666
      for (const auto& task : source_regions_.volume_task(sr)) {
35,771,350✔
667
        if (task.score_type == SCORE_FLUX) {
20,707,275✔
668
#pragma omp atomic
669
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
18,752,875✔
670
            volume;
671
        }
672
      }
673
    }
674
  } // end FSR loop
675

676
  // Normalize any flux scores by the total volume of the FSRs scoring to that
677
  // bin. To do this, we loop over all tallies, and then all filter bins,
678
  // and then scores. For each score, we check the tally data structure to
679
  // see what index that score corresponds to. If that score is a flux score,
680
  // then we divide it by volume.
681
  if (volume_normalized_flux_tallies_) {
4,730✔
682
    for (int i = 0; i < model::tallies.size(); i++) {
14,520✔
683
      Tally& tally {*model::tallies[i]};
10,670✔
684
#pragma omp parallel for
5,820✔
685
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
704,025✔
686
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,416,550✔
687
          auto score_type = tally.scores_[score_idx];
717,375✔
688
          if (score_type == SCORE_FLUX) {
717,375✔
689
            double vol = tally_volumes_[i](bin, score_idx);
699,175✔
690
            if (vol > 0.0) {
699,175✔
691
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
699,175✔
692
            }
693
          }
694
        }
695
      }
696
    }
697
  }
698

699
  openmc::simulation::time_tallies.stop();
4,730✔
700
}
4,730✔
701

702
double FlatSourceDomain::evaluate_flux_at_point(
×
703
  Position r, int64_t sr, int g) const
704
{
705
  return source_regions_.scalar_flux_final(sr, g) /
×
706
         (settings::n_batches - settings::n_inactive);
×
707
}
708

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

723
  // Print header information
724
  print_plot();
×
725

726
  // Outer loop over plots
727
  for (int p = 0; p < model::plots.size(); p++) {
×
728

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

732
    // Random ray plots only support voxel plots
733
    if (!openmc_plot) {
×
734
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
735
                          "is allowed in random ray mode.",
736
        p));
737
      continue;
×
738
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
739
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
740
                          "is allowed in random ray mode.",
741
        p));
742
      continue;
×
743
    }
744

745
    int Nx = openmc_plot->pixels_[0];
×
746
    int Ny = openmc_plot->pixels_[1];
×
747
    int Nz = openmc_plot->pixels_[2];
×
748
    Position origin = openmc_plot->origin_;
×
749
    Position width = openmc_plot->width_;
×
750
    Position ll = origin - width / 2.0;
×
751
    double x_delta = width.x / Nx;
×
752
    double y_delta = width.y / Ny;
×
753
    double z_delta = width.z / Nz;
×
754
    std::string filename = openmc_plot->path_plot();
×
755

756
    // Perform sanity checks on file size
757
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
758
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
759
      openmc_plot->id(), filename, bytes / 1.0e6);
×
760
    if (bytes / 1.0e9 > 1.0) {
×
761
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
762
              "slow.");
763
    } else if (bytes / 1.0e9 > 100.0) {
×
764
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
765
    }
766

767
    // Relate voxel spatial locations to random ray source regions
768
    vector<int> voxel_indices(Nx * Ny * Nz);
×
769
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
770
    vector<double> weight_windows(Nx * Ny * Nz);
×
771
    float min_weight = 1e20;
×
772
#pragma omp parallel for collapse(3) reduction(min : min_weight)
773
    for (int z = 0; z < Nz; z++) {
774
      for (int y = 0; y < Ny; y++) {
775
        for (int x = 0; x < Nx; x++) {
776
          Position sample;
777
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
778
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
779
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
780
          Particle p;
781
          p.r() = sample;
782
          p.r_last() = sample;
783
          p.E() = 1.0;
784
          p.E_last() = 1.0;
785
          p.u() = {1.0, 0.0, 0.0};
786

787
          bool found = exhaustive_find_cell(p);
788
          if (!found) {
789
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
790
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
791
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
792
            continue;
793
          }
794

795
          int i_cell = p.lowest_coord().cell;
796
          int64_t sr = source_region_offsets_[i_cell] + p.cell_instance();
797
          if (RandomRay::mesh_subdivision_enabled_) {
798
            int mesh_idx = base_source_regions_.mesh(sr);
799
            int mesh_bin;
800
            if (mesh_idx == C_NONE) {
801
              mesh_bin = 0;
802
            } else {
803
              mesh_bin = model::meshes[mesh_idx]->get_bin(p.r());
804
            }
805
            SourceRegionKey sr_key {sr, mesh_bin};
806
            auto it = source_region_map_.find(sr_key);
807
            if (it != source_region_map_.end()) {
808
              sr = it->second;
809
            } else {
810
              sr = -1;
811
            }
812
          }
813

814
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
815
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
816

817
          if (variance_reduction::weight_windows.size() == 1) {
818
            WeightWindow ww =
819
              variance_reduction::weight_windows[0]->get_weight_window(p);
820
            float weight = ww.lower_weight;
821
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
822
            if (weight < min_weight)
823
              min_weight = weight;
824
          }
825
        }
826
      }
827
    }
828

829
    double source_normalization_factor =
830
      compute_fixed_source_normalization_factor();
×
831

832
    // Open file for writing
833
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
834

835
    // Write vtk metadata
836
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
837
    std::fprintf(plot, "Dataset File\n");
×
838
    std::fprintf(plot, "BINARY\n");
×
839
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
840
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
841
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
842
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
843
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
844

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

877
    // Slightly negative fluxes can be normal when sampling corners of linear
878
    // source regions. However, very common and high magnitude negative fluxes
879
    // may indicate numerical instability.
880
    if (num_neg > 0) {
×
881
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
882
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
883
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
884
    }
885

886
    // Plot FSRs
887
    std::fprintf(plot, "SCALARS FSRs float\n");
×
888
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
889
    for (int fsr : voxel_indices) {
×
890
      float value = future_prn(10, fsr);
×
891
      value = convert_to_big_endian<float>(value);
×
892
      std::fwrite(&value, sizeof(float), 1, plot);
×
893
    }
894

895
    // Plot Materials
896
    std::fprintf(plot, "SCALARS Materials int\n");
×
897
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
898
    for (int fsr : voxel_indices) {
×
899
      int mat = -1;
×
900
      if (fsr >= 0)
×
901
        mat = source_regions_.material(fsr);
×
902
      mat = convert_to_big_endian<int>(mat);
×
903
      std::fwrite(&mat, sizeof(int), 1, plot);
×
904
    }
905

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

950
    // Plot weight window data
951
    if (variance_reduction::weight_windows.size() == 1) {
×
952
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
953
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
954
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
955
        float weight = weight_windows[i];
×
956
        if (weight == 0.0)
×
957
          weight = min_weight;
×
958
        weight = convert_to_big_endian<float>(weight);
×
959
        std::fwrite(&weight, sizeof(float), 1, plot);
×
960
      }
961
    }
962

963
    std::fclose(plot);
×
964
  }
965
}
966

967
void FlatSourceDomain::apply_external_source_to_source_region(
2,747✔
968
  Discrete* discrete, double strength_factor, SourceRegionHandle& srh)
969
{
970
  srh.external_source_present() = 1;
2,747✔
971

972
  const auto& discrete_energies = discrete->x();
2,747✔
973
  const auto& discrete_probs = discrete->prob();
2,747✔
974

975
  for (int i = 0; i < discrete_energies.size(); i++) {
5,686✔
976
    int g = data::mg.get_group_index(discrete_energies[i]);
2,939✔
977
    srh.external_source(g) += discrete_probs[i] * strength_factor;
2,939✔
978
  }
979
}
2,747✔
980

981
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
416✔
982
  Discrete* discrete, double strength_factor, int target_material_id,
983
  const vector<int32_t>& instances)
984
{
985
  Cell& cell = *model::cells[i_cell];
416✔
986

987
  if (cell.type_ != Fill::MATERIAL)
416✔
988
    return;
×
989

990
  for (int j : instances) {
30,672✔
991
    int cell_material_idx = cell.material(j);
30,256✔
992
    int cell_material_id;
993
    if (cell_material_idx == MATERIAL_VOID) {
30,256✔
994
      cell_material_id = MATERIAL_VOID;
256✔
995
    } else {
996
      cell_material_id = model::materials[cell_material_idx]->id();
30,000✔
997
    }
998
    if (target_material_id == C_NONE ||
30,256✔
999
        cell_material_id == target_material_id) {
1000
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,736✔
1001
      SourceRegionHandle srh =
1002
        source_regions_.get_source_region_handle(source_region);
2,736✔
1003
      apply_external_source_to_source_region(discrete, strength_factor, srh);
2,736✔
1004
    }
1005
  }
1006
}
1007

1008
void FlatSourceDomain::apply_external_source_to_cell_and_children(
448✔
1009
  int32_t i_cell, Discrete* discrete, double strength_factor,
1010
  int32_t target_material_id)
1011
{
1012
  Cell& cell = *model::cells[i_cell];
448✔
1013

1014
  if (cell.type_ == Fill::MATERIAL) {
448✔
1015
    vector<int> instances(cell.n_instances_);
416✔
1016
    std::iota(instances.begin(), instances.end(), 0);
416✔
1017
    apply_external_source_to_cell_instances(
416✔
1018
      i_cell, discrete, strength_factor, target_material_id, instances);
1019
  } else if (target_material_id == C_NONE) {
448✔
1020
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1021
      cell.get_contained_cells(0, nullptr);
×
1022
    for (const auto& pair : cell_instance_list) {
×
1023
      int32_t i_child_cell = pair.first;
×
1024
      apply_external_source_to_cell_instances(i_child_cell, discrete,
×
1025
        strength_factor, target_material_id, pair.second);
×
1026
    }
1027
  }
1028
}
448✔
1029

1030
void FlatSourceDomain::count_external_source_regions()
1,056✔
1031
{
1032
  n_external_source_regions_ = 0;
1,056✔
1033
#pragma omp parallel for reduction(+ : n_external_source_regions_)
594✔
1034
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,533,040✔
1035
    if (source_regions_.external_source_present(sr)) {
1,532,578✔
1036
      n_external_source_regions_++;
158,714✔
1037
    }
1038
  }
1039
}
1,056✔
1040

1041
void FlatSourceDomain::convert_external_sources()
400✔
1042
{
1043
  // Loop over external sources
1044
  for (int es = 0; es < model::external_sources.size(); es++) {
800✔
1045

1046
    // Extract source information
1047
    Source* s = model::external_sources[es].get();
400✔
1048
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
400✔
1049
    auto* energy = is->energy();
400✔
1050

1051
    // Convert energy distribution to discrete
1052
    std::vector<double> x;
400✔
1053
    std::vector<double> p;
400✔
1054
    for (int g = 0; g < data::mg.num_energy_groups_; ++g) {
992✔
1055
      double prob = energy->integral(
592✔
1056
        data::mg.energy_bins_[g + 1], data::mg.energy_bins_[g]);
592✔
1057
      if (prob > 0.0) {
592✔
1058
        x.push_back(data::mg.energy_bin_avg_[g]);
592✔
1059
        p.push_back(prob);
592✔
1060
      }
1061
    }
1062
    is->set_energy(UPtrDist {new Discrete(&x[0], &p[0], x.size())});
400✔
1063
    Discrete* discrete = dynamic_cast<Discrete*>(is->energy());
400✔
1064
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
400✔
1065
    double strength_factor = is->strength();
400✔
1066

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

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

1088
      if (RandomRay::mesh_subdivision_enabled_) {
16✔
1089
        // If mesh subdivision is enabled, we need to determine which subdivided
1090
        // mesh bin the point source coordinate is in as well
1091
        int mesh_idx = source_regions_.mesh(sr);
16✔
1092
        int mesh_bin;
1093
        if (mesh_idx == C_NONE) {
16✔
1094
          mesh_bin = 0;
×
1095
        } else {
1096
          mesh_bin = model::meshes[mesh_idx]->get_bin(gs.r());
16✔
1097
        }
1098
        // With the source region and mesh bin known, we can use the
1099
        // accompanying SourceRegionKey as a key into a map that stores the
1100
        // corresponding external source index for the point source. Notably, we
1101
        // do not actually apply the external source to any source regions here,
1102
        // as if mesh subdivision is enabled, they haven't actually been
1103
        // discovered & initilized yet. When discovered, they will read from the
1104
        // point_source_map to determine if there are any point source terms
1105
        // that should be applied.
1106
        SourceRegionKey key {sr, mesh_bin};
16✔
1107
        point_source_map_[key] = es;
16✔
1108
      } else {
1109
        // If we are not using mesh subdivision, we can apply the external
1110
        // source directly to the source region as we do for volumetric domain
1111
        // constraint sources.
1112
        SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
×
NEW
1113
        apply_external_source_to_source_region(discrete, strength_factor, srh);
×
1114
      }
1115

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

1145
// Divide the fixed source term by sigma t (to save time when applying each
1146
// iteration)
1147
#pragma omp parallel for
225✔
1148
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
271,180✔
1149
    int material = source_regions_.material(sr);
271,005✔
1150
    if (material == MATERIAL_VOID) {
271,005✔
1151
      continue;
14,000✔
1152
    }
1153
    for (int g = 0; g < negroups_; g++) {
543,242✔
1154
      double sigma_t = sigma_t_[material * negroups_ + g];
286,237✔
1155
      source_regions_.external_source(sr, g) /= sigma_t;
286,237✔
1156
    }
1157
  }
1158
}
400✔
1159

1160
void FlatSourceDomain::flux_swap()
11,660✔
1161
{
1162
  source_regions_.flux_swap();
11,660✔
1163
}
11,660✔
1164

1165
void FlatSourceDomain::flatten_xs()
656✔
1166
{
1167
  // Temperature and angle indices, if using multiple temperature
1168
  // data sets and/or anisotropic data sets.
1169
  // TODO: Currently assumes we are only using single temp/single angle data.
1170
  const int t = 0;
656✔
1171
  const int a = 0;
656✔
1172

1173
  n_materials_ = data::mg.macro_xs_.size();
656✔
1174
  for (auto& m : data::mg.macro_xs_) {
2,448✔
1175
    for (int g_out = 0; g_out < negroups_; g_out++) {
8,768✔
1176
      if (m.exists_in_model) {
6,976✔
1177
        double sigma_t =
1178
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
6,912✔
1179
        sigma_t_.push_back(sigma_t);
6,912✔
1180

1181
        double nu_sigma_f =
1182
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
6,912✔
1183
        nu_sigma_f_.push_back(nu_sigma_f);
6,912✔
1184

1185
        double sigma_f =
1186
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
6,912✔
1187
        sigma_f_.push_back(sigma_f);
6,912✔
1188

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

1198
        for (int g_in = 0; g_in < negroups_; g_in++) {
258,048✔
1199
          double sigma_s =
1200
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
251,136✔
1201
          sigma_s_.push_back(sigma_s);
251,136✔
1202
          // For transport corrected XS data, diagonal elements may be negative.
1203
          // In this case, set a flag to enable transport stabilization for the
1204
          // simulation.
1205
          if (g_out == g_in && sigma_s < 0.0)
251,136✔
1206
            is_transport_stabilization_needed_ = true;
880✔
1207
        }
1208
      } else {
1209
        sigma_t_.push_back(0);
64✔
1210
        nu_sigma_f_.push_back(0);
64✔
1211
        sigma_f_.push_back(0);
64✔
1212
        chi_.push_back(0);
64✔
1213
        for (int g_in = 0; g_in < negroups_; g_in++) {
128✔
1214
          sigma_s_.push_back(0);
64✔
1215
        }
1216
      }
1217
    }
1218
  }
1219
}
656✔
1220

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

1231
  // First, find the maximum forward flux value
1232
  double max_flux = 0.0;
64✔
1233
#pragma omp parallel for reduction(max : max_flux)
36✔
1234
  for (int64_t se = 0; se < n_source_elements(); se++) {
169,372✔
1235
    double flux = forward_flux[se];
169,344✔
1236
    if (flux > max_flux) {
169,344✔
1237
      max_flux = flux;
25✔
1238
    }
1239
  }
1240

1241
  // Then, compute the adjoint source for each source region
1242
#pragma omp parallel for
36✔
1243
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
169,372✔
1244
    for (int g = 0; g < negroups_; g++) {
338,688✔
1245
      double flux = forward_flux[sr * negroups_ + g];
169,344✔
1246
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
169,344✔
1247
        source_regions_.external_source(sr, g) = 0.0;
325✔
1248
      } else {
1249
        source_regions_.external_source(sr, g) = 1.0 / flux;
169,019✔
1250
      }
1251
      if (flux > 0.0) {
169,344✔
1252
        source_regions_.external_source_present(sr) = 1;
155,195✔
1253
      }
1254
    }
1255
  }
1256

1257
  // Divide the fixed source term by sigma t (to save time when applying each
1258
  // iteration)
1259
#pragma omp parallel for
36✔
1260
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
169,372✔
1261
    int material = source_regions_.material(sr);
169,344✔
1262
    if (material == MATERIAL_VOID) {
169,344✔
1263
      continue;
1264
    }
1265
    for (int g = 0; g < negroups_; g++) {
338,688✔
1266
      double sigma_t = sigma_t_[material * negroups_ + g];
169,344✔
1267
      source_regions_.external_source(sr, g) /= sigma_t;
169,344✔
1268
    }
1269
  }
1270
}
64✔
1271

1272
void FlatSourceDomain::transpose_scattering_matrix()
80✔
1273
{
1274
  // Transpose the inner two dimensions for each material
1275
  for (int m = 0; m < n_materials_; ++m) {
304✔
1276
    int material_offset = m * negroups_ * negroups_;
224✔
1277
    for (int i = 0; i < negroups_; ++i) {
640✔
1278
      for (int j = i + 1; j < negroups_; ++j) {
1,088✔
1279
        // Calculate indices of the elements to swap
1280
        int idx1 = material_offset + i * negroups_ + j;
672✔
1281
        int idx2 = material_offset + j * negroups_ + i;
672✔
1282

1283
        // Swap the elements to transpose the matrix
1284
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
672✔
1285
      }
1286
    }
1287
  }
1288
}
80✔
1289

1290
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
576✔
1291
{
1292
  // Ensure array is correct size
1293
  flux.resize(n_source_regions() * negroups_);
576✔
1294
// Serialize the final fluxes for output
1295
#pragma omp parallel for
324✔
1296
  for (int64_t se = 0; se < n_source_elements(); se++) {
1,258,605✔
1297
    flux[se] = source_regions_.scalar_flux_final(se);
1,258,353✔
1298
  }
1299
}
576✔
1300

1301
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
608✔
1302
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1303
  bool is_target_void)
1304
{
1305
  Cell& cell = *model::cells[i_cell];
608✔
1306
  if (cell.type_ != Fill::MATERIAL)
608✔
1307
    return;
×
1308
  for (int32_t j : instances) {
174,944✔
1309
    int cell_material_idx = cell.material(j);
174,336✔
1310
    int cell_material_id = (cell_material_idx == C_NONE)
1311
                             ? C_NONE
174,336✔
1312
                             : model::materials[cell_material_idx]->id();
174,336✔
1313

1314
    if ((target_material_id == C_NONE && !is_target_void) ||
174,336✔
1315
        cell_material_id == target_material_id) {
1316
      int64_t sr = source_region_offsets_[i_cell] + j;
142,336✔
1317
      if (source_regions_.mesh(sr) != C_NONE) {
142,336✔
1318
        // print out the source region that is broken:
1319
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1320
                                "applied, but trying to apply mesh idx {}",
1321
          sr, source_regions_.mesh(sr), mesh_idx));
1322
      }
1323
      source_regions_.mesh(sr) = mesh_idx;
142,336✔
1324
    }
1325
  }
1326
}
1327

1328
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
480✔
1329
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1330
{
1331
  Cell& cell = *model::cells[i_cell];
480✔
1332

1333
  if (cell.type_ == Fill::MATERIAL) {
480✔
1334
    vector<int> instances(cell.n_instances_);
352✔
1335
    std::iota(instances.begin(), instances.end(), 0);
352✔
1336
    apply_mesh_to_cell_instances(
352✔
1337
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1338
  } else if (target_material_id == C_NONE && !is_target_void) {
480✔
1339
    for (int j = 0; j < cell.n_instances_; j++) {
128✔
1340
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1341
        cell.get_contained_cells(j, nullptr);
64✔
1342
      for (const auto& pair : cell_instance_list) {
320✔
1343
        int32_t i_child_cell = pair.first;
256✔
1344
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
256✔
1345
          pair.second, is_target_void);
256✔
1346
      }
1347
    }
64✔
1348
  }
1349
}
480✔
1350

1351
void FlatSourceDomain::apply_meshes()
576✔
1352
{
1353
  // Skip if there are no mappings between mesh IDs and domains
1354
  if (mesh_domain_map_.empty())
576✔
1355
    return;
416✔
1356

1357
  // Loop over meshes
1358
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
400✔
1359
    Mesh* mesh = model::meshes[mesh_idx].get();
240✔
1360
    int mesh_id = mesh->id();
240✔
1361

1362
    // Skip if mesh id is not present in the map
1363
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
240✔
1364
      continue;
16✔
1365

1366
    // Loop over domains associated with the mesh
1367
    for (auto& domain : mesh_domain_map_[mesh_id]) {
448✔
1368
      Source::DomainType domain_type = domain.first;
224✔
1369
      int domain_id = domain.second;
224✔
1370

1371
      if (domain_type == Source::DomainType::MATERIAL) {
224✔
1372
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1373
          if (domain_id == C_NONE) {
160✔
1374
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1375
          } else {
1376
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1377
          }
1378
        }
1379
      } else if (domain_type == Source::DomainType::CELL) {
192✔
1380
        int32_t i_cell = model::cell_map[domain_id];
32✔
1381
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1382
      } else if (domain_type == Source::DomainType::UNIVERSE) {
160✔
1383
        int32_t i_universe = model::universe_map[domain_id];
160✔
1384
        Universe& universe = *model::universes[i_universe];
160✔
1385
        for (int32_t i_cell : universe.cells_) {
448✔
1386
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
288✔
1387
        }
1388
      }
1389
    }
1390
  }
1391
}
1392

1393
void FlatSourceDomain::prepare_base_source_regions()
110✔
1394
{
1395
  std::swap(source_regions_, base_source_regions_);
110✔
1396
  source_regions_.negroups() = base_source_regions_.negroups();
110✔
1397
  source_regions_.is_linear() = base_source_regions_.is_linear();
110✔
1398
}
110✔
1399

1400
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
871,515,854✔
1401
  int64_t sr, int mesh_bin, Position r, double dist, Direction u)
1402
{
1403
  SourceRegionKey sr_key {sr, mesh_bin};
871,515,854✔
1404

1405
  // Case 1: Check if the source region key is already present in the permanent
1406
  // map. This is the most common condition, as any source region visited in a
1407
  // previous power iteration will already be present in the permanent map. If
1408
  // the source region key is found, we translate the key into a specific 1D
1409
  // source region index and return a handle its position in the
1410
  // source_regions_ vector.
1411
  auto it = source_region_map_.find(sr_key);
871,515,854✔
1412
  if (it != source_region_map_.end()) {
871,515,854✔
1413
    int64_t sr = it->second;
850,445,145✔
1414
    return source_regions_.get_source_region_handle(sr);
850,445,145✔
1415
  }
1416

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

1425
  // If the key is found in the temporary map, then we return a handle to the
1426
  // source region that is stored in the temporary map.
1427
  if (discovered_source_regions_.contains(sr_key)) {
21,070,709✔
1428
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
19,099,003✔
1429
    discovered_source_regions_.unlock(sr_key);
19,099,003✔
1430
    return handle;
19,099,003✔
1431
  }
1432

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

1461
  // Sanity check on source region id
1462
  GeometryState gs;
1,971,706✔
1463
  gs.r() = r + TINY_BIT * u;
1,971,706✔
1464
  gs.u() = {1.0, 0.0, 0.0};
1,971,706✔
1465
  exhaustive_find_cell(gs);
1,971,706✔
1466
  int gs_i_cell = gs.lowest_coord().cell;
1,971,706✔
1467
  int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance();
1,971,706✔
1468
  if (sr_found != sr) {
1,971,706✔
1469
    discovered_source_regions_.unlock(sr_key);
88✔
1470
    SourceRegionHandle handle;
88✔
1471
    handle.is_numerical_fp_artifact_ = true;
88✔
1472
    return handle;
88✔
1473
  }
1474

1475
  // Sanity check on mesh bin
1476
  int mesh_idx = base_source_regions_.mesh(sr);
1,971,618✔
1477
  if (mesh_idx == C_NONE) {
1,971,618✔
1478
    if (mesh_bin != 0) {
×
1479
      discovered_source_regions_.unlock(sr_key);
×
1480
      SourceRegionHandle handle;
×
1481
      handle.is_numerical_fp_artifact_ = true;
×
1482
      return handle;
×
1483
    }
1484
  } else {
1485
    Mesh* mesh = model::meshes[mesh_idx].get();
1,971,618✔
1486
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,971,618✔
1487
    if (bin_found != mesh_bin) {
1,971,618✔
1488
      discovered_source_regions_.unlock(sr_key);
×
1489
      SourceRegionHandle handle;
×
1490
      handle.is_numerical_fp_artifact_ = true;
×
1491
      return handle;
×
1492
    }
1493
  }
1494

1495
  // Case 4: The source region key is valid, but is not present anywhere. This
1496
  // condition only occurs the first time the source region is discovered
1497
  // (typically in the first power iteration). In this case, we need to handle
1498
  // creation of the new source region and its storage into the parallel map.
1499
  // The new source region is created by copying the base source region, so as
1500
  // to inherit material, external source, and some flux properties etc. We
1501
  // also pass the base source region id to allow the new source region to
1502
  // know which base source region it is derived from.
1503
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
1,971,618✔
1504
    sr_key, {base_source_regions_.get_source_region_handle(sr), sr});
1,971,618✔
1505
  discovered_source_regions_.unlock(sr_key);
1,971,618✔
1506
  SourceRegionHandle handle {*sr_ptr};
1,971,618✔
1507

1508
  // Check if the new source region contains a point source and apply it if so
1509
  auto it2 = point_source_map_.find(sr_key);
1,971,618✔
1510
  if (it2 != point_source_map_.end()) {
1,971,618✔
1511
    int es = it2->second;
11✔
1512
    auto s = model::external_sources[es].get();
11✔
1513
    auto is = dynamic_cast<IndependentSource*>(s);
11✔
1514
    auto energy = dynamic_cast<Discrete*>(is->energy());
11✔
1515
    double strength_factor = is->strength();
11✔
1516
    apply_external_source_to_source_region(energy, strength_factor, handle);
11✔
1517
    int material = handle.material();
11✔
1518
    if (material != MATERIAL_VOID) {
11✔
1519
      for (int g = 0; g < negroups_; g++) {
22✔
1520
        double sigma_t = sigma_t_[material * negroups_ + g];
11✔
1521
        handle.external_source(g) /= sigma_t;
11✔
1522
      }
1523
    }
1524
  }
1525

1526
  return handle;
1,971,618✔
1527
}
1,971,706✔
1528

1529
void FlatSourceDomain::finalize_discovered_source_regions()
2,970✔
1530
{
1531
  // Extract keys for entries with a valid volume.
1532
  vector<SourceRegionKey> keys;
2,970✔
1533
  for (const auto& pair : discovered_source_regions_) {
1,974,588✔
1534
    if (pair.second.volume_ > 0.0) {
1,971,618✔
1535
      keys.push_back(pair.first);
1,870,176✔
1536
    }
1537
  }
1538

1539
  if (!keys.empty()) {
2,970✔
1540
    // Sort the keys, so as to ensure reproducible ordering given that source
1541
    // regions may have been added to discovered_source_regions_ in an arbitrary
1542
    // order due to shared memory threading.
1543
    std::sort(keys.begin(), keys.end());
440✔
1544

1545
    // Remember the index of the first new source region
1546
    int64_t start_sr_id = source_regions_.n_source_regions();
440✔
1547

1548
    // Append the source regions in the sorted key order.
1549
    for (const auto& key : keys) {
1,870,616✔
1550
      const SourceRegion& sr = discovered_source_regions_[key];
1,870,176✔
1551
      source_region_map_[key] = source_regions_.n_source_regions();
1,870,176✔
1552
      source_regions_.push_back(sr);
1,870,176✔
1553
    }
1554

1555
    // Map all new source regions to tallies
1556
    convert_source_regions_to_tallies(start_sr_id);
440✔
1557
  }
1558

1559
  discovered_source_regions_.clear();
2,970✔
1560
}
2,970✔
1561

1562
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1563
//
1564
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1565
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1566
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1567
// https://doi.org/10.1016/j.anucene.2018.10.036.
1568
void FlatSourceDomain::apply_transport_stabilization()
11,660✔
1569
{
1570
  // Don't do anything if all in-group scattering
1571
  // cross sections are positive
1572
  if (!is_transport_stabilization_needed_) {
11,660✔
1573
    return;
11,440✔
1574
  }
1575

1576
  // Apply the stabilization factor to all source elements
1577
#pragma omp parallel for
120✔
1578
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,300✔
1579
    int material = source_regions_.material(sr);
1,200✔
1580
    if (material == MATERIAL_VOID) {
1,200✔
1581
      continue;
1582
    }
1583
    for (int g = 0; g < negroups_; g++) {
85,200✔
1584
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1585
      // negative
1586
      double sigma_s =
1587
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g];
84,000✔
1588
      if (sigma_s < 0.0) {
84,000✔
1589
        double sigma_t = sigma_t_[material * negroups_ + g];
22,000✔
1590
        double phi_new = source_regions_.scalar_flux_new(sr, g);
22,000✔
1591
        double phi_old = source_regions_.scalar_flux_old(sr, g);
22,000✔
1592

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

1603
        // Equation 16 in the above Gunow et al. 2019 paper
1604
        source_regions_.scalar_flux_new(sr, g) =
22,000✔
1605
          (phi_new - D * phi_old) / (1.0 - D);
22,000✔
1606
      }
1607
    }
1608
  }
1609
}
1610

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

© 2025 Coveralls, Inc