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

openmc-dev / openmc / 19918574733

04 Dec 2025 05:19AM UTC coverage: 82.123% (+0.07%) from 82.055%
19918574733

Pull #3667

github

web-flow
Merge 3127c6bd7 into ad5a876be
Pull Request #3667: Allow DistribcellFilter to work with apply_tally_results=True

16952 of 23523 branches covered (72.07%)

Branch coverage included in aggregate %.

29 of 30 new or added lines in 7 files covered. (96.67%)

139 existing lines in 6 files now uncovered.

54963 of 64047 relevant lines covered (85.82%)

43580650.96 hits per line

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

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

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

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

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

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

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

88
#pragma omp parallel for
6,612✔
89
  for (int64_t se = 0; se < n_source_elements(); se++) {
36,567,335✔
90
    source_regions_.scalar_flux_new(se) = 0.0;
36,561,835✔
91
  }
92
}
12,112✔
93

94
void FlatSourceDomain::accumulate_iteration_flux()
4,956✔
95
{
96
#pragma omp parallel for
2,706✔
97
  for (int64_t se = 0; se < n_source_elements(); se++) {
16,644,450✔
98
    source_regions_.scalar_flux_final(se) +=
16,642,200✔
99
      source_regions_.scalar_flux_new(se);
16,642,200✔
100
  }
101
}
4,956✔
102

103
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
43,807,246✔
104
{
105
  // Reset all source regions to zero (important for void regions)
106
  for (int g = 0; g < negroups_; g++) {
92,883,590✔
107
    srh.source(g) = 0.0;
49,076,344✔
108
  }
109

110
  // Add scattering + fission source
111
  int material = srh.material();
43,807,246✔
112
  if (material != MATERIAL_VOID) {
43,807,246✔
113
    double inverse_k_eff = 1.0 / k_eff_;
43,365,598✔
114
    for (int g_out = 0; g_out < negroups_; g_out++) {
91,999,526✔
115
      double sigma_t = sigma_t_[material * negroups_ + g_out];
48,633,928✔
116
      double scatter_source = 0.0;
48,633,928✔
117
      double fission_source = 0.0;
48,633,928✔
118

119
      for (int g_in = 0; g_in < negroups_; g_in++) {
134,144,786✔
120
        double scalar_flux = srh.scalar_flux_old(g_in);
85,510,858✔
121
        double sigma_s =
122
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
85,510,858✔
123
        double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
85,510,858✔
124
        double chi = chi_[material * negroups_ + g_out];
85,510,858✔
125

126
        scatter_source += sigma_s * scalar_flux;
85,510,858✔
127
        if (settings::create_fission_neutrons) {
85,510,858!
128
          fission_source += nu_sigma_f * scalar_flux * chi;
85,510,858✔
129
        }
130
      }
131
      srh.source(g_out) =
48,633,928✔
132
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
48,633,928✔
133
    }
134
  }
135

136
  // Add external source if in fixed source mode
137
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
43,807,246✔
138
    for (int g = 0; g < negroups_; g++) {
89,574,878✔
139
      srh.source(g) += srh.external_source(g);
46,223,461✔
140
    }
141
  }
142
}
43,807,246✔
143

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

150
#pragma omp parallel for
6,612✔
151
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
32,323,955✔
152
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
32,318,455✔
153
    update_single_neutron_source(srh);
32,318,455✔
154
  }
155

156
  simulation::time_update_src.stop();
12,112✔
157
}
12,112✔
158

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

167
// Normalize scalar flux to total distance travelled by all rays this
168
// iteration
169
#pragma omp parallel for
2,862✔
170
  for (int64_t se = 0; se < n_source_elements(); se++) {
22,277,785✔
171
    source_regions_.scalar_flux_new(se) *= normalization_factor;
22,275,410✔
172
  }
173

174
// Accumulate cell-wise ray length tallies collected this iteration, then
175
// update the simulation-averaged cell-wise volume estimates
176
#pragma omp parallel for
2,862✔
177
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
19,883,245✔
178
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
19,880,870✔
179
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
19,880,870✔
180
    source_regions_.volume_naive(sr) =
39,761,740✔
181
      source_regions_.volume(sr) * normalization_factor;
19,880,870✔
182
    source_regions_.volume_sq(sr) =
39,761,740✔
183
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
19,880,870✔
184
    source_regions_.volume(sr) =
19,880,870✔
185
      source_regions_.volume_t(sr) * volume_normalization_factor;
19,880,870✔
186
  }
187
}
5,237✔
188

189
void FlatSourceDomain::set_flux_to_flux_plus_source(
43,412,209✔
190
  int64_t sr, double volume, int g)
191
{
192
  int material = source_regions_.material(sr);
43,412,209✔
193
  if (material == MATERIAL_VOID) {
43,412,209✔
194
    source_regions_.scalar_flux_new(sr, g) /= volume;
882,416✔
195
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
882,416!
196
      source_regions_.scalar_flux_new(sr, g) +=
882,416✔
197
        0.5f * source_regions_.external_source(sr, g) *
882,416✔
198
        source_regions_.volume_sq(sr);
882,416✔
199
    }
200
  } else {
201
    double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g];
42,529,793✔
202
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
42,529,793✔
203
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
42,529,793✔
204
  }
205
}
43,412,209✔
206

207
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,542✔
208
{
209
  source_regions_.scalar_flux_new(sr, g) =
3,084✔
210
    source_regions_.scalar_flux_old(sr, g);
1,542✔
211
}
1,542✔
212

213
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
8,927,547✔
214
{
215
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
8,927,547✔
216
}
8,927,547✔
217

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

225
#pragma omp parallel for reduction(+ : n_hits)
6,612✔
226
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
33,341,935✔
227

228
    double volume_simulation_avg = source_regions_.volume(sr);
33,336,435✔
229
    double volume_iteration = source_regions_.volume_naive(sr);
33,336,435✔
230

231
    // Increment the number of hits if cell was hit this iteration
232
    if (volume_iteration) {
33,336,435✔
233
      n_hits++;
29,277,280✔
234
    }
235

236
    // Set the SR to small status if its expected number of hits
237
    // per iteration is less than 1.5
238
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
33,336,435✔
239
      source_regions_.is_small(sr) = 1;
6,812,705✔
240
    } else {
241
      source_regions_.is_small(sr) = 0;
26,523,730✔
242
    }
243

244
    // The volume treatment depends on the volume estimator type
245
    // and whether or not an external source is present in the cell.
246
    double volume;
247
    switch (volume_estimator_) {
33,336,435!
248
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
249
      volume = volume_iteration;
777,600✔
250
      break;
777,600✔
251
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
252
      volume = volume_simulation_avg;
432,000✔
253
      break;
432,000✔
254
    case RandomRayVolumeEstimator::HYBRID:
32,126,835✔
255
      if (source_regions_.external_source_present(sr) ||
60,065,670✔
256
          source_regions_.is_small(sr)) {
27,938,835✔
257
        volume = volume_iteration;
11,000,325✔
258
      } else {
259
        volume = volume_simulation_avg;
21,126,510✔
260
      }
261
      break;
32,126,835✔
262
    default:
263
      fatal_error("Invalid volume estimator type");
264
    }
265

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

303
  // Return the number of source regions that were hit this iteration
304
  return n_hits;
12,112✔
305
}
306

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

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

317
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
1,380✔
318
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
311,340✔
319

320
    // If simulation averaged volume is zero, don't include this cell
321
    double volume = source_regions_.volume(sr);
310,190✔
322
    if (volume == 0.0) {
310,190!
323
      continue;
324
    }
325

326
    int material = source_regions_.material(sr);
310,190✔
327
    if (material == MATERIAL_VOID) {
310,190!
328
      continue;
329
    }
330

331
    double sr_fission_source_old = 0;
310,190✔
332
    double sr_fission_source_new = 0;
310,190✔
333

334
    for (int g = 0; g < negroups_; g++) {
2,396,920✔
335
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
2,086,730✔
336
      sr_fission_source_old +=
2,086,730✔
337
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
2,086,730✔
338
      sr_fission_source_new +=
2,086,730✔
339
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
2,086,730✔
340
    }
341

342
    // Compute total fission rates in FSR
343
    sr_fission_source_old *= volume;
310,190✔
344
    sr_fission_source_new *= volume;
310,190✔
345

346
    // Accumulate totals
347
    fission_rate_old += sr_fission_source_old;
310,190✔
348
    fission_rate_new += sr_fission_source_new;
310,190✔
349

350
    // Store total fission rate in the FSR for Shannon calculation
351
    p[sr] = sr_fission_source_new;
310,190✔
352
  }
353

354
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
2,530✔
355

356
  double H = 0.0;
2,530✔
357
  // defining an inverse sum for better performance
358
  double inverse_sum = 1 / fission_rate_new;
2,530✔
359

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

371
  // Adds entropy value to shared entropy vector in openmc namespace.
372
  simulation::entropy.push_back(H);
2,530✔
373

374
  fission_rate_ = fission_rate_new;
2,530✔
375
  k_eff_ = k_eff_new;
2,530✔
376
}
2,530✔
377

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

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

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

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

416
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
771✔
417
{
418
  openmc::simulation::time_tallies.start();
771✔
419

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

423
// Attempt to generate mapping for all source regions
424
#pragma omp parallel for
421✔
425
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,018,330✔
426

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

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

443
    // Loop over energy groups (so as to support energy filters)
444
    for (int g = 0; g < negroups_; g++) {
2,179,120✔
445

446
      // Set particle to the current energy
447
      p.g() = g;
1,161,140✔
448
      p.g_last() = g;
1,161,140✔
449
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,161,140✔
450
      p.E_last() = p.E();
1,161,140✔
451

452
      int64_t source_element = sr * negroups_ + g;
1,161,140✔
453

454
      // If this task has already been populated, we don't need to do
455
      // it again.
456
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,161,140!
457
        continue;
458
      }
459

460
      // Loop over all active tallies. This logic is essentially identical
461
      // to what happens when scanning for applicable tallies during
462
      // MC transport.
463
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
4,444,280✔
464
        Tally& tally {*model::tallies[i_tally]};
3,283,140✔
465

466
        // Initialize an iterator over valid filter bin combinations.
467
        // If there are no valid combinations, use a continue statement
468
        // to ensure we skip the assume_separate break below.
469
        auto filter_iter = FilterBinIter(tally, p);
3,283,140✔
470
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,283,140✔
471
        if (filter_iter == end)
3,283,140✔
472
          continue;
1,984,160✔
473

474
        // Loop over filter bins.
475
        for (; filter_iter != end; ++filter_iter) {
2,597,960✔
476
          auto filter_index = filter_iter.index_;
1,298,980✔
477
          auto filter_weight = filter_iter.weight_;
1,298,980✔
478

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

487
            // Also add this task to the list of volume tasks for this source
488
            // region.
489
            source_regions_.volume_task(sr).insert(task);
1,619,020✔
490
          }
491
        }
492
      }
493
      // Reset all the filter matches for the next tally event.
494
      for (auto& match : p.filter_matches())
4,898,060✔
495
        match.bins_present_ = false;
3,736,920✔
496
    }
497
  }
1,017,980✔
498
  openmc::simulation::time_tallies.stop();
771✔
499

500
  mapped_all_tallies_ = all_source_regions_mapped;
771✔
501
}
771✔
502

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

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

543
  // If we are in adjoint mode of a fixed source problem, the external
544
  // source is already normalized, such that all resulting fluxes are
545
  // also normalized.
546
  if (adjoint_) {
4,147✔
547
    return 1.0;
333✔
548
  }
549

550
  // Fixed source mode normalization
551

552
  // Step 1 is to sum over all source regions and energy groups to get the
553
  // total external source strength in the simulation.
554
  double simulation_external_source_strength = 0.0;
3,814✔
555
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,089✔
556
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,626,285✔
557
    int material = source_regions_.material(sr);
14,624,560✔
558
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,624,560✔
559
    for (int g = 0; g < negroups_; g++) {
29,792,000✔
560
      // For non-void regions, we store the external source pre-divided by
561
      // sigma_t. We need to multiply non-void regions back up by sigma_t
562
      // to get the total source strength in the expected units.
563
      double sigma_t = 1.0;
15,167,440✔
564
      if (material != MATERIAL_VOID) {
15,167,440✔
565
        sigma_t = sigma_t_[material * negroups_ + g];
14,957,200✔
566
      }
567
      simulation_external_source_strength +=
15,167,440✔
568
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,167,440✔
569
    }
570
  }
571

572
  // Step 2 is to determine the total user-specified external source strength
573
  double user_external_source_strength = 0.0;
3,814✔
574
  for (auto& ext_source : model::external_sources) {
7,628✔
575
    user_external_source_strength += ext_source->strength();
3,814✔
576
  }
577

578
  // The correction factor is the ratio of the user-specified external source
579
  // strength to the simulation external source strength.
580
  double source_normalization_factor =
3,814✔
581
    user_external_source_strength / simulation_external_source_strength;
582

583
  return source_normalization_factor;
3,814✔
584
}
585

586
// Tallying in random ray is not done directly during transport, rather,
587
// it is done only once after each power iteration. This is made possible
588
// by way of a mapping data structure that relates spatial source regions
589
// (FSRs) to tally/filter/score combinations. The mechanism by which the
590
// mapping is done (and the limitations incurred) is documented in the
591
// "convert_source_regions_to_tallies()" function comments above. The present
592
// tally function simply traverses the mapping data structure and executes
593
// the scoring operations to OpenMC's native tally result arrays.
594

595
void FlatSourceDomain::random_ray_tally()
4,956✔
596
{
597
  openmc::simulation::time_tallies.start();
4,956✔
598

599
  // Reset our tally volumes to zero
600
  reset_tally_volumes();
4,956✔
601

602
  double source_normalization_factor =
603
    compute_fixed_source_normalization_factor();
4,956✔
604

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

621
    double material = source_regions_.material(sr);
15,252,600✔
622
    for (int g = 0; g < negroups_; g++) {
31,894,800✔
623
      double flux =
624
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
16,642,200✔
625

626
      // Determine numerical score value
627
      for (auto& task : source_regions_.tally_task(sr, g)) {
39,262,800✔
628
        double score = 0.0;
22,620,600✔
629
        switch (task.score_type) {
22,620,600!
630

631
        case SCORE_FLUX:
19,448,200✔
632
          score = flux * volume;
19,448,200✔
633
          break;
19,448,200✔
634

635
        case SCORE_TOTAL:
636
          if (material != MATERIAL_VOID) {
×
637
            score = flux * volume * sigma_t_[material * negroups_ + g];
638
          }
639
          break;
640

641
        case SCORE_FISSION:
1,586,200✔
642
          if (material != MATERIAL_VOID) {
1,586,200!
643
            score = flux * volume * sigma_f_[material * negroups_ + g];
1,586,200✔
644
          }
645
          break;
1,586,200✔
646

647
        case SCORE_NU_FISSION:
1,586,200✔
648
          if (material != MATERIAL_VOID) {
1,586,200!
649
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
1,586,200✔
650
          }
651
          break;
1,586,200✔
652

653
        case SCORE_EVENTS:
654
          score = 1.0;
655
          break;
656

657
        default:
658
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
659
                      "total, fission, nu-fission, and events are supported in "
660
                      "random ray mode.");
661
          break;
662
        }
663
        // Apply score to the appropriate tally bin
664
        Tally& tally {*model::tallies[task.tally_idx]};
22,620,600✔
665
#pragma omp atomic
666
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
22,620,600✔
667
          score;
668
      }
669
    }
670

671
    // For flux tallies, the total volume of the spatial region is needed
672
    // for normalizing the flux. We store this volume in a separate tensor.
673
    // We only contribute to each volume tally bin once per FSR.
674
    if (volume_normalized_flux_tallies_) {
15,252,600✔
675
      for (const auto& task : source_regions_.volume_task(sr)) {
35,857,600✔
676
        if (task.score_type == SCORE_FLUX) {
20,750,400✔
677
#pragma omp atomic
678
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
18,796,000✔
679
            volume;
680
        }
681
      }
682
    }
683
  } // end FSR loop
684

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

708
  openmc::simulation::time_tallies.stop();
4,956✔
709
}
4,956✔
710

UNCOV
711
double FlatSourceDomain::evaluate_flux_at_point(
×
712
  Position r, int64_t sr, int g) const
713
{
UNCOV
714
  return source_regions_.scalar_flux_final(sr, g) /
×
715
         (settings::n_batches - settings::n_inactive);
×
716
}
717

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

732
  // Print header information
733
  print_plot();
×
734

735
  // Outer loop over plots
736
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
737

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

741
    // Random ray plots only support voxel plots
742
    if (!openmc_plot) {
×
743
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
744
                          "is allowed in random ray mode.",
745
        plt));
746
      continue;
×
UNCOV
747
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
748
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
749
                          "is allowed in random ray mode.",
750
        plt));
UNCOV
751
      continue;
×
752
    }
753

754
    int Nx = openmc_plot->pixels_[0];
×
755
    int Ny = openmc_plot->pixels_[1];
×
756
    int Nz = openmc_plot->pixels_[2];
×
UNCOV
757
    Position origin = openmc_plot->origin_;
×
UNCOV
758
    Position width = openmc_plot->width_;
×
UNCOV
759
    Position ll = origin - width / 2.0;
×
UNCOV
760
    double x_delta = width.x / Nx;
×
UNCOV
761
    double y_delta = width.y / Ny;
×
UNCOV
762
    double z_delta = width.z / Nz;
×
UNCOV
763
    std::string filename = openmc_plot->path_plot();
×
764

765
    // Perform sanity checks on file size
UNCOV
766
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
UNCOV
767
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
UNCOV
768
      openmc_plot->id(), filename, bytes / 1.0e6);
×
UNCOV
769
    if (bytes / 1.0e9 > 1.0) {
×
UNCOV
770
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
771
              "slow.");
UNCOV
772
    } else if (bytes / 1.0e9 > 100.0) {
×
UNCOV
773
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
774
    }
775

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

796
          bool found = exhaustive_find_cell(p);
×
797
          if (!found) {
×
798
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
799
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
800
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
801
            continue;
802
          }
803

804
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
805
          int64_t sr = -1;
806
          auto it = source_region_map_.find(sr_key);
×
807
          if (it != source_region_map_.end()) {
×
808
            sr = it->second;
809
          }
810

811
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
812
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
813

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

826
    double source_normalization_factor =
827
      compute_fixed_source_normalization_factor();
×
828

829
    // Open file for writing
830
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
831

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

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

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

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

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

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

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

UNCOV
960
    std::fclose(plot);
×
UNCOV
961
  }
×
UNCOV
962
}
×
963

964
void FlatSourceDomain::apply_external_source_to_source_region(
4,500✔
965
  int src_idx, SourceRegionHandle& srh)
966
{
967
  auto s = model::external_sources[src_idx].get();
4,500✔
968
  auto is = dynamic_cast<IndependentSource*>(s);
4,500!
969
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,500!
970
  double strength_factor = is->strength();
4,500✔
971
  const auto& discrete_energies = discrete->x();
4,500✔
972
  const auto& discrete_probs = discrete->prob();
4,500✔
973

974
  srh.external_source_present() = 1;
4,500✔
975

976
  for (int i = 0; i < discrete_energies.size(); i++) {
9,132✔
977
    int g = data::mg.get_group_index(discrete_energies[i]);
4,632✔
978
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,632✔
979
  }
980
}
4,500✔
981

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

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

990
  for (int j : instances) {
30,784✔
991
    int cell_material_idx = cell.material(j);
30,368✔
992
    int cell_material_id;
993
    if (cell_material_idx == MATERIAL_VOID) {
30,368✔
994
      cell_material_id = MATERIAL_VOID;
256✔
995
    } else {
996
      cell_material_id = model::materials[cell_material_idx]->id();
30,112✔
997
    }
998
    if (target_material_id == C_NONE ||
30,368✔
999
        cell_material_id == target_material_id) {
1000
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,848✔
1001
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,848✔
1002
    }
1003
  }
1004
}
1005

1006
void FlatSourceDomain::apply_external_source_to_cell_and_children(
448✔
1007
  int32_t i_cell, int src_idx, int32_t target_material_id)
1008
{
1009
  Cell& cell = *model::cells[i_cell];
448✔
1010

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

1027
void FlatSourceDomain::count_external_source_regions()
1,123✔
1028
{
1029
  n_external_source_regions_ = 0;
1,123✔
1030
#pragma omp parallel for reduction(+ : n_external_source_regions_)
633✔
1031
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,175,210✔
1032
    if (source_regions_.external_source_present(sr)) {
1,174,720✔
1033
      n_external_source_regions_++;
157,210✔
1034
    }
1035
  }
1036
}
1,123✔
1037

1038
void FlatSourceDomain::convert_external_sources()
401✔
1039
{
1040
  // Loop over external sources
1041
  for (int es = 0; es < model::external_sources.size(); es++) {
802✔
1042

1043
    // Extract source information
1044
    Source* s = model::external_sources[es].get();
401✔
1045
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
401!
1046
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
401!
1047
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
401✔
1048
    double strength_factor = is->strength();
401✔
1049

1050
    // If there is no domain constraint specified, then this must be a point
1051
    // source. In this case, we need to find the source region that contains the
1052
    // point source and apply or relate it to the external source.
1053
    if (is->domain_ids().size() == 0) {
401✔
1054

1055
      // Extract the point source coordinate and find the base source region at
1056
      // that point
1057
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
17!
1058
      GeometryState gs;
17✔
1059
      gs.r() = sp->r();
17✔
1060
      gs.r_last() = sp->r();
17✔
1061
      gs.u() = {1.0, 0.0, 0.0};
17✔
1062
      bool found = exhaustive_find_cell(gs);
17✔
1063
      if (!found) {
17!
UNCOV
1064
        fatal_error(fmt::format("Could not find cell containing external "
×
1065
                                "point source at {}",
UNCOV
1066
          sp->r()));
×
1067
      }
1068
      SourceRegionKey key = lookup_source_region_key(gs);
17✔
1069

1070
      // With the source region and mesh bin known, we can use the
1071
      // accompanying SourceRegionKey as a key into a map that stores the
1072
      // corresponding external source index for the point source. Notably, we
1073
      // do not actually apply the external source to any source regions here,
1074
      // as if mesh subdivision is enabled, they haven't actually been
1075
      // discovered & initilized yet. When discovered, they will read from the
1076
      // external_source_map to determine if there are any external source
1077
      // terms that should be applied.
1078
      external_point_source_map_[key].push_back(es);
17✔
1079

1080
    } else {
17✔
1081
      // If not a point source, then use the volumetric domain constraints to
1082
      // determine which source regions to apply the external source to.
1083
      if (is->domain_type() == Source::DomainType::MATERIAL) {
384✔
1084
        for (int32_t material_id : domain_ids) {
32✔
1085
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
96✔
1086
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
80✔
1087
          }
1088
        }
1089
      } else if (is->domain_type() == Source::DomainType::CELL) {
368✔
1090
        for (int32_t cell_id : domain_ids) {
32✔
1091
          int32_t i_cell = model::cell_map[cell_id];
16✔
1092
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
16✔
1093
        }
1094
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
352!
1095
        for (int32_t universe_id : domain_ids) {
704✔
1096
          int32_t i_universe = model::universe_map[universe_id];
352✔
1097
          Universe& universe = *model::universes[i_universe];
352✔
1098
          for (int32_t i_cell : universe.cells_) {
704✔
1099
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
352✔
1100
          }
1101
        }
1102
      }
1103
    }
1104
  } // End loop over external sources
1105
}
401✔
1106

1107
void FlatSourceDomain::flux_swap()
12,112✔
1108
{
1109
  source_regions_.flux_swap();
12,112✔
1110
}
12,112✔
1111

1112
void FlatSourceDomain::flatten_xs()
641✔
1113
{
1114
  // Temperature and angle indices, if using multiple temperature
1115
  // data sets and/or anisotropic data sets.
1116
  // TODO: Currently assumes we are only using single temp/single angle data.
1117
  const int t = 0;
641✔
1118
  const int a = 0;
641✔
1119

1120
  n_materials_ = data::mg.macro_xs_.size();
641✔
1121
  for (int i = 0; i < n_materials_; i++) {
2,402✔
1122
    auto& m = data::mg.macro_xs_[i];
1,761✔
1123
    for (int g_out = 0; g_out < negroups_; g_out++) {
9,859✔
1124
      if (m.exists_in_model) {
8,098✔
1125
        double sigma_t =
1126
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
8,034✔
1127
        sigma_t_.push_back(sigma_t);
8,034✔
1128

1129
        if (sigma_t < MINIMUM_MACRO_XS) {
8,034✔
1130
          Material* mat = model::materials[i].get();
16✔
1131
          warning(fmt::format(
16✔
1132
            "Material \"{}\" (id: {}) has a group {} total cross section "
1133
            "({:.3e}) below the minimum threshold "
1134
            "({:.3e}). Material will be treated as pure void.",
1135
            mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
32✔
1136
        }
1137

1138
        double nu_sigma_f =
1139
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
8,034✔
1140
        nu_sigma_f_.push_back(nu_sigma_f);
8,034✔
1141

1142
        double sigma_f =
1143
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
8,034✔
1144
        sigma_f_.push_back(sigma_f);
8,034✔
1145

1146
        double chi =
1147
          m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
8,034✔
1148
        if (!std::isfinite(chi)) {
8,034✔
1149
          // MGXS interface may return NaN in some cases, such as when material
1150
          // is fissionable but has very small sigma_f.
1151
          chi = 0.0;
576✔
1152
        }
1153
        chi_.push_back(chi);
8,034✔
1154

1155
        for (int g_in = 0; g_in < negroups_; g_in++) {
269,702✔
1156
          double sigma_s =
1157
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
261,668✔
1158
          sigma_s_.push_back(sigma_s);
261,668✔
1159
          // For transport corrected XS data, diagonal elements may be negative.
1160
          // In this case, set a flag to enable transport stabilization for the
1161
          // simulation.
1162
          if (g_out == g_in && sigma_s < 0.0)
261,668✔
1163
            is_transport_stabilization_needed_ = true;
880✔
1164
        }
1165
      } else {
1166
        sigma_t_.push_back(0);
64✔
1167
        nu_sigma_f_.push_back(0);
64✔
1168
        sigma_f_.push_back(0);
64✔
1169
        chi_.push_back(0);
64✔
1170
        for (int g_in = 0; g_in < negroups_; g_in++) {
128✔
1171
          sigma_s_.push_back(0);
64✔
1172
        }
1173
      }
1174
    }
1175
  }
1176
}
641✔
1177

1178
void FlatSourceDomain::set_adjoint_sources()
65✔
1179
{
1180
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1181
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1182
  // as this is likely a very small source region that we don't need to bother
1183
  // trying to vector particles towards. In the case of flux "being extremely
1184
  // close to zero", we define this as being a fixed fraction of the maximum
1185
  // forward flux, below which we assume the flux would be physically
1186
  // undetectable.
1187

1188
  // First, find the maximum forward flux value
1189
  double max_flux = 0.0;
65✔
1190
#pragma omp parallel for reduction(max : max_flux)
37✔
1191
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,548✔
1192
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1193
    if (flux > max_flux) {
155,520✔
1194
      max_flux = flux;
25✔
1195
    }
1196
  }
1197

1198
  // Then, compute the adjoint source for each source region
1199
#pragma omp parallel for
37✔
1200
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1201
    for (int g = 0; g < negroups_; g++) {
311,040✔
1202
      double flux = source_regions_.scalar_flux_final(sr, g);
155,520✔
1203
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
155,520✔
1204
        source_regions_.external_source(sr, g) = 0.0;
325✔
1205
      } else {
1206
        source_regions_.external_source(sr, g) = 1.0 / flux;
155,195✔
1207
      }
1208
      if (flux > 0.0) {
155,520✔
1209
        source_regions_.external_source_present(sr) = 1;
155,195✔
1210
      }
1211
      source_regions_.scalar_flux_final(sr, g) = 0.0;
155,520✔
1212
    }
1213
  }
1214

1215
  // "Small" source regions in OpenMC are defined as those that are hit by
1216
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1217
  // very small volumes combined with a low aspect ratio, and are often
1218
  // generated when applying a source region mesh that clips the edge of a
1219
  // curved surface. As perhaps only a few rays will visit these regions over
1220
  // the entire forward simulation, the forward flux estimates are extremely
1221
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1222
  // extremely low, leading to unphysically large adjoint source terms,
1223
  // resulting in weight windows that aggressively try to drive particles
1224
  // towards these regions. To fix this, we simply filter out any "small" source
1225
  // regions from consideration. If a source region is "small", we
1226
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1227
  // flux solution, as the true total adjoint source contribution from small
1228
  // regions is likely to be negligible.
1229
#pragma omp parallel for
37✔
1230
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1231
    if (source_regions_.is_small(sr)) {
155,520!
1232
      for (int g = 0; g < negroups_; g++) {
×
1233
        source_regions_.external_source(sr, g) = 0.0;
1234
      }
1235
      source_regions_.external_source_present(sr) = 0;
1236
    }
1237
  }
1238
  // Divide the fixed source term by sigma t (to save time when applying each
1239
  // iteration)
1240
#pragma omp parallel for
37✔
1241
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1242
    int material = source_regions_.material(sr);
155,520✔
1243
    if (material == MATERIAL_VOID) {
155,520!
1244
      continue;
1245
    }
1246
    for (int g = 0; g < negroups_; g++) {
311,040✔
1247
      double sigma_t = sigma_t_[material * negroups_ + g];
155,520✔
1248
      source_regions_.external_source(sr, g) /= sigma_t;
155,520✔
1249
    }
1250
  }
1251
}
65✔
1252

1253
void FlatSourceDomain::transpose_scattering_matrix()
81✔
1254
{
1255
  // Transpose the inner two dimensions for each material
1256
  for (int m = 0; m < n_materials_; ++m) {
306✔
1257
    int material_offset = m * negroups_ * negroups_;
225✔
1258
    for (int i = 0; i < negroups_; ++i) {
643✔
1259
      for (int j = i + 1; j < negroups_; ++j) {
1,091✔
1260
        // Calculate indices of the elements to swap
1261
        int idx1 = material_offset + i * negroups_ + j;
673✔
1262
        int idx2 = material_offset + j * negroups_ + i;
673✔
1263

1264
        // Swap the elements to transpose the matrix
1265
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
673✔
1266
      }
1267
    }
1268
  }
1269
}
81✔
1270

UNCOV
1271
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1272
{
1273
  // Ensure array is correct size
UNCOV
1274
  flux.resize(n_source_regions() * negroups_);
×
1275
// Serialize the final fluxes for output
1276
#pragma omp parallel for
1277
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1278
    flux[se] = source_regions_.scalar_flux_final(se);
1279
  }
UNCOV
1280
}
×
1281

1282
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
802✔
1283
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1284
  bool is_target_void)
1285
{
1286
  Cell& cell = *model::cells[i_cell];
802✔
1287
  if (cell.type_ != Fill::MATERIAL)
802!
UNCOV
1288
    return;
×
1289
  for (int32_t j : instances) {
175,332✔
1290
    int cell_material_idx = cell.material(j);
174,530✔
1291
    int cell_material_id = (cell_material_idx == C_NONE)
1292
                             ? C_NONE
174,530✔
1293
                             : model::materials[cell_material_idx]->id();
174,529✔
1294

1295
    if ((target_material_id == C_NONE && !is_target_void) ||
174,530!
1296
        cell_material_id == target_material_id) {
1297
      int64_t sr = source_region_offsets_[i_cell] + j;
142,530✔
1298
      // Check if the key is already present in the mesh_map_
1299
      if (mesh_map_.find(sr) != mesh_map_.end()) {
142,530!
UNCOV
1300
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1301
                                "applied, but trying to apply mesh idx {}",
UNCOV
1302
          sr, mesh_map_[sr], mesh_idx));
×
1303
      }
1304
      // If the SR has not already been assigned, then we can write to it
1305
      mesh_map_[sr] = mesh_idx;
142,530✔
1306
    }
1307
  }
1308
}
1309

1310
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
673✔
1311
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1312
{
1313
  Cell& cell = *model::cells[i_cell];
673✔
1314

1315
  if (cell.type_ == Fill::MATERIAL) {
673✔
1316
    vector<int> instances(cell.n_instances());
544✔
1317
    std::iota(instances.begin(), instances.end(), 0);
544✔
1318
    apply_mesh_to_cell_instances(
544✔
1319
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1320
  } else if (target_material_id == C_NONE && !is_target_void) {
673!
1321
    for (int j = 0; j < cell.n_instances(); j++) {
130✔
1322
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1323
        cell.get_contained_cells(j, nullptr);
65✔
1324
      for (const auto& pair : cell_instance_list) {
323✔
1325
        int32_t i_child_cell = pair.first;
258✔
1326
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
258✔
1327
          pair.second, is_target_void);
258✔
1328
      }
1329
    }
65✔
1330
  }
1331
}
673✔
1332

1333
void FlatSourceDomain::apply_meshes()
641✔
1334
{
1335
  // Skip if there are no mappings between mesh IDs and domains
1336
  if (mesh_domain_map_.empty())
641✔
1337
    return;
416✔
1338

1339
  // Loop over meshes
1340
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
530✔
1341
    Mesh* mesh = model::meshes[mesh_idx].get();
305✔
1342
    int mesh_id = mesh->id();
305✔
1343

1344
    // Skip if mesh id is not present in the map
1345
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
305✔
1346
      continue;
16✔
1347

1348
    // Loop over domains associated with the mesh
1349
    for (auto& domain : mesh_domain_map_[mesh_id]) {
578✔
1350
      Source::DomainType domain_type = domain.first;
289✔
1351
      int domain_id = domain.second;
289✔
1352

1353
      if (domain_type == Source::DomainType::MATERIAL) {
289✔
1354
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1355
          if (domain_id == C_NONE) {
160!
UNCOV
1356
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1357
          } else {
1358
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1359
          }
1360
        }
1361
      } else if (domain_type == Source::DomainType::CELL) {
257✔
1362
        int32_t i_cell = model::cell_map[domain_id];
32✔
1363
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1364
      } else if (domain_type == Source::DomainType::UNIVERSE) {
225!
1365
        int32_t i_universe = model::universe_map[domain_id];
225✔
1366
        Universe& universe = *model::universes[i_universe];
225✔
1367
        for (int32_t i_cell : universe.cells_) {
706✔
1368
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
481✔
1369
        }
1370
      }
1371
    }
1372
  }
1373
}
1374

1375
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
1,245,372,585✔
1376
  SourceRegionKey sr_key, Position r, Direction u)
1377
{
1378
  // Case 1: Check if the source region key is already present in the permanent
1379
  // map. This is the most common condition, as any source region visited in a
1380
  // previous power iteration will already be present in the permanent map. If
1381
  // the source region key is found, we translate the key into a specific 1D
1382
  // source region index and return a handle its position in the
1383
  // source_regions_ vector.
1384
  auto it = source_region_map_.find(sr_key);
1,245,372,585✔
1385
  if (it != source_region_map_.end()) {
1,245,372,585✔
1386
    int64_t sr = it->second;
1,208,300,079✔
1387
    return source_regions_.get_source_region_handle(sr);
1,208,300,079✔
1388
  }
1389

1390
  // Case 2: Check if the source region key is present in the temporary (thread
1391
  // safe) map. This is a common occurrence in the first power iteration when
1392
  // the source region has already been visited already by some other ray. We
1393
  // begin by locking the temporary map before any operations are performed. The
1394
  // lock is not global over the full data structure -- it will be dependent on
1395
  // which key is used.
1396
  discovered_source_regions_.lock(sr_key);
37,072,506✔
1397

1398
  // If the key is found in the temporary map, then we return a handle to the
1399
  // source region that is stored in the temporary map.
1400
  if (discovered_source_regions_.contains(sr_key)) {
37,072,506✔
1401
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
34,731,333✔
1402
    discovered_source_regions_.unlock(sr_key);
34,731,333✔
1403
    return handle;
34,731,333✔
1404
  }
1405

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

1434
  // Sanity check on source region id
1435
  GeometryState gs;
2,341,173✔
1436
  gs.r() = r + TINY_BIT * u;
2,341,173✔
1437
  gs.u() = {1.0, 0.0, 0.0};
2,341,173✔
1438
  exhaustive_find_cell(gs);
2,341,173✔
1439
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,341,173✔
1440
  if (sr_found != sr_key.base_source_region_id) {
2,341,173✔
1441
    discovered_source_regions_.unlock(sr_key);
88✔
1442
    SourceRegionHandle handle;
88✔
1443
    handle.is_numerical_fp_artifact_ = true;
88✔
1444
    return handle;
88✔
1445
  }
1446

1447
  // Sanity check on mesh bin
1448
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,341,085✔
1449
  if (mesh_idx == C_NONE) {
2,341,085✔
1450
    if (sr_key.mesh_bin != 0) {
368,852!
UNCOV
1451
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1452
      SourceRegionHandle handle;
×
UNCOV
1453
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1454
      return handle;
×
1455
    }
1456
  } else {
1457
    Mesh* mesh = model::meshes[mesh_idx].get();
1,972,233✔
1458
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,972,233✔
1459
    if (bin_found != sr_key.mesh_bin) {
1,972,233!
UNCOV
1460
      discovered_source_regions_.unlock(sr_key);
×
UNCOV
1461
      SourceRegionHandle handle;
×
UNCOV
1462
      handle.is_numerical_fp_artifact_ = true;
×
UNCOV
1463
      return handle;
×
1464
    }
1465
  }
1466

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

1474
  // Call the basic constructor for the source region and store in the parallel
1475
  // map.
1476
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,341,085✔
1477
  SourceRegion* sr_ptr =
1478
    discovered_source_regions_.emplace(sr_key, {negroups_, is_linear});
2,341,085✔
1479
  SourceRegionHandle handle {*sr_ptr};
2,341,085✔
1480

1481
  // Determine the material
1482
  int gs_i_cell = gs.lowest_coord().cell();
2,341,085✔
1483
  Cell& cell = *model::cells[gs_i_cell];
2,341,085✔
1484
  int material = cell.material(gs.cell_instance());
2,341,085✔
1485

1486
  // If material total XS is extremely low, just set it to void to avoid
1487
  // problems with 1/Sigma_t
1488
  for (int g = 0; g < negroups_; g++) {
4,975,059✔
1489
    double sigma_t = sigma_t_[material * negroups_ + g];
2,656,126✔
1490
    if (sigma_t < MINIMUM_MACRO_XS) {
2,656,126✔
1491
      material = MATERIAL_VOID;
22,152✔
1492
      break;
22,152✔
1493
    }
1494
  }
1495

1496
  handle.material() = material;
2,341,085✔
1497

1498
  // Store the mesh index (if any) assigned to this source region
1499
  handle.mesh() = mesh_idx;
2,341,085✔
1500

1501
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,341,085✔
1502
    // Determine if there are any volumetric sources, and apply them.
1503
    // Volumetric sources are specifc only to the base SR idx.
1504
    auto it_vol =
1505
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,291,750✔
1506
    if (it_vol != external_volumetric_source_map_.end()) {
2,291,750✔
1507
      const vector<int>& vol_sources = it_vol->second;
4,488✔
1508
      for (int src_idx : vol_sources) {
8,976✔
1509
        apply_external_source_to_source_region(src_idx, handle);
4,488✔
1510
      }
1511
    }
1512

1513
    // Determine if there are any point sources, and apply them.
1514
    // Point sources are specific to the source region key.
1515
    auto it_point = external_point_source_map_.find(sr_key);
2,291,750✔
1516
    if (it_point != external_point_source_map_.end()) {
2,291,750✔
1517
      const vector<int>& point_sources = it_point->second;
12✔
1518
      for (int src_idx : point_sources) {
24✔
1519
        apply_external_source_to_source_region(src_idx, handle);
12✔
1520
      }
1521
    }
1522

1523
    // Divide external source term by sigma_t
1524
    if (material != C_NONE) {
2,291,750✔
1525
      for (int g = 0; g < negroups_; g++) {
4,585,155✔
1526
        double sigma_t = sigma_t_[material * negroups_ + g];
2,315,557✔
1527
        handle.external_source(g) /= sigma_t;
2,315,557✔
1528
      }
1529
    }
1530
  }
1531

1532
  // Compute the combined source term
1533
  update_single_neutron_source(handle);
2,341,085✔
1534

1535
  // Unlock the parallel map. Note: we may be tempted to release
1536
  // this lock earlier, and then just use the source region's lock to protect
1537
  // the flux/source initialization stages above. However, the rest of the code
1538
  // only protects updates to the new flux and volume fields, and assumes that
1539
  // the source is constant for the duration of transport. Thus, using just the
1540
  // source region's lock by itself would result in other threads potentially
1541
  // reading from the source before it is computed, as they won't use the lock
1542
  // when only reading from the SR's source. It would be expensive to protect
1543
  // those operations, whereas generating the SR is only done once, so we just
1544
  // hold the map's bucket lock until the source region is fully initialized.
1545
  discovered_source_regions_.unlock(sr_key);
2,341,085✔
1546

1547
  return handle;
2,341,085✔
1548
}
2,341,173✔
1549

1550
void FlatSourceDomain::finalize_discovered_source_regions()
12,112✔
1551
{
1552
  // Extract keys for entries with a valid volume.
1553
  vector<SourceRegionKey> keys;
12,112✔
1554
  for (const auto& pair : discovered_source_regions_) {
2,353,197✔
1555
    if (pair.second.volume_ > 0.0) {
2,341,085✔
1556
      keys.push_back(pair.first);
2,239,643✔
1557
    }
1558
  }
1559

1560
  if (!keys.empty()) {
12,112✔
1561
    // Sort the keys, so as to ensure reproducible ordering given that source
1562
    // regions may have been added to discovered_source_regions_ in an arbitrary
1563
    // order due to shared memory threading.
1564
    std::sort(keys.begin(), keys.end());
771✔
1565

1566
    // Remember the index of the first new source region
1567
    int64_t start_sr_id = source_regions_.n_source_regions();
771✔
1568

1569
    // Append the source regions in the sorted key order.
1570
    for (const auto& key : keys) {
2,240,414✔
1571
      const SourceRegion& sr = discovered_source_regions_[key];
2,239,643✔
1572
      source_region_map_[key] = source_regions_.n_source_regions();
2,239,643✔
1573
      source_regions_.push_back(sr);
2,239,643✔
1574
    }
1575

1576
    // Map all new source regions to tallies
1577
    convert_source_regions_to_tallies(start_sr_id);
771✔
1578
  }
1579

1580
  discovered_source_regions_.clear();
12,112✔
1581
}
12,112✔
1582

1583
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1584
//
1585
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1586
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1587
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1588
// https://doi.org/10.1016/j.anucene.2018.10.036.
1589
void FlatSourceDomain::apply_transport_stabilization()
12,112✔
1590
{
1591
  // Don't do anything if all in-group scattering
1592
  // cross sections are positive
1593
  if (!is_transport_stabilization_needed_) {
12,112✔
1594
    return;
11,892✔
1595
  }
1596

1597
  // Apply the stabilization factor to all source elements
1598
#pragma omp parallel for
120✔
1599
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,300✔
1600
    int material = source_regions_.material(sr);
1,200✔
1601
    if (material == MATERIAL_VOID) {
1,200!
1602
      continue;
1603
    }
1604
    for (int g = 0; g < negroups_; g++) {
85,200✔
1605
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1606
      // negative
1607
      double sigma_s =
1608
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g];
84,000✔
1609
      if (sigma_s < 0.0) {
84,000✔
1610
        double sigma_t = sigma_t_[material * negroups_ + g];
22,000✔
1611
        double phi_new = source_regions_.scalar_flux_new(sr, g);
22,000✔
1612
        double phi_old = source_regions_.scalar_flux_old(sr, g);
22,000✔
1613

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

1624
        // Equation 16 in the above Gunow et al. 2019 paper
1625
        source_regions_.scalar_flux_new(sr, g) =
22,000✔
1626
          (phi_new - D * phi_old) / (1.0 - D);
22,000✔
1627
      }
1628
    }
1629
  }
1630
}
1631

1632
// Determines the base source region index (i.e., a material filled cell
1633
// instance) that corresponds to a particular location in the geometry. Requires
1634
// that the "gs" object passed in has already been initialized and has called
1635
// find_cell etc.
1636
int64_t FlatSourceDomain::lookup_base_source_region_idx(
796,150,019✔
1637
  const GeometryState& gs) const
1638
{
1639
  int i_cell = gs.lowest_coord().cell();
796,150,019✔
1640
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
796,150,019✔
1641
  return sr;
796,150,019✔
1642
}
1643

1644
// Determines the index of the mesh (if any) that has been applied
1645
// to a particular base source region index.
1646
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
796,149,931✔
1647
{
1648
  int mesh_idx = C_NONE;
796,149,931✔
1649
  auto mesh_it = mesh_map_.find(sr);
796,149,931✔
1650
  if (mesh_it != mesh_map_.end()) {
796,149,931✔
1651
    mesh_idx = mesh_it->second;
447,966,370✔
1652
  }
1653
  return mesh_idx;
796,149,931✔
1654
}
1655

1656
// Determines the source region key that corresponds to a particular location in
1657
// the geometry. This takes into account both the base source region index as
1658
// well as the mesh bin if a mesh is applied to this source region for
1659
// subdivision.
1660
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
1,911,437✔
1661
  const GeometryState& gs) const
1662
{
1663
  int64_t sr = lookup_base_source_region_idx(gs);
1,911,437✔
1664
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
1,911,437✔
1665
  return SourceRegionKey {sr, mesh_bin};
1,911,437✔
1666
}
1667

1668
// Determines the mesh bin that corresponds to a particular base source region
1669
// index and position.
1670
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
1,911,437✔
1671
{
1672
  int mesh_idx = lookup_mesh_idx(sr);
1,911,437✔
1673
  int mesh_bin = 0;
1,911,437✔
1674
  if (mesh_idx != C_NONE) {
1,911,437✔
1675
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
1,189,837✔
1676
  }
1677
  return mesh_bin;
1,911,437✔
1678
}
1679

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