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

openmc-dev / openmc / 24201667475

09 Apr 2026 04:34PM UTC coverage: 81.306% (-0.03%) from 81.337%
24201667475

Pull #3702

github

web-flow
Merge 85e3d3a8b into 1eb368bbd
Pull Request #3702: Random Ray Kinetic Simulation Mode

18160 of 26198 branches covered (69.32%)

Branch coverage included in aggregate %.

935 of 1075 new or added lines in 20 files covered. (86.98%)

74 existing lines in 8 files now uncovered.

58936 of 68624 relevant lines covered (85.88%)

52648244.8 hits per line

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

75.76
/src/random_ray/flat_source_domain.cpp
1
#include "openmc/random_ray/flat_source_domain.h"
2
#include "openmc/random_ray/bd_utilities.h"
3

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

21
#include <cmath>
22
#include <cstdio>
23
#include <deque>
24
#include <numeric>
25

26
namespace openmc {
27

28
//==============================================================================
29
// FlatSourceDomain implementation
30
//==============================================================================
31

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

41
FlatSourceDomain::FlatSourceDomain()
942✔
42
  : negroups_(data::mg.num_energy_groups_),
942✔
43
    ndgroups_(data::mg.num_delayed_groups_)
942✔
44

45
{
46
  // Count the number of source regions, compute the cell offset
47
  // indices, and store the material type The reason for the offsets is that
48
  // some cell types may not have material fills, and therefore do not
49
  // produce FSRs. Thus, we cannot index into the global arrays directly
50
  int base_source_regions = 0;
942✔
51
  for (const auto& c : model::cells) {
7,515✔
52
    if (c->type_ != Fill::MATERIAL) {
6,573✔
53
      source_region_offsets_.push_back(-1);
2,935✔
54
    } else {
55
      source_region_offsets_.push_back(base_source_regions);
3,638✔
56
      base_source_regions += c->n_instances();
3,638✔
57
    }
58
  }
59

60
  // Initialize source regions.
61
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
942✔
62
  source_regions_ = SourceRegionContainer(negroups_, ndgroups_, is_linear);
942✔
63

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

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

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

85
void FlatSourceDomain::batch_reset()
146,532✔
86
{
87
// Reset scalar fluxes and iteration volume tallies to zero
88
#pragma omp parallel for
79,932✔
89
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,582,005✔
90
    source_regions_.volume(sr) = 0.0;
108,515,405✔
91
    source_regions_.volume_sq(sr) = 0.0;
108,515,405✔
92
  }
93

94
#pragma omp parallel for
79,932✔
95
  for (int64_t se = 0; se < n_source_elements(); se++) {
577,546,505✔
96
    source_regions_.scalar_flux_new(se) = 0.0;
577,479,905✔
97
  }
98

99
  if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
146,532!
100
#pragma omp parallel for
71,400✔
101
    for (int64_t de = 0; de < n_delay_elements(); de++)
607,698,060✔
102
      source_regions_.precursors_new(de) = 0.0;
607,638,560✔
103
  }
104
}
146,532✔
105

106
void FlatSourceDomain::accumulate_iteration_flux()
72,001✔
107
{
108
#pragma omp parallel for
39,276✔
109
  for (int64_t se = 0; se < n_source_elements(); se++) {
284,818,525✔
110
    source_regions_.scalar_flux_final(se) +=
284,785,800✔
111
      source_regions_.scalar_flux_new(se);
284,785,800✔
112
  }
113
}
72,001✔
114

115
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
211,527,084✔
116
{
117
  // Reset all source regions to zero (important for void regions)
118
  for (int g = 0; g < negroups_; g++) {
1,451,259,774✔
119
    srh.source(g) = 0.0;
1,239,732,690✔
120
  }
121

122
  // Add scattering + fission source
123
  int material = srh.material();
211,527,084✔
124
  int temp = srh.temperature_idx();
211,527,084✔
125
  double density_mult = srh.density_mult();
211,527,084✔
126
  if (material != MATERIAL_VOID) {
211,527,084✔
127
    double inverse_k_eff = 1.0 / k_eff_;
211,085,436✔
128
    const int material_offset = (material * ntemperature_ + temp) * negroups_;
211,085,436✔
129
    const int scatter_offset =
211,085,436✔
130
      (material * ntemperature_ + temp) * negroups_ * negroups_;
131
    for (int g_out = 0; g_out < negroups_; g_out++) {
1,450,375,710✔
132
      double sigma_t = sigma_t_[material_offset + g_out] * density_mult;
1,239,290,274✔
133
      double scatter_source = 0.0;
1,239,290,274✔
134
      double fission_source = 0.0;
1,239,290,274✔
135
      double total_source = 0.0;
1,239,290,274✔
136

137
      for (int g_in = 0; g_in < negroups_; g_in++) {
2,147,483,647✔
138
        double scalar_flux = srh.scalar_flux_old(g_in);
2,147,483,647✔
139
        double sigma_s =
2,147,483,647✔
140
          sigma_s_[scatter_offset + g_out * negroups_ + g_in] * density_mult;
2,147,483,647✔
141
        double nu_sigma_f;
2,147,483,647✔
142
        double chi;
2,147,483,647✔
143
        if (settings::kinetic_simulation && !simulation::is_initial_condition) {
2,147,483,647✔
144
          nu_sigma_f = nu_p_sigma_f_[material_offset + g_in];
2,147,483,647✔
145
          chi = chi_p_[material_offset + g_out];
2,147,483,647✔
146
        } else {
147
          nu_sigma_f = nu_sigma_f_[material_offset + g_in];
2,147,483,647✔
148
          chi = chi_[material_offset + g_out];
2,147,483,647✔
149
        }
150
        nu_sigma_f *= density_mult;
2,147,483,647✔
151
        scatter_source += sigma_s * scalar_flux;
2,147,483,647✔
152
        if (settings::create_fission_neutrons) {
2,147,483,647!
153
          fission_source += nu_sigma_f * scalar_flux * chi;
2,147,483,647✔
154
        }
155
      }
156
      total_source = (scatter_source + fission_source * inverse_k_eff);
1,239,290,274✔
157

158
      // Add delayed source for kinetic simulation if delayed neutrons are
159
      // turned on
160
      if (settings::kinetic_simulation && !simulation::is_initial_condition &&
1,239,290,274!
161
          settings::create_delayed_neutrons) {
162
        const int delay_offset =
849,534,400✔
163
          (material * ntemperature_ + temp) * negroups_ * ndgroups_;
849,534,400✔
164
        double delayed_source = 0.0;
849,534,400✔
165
        for (int dg = 0; dg < ndgroups_; dg++) {
2,147,483,647✔
166
          double chi_d_lambda =
2,147,483,647✔
167
            chi_d_lambda_[delay_offset + dg * negroups_ + g_out];
2,147,483,647✔
168
          double precursors = srh.precursors_old(dg);
2,147,483,647✔
169
          delayed_source += chi_d_lambda * precursors;
2,147,483,647✔
170
        }
171
        total_source += delayed_source;
849,534,400✔
172
      }
173
      srh.source(g_out) = total_source / sigma_t;
1,239,290,274✔
174
    }
175
  }
176

177
  // Add external source if in fixed source mode
178
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
211,527,084✔
179
    for (int g = 0; g < negroups_; g++) {
89,961,638✔
180
      srh.source(g) += srh.external_source(g);
46,416,841✔
181
    }
182
  }
183
}
211,527,084✔
184

185
// Compute new estimate of scattering + fission sources in each source region
186
// based on the flux estimate from the previous iteration.
187
void FlatSourceDomain::update_all_neutron_sources()
146,532✔
188
{
189
  simulation::time_update_src.start();
146,532✔
190

191
#pragma omp parallel for
79,932✔
192
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,582,005✔
193
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
108,515,405✔
194
    update_single_neutron_source(srh);
108,515,405✔
195
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
108,515,405✔
196
      if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
54,340,000✔
197
        compute_single_phi_prime(srh);
54,210,000✔
198
      else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
130,000!
199
        compute_single_T1(srh);
130,000✔
200
    }
201
  }
202

203
  simulation::time_update_src.stop();
146,532✔
204
}
146,532✔
205

206
// Normalizes flux and updates simulation-averaged volume estimate
207
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
138,997✔
208
  double total_active_distance_per_iteration)
209
{
210
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
138,997✔
211
  double volume_normalization_factor =
138,997✔
212
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
138,997✔
213

214
// Normalize scalar flux to total distance travelled by all rays this
215
// iteration
216
#pragma omp parallel for
75,822✔
217
  for (int64_t se = 0; se < n_source_elements(); se++) {
563,545,945✔
218
    source_regions_.scalar_flux_new(se) *= normalization_factor;
563,482,770✔
219
  }
220

221
// Accumulate cell-wise ray length tallies collected this iteration, then
222
// update the simulation-averaged cell-wise volume estimates
223
#pragma omp parallel for
75,822✔
224
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
96,180,325✔
225
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
96,117,150✔
226
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
96,117,150✔
227
    source_regions_.volume_naive(sr) =
96,117,150✔
228
      source_regions_.volume(sr) * normalization_factor;
96,117,150✔
229
    source_regions_.volume_sq(sr) =
96,117,150✔
230
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
96,117,150✔
231
    source_regions_.volume(sr) =
96,117,150✔
232
      source_regions_.volume_t(sr) * volume_normalization_factor;
96,117,150✔
233
  }
234
}
138,997✔
235

236
void FlatSourceDomain::set_flux_to_flux_plus_source(
1,233,684,941✔
237
  int64_t sr, double volume, int g)
238
{
239
  int material = source_regions_.material(sr);
1,233,684,941✔
240
  int temp = source_regions_.temperature_idx(sr);
1,233,684,941✔
241
  if (material == MATERIAL_VOID) {
1,233,684,941✔
242
    source_regions_.scalar_flux_new(sr, g) /= volume;
882,416!
243
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
882,416!
244
      source_regions_.scalar_flux_new(sr, g) +=
882,416✔
245
        0.5f * source_regions_.external_source(sr, g) *
882,416✔
246
        source_regions_.volume_sq(sr);
882,416✔
247
    }
248
  } else {
249
    double sigma_t =
1,232,802,525✔
250
      sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
1,232,802,525✔
251
      source_regions_.density_mult(sr);
1,232,802,525✔
252
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
1,232,802,525✔
253
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
1,232,802,525✔
254
  }
255
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,233,684,941✔
256
    double inverse_vbar =
849,260,280✔
257
      inverse_vbar_[(material * ntemperature_ + temp) * negroups_ + g];
849,260,280✔
258
    double scalar_flux_rhs_bd = source_regions_.scalar_flux_rhs_bd(sr, g);
849,260,280✔
259
    double A0 =
849,260,280✔
260
      (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
849,260,280✔
261

262
    // TODO: Add support for expicit void regions
263
    double sigma_t =
849,260,280✔
264
      sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
849,260,280✔
265
      source_regions_.density_mult(sr);
849,260,280✔
266
    source_regions_.scalar_flux_new(sr, g) -=
849,260,280✔
267
      scalar_flux_rhs_bd * inverse_vbar / sigma_t;
849,260,280✔
268
    source_regions_.scalar_flux_new(sr, g) /= 1 + A0 * inverse_vbar / sigma_t;
849,260,280✔
269
  }
270
}
1,233,684,941✔
271

272
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,542✔
273
{
274
  source_regions_.scalar_flux_new(sr, g) =
1,542✔
275
    source_regions_.scalar_flux_old(sr, g);
1,542✔
276
}
1,542✔
277

278
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
9,309,159✔
279
{
280
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
9,309,159✔
281
}
9,309,159✔
282

283
// Combine transport flux contributions and flat source contributions from the
284
// previous iteration to generate this iteration's estimate of scalar flux.
285
int64_t FlatSourceDomain::add_source_to_scalar_flux()
146,532✔
286
{
287
  int64_t n_hits = 0;
146,532✔
288
  double inverse_batch = 1.0 / simulation::current_batch;
146,532✔
289

290
#pragma omp parallel for reduction(+ : n_hits)
79,932✔
291
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
109,642,915✔
292

293
    double volume_simulation_avg = source_regions_.volume(sr);
109,576,315✔
294
    double volume_iteration = source_regions_.volume_naive(sr);
109,576,315✔
295

296
    // Increment the number of hits if cell was hit this iteration
297
    if (volume_iteration) {
109,576,315✔
298
      n_hits++;
105,492,260✔
299
    }
300

301
    // Set the SR to small status if its expected number of hits
302
    // per iteration is less than 1.5
303
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
109,576,315✔
304
      source_regions_.is_small(sr) = 1;
6,813,175✔
305
    } else {
306
      source_regions_.is_small(sr) = 0;
102,763,140✔
307
    }
308

309
    // The volume treatment depends on the volume estimator type
310
    // and whether or not an external source is present in the cell.
311
    double volume;
109,576,315✔
312
    switch (volume_estimator_) {
109,576,315!
313
    case RandomRayVolumeEstimator::NAIVE:
314
      volume = volume_iteration;
315
      break;
316
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
317
      volume = volume_simulation_avg;
432,000✔
318
      break;
432,000✔
319
    case RandomRayVolumeEstimator::HYBRID:
108,366,715✔
320
      if (source_regions_.external_source_present(sr) ||
108,366,715✔
321
          source_regions_.is_small(sr)) {
104,176,815✔
322
        volume = volume_iteration;
323
      } else {
324
        volume = volume_simulation_avg;
325
      }
326
      break;
327
    default:
328
      fatal_error("Invalid volume estimator type");
329
    }
330

331
    for (int g = 0; g < negroups_; g++) {
688,513,850✔
332
      // There are three scenarios we need to consider:
333
      if (volume_iteration > 0.0) {
578,937,535✔
334
        // 1. If the FSR was hit this iteration, then the new flux is equal to
335
        // the flat source from the previous iteration plus the contributions
336
        // from rays passing through the source region (computed during the
337
        // transport sweep)
338
        set_flux_to_flux_plus_source(sr, volume, g);
574,703,930✔
339
      } else if (volume_simulation_avg > 0.0) {
4,233,605✔
340
        // 2. If the FSR was not hit this iteration, but has been hit some
341
        // previous iteration, then we need to make a choice about what
342
        // to do. Naively we will usually want to set the flux to be equal
343
        // to the reduced source. However, in fixed source problems where
344
        // there is a strong external source present in the cell, and where
345
        // the cell has a very low cross section, this approximation will
346
        // cause a huge upward bias in the flux estimate of the cell (in these
347
        // conditions, the flux estimate can be orders of magnitude too large).
348
        // Thus, to avoid this bias, if any external source is present
349
        // in the cell we will use the previous iteration's flux estimate. This
350
        // injects a small degree of correlation into the simulation, but this
351
        // is going to be trivial when the miss rate is a few percent or less.
352
        if (source_regions_.external_source_present(sr)) {
4,232,755✔
353
          set_flux_to_old_flux(sr, g);
1,320✔
354
        } else {
355
          set_flux_to_source(sr, g);
4,231,435✔
356
        }
357
      }
358
      // Halt if NaN implosion is detected
359
      if (!std::isfinite(source_regions_.scalar_flux_new(sr, g))) {
578,937,535!
360
        fatal_error("A source region scalar flux is not finite. "
361
                    "This indicates a numerical instability in the "
362
                    "simulation. Consider increasing ray density or adjusting "
363
                    "the source region mesh.");
364
      }
365
    }
366
  }
367

368
  // Return the number of source regions that were hit this iteration
369
  return n_hits;
146,532✔
370
}
371

372
// Generates new estimate of k_eff based on the differences between this
373
// iteration's estimate of the scalar flux and the last iteration's estimate.
374
void FlatSourceDomain::compute_k_eff()
43,010✔
375
{
376
  double fission_rate_old = 0;
43,010✔
377
  double fission_rate_new = 0;
43,010✔
378

379
  // Vector for gathering fission source terms for Shannon entropy calculation
380
  vector<float> p(n_source_regions(), 0.0f);
43,010✔
381

382
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
23,460✔
383
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,141,720✔
384

385
    // If simulation averaged volume is zero, don't include this cell
386
    double volume = source_regions_.volume(sr);
22,122,170✔
387
    if (volume == 0.0) {
22,122,170✔
388
      continue;
20✔
389
    }
390

391
    int material = source_regions_.material(sr);
22,122,150!
392
    int temp = source_regions_.temperature_idx(sr);
22,122,150!
393
    if (material == MATERIAL_VOID) {
22,122,150!
394
      continue;
395
    }
396

397
    double sr_fission_source_old = 0;
398
    double sr_fission_source_new = 0;
399

400
    for (int g = 0; g < negroups_; g++) {
179,183,400✔
401
      double nu_sigma_f =
157,061,250✔
402
        nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
157,061,250✔
403
        source_regions_.density_mult(sr);
157,061,250✔
404
      sr_fission_source_old +=
157,061,250✔
405
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
157,061,250✔
406
      sr_fission_source_new +=
157,061,250✔
407
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
157,061,250✔
408
    }
409

410
    // Compute total fission rates in FSR
411
    sr_fission_source_old *= volume;
22,122,150✔
412
    sr_fission_source_new *= volume;
22,122,150✔
413

414
    // Accumulate totals
415
    fission_rate_old += sr_fission_source_old;
22,122,150✔
416
    fission_rate_new += sr_fission_source_new;
22,122,150✔
417

418
    // Store total fission rate in the FSR for Shannon calculation
419
    p[sr] = sr_fission_source_new;
22,122,150✔
420
  }
421

422
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
43,010✔
423

424
  double H = 0.0;
43,010✔
425
  // defining an inverse sum for better performance
426
  double inverse_sum = 1 / fission_rate_new;
43,010✔
427

428
#pragma omp parallel for reduction(+ : H)
23,460✔
429
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,141,720✔
430
    // Only if FSR has non-negative and non-zero fission source
431
    if (p[sr] > 0.0f) {
22,122,170✔
432
      // Normalize to total weight of bank sites. p_i for better performance
433
      float p_i = p[sr] * inverse_sum;
10,180,155✔
434
      // Sum values to obtain Shannon entropy.
435
      H -= p_i * std::log2(p_i);
10,180,155✔
436
    }
437
  }
438

439
  // Adds entropy value to shared entropy vector in openmc namespace.
440
  simulation::entropy.push_back(H);
43,010✔
441

442
  fission_rate_ = fission_rate_new;
43,010✔
443
  k_eff_ = k_eff_new;
43,010✔
444
}
43,010✔
445

446
// This function is responsible for generating a mapping between random
447
// ray flat source regions (cell instances) and tally bins. The mapping
448
// takes the form of a "TallyTask" object, which accounts for one single
449
// score being applied to a single tally. Thus, a single source region
450
// may have anywhere from zero to many tally tasks associated with it ---
451
// meaning that the global "tally_task" data structure is in 2D. The outer
452
// dimension corresponds to the source element (i.e., each entry corresponds
453
// to a specific energy group within a specific source region), and the
454
// inner dimension corresponds to the tallying task itself. Mechanically,
455
// the mapping between FSRs and spatial filters is done by considering
456
// the location of a single known ray midpoint that passed through the
457
// FSR. I.e., during transport, the first ray to pass through a given FSR
458
// will write down its midpoint for use with this function. This is a cheap
459
// and easy way of mapping FSRs to spatial tally filters, but comes with
460
// the downside of adding the restriction that spatial tally filters must
461
// share boundaries with the physical geometry of the simulation (so as
462
// not to subdivide any FSR). It is acceptable for a spatial tally region
463
// to contain multiple FSRs, but not the other way around.
464

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

469
// Besides generating the mapping structure, this function also keeps track
470
// of whether or not all flat source regions have been hit yet. This is
471
// required, as there is no guarantee that all flat source regions will
472
// be hit every iteration, such that in the first few iterations some FSRs
473
// may not have a known position within them yet to facilitate mapping to
474
// spatial tally filters. However, after several iterations, if all FSRs
475
// have been hit and have had a tally map generated, then this status will
476
// be passed back to the caller to alert them that this function doesn't
477
// need to be called for the remainder of the simulation.
478

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

484
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
1,046✔
485
{
486
  openmc::simulation::time_tallies.start();
1,046✔
487

488
  // Tracks if we've generated a mapping yet for all source regions.
489
  bool all_source_regions_mapped = true;
1,046✔
490

491
// Attempt to generate mapping for all source regions
492
#pragma omp parallel for
571✔
493
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,061,385✔
494

495
    // If this source region has not been hit by a ray yet, then
496
    // we aren't going to be able to map it, so skip it.
497
    if (!source_regions_.position_recorded(sr)) {
1,060,910!
498
      all_source_regions_mapped = false;
499
      continue;
500
    }
501

502
    // A particle located at the recorded midpoint of a ray
503
    // crossing through this source region is used to estabilish
504
    // the spatial location of the source region
505
    Particle p;
1,060,910✔
506
    p.r() = source_regions_.position(sr);
1,060,910✔
507
    p.r_last() = source_regions_.position(sr);
1,060,910✔
508
    p.u() = {1.0, 0.0, 0.0};
1,060,910✔
509
    bool found = exhaustive_find_cell(p);
1,060,910✔
510

511
    // Loop over energy groups (so as to support energy filters)
512
    for (int g = 0; g < negroups_; g++) {
2,518,540✔
513

514
      // Set particle to the current energy
515
      p.g() = g;
1,457,630✔
516
      p.g_last() = g;
1,457,630✔
517
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,457,630!
518
      p.E_last() = p.E();
1,457,630✔
519

520
      int64_t source_element = sr * negroups_ + g;
1,457,630✔
521

522
      // If this task has already been populated, we don't need to do
523
      // it again.
524
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,457,630!
525
        continue;
526
      }
527

528
      // Loop over all active tallies. This logic is essentially identical
529
      // to what happens when scanning for applicable tallies during
530
      // MC transport.
531
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
5,164,420✔
532
        Tally& tally {*model::tallies[i_tally]};
3,706,790✔
533

534
        // Initialize an iterator over valid filter bin combinations.
535
        // If there are no valid combinations, use a continue statement
536
        // to ensure we skip the assume_separate break below.
537
        auto filter_iter = FilterBinIter(tally, p);
3,706,790✔
538
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,706,790✔
539
        if (filter_iter == end)
3,706,790✔
540
          continue;
2,001,680✔
541

542
        // Loop over filter bins.
543
        for (; filter_iter != end; ++filter_iter) {
5,115,330✔
544
          auto filter_index = filter_iter.index_;
545
          auto filter_weight = filter_iter.weight_;
3,957,060✔
546

547
          // Loop over scores
548
          for (int score = 0; score < tally.scores_.size(); score++) {
3,957,060✔
549
            auto score_bin = tally.scores_[score];
2,441,230✔
550
            // Skip precursor score. These must be scored by delay group via
551
            // tally_delay_task.
552
            if (score_bin == SCORE_PRECURSORS)
2,441,230✔
553
              break;
554
            // If a valid tally, filter, and score combination has been found,
555
            // then add it to the list of tally tasks for this source element.
556
            TallyTask task(i_tally, filter_index, score, score_bin);
2,251,950✔
557
            source_regions_.tally_task(sr, g).push_back(task);
2,251,950✔
558

559
            // Also add this task to the list of volume tasks for this source
560
            // region.
561
            source_regions_.volume_task(sr).insert(task);
2,251,950✔
562
          }
563
        }
564
      }
565
      // Reset all the filter matches for the next tally event.
566
      for (auto& match : p.filter_matches())
5,816,160✔
567
        match.bins_present_ = false;
4,358,530✔
568
    }
569
    // Loop over delayed groups (so as to support tallying delayed quantities)
570
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,060,910!
571
      for (int dg = 0; dg < ndgroups_; dg++) {
272,480✔
572

573
        // Set particle to the current delay group
574
        p.delayed_group() = dg;
241,280✔
575

576
        int64_t delay_element = sr * ndgroups_ + dg;
241,280✔
577

578
        // If this task has already been populated, we don't need to do
579
        // it again.
580
        if (source_regions_.tally_delay_task(sr, dg).size() > 0) {
241,280!
581
          continue;
582
        }
583

584
        // Loop over all active tallies. This logic is essentially identical
585
        // to what happens when scanning for applicable tallies during
586
        // MC transport.
587
        // Loop over all active tallies. This logic is essentially identical
588
        for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
673,920✔
589
          Tally& tally {*model::tallies[i_tally]};
432,640✔
590

591
          // Initialize an iterator over valid filter bin combinations.
592
          // If there are no valid combinations, use a continue statement
593
          // to ensure we skip the assume_separate break below.
594
          auto filter_iter = FilterBinIter(tally, p);
432,640✔
595
          auto end = FilterBinIter(tally, true, &p.filter_matches());
432,640✔
596
          if (filter_iter == end)
432,640!
597
            continue;
598

599
          // Loop over filter bins.
600
          for (; filter_iter != end; ++filter_iter) {
1,297,920✔
601
            auto filter_index = filter_iter.index_;
602
            auto filter_weight = filter_iter.weight_;
648,960✔
603

604
            // Loop over scores
605
            for (int score = 0; score < tally.scores_.size(); score++) {
648,960✔
606
              auto score_bin = tally.scores_[score];
432,640✔
607
              // We only want to score precursors
608
              if (score_bin != SCORE_PRECURSORS)
432,640✔
609
                break;
610
              // If a valid tally, filter, and score combination has been found,
611
              // then add it to the list of tally tasks for this source element.
612
              TallyTask task(i_tally, filter_index + dg, score, score_bin);
216,320✔
613
              source_regions_.tally_delay_task(sr, dg).push_back(task);
216,320✔
614

615
              // Also add this task to the list of volume tasks for this source
616
              // region.
617
              source_regions_.volume_task(sr).insert(task);
216,320✔
618
            }
619
          }
620
        }
621
        // Reset all the filter matches for the next tally event.
622
        for (auto& match : p.filter_matches())
882,560✔
623
          match.bins_present_ = false;
641,280✔
624
      }
625
    }
626
  }
1,060,910✔
627
  openmc::simulation::time_tallies.stop();
1,046✔
628

629
  mapped_all_tallies_ = all_source_regions_mapped;
1,046✔
630
}
1,046✔
631

632
// Set the volume accumulators to zero for all tallies
633
void FlatSourceDomain::reset_tally_volumes()
72,001✔
634
{
635
  if (volume_normalized_flux_tallies_) {
72,001✔
636
#pragma omp parallel for
36,690✔
637
    for (int i = 0; i < tally_volumes_.size(); i++) {
92,300✔
638
      auto& tensor = tally_volumes_[i];
61,725✔
639
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
123,450✔
640
    }
641
  }
642
}
72,001✔
643

644
// In fixed source mode, due to the way that volumetric fixed sources are
645
// converted and applied as volumetric sources in one or more source regions,
646
// we need to perform an additional normalization step to ensure that the
647
// reported scalar fluxes are in units per source neutron. This allows for
648
// direct comparison of reported tallies to Monte Carlo flux results.
649
// This factor needs to be computed at each iteration, as it is based on the
650
// volume estimate of each FSR, which improves over the course of the
651
// simulation
652
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
73,019✔
653
{
654
  // Eigenvalue mode normalization
655
  if (settings::run_mode == RunMode::EIGENVALUE) {
73,019✔
656
    // Normalize fluxes by total number of fission neutrons produced. This
657
    // ensures consistent scaling of the eigenvector such that its magnitude is
658
    // comparable to the eigenvector produced by the Monte Carlo solver.
659
    // Multiplying by the eigenvalue is unintuitive, but it is necessary.
660
    // If the eigenvalue is 1.2, per starting source neutron, you will
661
    // generate 1.2 neutrons. Thus if we normalize to generating only ONE
662
    // neutron in total for the whole domain, then we don't actually have enough
663
    // flux to generate the required 1.2 neutrons. We only know the flux
664
    // required to generate 1 neutron (which would have required less than one
665
    // starting neutron). Thus, you have to scale the flux up by the eigenvalue
666
    // such that 1.2 neutrons are generated, so as to be consistent with the
667
    // bookkeeping in MC which is all done per starting source neutron (not per
668
    // neutron produced).
669
    return k_eff_ / (fission_rate_ * simulation_volume_);
68,531✔
670
  }
671

672
  // If we are in adjoint mode of a fixed source problem, the external
673
  // source is already normalized, such that all resulting fluxes are
674
  // also normalized.
675
  if (adjoint_) {
4,488✔
676
    return 1.0;
677
  }
678

679
  // Fixed source mode normalization
680

681
  // Step 1 is to sum over all source regions and energy groups to get the
682
  // total external source strength in the simulation.
683
  double simulation_external_source_strength = 0.0;
2,257✔
684
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,257✔
685
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,679,287✔
686
    int material = source_regions_.material(sr);
14,677,450✔
687
    int temp = source_regions_.temperature_idx(sr);
14,677,450✔
688
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,677,450✔
689
    for (int g = 0; g < negroups_; g++) {
29,897,780✔
690
      // For non-void regions, we store the external source pre-divided by
691
      // sigma_t. We need to multiply non-void regions back up by sigma_t
692
      // to get the total source strength in the expected units.
693
      double sigma_t = 1.0;
15,220,330✔
694
      if (material != MATERIAL_VOID) {
15,220,330✔
695
        sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
15,010,090✔
696
                  source_regions_.density_mult(sr);
15,010,090✔
697
      }
698
      simulation_external_source_strength +=
15,220,330✔
699
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,220,330✔
700
    }
701
  }
702

703
  // Step 2 is to determine the total user-specified external source strength
704
  double user_external_source_strength = 0.0;
4,094✔
705
  for (auto& ext_source : model::external_sources) {
8,188✔
706
    user_external_source_strength += ext_source->strength();
4,094✔
707
  }
708

709
  // The correction factor is the ratio of the user-specified external source
710
  // strength to the simulation external source strength.
711
  double source_normalization_factor =
4,094✔
712
    user_external_source_strength / simulation_external_source_strength;
713

714
  return source_normalization_factor;
4,094✔
715
}
716

717
// Tallying in random ray is not done directly during transport, rather,
718
// it is done only once after each power iteration. This is made possible
719
// by way of a mapping data structure that relates spatial source regions
720
// (FSRs) to tally/filter/score combinations. The mechanism by which the
721
// mapping is done (and the limitations incurred) is documented in the
722
// "convert_source_regions_to_tallies()" function comments above. The present
723
// tally function simply traverses the mapping data structure and executes
724
// the scoring operations to OpenMC's native tally result arrays.
725

726
// TODO: Add support for prompt and delayed nu fission tallies
727
void FlatSourceDomain::random_ray_tally()
72,001✔
728
{
729
  openmc::simulation::time_tallies.start();
72,001✔
730

731
  // Reset our tally volumes to zero
732
  reset_tally_volumes();
72,001✔
733

734
  double source_normalization_factor =
72,001✔
735
    compute_fixed_source_normalization_factor();
72,001✔
736

737
// We loop over all source regions and energy groups. For each
738
// element, we check if there are any scores needed and apply
739
// them.
740
#pragma omp parallel for
39,276✔
741
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
53,381,125✔
742
    // The fsr.volume_ is the unitless fractional simulation averaged volume
743
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
744
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
745
    // entire global simulation domain (as defined by the ray source box).
746
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
747
    // its fraction of the total volume by the total volume. Not important in
748
    // eigenvalue solves, but useful in fixed source solves for returning the
749
    // flux shape with a magnitude that makes sense relative to the fixed
750
    // source strength.
751
    double volume = source_regions_.volume(sr) * simulation_volume_;
53,348,400✔
752

753
    double material = source_regions_.material(sr);
53,348,400✔
754
    int temp = source_regions_.temperature_idx(sr);
53,348,400✔
755
    double density_mult = source_regions_.density_mult(sr);
53,348,400✔
756
    for (int g = 0; g < negroups_; g++) {
338,134,200✔
757
      double flux =
284,785,800✔
758
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
284,785,800✔
759

760
      // Determine numerical score value
761
      for (auto& task : source_regions_.tally_task(sr, g)) {
1,103,439,400✔
762
        double score = 0.0;
818,653,600✔
763
        switch (task.score_type) {
818,653,600!
764

765
        case SCORE_FLUX:
284,821,800✔
766
          score = flux * volume;
284,821,800✔
767
          break;
284,821,800✔
768

769
        case SCORE_TOTAL:
770
          if (material != MATERIAL_VOID) {
×
771
            score =
772
              flux * volume *
773
              sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
774
              density_mult;
775
          }
776
          break;
777

778
        case SCORE_FISSION:
266,915,600✔
779
          if (material != MATERIAL_VOID) {
266,915,600!
780
            score =
266,915,600✔
781
              flux * volume *
266,915,600✔
782
              sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
266,915,600✔
783
              density_mult;
784
          }
785
          break;
786

787
        case SCORE_NU_FISSION:
266,915,600✔
788
          if (material != MATERIAL_VOID) {
266,915,600!
789
            score =
266,915,600✔
790
              flux * volume *
266,915,600✔
791
              nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] *
266,915,600✔
792
              density_mult;
793
          }
794
          break;
795

796
        case SCORE_EVENTS:
797
          score = 1.0;
798
          break;
799

800
        case SCORE_KAPPA_FISSION:
600✔
801
          score =
600✔
802
            flux * volume *
600✔
803
            kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] *
600✔
804
            density_mult;
805
          break;
600✔
806

807
        case SCORE_PRECURSORS:
808
          // Score precursors in tally_delay_tasks
809
          if (settings::kinetic_simulation &&
×
810
              settings::create_delayed_neutrons) {
811
            break;
812
          } else {
813
            fatal_error("Invalid score specified in tallies.xml. Precursors "
814
                        "are only supported in random ray mode for kinetic "
815
                        "simulations when delayed neutrons are turned on.");
816
          }
817

818
        default:
819
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
820
                      "total, fission, nu-fission, kappa-fission, and events "
821
                      "are supported in random ray mode (precursors are "
822
                      "supported in kinetic simulations when delayed neutrons "
823
                      "are turned on).");
824
          break;
818,653,600✔
825
        }
826
        // Apply score to the appropriate tally bin
827
        Tally& tally {*model::tallies[task.tally_idx]};
818,653,600✔
828
#pragma omp atomic
829
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
818,653,600✔
830
          score;
831
      }
832
    }
833
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
53,348,400!
834
      for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
835
        // Determine numerical score value
836
        for (auto& task : source_regions_.tally_delay_task(sr, dg)) {
606,569,600✔
837
          double score = 0.0;
302,848,000✔
838
          switch (task.score_type) {
302,848,000!
839

840
          // Certain scores already tallied
841
          case SCORE_FLUX:
842
          case SCORE_TOTAL:
843
          case SCORE_FISSION:
844
          case SCORE_NU_FISSION:
845
          case SCORE_EVENTS:
846
            break;
847

848
          case SCORE_PRECURSORS:
302,848,000✔
849
            score = source_regions_.precursors_new(sr, dg) *
302,848,000✔
850
                    source_normalization_factor * volume;
851
            break;
302,848,000✔
852

853
          default:
854
            fatal_error(
855
              "Invalid score specified in tallies.xml. Only flux, "
856
              "total, fission, nu-fission, and events are supported in "
857
              "random ray mode (precursors are supported in kinetic "
858
              "simulations when delayed neutrons are turned on).");
859
            break;
302,848,000✔
860
          }
861

862
          // Apply score to the appropriate tally bin
863
          Tally& tally {*model::tallies[task.tally_idx]};
302,848,000✔
864
#pragma omp atomic
865
          tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
302,848,000✔
866
            score;
867
        }
868
      }
869
    }
870

871
    // For flux and precursor tallies, the total volume of the spatial region is
872
    // needed for normalizing the tally. We store this volume in a separate
873
    // tensor. We only contribute to each volume tally bin once per FSR.
874
    if (volume_normalized_flux_tallies_) {
53,348,400✔
875
      for (const auto& task : source_regions_.volume_task(sr)) {
1,159,940,400✔
876
        if (task.score_type == SCORE_FLUX ||
1,106,885,800✔
877
            task.score_type == SCORE_PRECURSORS) {
878
#pragma omp atomic
879
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
582,768,600✔
880
            volume;
881
        }
882
      }
883
    }
884
  } // end FSR loop
885

886
  // Normalize any flux or precursor scores by the total volume of the FSRs
887
  // scoring to that bin. To do this, we loop over all tallies, and then all
888
  // filter bins, and then scores. For each score, we check the tally data
889
  // structure to see what index that score corresponds to. If that score is a
890
  // flux or precursor score, then we divide it by volume.
891
  if (volume_normalized_flux_tallies_) {
72,001✔
892
    for (int i = 0; i < model::tallies.size(); i++) {
203,060✔
893
      Tally& tally {*model::tallies[i]};
135,795✔
894
#pragma omp parallel for
74,070✔
895
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
1,729,125✔
896
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
4,169,300✔
897
          auto score_type = tally.scores_[score_idx];
2,501,900✔
898
          if (score_type == SCORE_FLUX || score_type == SCORE_PRECURSORS) {
2,501,900✔
899
            double vol = tally_volumes_[i](bin, score_idx);
1,667,400!
900
            if (vol > 0.0) {
1,667,400!
901
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
1,667,400✔
902
            }
903
          }
904
        }
905
      }
906
    }
907
  }
908

909
  openmc::simulation::time_tallies.stop();
72,001✔
910
}
72,001✔
911

912
double FlatSourceDomain::evaluate_flux_at_point(
×
913
  Position r, int64_t sr, int g) const
914
{
915
  return source_regions_.scalar_flux_final(sr, g) /
×
916
         (settings::n_batches - settings::n_inactive);
×
917
}
918

919
// Outputs all basic material, FSR ID, multigroup flux, and
920
// fission source data to .vtk file that can be directly
921
// loaded and displayed by Paraview. Note that .vtk binary
922
// files require big endian byte ordering, so endianness
923
// is checked and flipped if necessary.
924
void FlatSourceDomain::output_to_vtk() const
×
925
{
926
  // Rename .h5 plot filename(s) to .vtk filenames
927
  for (int p = 0; p < model::plots.size(); p++) {
×
928
    PlottableInterface* plot = model::plots[p].get();
×
929
    plot->path_plot() =
×
930
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
931
  }
932

933
  // Print header information
934
  print_plot();
×
935

936
  // Outer loop over plots
937
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
938

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

942
    // Random ray plots only support voxel plots
943
    if (!openmc_plot) {
×
944
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
945
                          "is allowed in random ray mode.",
946
        plt));
947
      continue;
×
948
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
949
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
950
                          "is allowed in random ray mode.",
951
        plt));
952
      continue;
×
953
    }
954

955
    int Nx = openmc_plot->pixels_[0];
×
956
    int Ny = openmc_plot->pixels_[1];
×
957
    int Nz = openmc_plot->pixels_[2];
×
958
    Position origin = openmc_plot->origin_;
×
959
    Position width = openmc_plot->width_;
×
960
    Position ll = origin - width / 2.0;
×
961
    double x_delta = width.x / Nx;
×
962
    double y_delta = width.y / Ny;
×
963
    double z_delta = width.z / Nz;
×
964
    std::string filename = openmc_plot->path_plot();
×
965

966
    // Perform sanity checks on file size
967
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
968
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
969
      openmc_plot->id(), filename, bytes / 1.0e6);
×
970
    if (bytes / 1.0e9 > 1.0) {
×
971
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
972
              "slow.");
973
    } else if (bytes / 1.0e9 > 100.0) {
×
974
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
975
    }
976

977
    // Relate voxel spatial locations to random ray source regions
978
    vector<int> voxel_indices(Nx * Ny * Nz);
×
979
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
980
    vector<double> weight_windows(Nx * Ny * Nz);
×
981
    float min_weight = 1e20;
×
982
#pragma omp parallel for collapse(3) reduction(min : min_weight)
983
    for (int z = 0; z < Nz; z++) {
×
984
      for (int y = 0; y < Ny; y++) {
×
985
        for (int x = 0; x < Nx; x++) {
×
986
          Position sample;
987
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
988
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
989
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
990
          Particle p;
×
991
          p.r() = sample;
×
992
          p.r_last() = sample;
993
          p.E() = 1.0;
994
          p.E_last() = 1.0;
995
          p.u() = {1.0, 0.0, 0.0};
×
996

997
          bool found = exhaustive_find_cell(p);
×
998
          if (!found) {
×
999
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
1000
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
1001
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
1002
            continue;
1003
          }
1004

1005
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
1006
          int64_t sr = -1;
1007
          auto it = source_region_map_.find(sr_key);
×
1008
          if (it != source_region_map_.end()) {
×
1009
            sr = it->second;
1010
          }
1011

1012
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
×
1013
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
1014

1015
          if (variance_reduction::weight_windows.size() == 1) {
×
1016
            auto [ww_found, ww] =
1017
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
1018
            float weight = ww.lower_weight;
1019
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
×
1020
            if (weight < min_weight)
×
1021
              min_weight = weight;
1022
          }
1023
        }
1024
      }
1025
    }
1026

1027
    double source_normalization_factor =
×
1028
      compute_fixed_source_normalization_factor();
×
1029

1030
    // Open file for writing
1031
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
1032

1033
    // Write vtk metadata
1034
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
1035
    std::fprintf(plot, "Dataset File\n");
×
1036
    std::fprintf(plot, "BINARY\n");
×
1037
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
1038
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
1039
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
1040
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
1041
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
1042

1043
    int64_t num_neg = 0;
1044
    int64_t num_samples = 0;
1045
    float min_flux = 0.0;
1046
    float max_flux = -1.0e20;
1047
    // Plot multigroup flux data
1048
    for (int g = 0; g < negroups_; g++) {
×
1049
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
1050
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1051
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1052
        int64_t fsr = voxel_indices[i];
×
1053
        int64_t source_element = fsr * negroups_ + g;
×
1054
        float flux = 0;
×
1055
        if (fsr >= 0) {
×
1056
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1057
          if (flux < 0.0)
×
1058
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
1059
              voxel_positions[i], fsr, g);
×
1060
        }
1061
        if (flux < 0.0) {
×
1062
          num_neg++;
×
1063
          if (flux < min_flux) {
×
1064
            min_flux = flux;
×
1065
          }
1066
        }
1067
        if (flux > max_flux)
×
1068
          max_flux = flux;
×
1069
        num_samples++;
×
1070
        flux = convert_to_big_endian<float>(flux);
×
1071
        std::fwrite(&flux, sizeof(float), 1, plot);
×
1072
      }
1073
    }
1074

1075
    // Slightly negative fluxes can be normal when sampling corners of linear
1076
    // source regions. However, very common and high magnitude negative fluxes
1077
    // may indicate numerical instability.
1078
    if (num_neg > 0) {
×
1079
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
1080
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
1081
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
1082
    }
1083

1084
    // Plot FSRs
1085
    std::fprintf(plot, "SCALARS FSRs float\n");
×
1086
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1087
    for (int fsr : voxel_indices) {
×
1088
      float value = future_prn(10, fsr);
×
1089
      value = convert_to_big_endian<float>(value);
×
1090
      std::fwrite(&value, sizeof(float), 1, plot);
×
1091
    }
1092

1093
    // Plot Materials
1094
    std::fprintf(plot, "SCALARS Materials int\n");
×
1095
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1096
    for (int fsr : voxel_indices) {
×
1097
      int mat = -1;
×
1098
      if (fsr >= 0)
×
1099
        mat = source_regions_.material(fsr);
×
1100
      mat = convert_to_big_endian<int>(mat);
×
1101
      std::fwrite(&mat, sizeof(int), 1, plot);
×
1102
    }
1103

1104
    // Plot fission source
1105
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
1106
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
1107
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1108
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1109
        int64_t fsr = voxel_indices[i];
×
1110
        float total_fission = 0.0;
×
1111
        if (fsr >= 0) {
×
1112
          int mat = source_regions_.material(fsr);
×
1113
          int temp = source_regions_.temperature_idx(fsr);
×
1114
          if (mat != MATERIAL_VOID) {
×
1115
            for (int g = 0; g < negroups_; g++) {
×
1116
              int64_t source_element = fsr * negroups_ + g;
×
1117
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1118
              double sigma_f =
×
1119
                sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
1120
                source_regions_.density_mult(fsr);
×
1121
              total_fission += sigma_f * flux;
×
1122
            }
1123
          }
1124
        }
1125
        total_fission = convert_to_big_endian<float>(total_fission);
×
1126
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
1127
      }
1128
    } else {
1129
      std::fprintf(plot, "SCALARS external_source float\n");
×
1130
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1131
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1132
        int64_t fsr = voxel_indices[i];
×
1133
        int mat = source_regions_.material(fsr);
×
1134
        int temp = source_regions_.temperature_idx(fsr);
×
1135
        float total_external = 0.0f;
×
1136
        if (fsr >= 0) {
×
1137
          for (int g = 0; g < negroups_; g++) {
×
1138
            // External sources are already divided by sigma_t, so we need to
1139
            // multiply it back to get the true external source.
1140
            double sigma_t = 1.0;
×
1141
            if (mat != MATERIAL_VOID) {
×
1142
              sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] *
×
1143
                        source_regions_.density_mult(fsr);
×
1144
            }
1145
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
1146
          }
1147
        }
1148
        total_external = convert_to_big_endian<float>(total_external);
×
1149
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
1150
      }
1151
    }
1152

1153
    // Plot weight window data
1154
    if (variance_reduction::weight_windows.size() == 1) {
×
1155
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
1156
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1157
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1158
        float weight = weight_windows[i];
×
1159
        if (weight == 0.0)
×
1160
          weight = min_weight;
×
1161
        weight = convert_to_big_endian<float>(weight);
×
1162
        std::fwrite(&weight, sizeof(float), 1, plot);
×
1163
      }
1164
    }
1165

1166
    std::fclose(plot);
×
1167
  }
×
1168
}
×
1169

1170
void FlatSourceDomain::apply_external_source_to_source_region(
4,698✔
1171
  int src_idx, SourceRegionHandle& srh)
1172
{
1173
  auto s = model::external_sources[src_idx].get();
4,698!
1174
  auto is = dynamic_cast<IndependentSource*>(s);
4,698!
1175
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,698!
1176
  double strength_factor = is->strength();
4,698✔
1177
  const auto& discrete_energies = discrete->x();
4,698✔
1178
  const auto& discrete_probs = discrete->prob();
4,698✔
1179

1180
  srh.external_source_present() = 1;
4,698✔
1181

1182
  for (int i = 0; i < discrete_energies.size(); i++) {
9,528✔
1183
    int g = data::mg.get_group_index(discrete_energies[i]);
4,830✔
1184
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,830✔
1185
  }
1186
}
4,698✔
1187

1188
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
420✔
1189
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1190
{
1191
  Cell& cell = *model::cells[i_cell];
420!
1192

1193
  if (cell.type_ != Fill::MATERIAL)
420!
1194
    return;
1195

1196
  for (int j : instances) {
29,025✔
1197
    int cell_material_idx = cell.material(j);
28,605!
1198
    int cell_material_id;
28,605✔
1199
    if (cell_material_idx == MATERIAL_VOID) {
28,605✔
1200
      cell_material_id = MATERIAL_VOID;
1201
    } else {
1202
      cell_material_id = model::materials[cell_material_idx]->id();
28,365✔
1203
    }
1204
    if (target_material_id == C_NONE ||
28,605✔
1205
        cell_material_id == target_material_id) {
28,605✔
1206
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,805✔
1207
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,805✔
1208
    }
1209
  }
1210
}
1211

1212
void FlatSourceDomain::apply_external_source_to_cell_and_children(
450✔
1213
  int32_t i_cell, int src_idx, int32_t target_material_id)
1214
{
1215
  Cell& cell = *model::cells[i_cell];
450✔
1216

1217
  if (cell.type_ == Fill::MATERIAL) {
450✔
1218
    vector<int> instances(cell.n_instances());
420✔
1219
    std::iota(instances.begin(), instances.end(), 0);
420✔
1220
    apply_external_source_to_cell_instances(
420✔
1221
      i_cell, src_idx, target_material_id, instances);
1222
  } else if (target_material_id == C_NONE) {
450!
1223
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
×
1224
      cell.get_contained_cells(0, nullptr);
×
1225
    for (const auto& pair : cell_instance_list) {
×
1226
      int32_t i_child_cell = pair.first;
×
1227
      apply_external_source_to_cell_instances(
×
1228
        i_child_cell, src_idx, target_material_id, pair.second);
×
1229
    }
1230
  }
×
1231
}
450✔
1232

1233
void FlatSourceDomain::count_external_source_regions()
2,504✔
1234
{
1235
  n_external_source_regions_ = 0;
2,504✔
1236
#pragma omp parallel for reduction(+ : n_external_source_regions_)
1,503✔
1237
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,405,851✔
1238
    if (source_regions_.external_source_present(sr)) {
1,404,850✔
1239
      n_external_source_regions_++;
157,300✔
1240
    }
1241
  }
1242
}
2,504✔
1243

1244
void FlatSourceDomain::convert_external_sources()
406✔
1245
{
1246
  // Loop over external sources
1247
  for (int es = 0; es < model::external_sources.size(); es++) {
812✔
1248

1249
    // Extract source information
1250
    Source* s = model::external_sources[es].get();
406!
1251
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
406!
1252
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
406✔
1253
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
406✔
1254
    double strength_factor = is->strength();
406✔
1255

1256
    // If there is no domain constraint specified, then this must be a point
1257
    // source. In this case, we need to find the source region that contains the
1258
    // point source and apply or relate it to the external source.
1259
    if (is->domain_ids().size() == 0) {
406✔
1260

1261
      // Extract the point source coordinate and find the base source region at
1262
      // that point
1263
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
16!
1264
      GeometryState gs;
16✔
1265
      gs.r() = sp->r();
16✔
1266
      gs.r_last() = sp->r();
16✔
1267
      gs.u() = {1.0, 0.0, 0.0};
16✔
1268
      bool found = exhaustive_find_cell(gs);
16✔
1269
      if (!found) {
16!
1270
        fatal_error(fmt::format("Could not find cell containing external "
×
1271
                                "point source at {}",
1272
          sp->r()));
×
1273
      }
1274
      SourceRegionKey key = lookup_source_region_key(gs);
16✔
1275

1276
      // With the source region and mesh bin known, we can use the
1277
      // accompanying SourceRegionKey as a key into a map that stores the
1278
      // corresponding external source index for the point source. Notably, we
1279
      // do not actually apply the external source to any source regions here,
1280
      // as if mesh subdivision is enabled, they haven't actually been
1281
      // discovered & initilized yet. When discovered, they will read from the
1282
      // external_source_map to determine if there are any external source
1283
      // terms that should be applied.
1284
      external_point_source_map_[key].push_back(es);
16✔
1285

1286
    } else {
16✔
1287
      // If not a point source, then use the volumetric domain constraints to
1288
      // determine which source regions to apply the external source to.
1289
      if (is->domain_type() == Source::DomainType::MATERIAL) {
390✔
1290
        for (int32_t material_id : domain_ids) {
60✔
1291
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
120✔
1292
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
90✔
1293
          }
1294
        }
1295
      } else if (is->domain_type() == Source::DomainType::CELL) {
360✔
1296
        for (int32_t cell_id : domain_ids) {
30✔
1297
          int32_t i_cell = model::cell_map[cell_id];
15✔
1298
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
15✔
1299
        }
1300
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
345!
1301
        for (int32_t universe_id : domain_ids) {
690✔
1302
          int32_t i_universe = model::universe_map[universe_id];
345✔
1303
          Universe& universe = *model::universes[i_universe];
345✔
1304
          for (int32_t i_cell : universe.cells_) {
690✔
1305
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
345✔
1306
          }
1307
        }
1308
      }
1309
    }
1310
  } // End loop over external sources
1311
}
406✔
1312

1313
void FlatSourceDomain::flux_swap()
146,532✔
1314
{
1315
  source_regions_.flux_swap();
146,532✔
1316
}
146,532✔
1317

1318
void FlatSourceDomain::flatten_xs()
942✔
1319
{
1320
  // Temperature and angle indices, if using multiple temperature
1321
  // data sets and/or anisotropic data sets.
1322
  // TODO: Currently assumes we are only using single angle data.
1323
  const int a = 0;
942✔
1324

1325
  n_materials_ = data::mg.macro_xs_.size();
942✔
1326
  ntemperature_ = 1;
942✔
1327
  for (int i = 0; i < n_materials_; i++) {
3,515✔
1328
    ntemperature_ =
5,146✔
1329
      std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points());
2,588✔
1330
  }
1331

1332
  for (int i = 0; i < n_materials_; i++) {
3,515✔
1333
    auto& m = data::mg.macro_xs_[i];
2,573✔
1334
    for (int t = 0; t < ntemperature_; t++) {
5,176✔
1335
      for (int g_out = 0; g_out < negroups_; g_out++) {
19,469✔
1336
        if (m.exists_in_model && t < m.n_temperature_points()) {
16,866✔
1337
          double sigma_t =
16,701✔
1338
            m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
16,701✔
1339
          sigma_t_.push_back(sigma_t);
16,701✔
1340

1341
          if (sigma_t < MINIMUM_MACRO_XS) {
16,701✔
1342
            Material* mat = model::materials[i].get();
15✔
1343
            warning(fmt::format(
21✔
1344
              "Material \"{}\" (id: {}) has a group {} total cross section "
1345
              "({:.3e}) below the minimum threshold "
1346
              "({:.3e}). Material will be treated as pure void.",
1347
              mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
27✔
1348
          }
1349

1350
          double nu_sigma_f =
16,701✔
1351
            m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
16,701✔
1352
          nu_sigma_f_.push_back(nu_sigma_f);
16,701✔
1353

1354
          double sigma_f =
16,701✔
1355
            m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
16,701✔
1356
          sigma_f_.push_back(sigma_f);
16,701✔
1357

1358
          double chi = m.get_xs(MgxsType::CHI, g_out, &g_out, NULL, NULL, t, a);
16,701✔
1359
          if (!std::isfinite(chi)) {
16,701!
1360
            // MGXS interface may return NaN in some cases, such as when
1361
            // material is fissionable but has very small sigma_f.
UNCOV
1362
            chi = 0.0;
×
1363
          }
1364
          chi_.push_back(chi);
16,701✔
1365

1366
          double kappa_fission =
16,701✔
1367
            m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a);
16,701✔
1368
          kappa_fission_.push_back(kappa_fission);
16,701✔
1369

1370
          for (int g_in = 0; g_in < negroups_; g_in++) {
717,338✔
1371
            double sigma_s =
700,637✔
1372
              m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
700,637✔
1373
            sigma_s_.push_back(sigma_s);
700,637✔
1374
            // For transport corrected XS data, diagonal elements may be
1375
            // negative. In this case, set a flag to enable transport
1376
            // stabilization for the simulation.
1377
            if (g_out == g_in && sigma_s < 0.0)
700,637✔
1378
              is_transport_stabilization_needed_ = true;
2,415✔
1379
          }
1380
          // Prompt cross-sections for kinetic simulations
1381
          if (settings::kinetic_simulation) {
16,701✔
1382
            double chi_p =
7,890✔
1383
              m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
7,890✔
1384
            if (!std::isfinite(chi_p)) {
7,890!
1385
              // MGXS interface may return NaN in some cases, such as when
1386
              // material is fissionable but has very small sigma_f.
NEW
1387
              chi_p = 0.0;
×
1388
            }
1389
            chi_p_.push_back(chi_p);
7,890✔
1390

1391
            double inverse_vbar = m.get_xs(
7,890✔
1392
              MgxsType::INVERSE_VELOCITY, g_out, NULL, NULL, NULL, t, a);
7,890✔
1393
            inverse_vbar_.push_back(inverse_vbar);
7,890✔
1394

1395
            double nu_p_Sigma_f = m.get_xs(
7,890✔
1396
              MgxsType::PROMPT_NU_FISSION, g_out, NULL, NULL, NULL, t, a);
7,890✔
1397
            nu_p_sigma_f_.push_back(nu_p_Sigma_f);
7,890✔
1398
          }
1399
        } else {
1400
          sigma_t_.push_back(0);
165✔
1401
          nu_sigma_f_.push_back(0);
165✔
1402
          sigma_f_.push_back(0);
165✔
1403
          chi_.push_back(0);
165✔
1404
          kappa_fission_.push_back(0);
165✔
1405
          for (int g_in = 0; g_in < negroups_; g_in++) {
960✔
1406
            sigma_s_.push_back(0);
795✔
1407
          }
1408
          if (settings::kinetic_simulation) {
165!
NEW
1409
            chi_p_.push_back(0);
×
NEW
1410
            inverse_vbar_.push_back(0);
×
NEW
1411
            nu_p_sigma_f_.push_back(0);
×
1412
          }
1413
        }
1414
      }
1415
      // Delayed cross sections for time-dependent simulations
1416
      if (settings::kinetic_simulation) {
2,603✔
1417
        for (int dg = 0; dg < ndgroups_; dg++) {
3,870✔
1418
          if (m.exists_in_model) {
3,360!
1419
            double lambda =
3,360✔
1420
              m.get_xs(MgxsType::DECAY_RATE, 0, NULL, NULL, &dg, t, a);
3,360✔
1421
            lambda_.push_back(lambda);
3,360✔
1422
            for (int g_out = 0; g_out < negroups_; g_out++) {
52,800✔
1423
              double nu_d_Sigma_f = m.get_xs(
49,440✔
1424
                MgxsType::DELAYED_NU_FISSION, g_out, NULL, NULL, &dg, t, a);
49,440✔
1425
              nu_d_sigma_f_.push_back(nu_d_Sigma_f);
49,440✔
1426
              double chi_d =
49,440✔
1427
                m.get_xs(MgxsType::CHI_DELAYED, g_out, &g_out, NULL, &dg, t, a);
49,440✔
1428
              if (!std::isfinite(chi_d)) {
49,440!
1429
                // MGXS interface may return NaN in some cases, such as when
1430
                // material is fissionable but has very small sigma_f.
NEW
1431
                chi_d = 0.0;
×
1432
              }
1433
              chi_d_lambda_.push_back(chi_d * lambda);
49,440✔
1434
            }
1435
          } else {
NEW
1436
            lambda_.push_back(0);
×
NEW
1437
            for (int g_out = 0; g_out < negroups_; g_out++) {
×
NEW
1438
              nu_d_sigma_f_.push_back(0);
×
NEW
1439
              chi_d_lambda_.push_back(0);
×
1440
            }
1441
          }
1442
        }
1443
      }
1444
    }
1445
  }
1446
}
942✔
1447

1448
void FlatSourceDomain::set_adjoint_sources()
61✔
1449
{
1450
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1451
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1452
  // as this is likely a very small source region that we don't need to bother
1453
  // trying to vector particles towards. In the case of flux "being extremely
1454
  // close to zero", we define this as being a fixed fraction of the maximum
1455
  // forward flux, below which we assume the flux would be physically
1456
  // undetectable.
1457

1458
  // First, find the maximum forward flux value
1459
  double max_flux = 0.0;
61✔
1460
#pragma omp parallel for reduction(max : max_flux)
37✔
1461
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,544✔
1462
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1463
    if (flux > max_flux) {
155,520✔
1464
      max_flux = flux;
25✔
1465
    }
1466
  }
1467

1468
  // Then, compute the adjoint source for each source region
1469
#pragma omp parallel for
37✔
1470
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,544✔
1471
    for (int g = 0; g < negroups_; g++) {
311,040✔
1472
      double flux = source_regions_.scalar_flux_final(sr, g);
155,520✔
1473
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
155,520✔
1474
        source_regions_.external_source(sr, g) = 0.0;
325✔
1475
      } else {
1476
        source_regions_.external_source(sr, g) = 1.0 / flux;
155,195✔
1477
      }
1478
      if (flux > 0.0) {
155,520✔
1479
        source_regions_.external_source_present(sr) = 1;
155,195✔
1480
      }
1481
      source_regions_.scalar_flux_final(sr, g) = 0.0;
155,520✔
1482
    }
1483
  }
1484

1485
  // "Small" source regions in OpenMC are defined as those that are hit by
1486
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1487
  // very small volumes combined with a low aspect ratio, and are often
1488
  // generated when applying a source region mesh that clips the edge of a
1489
  // curved surface. As perhaps only a few rays will visit these regions over
1490
  // the entire forward simulation, the forward flux estimates are extremely
1491
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1492
  // extremely low, leading to unphysically large adjoint source terms,
1493
  // resulting in weight windows that aggressively try to drive particles
1494
  // towards these regions. To fix this, we simply filter out any "small" source
1495
  // regions from consideration. If a source region is "small", we
1496
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1497
  // flux solution, as the true total adjoint source contribution from small
1498
  // regions is likely to be negligible.
1499
#pragma omp parallel for
37✔
1500
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,544✔
1501
    if (source_regions_.is_small(sr)) {
155,520!
1502
      for (int g = 0; g < negroups_; g++) {
×
1503
        source_regions_.external_source(sr, g) = 0.0;
1504
      }
1505
      source_regions_.external_source_present(sr) = 0;
1506
    }
1507
  }
1508
  // Divide the fixed source term by sigma t (to save time when applying each
1509
  // iteration)
1510
#pragma omp parallel for
37✔
1511
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,544✔
1512
    int material = source_regions_.material(sr);
155,520!
1513
    int temp = source_regions_.temperature_idx(sr);
155,520!
1514
    if (material == MATERIAL_VOID) {
155,520!
1515
      continue;
1516
    }
1517
    for (int g = 0; g < negroups_; g++) {
311,040✔
1518
      double sigma_t =
155,520✔
1519
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
155,520✔
1520
        source_regions_.density_mult(sr);
155,520✔
1521
      source_regions_.external_source(sr, g) /= sigma_t;
155,520✔
1522
    }
1523
  }
1524
}
61✔
1525

1526
void FlatSourceDomain::transpose_scattering_matrix()
76✔
1527
{
1528
  // Transpose the inner two dimensions for each material
1529
#pragma omp parallel for
46✔
1530
  for (int m = 0; m < n_materials_; ++m) {
114✔
1531
    for (int t = 0; t < ntemperature_; t++) {
168✔
1532
      int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_;
84✔
1533
      for (int i = 0; i < negroups_; ++i) {
240✔
1534
        for (int j = i + 1; j < negroups_; ++j) {
408✔
1535
          // Calculate indices of the elements to swap
1536
          int idx1 = material_offset + i * negroups_ + j;
252✔
1537
          int idx2 = material_offset + j * negroups_ + i;
252✔
1538

1539
          // Swap the elements to transpose the matrix
1540
          std::swap(sigma_s_[idx1], sigma_s_[idx2]);
252✔
1541
        }
1542
      }
1543
    }
1544
  }
1545
}
76✔
1546

1547
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1548
{
1549
  // Ensure array is correct size
1550
  flux.resize(n_source_regions() * negroups_);
×
1551
// Serialize the final fluxes for output
1552
#pragma omp parallel for
1553
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1554
    flux[se] = source_regions_.scalar_flux_final(se);
1555
  }
1556
}
×
1557

NEW
1558
void FlatSourceDomain::serialize_final_sources(vector<double>& source)
×
1559
{
1560
  // Ensure array is correct size
NEW
1561
  source.resize(n_source_regions() * negroups_);
×
1562
  // Serialize the final sources for output
1563
#pragma omp parallel for
1564
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1565
    source[se] = source_regions_.source_final(se);
1566
  }
NEW
1567
}
×
1568

1569
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
1,607✔
1570
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1571
  bool is_target_void)
1572
{
1573
  Cell& cell = *model::cells[i_cell];
1,607!
1574
  if (cell.type_ != Fill::MATERIAL)
1,607!
1575
    return;
1576
  for (int32_t j : instances) {
173,194✔
1577
    int cell_material_idx = cell.material(j);
171,587!
1578
    int cell_material_id = (cell_material_idx == C_NONE)
171,587✔
1579
                             ? C_NONE
171,587✔
1580
                             : model::materials[cell_material_idx]->id();
171,586✔
1581

1582
    if ((target_material_id == C_NONE && !is_target_void) ||
171,587✔
1583
        cell_material_id == target_material_id) {
1584
      int64_t sr = source_region_offsets_[i_cell] + j;
141,587!
1585
      // Check if the key is already present in the mesh_map_
1586
      if (mesh_map_.find(sr) != mesh_map_.end()) {
141,587!
1587
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1588
                                "applied, but trying to apply mesh idx {}",
1589
          sr, mesh_map_[sr], mesh_idx));
×
1590
      }
1591
      // If the SR has not already been assigned, then we can write to it
1592
      mesh_map_[sr] = mesh_idx;
141,587✔
1593
    }
1594
  }
1595
}
1596

1597
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
1,306✔
1598
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1599
{
1600
  Cell& cell = *model::cells[i_cell];
1,306✔
1601

1602
  if (cell.type_ == Fill::MATERIAL) {
1,306✔
1603
    vector<int> instances(cell.n_instances());
1,155✔
1604
    std::iota(instances.begin(), instances.end(), 0);
1,155✔
1605
    apply_mesh_to_cell_instances(
1,155✔
1606
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1607
  } else if (target_material_id == C_NONE && !is_target_void) {
1,306✔
1608
    for (int j = 0; j < cell.n_instances(); j++) {
182✔
1609
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
91✔
1610
        cell.get_contained_cells(j, nullptr);
91✔
1611
      for (const auto& pair : cell_instance_list) {
543✔
1612
        int32_t i_child_cell = pair.first;
452✔
1613
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
452✔
1614
          pair.second, is_target_void);
452✔
1615
      }
1616
    }
91✔
1617
  }
1618
}
1,306✔
1619

1620
void FlatSourceDomain::apply_meshes()
942✔
1621
{
1622
  // Skip if there are no mappings between mesh IDs and domains
1623
  if (mesh_domain_map_.empty())
942✔
1624
    return;
1625

1626
  // Loop over meshes
1627
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
1,037✔
1628
    Mesh* mesh = model::meshes[mesh_idx].get();
571✔
1629
    int mesh_id = mesh->id();
571✔
1630

1631
    // Skip if mesh id is not present in the map
1632
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
571✔
1633
      continue;
45✔
1634

1635
    // Loop over domains associated with the mesh
1636
    for (auto& domain : mesh_domain_map_[mesh_id]) {
1,052✔
1637
      Source::DomainType domain_type = domain.first;
526✔
1638
      int domain_id = domain.second;
526✔
1639

1640
      if (domain_type == Source::DomainType::MATERIAL) {
526✔
1641
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
180✔
1642
          if (domain_id == C_NONE) {
150!
1643
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1644
          } else {
1645
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
150✔
1646
          }
1647
        }
1648
      } else if (domain_type == Source::DomainType::CELL) {
496✔
1649
        int32_t i_cell = model::cell_map[domain_id];
30✔
1650
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
30✔
1651
      } else if (domain_type == Source::DomainType::UNIVERSE) {
466!
1652
        int32_t i_universe = model::universe_map[domain_id];
466✔
1653
        Universe& universe = *model::universes[i_universe];
466✔
1654
        for (int32_t i_cell : universe.cells_) {
1,592✔
1655
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
1,126✔
1656
        }
1657
      }
1658
    }
1659
  }
1660
}
1661

1662
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
2,147,483,647✔
1663
  SourceRegionKey sr_key, Position r, Direction u)
1664
{
1665
  // Case 1: Check if the source region key is already present in the permanent
1666
  // map. This is the most common condition, as any source region visited in a
1667
  // previous power iteration will already be present in the permanent map. If
1668
  // the source region key is found, we translate the key into a specific 1D
1669
  // source region index and return a handle its position in the
1670
  // source_regions_ vector.
1671
  auto it = source_region_map_.find(sr_key);
2,147,483,647✔
1672
  if (it != source_region_map_.end()) {
2,147,483,647✔
1673
    int64_t sr = it->second;
2,147,483,647✔
1674
    return source_regions_.get_source_region_handle(sr);
2,147,483,647✔
1675
  }
1676

1677
  // Case 2: Check if the source region key is present in the temporary (thread
1678
  // safe) map. This is a common occurrence in the first power iteration when
1679
  // the source region has already been visited already by some other ray. We
1680
  // begin by locking the temporary map before any operations are performed. The
1681
  // lock is not global over the full data structure -- it will be dependent on
1682
  // which key is used.
1683
  discovered_source_regions_.lock(sr_key);
63,777,943✔
1684

1685
  // If the key is found in the temporary map, then we return a handle to the
1686
  // source region that is stored in the temporary map.
1687
  if (discovered_source_regions_.contains(sr_key)) {
63,777,943✔
1688
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
61,341,532✔
1689
    discovered_source_regions_.unlock(sr_key);
61,341,532✔
1690
    return handle;
61,341,532✔
1691
  }
1692

1693
  // Case 3: The source region key is not present anywhere, but it is only due
1694
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1695
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1696
  // result in the ray tracer detecting an additional (very short) segment
1697
  // though a mesh bin that is actually past the physical source region
1698
  // boundary. This is a result of the the multi-level ray tracing treatment in
1699
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1700
  // result in the wrong surface being selected as the nearest. This can happen
1701
  // in a lattice when there are two directions that both are very close in
1702
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1703
  // treated as being equivalent so alternative logic is used. However, when we
1704
  // go and ray trace on this with the mesh tracer we may go past the surface
1705
  // bounding the current source region.
1706
  //
1707
  // To filter out this case, before we create the new source region, we double
1708
  // check that the actual starting point of this segment (r) is still in the
1709
  // same geometry source region that we started in. If an artifact is detected,
1710
  // we discard the segment (and attenuation through it) as it is not really a
1711
  // valid source region and will have only an infinitessimally small cell
1712
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1713
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1714
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1715
  // consequences for the general ray tracer, so for now we do the below sanity
1716
  // checks before generating phantom source regions. A significant extra cost
1717
  // is incurred in instantiating the GeometryState object and doing a cell
1718
  // lookup, but again, this is going to be an extremely rare thing to check
1719
  // after the first power iteration has completed.
1720

1721
  // Sanity check on source region id
1722
  GeometryState gs;
2,436,411✔
1723
  gs.r() = r + TINY_BIT * u;
2,436,411✔
1724
  gs.u() = {1.0, 0.0, 0.0};
2,436,411✔
1725
  exhaustive_find_cell(gs);
2,436,411✔
1726
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,436,411✔
1727
  if (sr_found != sr_key.base_source_region_id) {
2,436,411✔
1728
    discovered_source_regions_.unlock(sr_key);
858✔
1729
    SourceRegionHandle handle;
858✔
1730
    handle.is_numerical_fp_artifact_ = true;
858✔
1731
    return handle;
858✔
1732
  }
1733

1734
  // Sanity check on mesh bin
1735
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,435,553✔
1736
  if (mesh_idx == C_NONE) {
2,435,553✔
1737
    if (sr_key.mesh_bin != 0) {
394,812!
1738
      discovered_source_regions_.unlock(sr_key);
×
1739
      SourceRegionHandle handle;
×
1740
      handle.is_numerical_fp_artifact_ = true;
×
1741
      return handle;
×
1742
    }
1743
  } else {
1744
    Mesh* mesh = model::meshes[mesh_idx].get();
2,040,741✔
1745
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
2,040,741✔
1746
    if (bin_found != sr_key.mesh_bin) {
2,040,741!
1747
      discovered_source_regions_.unlock(sr_key);
×
1748
      SourceRegionHandle handle;
×
1749
      handle.is_numerical_fp_artifact_ = true;
×
1750
      return handle;
×
1751
    }
1752
  }
1753

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

1761
  // Call the basic constructor for the source region and store in the parallel
1762
  // map.
1763
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,435,553✔
1764
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
2,435,553✔
1765
    sr_key, {negroups_, ndgroups_, is_linear});
1766
  SourceRegionHandle handle {*sr_ptr};
2,435,553✔
1767

1768
  // Determine the material
1769
  int gs_i_cell = gs.lowest_coord().cell();
2,435,553!
1770
  Cell& cell = *model::cells[gs_i_cell];
2,435,553!
1771
  int material = cell.material(gs.cell_instance());
2,435,553!
1772
  int temp = 0;
2,435,553✔
1773

1774
  // If material total XS is extremely low, just set it to void to avoid
1775
  // problems with 1/Sigma_t
1776
  if (material != MATERIAL_VOID) {
2,435,553✔
1777
    temp = data::mg.macro_xs_[material].get_temperature_index(
4,826,978✔
1778
      cell.sqrtkT(gs.cell_instance()));
2,413,489✔
1779
    for (int g = 0; g < negroups_; g++) {
5,699,895✔
1780
      double sigma_t =
3,286,494✔
1781
        sigma_t_[(material * ntemperature_ + temp) * negroups_ + g];
3,286,494✔
1782
      if (sigma_t < MINIMUM_MACRO_XS) {
3,286,494✔
1783
        material = MATERIAL_VOID;
1784
        temp = 0;
1785
        break;
1786
      }
1787
    }
1788
  }
1789

1790
  if (settings::kinetic_simulation && material == MATERIAL_VOID) {
2,435,553!
NEW
1791
    fatal_error("Explicit void treatment for kinetic simulations "
×
1792
                " is not currently supported.");
1793
  }
1794

1795
  handle.material() = material;
2,435,553✔
1796
  handle.temperature_idx() = temp;
2,435,553✔
1797

1798
  handle.density_mult() = cell.density_mult(gs.cell_instance());
2,435,553✔
1799

1800
  // Store the mesh index (if any) assigned to this source region
1801
  handle.mesh() = mesh_idx;
2,435,553✔
1802

1803
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,435,553✔
1804
    // Determine if there are any volumetric sources, and apply them.
1805
    // Volumetric sources are specifc only to the base SR idx.
1806
    auto it_vol =
2,310,868✔
1807
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,310,868✔
1808
    if (it_vol != external_volumetric_source_map_.end()) {
2,310,868✔
1809
      const vector<int>& vol_sources = it_vol->second;
4,686✔
1810
      for (int src_idx : vol_sources) {
9,372✔
1811
        apply_external_source_to_source_region(src_idx, handle);
4,686✔
1812
      }
1813
    }
1814

1815
    // Determine if there are any point sources, and apply them.
1816
    // Point sources are specific to the source region key.
1817
    auto it_point = external_point_source_map_.find(sr_key);
2,310,868✔
1818
    if (it_point != external_point_source_map_.end()) {
2,310,868✔
1819
      const vector<int>& point_sources = it_point->second;
12✔
1820
      for (int src_idx : point_sources) {
24✔
1821
        apply_external_source_to_source_region(src_idx, handle);
12✔
1822
      }
1823
    }
1824

1825
    // Divide external source term by sigma_t
1826
    if (material != C_NONE) {
2,310,868✔
1827
      for (int g = 0; g < negroups_; g++) {
4,623,391✔
1828
        double sigma_t =
2,334,675✔
1829
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
2,334,675✔
1830
          handle.density_mult();
2,334,675✔
1831
        handle.external_source(g) /= sigma_t;
2,334,675✔
1832
      }
1833
    }
1834
  }
1835

1836
  // Compute the combined source term
1837
  update_single_neutron_source(handle);
2,435,553✔
1838
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
2,435,553!
NEW
1839
    if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
×
NEW
1840
      compute_single_phi_prime(handle);
×
NEW
1841
    else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
×
NEW
1842
      compute_single_T1(handle);
×
1843
  }
1844

1845
  // Unlock the parallel map. Note: we may be tempted to release
1846
  // this lock earlier, and then just use the source region's lock to protect
1847
  // the flux/source initialization stages above. However, the rest of the code
1848
  // only protects updates to the new flux and volume fields, and assumes that
1849
  // the source is constant for the duration of transport. Thus, using just the
1850
  // source region's lock by itself would result in other threads potentially
1851
  // reading from the source before it is computed, as they won't use the lock
1852
  // when only reading from the SR's source. It would be expensive to protect
1853
  // those operations, whereas generating the SR is only done once, so we just
1854
  // hold the map's bucket lock until the source region is fully initialized.
1855
  discovered_source_regions_.unlock(sr_key);
2,435,553✔
1856

1857
  return handle;
2,435,553✔
1858
}
2,436,411✔
1859

1860
void FlatSourceDomain::finalize_discovered_source_regions()
146,532✔
1861
{
1862
  // Extract keys for entries with a valid volume.
1863
  vector<SourceRegionKey> keys;
146,532✔
1864
  for (const auto& pair : discovered_source_regions_) {
5,164,170✔
1865
    if (pair.second.volume_ > 0.0) {
2,435,553✔
1866
      keys.push_back(pair.first);
2,334,089✔
1867
    }
1868
  }
1869

1870
  if (!keys.empty()) {
146,532✔
1871
    // Sort the keys, so as to ensure reproducible ordering given that source
1872
    // regions may have been added to discovered_source_regions_ in an arbitrary
1873
    // order due to shared memory threading.
1874
    std::sort(keys.begin(), keys.end());
1,046✔
1875

1876
    // Remember the index of the first new source region
1877
    int64_t start_sr_id = source_regions_.n_source_regions();
1,046✔
1878

1879
    // Append the source regions in the sorted key order.
1880
    for (const auto& key : keys) {
2,335,135✔
1881
      const SourceRegion& sr = discovered_source_regions_[key];
2,334,089✔
1882
      source_region_map_[key] = source_regions_.n_source_regions();
2,334,089✔
1883
      source_regions_.push_back(sr);
2,334,089✔
1884
    }
1885

1886
    // Map all new source regions to tallies
1887
    convert_source_regions_to_tallies(start_sr_id);
1,046✔
1888
  }
1889

1890
  discovered_source_regions_.clear();
146,532✔
1891
}
146,532✔
1892

1893
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1894
//
1895
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1896
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1897
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1898
// https://doi.org/10.1016/j.anucene.2018.10.036.
1899
void FlatSourceDomain::apply_transport_stabilization()
146,532✔
1900
{
1901
  // Don't do anything if all in-group scattering
1902
  // cross sections are positive
1903
  if (!is_transport_stabilization_needed_) {
146,532✔
1904
    return;
1905
  }
1906

1907
  // Apply the stabilization factor to all source elements
1908
#pragma omp parallel for
1,800✔
1909
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
148,300✔
1910
    int material = source_regions_.material(sr);
146,800!
1911
    int temp = source_regions_.temperature_idx(sr);
146,800!
1912
    double density_mult = source_regions_.density_mult(sr);
146,800!
1913
    if (material == MATERIAL_VOID) {
146,800!
1914
      continue;
1915
    }
1916
    for (int g = 0; g < negroups_; g++) {
10,422,800✔
1917
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1918
      // negative
1919
      double sigma_s =
10,276,000✔
1920
        sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) *
10,276,000✔
1921
                   negroups_ +
10,276,000✔
1922
                 g] *
10,276,000✔
1923
        density_mult;
10,276,000✔
1924
      if (sigma_s < 0.0) {
10,276,000✔
1925
        double sigma_t =
3,550,000✔
1926
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
3,550,000✔
1927
          density_mult;
3,550,000✔
1928
        double phi_new = source_regions_.scalar_flux_new(sr, g);
3,550,000✔
1929
        double phi_old = source_regions_.scalar_flux_old(sr, g);
3,550,000✔
1930

1931
        // Equation 18 in the above Gunow et al. 2019 paper. For a default
1932
        // rho of 1.0, this ensures there are no negative diagonal elements
1933
        // in the iteration matrix. A lesser rho could be used (or exposed
1934
        // as a user input parameter) to reduce the negative impact on
1935
        // convergence rate though would need to be experimentally tested to see
1936
        // if it doesn't become unstable. rho = 1.0 is good as it gives the
1937
        // highest assurance of stability, and the impacts on convergence rate
1938
        // are pretty mild.
1939
        double D = diagonal_stabilization_rho_ * sigma_s / sigma_t;
3,550,000✔
1940

1941
        // Equation 16 in the above Gunow et al. 2019 paper
1942
        source_regions_.scalar_flux_new(sr, g) =
3,550,000✔
1943
          (phi_new - D * phi_old) / (1.0 - D);
3,550,000✔
1944
      }
1945
    }
1946
  }
1947
}
1948

1949
// Determines the base source region index (i.e., a material filled cell
1950
// instance) that corresponds to a particular location in the geometry. Requires
1951
// that the "gs" object passed in has already been initialized and has called
1952
// find_cell etc.
1953
int64_t FlatSourceDomain::lookup_base_source_region_idx(
2,147,483,647✔
1954
  const GeometryState& gs) const
1955
{
1956
  int i_cell = gs.lowest_coord().cell();
2,147,483,647✔
1957
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
2,147,483,647✔
1958
  return sr;
2,147,483,647✔
1959
}
1960

1961
// Determines the index of the mesh (if any) that has been applied
1962
// to a particular base source region index.
1963
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
2,147,483,647✔
1964
{
1965
  int mesh_idx = C_NONE;
2,147,483,647✔
1966
  auto mesh_it = mesh_map_.find(sr);
2,147,483,647✔
1967
  if (mesh_it != mesh_map_.end()) {
2,147,483,647✔
1968
    mesh_idx = mesh_it->second;
2,147,483,647✔
1969
  }
1970
  return mesh_idx;
2,147,483,647✔
1971
}
1972

1973
// Determines the source region key that corresponds to a particular location in
1974
// the geometry. This takes into account both the base source region index as
1975
// well as the mesh bin if a mesh is applied to this source region for
1976
// subdivision.
1977
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
15,242,336✔
1978
  const GeometryState& gs) const
1979
{
1980
  int64_t sr = lookup_base_source_region_idx(gs);
15,242,336✔
1981
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
15,242,336✔
1982
  return SourceRegionKey {sr, mesh_bin};
15,242,336✔
1983
}
1984

1985
// Determines the mesh bin that corresponds to a particular base source region
1986
// index and position.
1987
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
15,242,336✔
1988
{
1989
  int mesh_idx = lookup_mesh_idx(sr);
15,242,336✔
1990
  int mesh_bin = 0;
15,242,336✔
1991
  if (mesh_idx != C_NONE) {
15,242,336✔
1992
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
8,218,836✔
1993
  }
1994
  return mesh_bin;
15,242,336✔
1995
}
1996

1997
//------------------------------------------------------------------------------
1998
// Methods for kinetic simulations
1999

2000
// Generates new estimate of k_dynamic based on the fraction between this
2001
// timestep's estimate of neutron production and loss. (previous timestep
2002
// fission vs current timestep fission?)
2003
// TODO: implement compute_k_dynamic
2004

2005
// Compute new estimate of scattering + fission (+ precursor decay for
2006
// kinetic simulations) sources in each source region based on the flux
2007
// estimate from the previous iteration.
2008

2009
// TODO: support void regions
2010
void FlatSourceDomain::compute_single_phi_prime(SourceRegionHandle& srh)
119,262,000✔
2011
{
2012
  double A0 =
119,262,000✔
2013
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
119,262,000✔
2014
  int material = srh.material();
119,262,000✔
2015
  int temp = srh.temperature_idx();
119,262,000✔
2016
  double density_mult = srh.density_mult();
119,262,000✔
2017
  const int material_offset = (material * ntemperature_ + temp) * negroups_;
119,262,000✔
2018
  for (int g = 0; g < negroups_; g++) {
960,445,200✔
2019
    double inverse_vbar = inverse_vbar_[material_offset + g];
841,183,200✔
2020
    // TODO: add support for explicit void
2021
    double sigma_t = sigma_t_[material_offset + g] * density_mult;
841,183,200✔
2022

2023
    double scalar_flux_time_derivative =
841,183,200✔
2024
      A0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd(g);
841,183,200✔
2025
    srh.phi_prime(g) = scalar_flux_time_derivative * inverse_vbar / sigma_t;
841,183,200✔
2026
  }
2027
}
119,262,000✔
2028

2029
// T1 calculation
2030
// TODO: support void regions
2031
void FlatSourceDomain::compute_single_T1(SourceRegionHandle& srh)
286,000✔
2032
{
2033
  double A0 =
286,000✔
2034
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
286,000✔
2035
  double B0 = (bd_coefficients_second_order_.at(RandomRay::bd_order_))[0] /
286,000✔
2036
              (settings::dt * settings::dt);
286,000✔
2037
  int material = srh.material();
286,000✔
2038
  int temp = srh.temperature_idx();
286,000✔
2039
  double density_mult = srh.density_mult();
286,000✔
2040
  const int material_offset = (material * ntemperature_ + temp) * negroups_;
286,000✔
2041
  for (int g = 0; g < negroups_; g++) {
8,637,200✔
2042
    double inverse_vbar = inverse_vbar_[material_offset + g];
8,351,200✔
2043
    // TODO: add support for explicit void
2044
    double sigma_t = sigma_t_[material_offset + g] * density_mult;
8,351,200✔
2045

2046
    // Multiply out sigma_t to correctly compute the derivative term
2047
    float source_time_derivative =
8,351,200✔
2048
      A0 * srh.source(g) * sigma_t + srh.source_rhs_bd(g);
8,351,200✔
2049

2050
    double scalar_flux_time_derivative_2 =
8,351,200✔
2051
      B0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd_2(g);
8,351,200✔
2052
    scalar_flux_time_derivative_2 *= inverse_vbar;
8,351,200✔
2053

2054
    // Divide by sigma_t to save time during transport
2055
    srh.T1(g) =
8,351,200✔
2056
      (source_time_derivative - scalar_flux_time_derivative_2) / sigma_t;
8,351,200✔
2057
  }
2058
}
286,000✔
2059

2060
void FlatSourceDomain::compute_single_delayed_fission_source(
167,367,156✔
2061
  SourceRegionHandle& srh)
2062
{
2063

2064
  // Reset all delayed fission sources to zero (important for void regions)
2065
  for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2066
    srh.delayed_fission_source(dg) = 0.0;
1,337,335,648✔
2067
  }
2068

2069
  int material = srh.material();
167,367,156!
2070
  int temp = srh.temperature_idx();
167,367,156!
2071
  double density_mult = srh.density_mult();
167,367,156!
2072
  const int delay_offset =
167,367,156✔
2073
    (material * ntemperature_ + temp) * negroups_ * ndgroups_;
167,367,156✔
2074
  if (material != MATERIAL_VOID) {
167,367,156!
2075
    double inverse_k_eff = 1.0 / k_eff_;
167,367,156✔
2076
    for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2077
      // We cannot have delayed neutrons if there is no delayed data
2078
      double lambda = lambda_[material * ndgroups_ + dg];
1,337,335,648✔
2079
      if (lambda != 0.0) {
1,337,335,648✔
2080
        for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
2081
          double scalar_flux = scalar_flux = srh.scalar_flux_new(g);
2,147,483,647✔
2082
          double nu_d_sigma_f =
2,147,483,647✔
2083
            nu_d_sigma_f_[delay_offset + dg * negroups_ + g] * density_mult;
2,147,483,647✔
2084
          srh.delayed_fission_source(dg) += nu_d_sigma_f * scalar_flux;
2,147,483,647✔
2085
        }
2086
        srh.delayed_fission_source(dg) *= inverse_k_eff;
616,492,624✔
2087
      }
2088
    }
2089
  }
2090
}
167,367,156✔
2091

2092
void FlatSourceDomain::compute_single_precursors(SourceRegionHandle& srh)
167,367,156✔
2093
{
2094
  // Reset all precursors to zero (important for void regions)
2095
  for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2096
    srh.precursors_new(dg) = 0.0;
1,337,335,648✔
2097
  }
2098

2099
  int material = srh.material();
167,367,156!
2100
  int temp = srh.temperature_idx();
167,367,156!
2101
  const int delay_offset = (material * ntemperature_ + temp) * ndgroups_;
167,367,156✔
2102
  if (material != MATERIAL_VOID) {
167,367,156!
2103
    for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2104
      double lambda = lambda_[delay_offset + dg];
1,337,335,648✔
2105
      if (lambda != 0.0) {
1,337,335,648✔
2106
        double delayed_fission_source = srh.delayed_fission_source(dg);
616,492,624✔
2107
        if (simulation::is_initial_condition) {
616,492,624✔
2108
          srh.precursors_new(dg) = delayed_fission_source / lambda;
176,140,624✔
2109
        } else {
2110
          double precursor_rhs_bd = srh.precursors_rhs_bd(dg);
440,352,000✔
2111
          srh.precursors_new(dg) = delayed_fission_source - precursor_rhs_bd;
440,352,000✔
2112
          double A0 =
440,352,000✔
2113
            (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] /
440,352,000✔
2114
            settings::dt;
440,352,000✔
2115
          srh.precursors_new(dg) /= A0 + lambda;
440,352,000✔
2116
        }
2117
      }
2118
    }
2119
  }
2120
}
167,367,156✔
2121

2122
void FlatSourceDomain::compute_all_precursors()
130,900✔
2123
{
2124
  simulation::time_compute_precursors.start();
130,900✔
2125

2126
#pragma omp parallel for
71,400✔
2127
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
76,135,480✔
2128
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
76,075,980✔
2129
    compute_single_delayed_fission_source(srh);
76,075,980✔
2130
    compute_single_precursors(srh);
76,075,980✔
2131
  }
2132

2133
  simulation::time_compute_precursors.start();
130,900✔
2134
}
130,900✔
2135

NEW
2136
void FlatSourceDomain::serialize_final_precursors(vector<double>& precursors)
×
2137
{
2138
  // Ensure array is correct size
NEW
2139
  precursors.resize(n_source_regions() * ndgroups_);
×
2140
// Serialize the precursors for output
2141
#pragma omp parallel for
2142
  for (int64_t de = 0; de < n_delay_elements(); de++) {
×
2143
    precursors[de] = source_regions_.precursors_final(de);
2144
  }
NEW
2145
}
×
2146

2147
void FlatSourceDomain::precursors_swap()
130,900✔
2148
{
2149
  source_regions_.precursors_swap();
130,900✔
2150
}
130,900✔
2151

2152
void FlatSourceDomain::accumulate_iteration_quantities()
72,001✔
2153
{
2154
  accumulate_iteration_flux();
72,001✔
2155
  if (settings::kinetic_simulation) {
72,001✔
2156
#pragma omp parallel for
35,280✔
2157
    for (int64_t sr = 0; sr < n_source_regions(); sr++) {
38,031,000✔
2158
      for (int g = 0; g < negroups_; g++) {
305,760,000✔
2159
        if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
267,758,400✔
2160
          source_regions_.source_final(sr, g) += source_regions_.source(sr, g);
1,383,200✔
2161
        }
2162
      }
2163
      if (settings::create_delayed_neutrons) {
38,001,600!
2164
        for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
2165
          source_regions_.precursors_final(sr, dg) +=
303,721,600✔
2166
            source_regions_.precursors_new(sr, dg);
303,721,600✔
2167
        }
2168
      }
2169
    }
2170
  }
2171
}
72,001✔
2172

2173
void FlatSourceDomain::normalize_final_quantities()
2,098✔
2174
{
2175
  double normalization_factor =
2,098✔
2176
    1.0 / (settings::n_batches - settings::n_inactive);
2,098✔
2177
  double source_normalization_factor;
2,098✔
2178
  if (!settings::kinetic_simulation ||
2,098✔
2179
      settings::kinetic_simulation &&
1,260✔
2180
        simulation::current_timestep == settings::n_timesteps)
1,260✔
2181
    source_normalization_factor =
1,018✔
2182
      compute_fixed_source_normalization_factor() * normalization_factor;
1,018✔
2183
  else
2184
    // The source normalization should only be applied to internal quantities at
2185
    // the end of time stepping in preparation for an adjoint solve.
2186
    source_normalization_factor = 1.0 * normalization_factor;
2187

2188
#pragma omp parallel for
1,259✔
2189
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,405,689✔
2190
    for (int g = 0; g < negroups_; g++) {
4,636,460✔
2191
      source_regions_.scalar_flux_final(sr, g) *= source_normalization_factor;
3,231,610✔
2192
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
3,231,610✔
2193
        source_regions_.source_final(sr, g) *= source_normalization_factor;
276,640✔
2194
      }
2195
    }
2196
    if (settings::kinetic_simulation) {
1,404,850✔
2197
      for (int dg = 0; dg < ndgroups_; dg++) {
1,907,360✔
2198
        source_regions_.precursors_final(sr, dg) *= source_normalization_factor;
1,688,960✔
2199
      }
2200
    }
2201
  }
2202
}
2,098✔
2203

2204
void FlatSourceDomain::propagate_final_quantities()
1,080✔
2205
{
2206
#pragma omp parallel for
648✔
2207
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,632✔
2208
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2209
      source_regions_.scalar_flux_old(sr, g) =
1,609,920✔
2210
        source_regions_.scalar_flux_final(sr, g);
1,609,920✔
2211
    }
2212
    if (settings::create_delayed_neutrons) {
187,200!
2213
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2214
        source_regions_.precursors_old(sr, dg) =
1,447,680✔
2215
          source_regions_.precursors_final(sr, dg);
1,447,680✔
2216
      }
2217
    }
2218
  }
2219
}
1,080✔
2220

2221
void FlatSourceDomain::store_time_step_quantities(bool increment_not_initialize)
1,080✔
2222
{
2223
#pragma omp parallel for
648✔
2224
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,632✔
2225
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2226
      int j = 0;
1,609,920✔
2227
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
1,609,920✔
2228
        j = 1;
237,120✔
2229
      add_value_to_bd_vector(source_regions_.scalar_flux_bd(sr, g),
1,609,920✔
2230
        source_regions_.scalar_flux_final(sr, g), increment_not_initialize,
2231
        RandomRay::bd_order_ + j);
2232
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,609,920✔
2233
        // Multiply out sigma_t to store the base source
2234
        int material = source_regions_.material(sr);
237,120✔
2235
        int temp = source_regions_.temperature_idx(sr);
237,120✔
2236
        double density_mult = source_regions_.density_mult(sr);
237,120✔
2237
        // TODO: add support for explicit void regions
2238
        double sigma_t =
237,120✔
2239
          sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] *
237,120✔
2240
          density_mult;
237,120✔
2241
        float source = source_regions_.source_final(sr, g) * sigma_t;
237,120✔
2242
        add_value_to_bd_vector(source_regions_.source_bd(sr, g), source,
237,120✔
2243
          increment_not_initialize, RandomRay::bd_order_);
2244
      }
2245
    }
2246
    if (settings::create_delayed_neutrons) {
187,200!
2247
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2248
        add_value_to_bd_vector(source_regions_.precursors_bd(sr, dg),
1,447,680✔
2249
          source_regions_.precursors_final(sr, dg), increment_not_initialize,
2250
          RandomRay::bd_order_);
2251
      }
2252
    }
2253
  }
2254
}
1,080✔
2255

2256
void FlatSourceDomain::compute_rhs_bd_quantities()
900✔
2257
{
2258
#pragma omp parallel for
540✔
2259
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
156,360✔
2260
    for (int g = 0; g < negroups_; g++) {
1,497,600✔
2261
      source_regions_.scalar_flux_rhs_bd(sr, g) =
1,341,600✔
2262
        rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
1,341,600✔
2263
          RandomRay::bd_order_, settings::dt);
2264

2265
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,341,600✔
2266
        source_regions_.source_rhs_bd(sr, g) = rhs_backwards_difference(
197,600✔
2267
          source_regions_.source_bd(sr, g), RandomRay::bd_order_, settings::dt);
2268

2269
        source_regions_.scalar_flux_rhs_bd_2(sr, g) =
197,600✔
2270
          rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
197,600✔
2271
            RandomRay::bd_order_, settings::dt, 2);
2272
      }
2273
    }
2274
    if (settings::create_delayed_neutrons) {
156,000!
2275
      for (int dg = 0; dg < ndgroups_; dg++) {
1,362,400✔
2276
        source_regions_.precursors_rhs_bd(sr, dg) =
1,206,400✔
2277
          rhs_backwards_difference(source_regions_.precursors_bd(sr, dg),
1,206,400✔
2278
            RandomRay::bd_order_, settings::dt);
2279
      }
2280
    }
2281
  }
2282
}
900✔
2283

2284
// Update material density by source region
2285
void FlatSourceDomain::update_material_density(int i)
900✔
2286
{
2287
#pragma omp parallel for
540✔
2288
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
156,360✔
2289
    int material = source_regions_.material(sr);
156,000✔
2290
    auto& mat {model::materials[material]};
156,000✔
2291
    if (mat->density_timeseries_.size() != 0) {
156,000✔
2292
      double density_factor = mat->density_timeseries_[i] / mat->density_;
62,400✔
2293
      source_regions_.density_mult(sr) = density_factor;
62,400✔
2294
    }
2295
  }
2296
}
900✔
2297

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

© 2026 Coveralls, Inc