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

openmc-dev / openmc / 20932149956

12 Jan 2026 07:24PM UTC coverage: 81.996% (-0.2%) from 82.195%
20932149956

Pull #3702

github

web-flow
Merge 80e4cff3d into 0486e433d
Pull Request #3702: Random Ray Kinetic Simulation Mode

17686 of 24568 branches covered (71.99%)

Branch coverage included in aggregate %.

1002 of 1145 new or added lines in 20 files covered. (87.51%)

484 existing lines in 23 files now uncovered.

56448 of 65844 relevant lines covered (85.73%)

52849177.45 hits per line

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

75.23
/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

25
namespace openmc {
26

27
//==============================================================================
28
// FlatSourceDomain implementation
29
//==============================================================================
30

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

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

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

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

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

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

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

85
void FlatSourceDomain::batch_reset()
143,012✔
86
{
87
// Reset scalar fluxes and iteration volume tallies to zero
88
#pragma omp parallel for
78,012✔
89
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,428,235✔
90
    source_regions_.volume(sr) = 0.0;
108,363,235✔
91
    source_regions_.volume_sq(sr) = 0.0;
108,363,235✔
92
  }
93

94
#pragma omp parallel for
78,012✔
95
  for (int64_t se = 0; se < n_source_elements(); se++) {
576,971,175✔
96
    source_regions_.scalar_flux_new(se) = 0.0;
576,906,175✔
97
  }
98

99
  if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
143,012!
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
}
143,012✔
105

106
void FlatSourceDomain::accumulate_iteration_flux()
69,636✔
107
{
108
#pragma omp parallel for
37,986✔
109
  for (int64_t se = 0; se < n_source_elements(); se++) {
284,432,250✔
110
    source_regions_.scalar_flux_final(se) +=
284,400,600✔
111
      source_regions_.scalar_flux_new(se);
284,400,600✔
112
  }
113
}
69,636✔
114

115
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
211,174,424✔
116
{
117
  // Reset all source regions to zero (important for void regions)
118
  for (int g = 0; g < negroups_; g++) {
1,449,598,774✔
119
    srh.source(g) = 0.0;
1,238,424,350✔
120
  }
121

122
  // Add scattering + fission source
123
  int material = srh.material();
211,174,424✔
124
  if (material != MATERIAL_VOID) {
211,174,424✔
125
    double inverse_k_eff = 1.0 / k_eff_;
210,732,776✔
126
    for (int g_out = 0; g_out < negroups_; g_out++) {
1,448,714,710✔
127
      double sigma_t = sigma_t_[material * negroups_ + g_out];
1,237,981,934✔
128
      double scatter_source = 0.0;
1,237,981,934✔
129
      double fission_source = 0.0;
1,237,981,934✔
130
      double total_source = 0.0;
1,237,981,934✔
131

132
      for (int g_in = 0; g_in < negroups_; g_in++) {
2,147,483,647✔
133
        double scalar_flux = srh.scalar_flux_old(g_in);
2,147,483,647✔
134
        double sigma_s =
135
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
2,147,483,647✔
136
        double nu_sigma_f;
137
        double chi;
138
        if (settings::kinetic_simulation && !simulation::is_initial_condition) {
2,147,483,647✔
139
          nu_sigma_f = nu_p_sigma_f_[material * negroups_ + g_in];
2,147,483,647✔
140
          chi = chi_p_[material * negroups_ + g_out];
2,147,483,647✔
141
        } else {
142
          nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
2,147,483,647✔
143
          chi = chi_[material * negroups_ + g_out];
2,147,483,647✔
144
        }
145
        scatter_source += sigma_s * scalar_flux;
2,147,483,647✔
146
        if (settings::create_fission_neutrons) {
2,147,483,647!
147
          fission_source += nu_sigma_f * scalar_flux * chi;
2,147,483,647✔
148
        }
149
      }
150
      total_source = (scatter_source + fission_source * inverse_k_eff);
1,237,981,934✔
151

152
      // Add delayed source for kinetic simulation if delayed neutrons are
153
      // turned on
154
      if (settings::kinetic_simulation && !simulation::is_initial_condition &&
1,237,981,934!
155
          settings::create_delayed_neutrons) {
156
        double delayed_source = 0.0;
849,534,400✔
157
        for (int dg = 0; dg < ndgroups_; dg++) {
2,147,483,647✔
158
          double chi_d =
159
            chi_d_[material * negroups_ * ndgroups_ + dg * negroups_ + g_out];
2,147,483,647✔
160
          double lambda = lambda_[material * ndgroups_ + dg];
2,147,483,647✔
161
          double precursors = srh.precursors_old(dg);
2,147,483,647✔
162
          delayed_source += chi_d * precursors * lambda;
2,147,483,647✔
163
        }
164
        total_source += delayed_source;
849,534,400✔
165
      }
166
      srh.source(g_out) = total_source / sigma_t;
1,237,981,934✔
167
    }
168
  }
169

170
  // Add external source if in fixed source mode
171
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
211,174,424✔
172
    for (int g = 0; g < negroups_; g++) {
89,574,878✔
173
      srh.source(g) += srh.external_source(g);
46,223,461✔
174
    }
175
  }
176
}
211,174,424✔
177

178
// Compute new estimate of scattering + fission sources in each source region
179
// based on the flux estimate from the previous iteration.
180
void FlatSourceDomain::update_all_neutron_sources()
143,012✔
181
{
182
  simulation::time_update_src.start();
143,012✔
183

184
#pragma omp parallel for
78,012✔
185
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,428,235✔
186
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
108,363,235✔
187
    update_single_neutron_source(srh);
108,363,235✔
188
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
108,363,235✔
189
      if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
54,340,000✔
190
        compute_single_phi_prime(srh);
54,210,000✔
191
      else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
130,000!
192
        compute_single_T1(srh);
130,000✔
193
    }
194
  }
195

196
  simulation::time_update_src.stop();
143,012✔
197
}
143,012✔
198

199
// Normalizes flux and updates simulation-averaged volume estimate
200
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
136,137✔
201
  double total_active_distance_per_iteration)
202
{
203
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
136,137✔
204
  double volume_normalization_factor =
136,137✔
205
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
136,137✔
206

207
// Normalize scalar flux to total distance travelled by all rays this
208
// iteration
209
#pragma omp parallel for
74,262✔
210
  for (int64_t se = 0; se < n_source_elements(); se++) {
562,949,945✔
211
    source_regions_.scalar_flux_new(se) *= normalization_factor;
562,888,070✔
212
  }
213

214
// Accumulate cell-wise ray length tallies collected this iteration, then
215
// update the simulation-averaged cell-wise volume estimates
216
#pragma omp parallel for
74,262✔
217
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
96,018,725✔
218
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
95,956,850✔
219
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
95,956,850✔
220
    source_regions_.volume_naive(sr) =
191,913,700✔
221
      source_regions_.volume(sr) * normalization_factor;
95,956,850✔
222
    source_regions_.volume_sq(sr) =
191,913,700✔
223
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
95,956,850✔
224
    source_regions_.volume(sr) =
95,956,850✔
225
      source_regions_.volume_t(sr) * volume_normalization_factor;
95,956,850✔
226
  }
227
}
136,137✔
228

229
void FlatSourceDomain::set_flux_to_flux_plus_source(
1,232,376,601✔
230
  int64_t sr, double volume, int g)
231
{
232
  int material = source_regions_.material(sr);
1,232,376,601✔
233
  if (material == MATERIAL_VOID) {
1,232,376,601✔
234
    source_regions_.scalar_flux_new(sr, g) /= volume;
882,416✔
235
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
882,416!
236
      source_regions_.scalar_flux_new(sr, g) +=
882,416✔
237
        0.5f * source_regions_.external_source(sr, g) *
882,416✔
238
        source_regions_.volume_sq(sr);
882,416✔
239
    }
240
  } else {
241
    double sigma_t = sigma_t_[material * negroups_ + g];
1,231,494,185✔
242
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
1,231,494,185✔
243
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
1,231,494,185✔
244
  }
245
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,232,376,601✔
246
    double inverse_vbar =
247
      inverse_vbar_[source_regions_.material(sr) * negroups_ + g];
849,260,280✔
248
    double scalar_flux_rhs_bd = source_regions_.scalar_flux_rhs_bd(sr, g);
849,260,280✔
249
    double A0 =
250
      (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
849,260,280✔
251
    // TODO: Add support for expicit void regions
252
    double sigma_t = sigma_t_[material * negroups_ + g];
849,260,280✔
253
    source_regions_.scalar_flux_new(sr, g) -=
849,260,280✔
254
      scalar_flux_rhs_bd * inverse_vbar / sigma_t;
849,260,280✔
255
    source_regions_.scalar_flux_new(sr, g) /= 1 + A0 * inverse_vbar / sigma_t;
849,260,280✔
256
  }
257
}
1,232,376,601✔
258

259
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,542✔
260
{
261
  source_regions_.scalar_flux_new(sr, g) =
3,084✔
262
    source_regions_.scalar_flux_old(sr, g);
1,542✔
263
}
1,542✔
264

265
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
9,309,159✔
266
{
267
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
9,309,159✔
268
}
9,309,159✔
269

270
// Combine transport flux contributions and flat source contributions from the
271
// previous iteration to generate this iteration's estimate of scalar flux.
272
int64_t FlatSourceDomain::add_source_to_scalar_flux()
143,012✔
273
{
274
  int64_t n_hits = 0;
143,012✔
275
  double inverse_batch = 1.0 / simulation::current_batch;
143,012✔
276

277
#pragma omp parallel for reduction(+ : n_hits)
78,012✔
278
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
109,477,415✔
279

280
    double volume_simulation_avg = source_regions_.volume(sr);
109,412,415✔
281
    double volume_iteration = source_regions_.volume_naive(sr);
109,412,415✔
282

283
    // Increment the number of hits if cell was hit this iteration
284
    if (volume_iteration) {
109,412,415✔
285
      n_hits++;
105,328,360✔
286
    }
287

288
    // Set the SR to small status if its expected number of hits
289
    // per iteration is less than 1.5
290
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
109,412,415✔
291
      source_regions_.is_small(sr) = 1;
6,813,175✔
292
    } else {
293
      source_regions_.is_small(sr) = 0;
102,599,240✔
294
    }
295

296
    // The volume treatment depends on the volume estimator type
297
    // and whether or not an external source is present in the cell.
298
    double volume;
299
    switch (volume_estimator_) {
109,412,415!
300
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
301
      volume = volume_iteration;
777,600✔
302
      break;
777,600✔
303
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
304
      volume = volume_simulation_avg;
432,000✔
305
      break;
432,000✔
306
    case RandomRayVolumeEstimator::HYBRID:
108,202,815✔
307
      if (source_regions_.external_source_present(sr) ||
212,217,630✔
308
          source_regions_.is_small(sr)) {
104,014,815✔
309
        volume = volume_iteration;
11,000,795✔
310
      } else {
311
        volume = volume_simulation_avg;
97,202,020✔
312
      }
313
      break;
108,202,815✔
314
    default:
315
      fatal_error("Invalid volume estimator type");
316
    }
317

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

355
  // Return the number of source regions that were hit this iteration
356
  return n_hits;
143,012✔
357
}
358

359
// Generates new estimate of k_eff based on the differences between this
360
// iteration's estimate of the scalar flux and the last iteration's estimate.
361
void FlatSourceDomain::compute_k_eff()
39,930✔
362
{
363
  double fission_rate_old = 0;
39,930✔
364
  double fission_rate_new = 0;
39,930✔
365

366
  // Vector for gathering fission source terms for Shannon entropy calculation
367
  vector<float> p(n_source_regions(), 0.0f);
39,930✔
368

369
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
21,780✔
370
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,064,320✔
371

372
    // If simulation averaged volume is zero, don't include this cell
373
    double volume = source_regions_.volume(sr);
22,046,170✔
374
    if (volume == 0.0) {
22,046,170✔
375
      continue;
20✔
376
    }
377

378
    int material = source_regions_.material(sr);
22,046,150✔
379
    if (material == MATERIAL_VOID) {
22,046,150!
380
      continue;
381
    }
382

383
    double sr_fission_source_old = 0;
22,046,150✔
384
    double sr_fission_source_new = 0;
22,046,150✔
385

386
    for (int g = 0; g < negroups_; g++) {
178,593,400✔
387
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
156,547,250✔
388
      sr_fission_source_old +=
156,547,250✔
389
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
156,547,250✔
390
      sr_fission_source_new +=
156,547,250✔
391
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
156,547,250✔
392
    }
393

394
    // Compute total fission rates in FSR
395
    sr_fission_source_old *= volume;
22,046,150✔
396
    sr_fission_source_new *= volume;
22,046,150✔
397

398
    // Accumulate totals
399
    fission_rate_old += sr_fission_source_old;
22,046,150✔
400
    fission_rate_new += sr_fission_source_new;
22,046,150✔
401

402
    // Store total fission rate in the FSR for Shannon calculation
403
    p[sr] = sr_fission_source_new;
22,046,150✔
404
  }
405

406
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
39,930✔
407

408
  double H = 0.0;
39,930✔
409
  // defining an inverse sum for better performance
410
  double inverse_sum = 1 / fission_rate_new;
39,930✔
411

412
#pragma omp parallel for reduction(+ : H)
21,780✔
413
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,064,320✔
414
    // Only if FSR has non-negative and non-zero fission source
415
    if (p[sr] > 0.0f) {
22,046,170✔
416
      // Normalize to total weight of bank sites. p_i for better performance
417
      float p_i = p[sr] * inverse_sum;
10,147,755✔
418
      // Sum values to obtain Shannon entropy.
419
      H -= p_i * std::log2(p_i);
10,147,755✔
420
    }
421
  }
422

423
  // Adds entropy value to shared entropy vector in openmc namespace.
424
  simulation::entropy.push_back(H);
39,930✔
425

426
  fission_rate_ = fission_rate_new;
39,930✔
427
  k_eff_ = k_eff_new;
39,930✔
428
}
39,930✔
429

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

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

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

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

468
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
925✔
469
{
470
  openmc::simulation::time_tallies.start();
925✔
471

472
  // Tracks if we've generated a mapping yet for all source regions.
473
  bool all_source_regions_mapped = true;
925✔
474

475
// Attempt to generate mapping for all source regions
476
#pragma omp parallel for
505✔
477
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,049,600✔
478

479
    // If this source region has not been hit by a ray yet, then
480
    // we aren't going to be able to map it, so skip it.
481
    if (!source_regions_.position_recorded(sr)) {
1,049,180!
482
      all_source_regions_mapped = false;
483
      continue;
484
    }
485

486
    // A particle located at the recorded midpoint of a ray
487
    // crossing through this source region is used to estabilish
488
    // the spatial location of the source region
489
    Particle p;
1,049,180✔
490
    p.r() = source_regions_.position(sr);
1,049,180✔
491
    p.r_last() = source_regions_.position(sr);
1,049,180✔
492
    p.u() = {1.0, 0.0, 0.0};
1,049,180✔
493
    bool found = exhaustive_find_cell(p);
1,049,180✔
494

495
    // Loop over energy groups (so as to support energy filters)
496
    for (int g = 0; g < negroups_; g++) {
2,478,640✔
497

498
      // Set particle to the current energy
499
      p.g() = g;
1,429,460✔
500
      p.g_last() = g;
1,429,460✔
501
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,429,460✔
502
      p.E_last() = p.E();
1,429,460✔
503

504
      int64_t source_element = sr * negroups_ + g;
1,429,460✔
505

506
      // If this task has already been populated, we don't need to do
507
      // it again.
508
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,429,460!
509
        continue;
510
      }
511

512
      // Loop over all active tallies. This logic is essentially identical
513
      // to what happens when scanning for applicable tallies during
514
      // MC transport.
515
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
5,091,160✔
516
        Tally& tally {*model::tallies[i_tally]};
3,661,700✔
517

518
        // Initialize an iterator over valid filter bin combinations.
519
        // If there are no valid combinations, use a continue statement
520
        // to ensure we skip the assume_separate break below.
521
        auto filter_iter = FilterBinIter(tally, p);
3,661,700✔
522
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,661,700✔
523
        if (filter_iter == end)
3,661,700✔
524
          continue;
1,984,160✔
525

526
        // Loop over filter bins.
527
        for (; filter_iter != end; ++filter_iter) {
3,355,080✔
528
          auto filter_index = filter_iter.index_;
1,677,540✔
529
          auto filter_weight = filter_iter.weight_;
1,677,540✔
530

531
          // Loop over scores
532
          for (int score = 0; score < tally.scores_.size(); score++) {
3,864,400✔
533
            auto score_bin = tally.scores_[score];
2,376,140✔
534
            // Skip precursor score. These must be scored by delay group via
535
            // tally_delay_task.
536
            if (score_bin == SCORE_PRECURSORS)
2,376,140✔
537
              break;
189,280✔
538
            // If a valid tally, filter, and score combination has been found,
539
            // then add it to the list of tally tasks for this source element.
540
            TallyTask task(i_tally, filter_index, score, score_bin);
2,186,860✔
541
            source_regions_.tally_task(sr, g).push_back(task);
2,186,860✔
542

543
            // Also add this task to the list of volume tasks for this source
544
            // region.
545
            source_regions_.volume_task(sr).insert(task);
2,186,860✔
546
          }
547
        }
548
      }
549
      // Reset all the filter matches for the next tally event.
550
      for (auto& match : p.filter_matches())
5,727,500✔
551
        match.bins_present_ = false;
4,298,040✔
552
    }
553
    // Loop over delayed groups (so as to support tallying delayed quantities)
554
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,049,180!
555
      for (int dg = 0; dg < ndgroups_; dg++) {
272,480✔
556

557
        // Set particle to the current delay group
558
        p.delayed_group() = dg;
241,280✔
559

560
        int64_t delay_element = sr * ndgroups_ + dg;
241,280✔
561

562
        // If this task has already been populated, we don't need to do
563
        // it again.
564
        if (source_regions_.tally_delay_task(sr, dg).size() > 0) {
241,280!
565
          continue;
566
        }
567

568
        // Loop over all active tallies. This logic is essentially identical
569
        // to what happens when scanning for applicable tallies during
570
        // MC transport.
571
        // Loop over all active tallies. This logic is essentially identical
572
        for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
673,920✔
573
          Tally& tally {*model::tallies[i_tally]};
432,640✔
574

575
          // Initialize an iterator over valid filter bin combinations.
576
          // If there are no valid combinations, use a continue statement
577
          // to ensure we skip the assume_separate break below.
578
          auto filter_iter = FilterBinIter(tally, p);
432,640✔
579
          auto end = FilterBinIter(tally, true, &p.filter_matches());
432,640✔
580
          if (filter_iter == end)
432,640!
581
            continue;
582

583
          // Loop over filter bins.
584
          for (; filter_iter != end; ++filter_iter) {
865,280✔
585
            auto filter_index = filter_iter.index_;
432,640✔
586
            auto filter_weight = filter_iter.weight_;
432,640✔
587

588
            // Loop over scores
589
            for (int score = 0; score < tally.scores_.size(); score++) {
648,960✔
590
              auto score_bin = tally.scores_[score];
432,640✔
591
              // We only want to score precursors
592
              if (score_bin != SCORE_PRECURSORS)
432,640✔
593
                break;
216,320✔
594
              // If a valid tally, filter, and score combination has been found,
595
              // then add it to the list of tally tasks for this source element.
596
              TallyTask task(i_tally, filter_index + dg, score, score_bin);
216,320✔
597
              source_regions_.tally_delay_task(sr, dg).push_back(task);
216,320✔
598

599
              // Also add this task to the list of volume tasks for this source
600
              // region.
601
              source_regions_.volume_task(sr).insert(task);
216,320✔
602
            }
603
          }
604
        }
605
        // Reset all the filter matches for the next tally event.
606
        for (auto& match : p.filter_matches())
882,560✔
607
          match.bins_present_ = false;
641,280✔
608
      }
609
    }
610
  }
1,049,180✔
611
  openmc::simulation::time_tallies.stop();
925✔
612

613
  mapped_all_tallies_ = all_source_regions_mapped;
925✔
614
}
925✔
615

616
// Set the volume accumulators to zero for all tallies
617
void FlatSourceDomain::reset_tally_volumes()
69,636✔
618
{
619
  if (volume_normalized_flux_tallies_) {
69,636✔
620
#pragma omp parallel for
35,700✔
621
    for (int i = 0; i < tally_volumes_.size(); i++) {
90,600✔
622
      auto& tensor = tally_volumes_[i];
60,850✔
623
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
60,850✔
624
    }
625
  }
626
}
69,636✔
627

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

656
  // If we are in adjoint mode of a fixed source problem, the external
657
  // source is already normalized, such that all resulting fluxes are
658
  // also normalized.
659
  if (adjoint_) {
4,212✔
660
    return 1.0;
398✔
661
  }
662

663
  // Fixed source mode normalization
664

665
  // Step 1 is to sum over all source regions and energy groups to get the
666
  // total external source strength in the simulation.
667
  double simulation_external_source_strength = 0.0;
3,814✔
668
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
2,089✔
669
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,626,285✔
670
    int material = source_regions_.material(sr);
14,624,560✔
671
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,624,560✔
672
    for (int g = 0; g < negroups_; g++) {
29,792,000✔
673
      // For non-void regions, we store the external source pre-divided by
674
      // sigma_t. We need to multiply non-void regions back up by sigma_t
675
      // to get the total source strength in the expected units.
676
      double sigma_t = 1.0;
15,167,440✔
677
      if (material != MATERIAL_VOID) {
15,167,440✔
678
        sigma_t = sigma_t_[material * negroups_ + g];
14,957,200✔
679
      }
680
      simulation_external_source_strength +=
15,167,440✔
681
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,167,440✔
682
    }
683
  }
684

685
  // Step 2 is to determine the total user-specified external source strength
686
  double user_external_source_strength = 0.0;
3,814✔
687
  for (auto& ext_source : model::external_sources) {
7,628✔
688
    user_external_source_strength += ext_source->strength();
3,814✔
689
  }
690

691
  // The correction factor is the ratio of the user-specified external source
692
  // strength to the simulation external source strength.
693
  double source_normalization_factor =
3,814✔
694
    user_external_source_strength / simulation_external_source_strength;
695

696
  return source_normalization_factor;
3,814✔
697
}
698

699
// Tallying in random ray is not done directly during transport, rather,
700
// it is done only once after each power iteration. This is made possible
701
// by way of a mapping data structure that relates spatial source regions
702
// (FSRs) to tally/filter/score combinations. The mechanism by which the
703
// mapping is done (and the limitations incurred) is documented in the
704
// "convert_source_regions_to_tallies()" function comments above. The present
705
// tally function simply traverses the mapping data structure and executes
706
// the scoring operations to OpenMC's native tally result arrays.
707

708
// TODO: Add support for prompt and delayed nu fission tallies
709
void FlatSourceDomain::random_ray_tally()
69,636✔
710
{
711
  openmc::simulation::time_tallies.start();
69,636✔
712

713
  // Reset our tally volumes to zero
714
  reset_tally_volumes();
69,636✔
715

716
  double source_normalization_factor =
717
    compute_fixed_source_normalization_factor();
69,636✔
718

719
// We loop over all source regions and energy groups. For each
720
// element, we check if there are any scores needed and apply
721
// them.
722
#pragma omp parallel for
37,986✔
723
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
53,285,850✔
724
    // The fsr.volume_ is the unitless fractional simulation averaged volume
725
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
726
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
727
    // entire global simulation domain (as defined by the ray source box).
728
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
729
    // its fraction of the total volume by the total volume. Not important in
730
    // eigenvalue solves, but useful in fixed source solves for returning the
731
    // flux shape with a magnitude that makes sense relative to the fixed
732
    // source strength.
733
    double volume = source_regions_.volume(sr) * simulation_volume_;
53,254,200✔
734

735
    double material = source_regions_.material(sr);
53,254,200✔
736
    for (int g = 0; g < negroups_; g++) {
337,654,800✔
737
      double flux =
738
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
284,400,600✔
739

740
      // Determine numerical score value
741
      for (auto& task : source_regions_.tally_task(sr, g)) {
1,101,997,200✔
742
        double score = 0.0;
817,596,600✔
743
        switch (task.score_type) {
817,596,600!
744

745
        case SCORE_FLUX:
284,440,200✔
746
          score = flux * volume;
284,440,200✔
747
          break;
284,440,200✔
748

749
        case SCORE_TOTAL:
750
          if (material != MATERIAL_VOID) {
×
751
            double sigma_t = sigma_t_[material * negroups_ + g];
752
            score = flux * volume * sigma_t_[material * negroups_ + g];
753
          }
754
          break;
755

756
        case SCORE_FISSION:
266,578,200✔
757
          if (material != MATERIAL_VOID) {
266,578,200!
758
            double sigma_f = sigma_f_[material * negroups_ + g];
266,578,200✔
759
            score = flux * volume * sigma_f_[material * negroups_ + g];
266,578,200✔
760
          }
761
          break;
266,578,200✔
762

763
        case SCORE_NU_FISSION:
266,578,200✔
764
          if (material != MATERIAL_VOID) {
266,578,200!
765
            double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
266,578,200✔
766
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
266,578,200✔
767
          }
768
          break;
266,578,200✔
769

770
        case SCORE_EVENTS:
771
          score = 1.0;
772
          break;
773

774
        case SCORE_PRECURSORS:
775
          // Score precursors in tally_delay_tasks
776
          if (settings::kinetic_simulation &&
×
777
              settings::create_delayed_neutrons) {
778
            break;
779
          } else {
780
            fatal_error("Invalid score specified in tallies.xml. Precursors "
781
                        "are only supported in random ray mode for kinetic "
782
                        "simulations when delayed neutrons are turned on.");
783
          }
784

785
        default:
786
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
787
                      "total, fission, nu-fission, and events are supported in "
788
                      "random ray mode (precursors are supported in kinetic "
789
                      "simulations when delayed neutrons are turned on).");
790
          break;
791
        }
792
        // Apply score to the appropriate tally bin
793
        Tally& tally {*model::tallies[task.tally_idx]};
817,596,600✔
794
#pragma omp atomic
795
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
817,596,600✔
796
          score;
797
      }
798
    }
799
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
53,254,200!
800
      for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
801
        // Determine numerical score value
802
        for (auto& task : source_regions_.tally_delay_task(sr, dg)) {
606,569,600✔
803
          double score = 0.0;
302,848,000✔
804
          switch (task.score_type) {
302,848,000!
805

806
          // Certain scores already tallied
807
          case SCORE_FLUX:
808
          case SCORE_TOTAL:
809
          case SCORE_FISSION:
810
          case SCORE_NU_FISSION:
811
          case SCORE_EVENTS:
812
            break;
813

814
          case SCORE_PRECURSORS:
302,848,000✔
815
            score = source_regions_.precursors_new(sr, dg) *
302,848,000✔
816
                    source_normalization_factor * volume;
817
            break;
302,848,000✔
818

819
          default:
820
            fatal_error(
821
              "Invalid score specified in tallies.xml. Only flux, "
822
              "total, fission, nu-fission, and events are supported in "
823
              "random ray mode (precursors are supported in kinetic "
824
              "simulations when delayed neutrons are turned on).");
825
            break;
826
          }
827

828
          // Apply score to the appropriate tally bin
829
          Tally& tally {*model::tallies[task.tally_idx]};
302,848,000✔
830
#pragma omp atomic
831
          tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
302,848,000✔
832
            score;
833
        }
834
      }
835
    }
836

837
    // For flux and precursor tallies, the total volume of the spatial region is
838
    // needed for normalizing the tally. We store this volume in a separate
839
    // tensor. We only contribute to each volume tally bin once per FSR.
840
    if (volume_normalized_flux_tallies_) {
53,254,200✔
841
      for (const auto& task : source_regions_.volume_task(sr)) {
1,159,441,600✔
842
        if (task.score_type == SCORE_FLUX ||
1,106,478,400✔
843
            task.score_type == SCORE_PRECURSORS) {
826,722,400✔
844
#pragma omp atomic
845
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
582,604,000✔
846
            volume;
847
        }
848
      }
849
    }
850
  } // end FSR loop
851

852
  // Normalize any flux or precursor scores by the total volume of the FSRs
853
  // scoring to that bin. To do this, we loop over all tallies, and then all
854
  // filter bins, and then scores. For each score, we check the tally data
855
  // structure to see what index that score corresponds to. If that score is a
856
  // flux or precursor score, then we divide it by volume.
857
  if (volume_normalized_flux_tallies_) {
69,636✔
858
    for (int i = 0; i < model::tallies.size(); i++) {
199,320✔
859
      Tally& tally {*model::tallies[i]};
133,870✔
860
#pragma omp parallel for
73,020✔
861
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
1,726,025✔
862
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
4,160,550✔
863
          auto score_type = tally.scores_[score_idx];
2,495,375✔
864
          if (score_type == SCORE_FLUX || score_type == SCORE_PRECURSORS) {
2,495,375✔
865
            double vol = tally_volumes_[i](bin, score_idx);
1,665,175✔
866
            if (vol > 0.0) {
1,665,175!
867
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
1,665,175✔
868
            }
869
          }
870
        }
871
      }
872
    }
873
  }
874

875
  openmc::simulation::time_tallies.stop();
69,636✔
876
}
69,636✔
877

878
double FlatSourceDomain::evaluate_flux_at_point(
×
879
  Position r, int64_t sr, int g) const
880
{
881
  return source_regions_.scalar_flux_final(sr, g) /
×
882
         (settings::n_batches - settings::n_inactive);
×
883
}
884

885
// Outputs all basic material, FSR ID, multigroup flux, and
886
// fission source data to .vtk file that can be directly
887
// loaded and displayed by Paraview. Note that .vtk binary
888
// files require big endian byte ordering, so endianness
889
// is checked and flipped if necessary.
890
void FlatSourceDomain::output_to_vtk() const
×
891
{
892
  // Rename .h5 plot filename(s) to .vtk filenames
893
  for (int p = 0; p < model::plots.size(); p++) {
×
894
    PlottableInterface* plot = model::plots[p].get();
×
895
    plot->path_plot() =
×
896
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
897
  }
898

899
  // Print header information
900
  print_plot();
×
901

902
  // Outer loop over plots
903
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
904

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

908
    // Random ray plots only support voxel plots
909
    if (!openmc_plot) {
×
910
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
911
                          "is allowed in random ray mode.",
912
        plt));
913
      continue;
×
914
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
915
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
916
                          "is allowed in random ray mode.",
917
        plt));
918
      continue;
×
919
    }
920

921
    int Nx = openmc_plot->pixels_[0];
×
922
    int Ny = openmc_plot->pixels_[1];
×
923
    int Nz = openmc_plot->pixels_[2];
×
924
    Position origin = openmc_plot->origin_;
×
925
    Position width = openmc_plot->width_;
×
926
    Position ll = origin - width / 2.0;
×
927
    double x_delta = width.x / Nx;
×
928
    double y_delta = width.y / Ny;
×
929
    double z_delta = width.z / Nz;
×
930
    std::string filename = openmc_plot->path_plot();
×
931

932
    // Perform sanity checks on file size
933
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
934
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
935
      openmc_plot->id(), filename, bytes / 1.0e6);
×
936
    if (bytes / 1.0e9 > 1.0) {
×
937
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
938
              "slow.");
939
    } else if (bytes / 1.0e9 > 100.0) {
×
940
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
941
    }
942

943
    // Relate voxel spatial locations to random ray source regions
944
    vector<int> voxel_indices(Nx * Ny * Nz);
×
945
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
946
    vector<double> weight_windows(Nx * Ny * Nz);
×
947
    float min_weight = 1e20;
×
948
#pragma omp parallel for collapse(3) reduction(min : min_weight)
949
    for (int z = 0; z < Nz; z++) {
×
950
      for (int y = 0; y < Ny; y++) {
×
951
        for (int x = 0; x < Nx; x++) {
×
952
          Position sample;
953
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
954
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
955
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
956
          Particle p;
×
957
          p.r() = sample;
958
          p.r_last() = sample;
959
          p.E() = 1.0;
960
          p.E_last() = 1.0;
961
          p.u() = {1.0, 0.0, 0.0};
962

963
          bool found = exhaustive_find_cell(p);
×
964
          if (!found) {
×
965
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
966
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
967
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
968
            continue;
969
          }
970

971
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
972
          int64_t sr = -1;
973
          auto it = source_region_map_.find(sr_key);
×
974
          if (it != source_region_map_.end()) {
×
975
            sr = it->second;
976
          }
977

978
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
979
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
980

981
          if (variance_reduction::weight_windows.size() == 1) {
×
982
            WeightWindow ww =
983
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
984
            float weight = ww.lower_weight;
985
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
986
            if (weight < min_weight)
×
987
              min_weight = weight;
988
          }
989
        }
×
990
      }
991
    }
992

993
    double source_normalization_factor =
994
      compute_fixed_source_normalization_factor();
×
995

996
    // Open file for writing
997
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
998

999
    // Write vtk metadata
1000
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
1001
    std::fprintf(plot, "Dataset File\n");
×
1002
    std::fprintf(plot, "BINARY\n");
×
1003
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
1004
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
1005
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
1006
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
1007
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
1008

1009
    int64_t num_neg = 0;
×
1010
    int64_t num_samples = 0;
×
1011
    float min_flux = 0.0;
×
1012
    float max_flux = -1.0e20;
×
1013
    // Plot multigroup flux data
1014
    for (int g = 0; g < negroups_; g++) {
×
1015
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
1016
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1017
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1018
        int64_t fsr = voxel_indices[i];
×
1019
        int64_t source_element = fsr * negroups_ + g;
×
1020
        float flux = 0;
×
1021
        if (fsr >= 0) {
×
1022
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1023
          if (flux < 0.0)
×
1024
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
1025
              voxel_positions[i], fsr, g);
×
1026
        }
1027
        if (flux < 0.0) {
×
1028
          num_neg++;
×
1029
          if (flux < min_flux) {
×
1030
            min_flux = flux;
×
1031
          }
1032
        }
1033
        if (flux > max_flux)
×
1034
          max_flux = flux;
×
1035
        num_samples++;
×
1036
        flux = convert_to_big_endian<float>(flux);
×
1037
        std::fwrite(&flux, sizeof(float), 1, plot);
×
1038
      }
1039
    }
1040

1041
    // Slightly negative fluxes can be normal when sampling corners of linear
1042
    // source regions. However, very common and high magnitude negative fluxes
1043
    // may indicate numerical instability.
1044
    if (num_neg > 0) {
×
1045
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
1046
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
1047
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
1048
    }
1049

1050
    // Plot FSRs
1051
    std::fprintf(plot, "SCALARS FSRs float\n");
×
1052
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1053
    for (int fsr : voxel_indices) {
×
1054
      float value = future_prn(10, fsr);
×
1055
      value = convert_to_big_endian<float>(value);
×
1056
      std::fwrite(&value, sizeof(float), 1, plot);
×
1057
    }
1058

1059
    // Plot Materials
1060
    std::fprintf(plot, "SCALARS Materials int\n");
×
1061
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1062
    for (int fsr : voxel_indices) {
×
1063
      int mat = -1;
×
1064
      if (fsr >= 0)
×
1065
        mat = source_regions_.material(fsr);
×
1066
      mat = convert_to_big_endian<int>(mat);
×
1067
      std::fwrite(&mat, sizeof(int), 1, plot);
×
1068
    }
1069

1070
    // Plot fission source
1071
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
1072
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
1073
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1074
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1075
        int64_t fsr = voxel_indices[i];
×
1076
        float total_fission = 0.0;
×
1077
        if (fsr >= 0) {
×
1078
          int mat = source_regions_.material(fsr);
×
1079
          if (mat != MATERIAL_VOID) {
×
1080
            for (int g = 0; g < negroups_; g++) {
×
1081
              int64_t source_element = fsr * negroups_ + g;
×
1082
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1083
              double sigma_f = sigma_f_[mat * negroups_ + g];
×
1084
              total_fission += sigma_f * flux;
×
1085
            }
1086
          }
1087
        }
1088
        total_fission = convert_to_big_endian<float>(total_fission);
×
1089
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
1090
      }
1091
    } else {
1092
      std::fprintf(plot, "SCALARS external_source float\n");
×
1093
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1094
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1095
        int64_t fsr = voxel_indices[i];
×
1096
        int mat = source_regions_.material(fsr);
×
1097
        float total_external = 0.0f;
×
1098
        if (fsr >= 0) {
×
1099
          for (int g = 0; g < negroups_; g++) {
×
1100
            // External sources are already divided by sigma_t, so we need to
1101
            // multiply it back to get the true external source.
1102
            double sigma_t = 1.0;
×
1103
            if (mat != MATERIAL_VOID) {
×
1104
              sigma_t = sigma_t_[mat * negroups_ + g];
×
1105
            }
1106
            total_external += source_regions_.external_source(fsr, g) * sigma_t;
×
1107
          }
1108
        }
1109
        total_external = convert_to_big_endian<float>(total_external);
×
1110
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
1111
      }
1112
    }
1113

1114
    // Plot weight window data
1115
    if (variance_reduction::weight_windows.size() == 1) {
×
1116
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
1117
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1118
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1119
        float weight = weight_windows[i];
×
1120
        if (weight == 0.0)
×
1121
          weight = min_weight;
×
1122
        weight = convert_to_big_endian<float>(weight);
×
1123
        std::fwrite(&weight, sizeof(float), 1, plot);
×
1124
      }
1125
    }
1126

1127
    std::fclose(plot);
×
1128
  }
×
1129
}
×
1130

1131
void FlatSourceDomain::apply_external_source_to_source_region(
4,500✔
1132
  int src_idx, SourceRegionHandle& srh)
1133
{
1134
  auto s = model::external_sources[src_idx].get();
4,500✔
1135
  auto is = dynamic_cast<IndependentSource*>(s);
4,500!
1136
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,500!
1137
  double strength_factor = is->strength();
4,500✔
1138
  const auto& discrete_energies = discrete->x();
4,500✔
1139
  const auto& discrete_probs = discrete->prob();
4,500✔
1140

1141
  srh.external_source_present() = 1;
4,500✔
1142

1143
  for (int i = 0; i < discrete_energies.size(); i++) {
9,132✔
1144
    int g = data::mg.get_group_index(discrete_energies[i]);
4,632✔
1145
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,632✔
1146
  }
1147
}
4,500✔
1148

1149
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
416✔
1150
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1151
{
1152
  Cell& cell = *model::cells[i_cell];
416✔
1153

1154
  if (cell.type_ != Fill::MATERIAL)
416!
1155
    return;
×
1156

1157
  for (int j : instances) {
30,784✔
1158
    int cell_material_idx = cell.material(j);
30,368✔
1159
    int cell_material_id;
1160
    if (cell_material_idx == MATERIAL_VOID) {
30,368✔
1161
      cell_material_id = MATERIAL_VOID;
256✔
1162
    } else {
1163
      cell_material_id = model::materials[cell_material_idx]->id();
30,112✔
1164
    }
1165
    if (target_material_id == C_NONE ||
30,368✔
1166
        cell_material_id == target_material_id) {
1167
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,848✔
1168
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,848✔
1169
    }
1170
  }
1171
}
1172

1173
void FlatSourceDomain::apply_external_source_to_cell_and_children(
448✔
1174
  int32_t i_cell, int src_idx, int32_t target_material_id)
1175
{
1176
  Cell& cell = *model::cells[i_cell];
448✔
1177

1178
  if (cell.type_ == Fill::MATERIAL) {
448✔
1179
    vector<int> instances(cell.n_instances());
416✔
1180
    std::iota(instances.begin(), instances.end(), 0);
416✔
1181
    apply_external_source_to_cell_instances(
416✔
1182
      i_cell, src_idx, target_material_id, instances);
1183
  } else if (target_material_id == C_NONE) {
448!
1184
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1185
      cell.get_contained_cells(0, nullptr);
×
1186
    for (const auto& pair : cell_instance_list) {
×
1187
      int32_t i_child_cell = pair.first;
×
1188
      apply_external_source_to_cell_instances(
×
1189
        i_child_cell, src_idx, target_material_id, pair.second);
×
1190
    }
1191
  }
×
1192
}
448✔
1193

1194
void FlatSourceDomain::count_external_source_regions()
2,467✔
1195
{
1196
  n_external_source_regions_ = 0;
2,467✔
1197
#pragma omp parallel for reduction(+ : n_external_source_regions_)
1,389✔
1198
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,394,198✔
1199
    if (source_regions_.external_source_present(sr)) {
1,393,120✔
1200
      n_external_source_regions_++;
157,210✔
1201
    }
1202
  }
1203
}
2,467✔
1204

1205
void FlatSourceDomain::convert_external_sources()
401✔
1206
{
1207
  // Loop over external sources
1208
  for (int es = 0; es < model::external_sources.size(); es++) {
802✔
1209

1210
    // Extract source information
1211
    Source* s = model::external_sources[es].get();
401✔
1212
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
401!
1213
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
401!
1214
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
401✔
1215
    double strength_factor = is->strength();
401✔
1216

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

1222
      // Extract the point source coordinate and find the base source region at
1223
      // that point
1224
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
17!
1225
      GeometryState gs;
17✔
1226
      gs.r() = sp->r();
17✔
1227
      gs.r_last() = sp->r();
17✔
1228
      gs.u() = {1.0, 0.0, 0.0};
17✔
1229
      bool found = exhaustive_find_cell(gs);
17✔
1230
      if (!found) {
17!
1231
        fatal_error(fmt::format("Could not find cell containing external "
×
1232
                                "point source at {}",
1233
          sp->r()));
×
1234
      }
1235
      SourceRegionKey key = lookup_source_region_key(gs);
17✔
1236

1237
      // With the source region and mesh bin known, we can use the
1238
      // accompanying SourceRegionKey as a key into a map that stores the
1239
      // corresponding external source index for the point source. Notably, we
1240
      // do not actually apply the external source to any source regions here,
1241
      // as if mesh subdivision is enabled, they haven't actually been
1242
      // discovered & initilized yet. When discovered, they will read from the
1243
      // external_source_map to determine if there are any external source
1244
      // terms that should be applied.
1245
      external_point_source_map_[key].push_back(es);
17✔
1246

1247
    } else {
17✔
1248
      // If not a point source, then use the volumetric domain constraints to
1249
      // determine which source regions to apply the external source to.
1250
      if (is->domain_type() == Source::DomainType::MATERIAL) {
384✔
1251
        for (int32_t material_id : domain_ids) {
32✔
1252
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
96✔
1253
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
80✔
1254
          }
1255
        }
1256
      } else if (is->domain_type() == Source::DomainType::CELL) {
368✔
1257
        for (int32_t cell_id : domain_ids) {
32✔
1258
          int32_t i_cell = model::cell_map[cell_id];
16✔
1259
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
16✔
1260
        }
1261
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
352!
1262
        for (int32_t universe_id : domain_ids) {
704✔
1263
          int32_t i_universe = model::universe_map[universe_id];
352✔
1264
          Universe& universe = *model::universes[i_universe];
352✔
1265
          for (int32_t i_cell : universe.cells_) {
704✔
1266
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
352✔
1267
          }
1268
        }
1269
      }
1270
    }
1271
  } // End loop over external sources
1272
}
401✔
1273

1274
void FlatSourceDomain::flux_swap()
143,012✔
1275
{
1276
  source_regions_.flux_swap();
143,012✔
1277
}
143,012✔
1278

1279
void FlatSourceDomain::flatten_xs()
833✔
1280
{
1281
  // Temperature and angle indices, if using multiple temperature
1282
  // data sets and/or anisotropic data sets.
1283
  // TODO: Currently assumes we are only using single temp/single angle data.
1284
  const int t = 0;
833✔
1285
  const int a = 0;
833✔
1286

1287
  n_materials_ = data::mg.macro_xs_.size();
833✔
1288
  for (int i = 0; i < n_materials_; i++) {
3,138✔
1289
    auto& m = data::mg.macro_xs_[i];
2,305✔
1290
    for (int g_out = 0; g_out < negroups_; g_out++) {
18,819✔
1291
      if (m.exists_in_model) {
16,514✔
1292
        double sigma_t =
1293
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
16,450✔
1294
        sigma_t_.push_back(sigma_t);
16,450✔
1295

1296
        if (sigma_t < MINIMUM_MACRO_XS) {
16,450✔
1297
          Material* mat = model::materials[i].get();
16✔
1298
          warning(fmt::format(
16✔
1299
            "Material \"{}\" (id: {}) has a group {} total cross section "
1300
            "({:.3e}) below the minimum threshold "
1301
            "({:.3e}). Material will be treated as pure void.",
1302
            mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
32✔
1303
        }
1304

1305
        double nu_sigma_f =
1306
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
16,450✔
1307
        nu_sigma_f_.push_back(nu_sigma_f);
16,450✔
1308

1309
        double sigma_f =
1310
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
16,450✔
1311
        sigma_f_.push_back(sigma_f);
16,450✔
1312

1313
        double chi = m.get_xs(MgxsType::CHI, g_out, &g_out, NULL, NULL, t, a);
16,450✔
1314
        if (!std::isfinite(chi)) {
16,450!
1315
          // MGXS interface may return NaN in some cases, such as when material
1316
          // is fissionable but has very small sigma_f.
UNCOV
1317
          chi = 0.0;
×
1318
        }
1319
        chi_.push_back(chi);
16,450✔
1320

1321
        for (int g_in = 0; g_in < negroups_; g_in++) {
757,510✔
1322
          double sigma_s =
1323
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
741,060✔
1324
          sigma_s_.push_back(sigma_s);
741,060✔
1325
          // For transport corrected XS data, diagonal elements may be negative.
1326
          // In this case, set a flag to enable transport stabilization for the
1327
          // simulation.
1328
          if (g_out == g_in && sigma_s < 0.0)
741,060✔
1329
            is_transport_stabilization_needed_ = true;
2,576✔
1330
        }
1331
        // Prompt cross-sections for kinetic simulations
1332
        if (settings::kinetic_simulation) {
16,450✔
1333
          double chi_p =
1334
            m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
8,416✔
1335
          if (!std::isfinite(chi_p)) {
8,416!
1336
            // MGXS interface may return NaN in some cases, such as when
1337
            // material is fissionable but has very small sigma_f.
NEW
1338
            chi_p = 0.0;
×
1339
          }
1340
          chi_p_.push_back(chi_p);
8,416✔
1341

1342
          double inverse_vbar =
1343
            m.get_xs(MgxsType::INVERSE_VELOCITY, g_out, NULL, NULL, NULL, t, a);
8,416✔
1344
          inverse_vbar_.push_back(inverse_vbar);
8,416✔
1345

1346
          double nu_p_Sigma_f = m.get_xs(
8,416✔
1347
            MgxsType::PROMPT_NU_FISSION, g_out, NULL, NULL, NULL, t, a);
8,416✔
1348
          nu_p_sigma_f_.push_back(nu_p_Sigma_f);
8,416✔
1349
        }
1350
      } else {
1351
        sigma_t_.push_back(0);
64✔
1352
        nu_sigma_f_.push_back(0);
64✔
1353
        sigma_f_.push_back(0);
64✔
1354
        chi_.push_back(0);
64✔
1355
        for (int g_in = 0; g_in < negroups_; g_in++) {
128✔
1356
          sigma_s_.push_back(0);
64✔
1357
        }
1358
        if (settings::kinetic_simulation) {
64!
NEW
1359
          chi_p_.push_back(0);
×
NEW
1360
          inverse_vbar_.push_back(0);
×
NEW
1361
          nu_p_sigma_f_.push_back(0);
×
1362
        }
1363
      }
1364
    }
1365
    // Delayed cross sections for time-dependent simulations
1366
    if (settings::kinetic_simulation) {
2,305✔
1367
      for (int dg = 0; dg < ndgroups_; dg++) {
4,128✔
1368
        if (m.exists_in_model) {
3,584!
1369
          double lambda =
1370
            m.get_xs(MgxsType::DECAY_RATE, 0, NULL, NULL, &dg, t, a);
3,584✔
1371
          lambda_.push_back(lambda);
3,584✔
1372
          for (int g_out = 0; g_out < negroups_; g_out++) {
56,320✔
1373
            double nu_d_Sigma_f = m.get_xs(
52,736✔
1374
              MgxsType::DELAYED_NU_FISSION, g_out, NULL, NULL, &dg, t, a);
52,736✔
1375
            nu_d_sigma_f_.push_back(nu_d_Sigma_f);
52,736✔
1376
            double chi_d =
1377
              m.get_xs(MgxsType::CHI_DELAYED, g_out, &g_out, NULL, &dg, t, a);
52,736✔
1378
            if (!std::isfinite(chi_d)) {
52,736!
1379
              // MGXS interface may return NaN in some cases, such as when
1380
              // material is fissionable but has very small sigma_f.
NEW
1381
              chi_d = 0.0;
×
1382
            }
1383
            chi_d_.push_back(chi_d);
52,736✔
1384
          }
1385
        } else {
NEW
1386
          lambda_.push_back(0);
×
NEW
1387
          for (int g_out = 0; g_out < negroups_; g_out++) {
×
NEW
1388
            nu_d_sigma_f_.push_back(0);
×
NEW
1389
            chi_d_.push_back(0);
×
1390
          }
1391
        }
1392
      }
1393
    }
1394
  }
1395
}
833✔
1396

1397
void FlatSourceDomain::set_adjoint_sources()
65✔
1398
{
1399
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1400
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1401
  // as this is likely a very small source region that we don't need to bother
1402
  // trying to vector particles towards. In the case of flux "being extremely
1403
  // close to zero", we define this as being a fixed fraction of the maximum
1404
  // forward flux, below which we assume the flux would be physically
1405
  // undetectable.
1406

1407
  // First, find the maximum forward flux value
1408
  double max_flux = 0.0;
65✔
1409
#pragma omp parallel for reduction(max : max_flux)
37✔
1410
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,548✔
1411
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1412
    if (flux > max_flux) {
155,520✔
1413
      max_flux = flux;
25✔
1414
    }
1415
  }
1416

1417
  // Then, compute the adjoint source for each source region
1418
#pragma omp parallel for
37✔
1419
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1420
    for (int g = 0; g < negroups_; g++) {
311,040✔
1421
      double flux = source_regions_.scalar_flux_final(sr, g);
155,520✔
1422
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
155,520✔
1423
        source_regions_.external_source(sr, g) = 0.0;
325✔
1424
      } else {
1425
        source_regions_.external_source(sr, g) = 1.0 / flux;
155,195✔
1426
      }
1427
      if (flux > 0.0) {
155,520✔
1428
        source_regions_.external_source_present(sr) = 1;
155,195✔
1429
      }
1430
      source_regions_.scalar_flux_final(sr, g) = 0.0;
155,520✔
1431
    }
1432
  }
1433

1434
  // "Small" source regions in OpenMC are defined as those that are hit by
1435
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1436
  // very small volumes combined with a low aspect ratio, and are often
1437
  // generated when applying a source region mesh that clips the edge of a
1438
  // curved surface. As perhaps only a few rays will visit these regions over
1439
  // the entire forward simulation, the forward flux estimates are extremely
1440
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1441
  // extremely low, leading to unphysically large adjoint source terms,
1442
  // resulting in weight windows that aggressively try to drive particles
1443
  // towards these regions. To fix this, we simply filter out any "small" source
1444
  // regions from consideration. If a source region is "small", we
1445
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1446
  // flux solution, as the true total adjoint source contribution from small
1447
  // regions is likely to be negligible.
1448
#pragma omp parallel for
37✔
1449
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1450
    if (source_regions_.is_small(sr)) {
155,520!
1451
      for (int g = 0; g < negroups_; g++) {
×
1452
        source_regions_.external_source(sr, g) = 0.0;
1453
      }
1454
      source_regions_.external_source_present(sr) = 0;
1455
    }
1456
  }
1457
  // Divide the fixed source term by sigma t (to save time when applying each
1458
  // iteration)
1459
#pragma omp parallel for
37✔
1460
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1461
    int material = source_regions_.material(sr);
155,520✔
1462
    if (material == MATERIAL_VOID) {
155,520!
1463
      continue;
1464
    }
1465
    for (int g = 0; g < negroups_; g++) {
311,040✔
1466
      double sigma_t = sigma_t_[material * negroups_ + g];
155,520✔
1467
      source_regions_.external_source(sr, g) /= sigma_t;
155,520✔
1468
    }
1469
  }
1470
}
65✔
1471

1472
void FlatSourceDomain::transpose_scattering_matrix()
81✔
1473
{
1474
  // Transpose the inner two dimensions for each material
1475
  for (int m = 0; m < n_materials_; ++m) {
306✔
1476
    int material_offset = m * negroups_ * negroups_;
225✔
1477
    for (int i = 0; i < negroups_; ++i) {
643✔
1478
      for (int j = i + 1; j < negroups_; ++j) {
1,091✔
1479
        // Calculate indices of the elements to swap
1480
        int idx1 = material_offset + i * negroups_ + j;
673✔
1481
        int idx2 = material_offset + j * negroups_ + i;
673✔
1482

1483
        // Swap the elements to transpose the matrix
1484
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
673✔
1485
      }
1486
    }
1487
  }
1488
}
81✔
1489

1490
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1491
{
1492
  // Ensure array is correct size
1493
  flux.resize(n_source_regions() * negroups_);
×
1494
// Serialize the final fluxes for output
1495
#pragma omp parallel for
1496
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1497
    flux[se] = source_regions_.scalar_flux_final(se);
1498
  }
1499
}
×
1500

NEW
1501
void FlatSourceDomain::serialize_final_sources(vector<double>& source)
×
1502
{
1503
  // Ensure array is correct size
NEW
1504
  source.resize(n_source_regions() * negroups_);
×
1505
  // Serialize the final sources for output
1506
#pragma omp parallel for
1507
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1508
    source[se] = source_regions_.source_final(se);
1509
  }
NEW
1510
}
×
1511

1512
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
1,410✔
1513
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1514
  bool is_target_void)
1515
{
1516
  Cell& cell = *model::cells[i_cell];
1,410✔
1517
  if (cell.type_ != Fill::MATERIAL)
1,410!
1518
    return;
×
1519
  for (int32_t j : instances) {
184,132✔
1520
    int cell_material_idx = cell.material(j);
182,722✔
1521
    int cell_material_id = (cell_material_idx == C_NONE)
1522
                             ? C_NONE
182,722✔
1523
                             : model::materials[cell_material_idx]->id();
182,721✔
1524

1525
    if ((target_material_id == C_NONE && !is_target_void) ||
182,722!
1526
        cell_material_id == target_material_id) {
1527
      int64_t sr = source_region_offsets_[i_cell] + j;
150,722✔
1528
      // Check if the key is already present in the mesh_map_
1529
      if (mesh_map_.find(sr) != mesh_map_.end()) {
150,722!
1530
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1531
                                "applied, but trying to apply mesh idx {}",
1532
          sr, mesh_map_[sr], mesh_idx));
×
1533
      }
1534
      // If the SR has not already been assigned, then we can write to it
1535
      mesh_map_[sr] = mesh_idx;
150,722✔
1536
    }
1537
  }
1538
}
1539

1540
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
1,089✔
1541
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1542
{
1543
  Cell& cell = *model::cells[i_cell];
1,089✔
1544

1545
  if (cell.type_ == Fill::MATERIAL) {
1,089✔
1546
    vector<int> instances(cell.n_instances());
928✔
1547
    std::iota(instances.begin(), instances.end(), 0);
928✔
1548
    apply_mesh_to_cell_instances(
928✔
1549
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1550
  } else if (target_material_id == C_NONE && !is_target_void) {
1,089!
1551
    for (int j = 0; j < cell.n_instances(); j++) {
194✔
1552
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1553
        cell.get_contained_cells(j, nullptr);
97✔
1554
      for (const auto& pair : cell_instance_list) {
579✔
1555
        int32_t i_child_cell = pair.first;
482✔
1556
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
482✔
1557
          pair.second, is_target_void);
482✔
1558
      }
1559
    }
97✔
1560
  }
1561
}
1,089✔
1562

1563
void FlatSourceDomain::apply_meshes()
833✔
1564
{
1565
  // Skip if there are no mappings between mesh IDs and domains
1566
  if (mesh_domain_map_.empty())
833✔
1567
    return;
448✔
1568

1569
  // Loop over meshes
1570
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
882✔
1571
    Mesh* mesh = model::meshes[mesh_idx].get();
497✔
1572
    int mesh_id = mesh->id();
497✔
1573

1574
    // Skip if mesh id is not present in the map
1575
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
497✔
1576
      continue;
48✔
1577

1578
    // Loop over domains associated with the mesh
1579
    for (auto& domain : mesh_domain_map_[mesh_id]) {
898✔
1580
      Source::DomainType domain_type = domain.first;
449✔
1581
      int domain_id = domain.second;
449✔
1582

1583
      if (domain_type == Source::DomainType::MATERIAL) {
449✔
1584
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1585
          if (domain_id == C_NONE) {
160!
1586
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1587
          } else {
1588
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1589
          }
1590
        }
1591
      } else if (domain_type == Source::DomainType::CELL) {
417✔
1592
        int32_t i_cell = model::cell_map[domain_id];
32✔
1593
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1594
      } else if (domain_type == Source::DomainType::UNIVERSE) {
385!
1595
        int32_t i_universe = model::universe_map[domain_id];
385✔
1596
        Universe& universe = *model::universes[i_universe];
385✔
1597
        for (int32_t i_cell : universe.cells_) {
1,282✔
1598
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
897✔
1599
        }
1600
      }
1601
    }
1602
  }
1603
}
1604

1605
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
2,147,483,647✔
1606
  SourceRegionKey sr_key, Position r, Direction u)
1607
{
1608
  // Case 1: Check if the source region key is already present in the permanent
1609
  // map. This is the most common condition, as any source region visited in a
1610
  // previous power iteration will already be present in the permanent map. If
1611
  // the source region key is found, we translate the key into a specific 1D
1612
  // source region index and return a handle its position in the
1613
  // source_regions_ vector.
1614
  auto it = source_region_map_.find(sr_key);
2,147,483,647✔
1615
  if (it != source_region_map_.end()) {
2,147,483,647✔
1616
    int64_t sr = it->second;
2,147,483,647✔
1617
    return source_regions_.get_source_region_handle(sr);
2,147,483,647✔
1618
  }
1619

1620
  // Case 2: Check if the source region key is present in the temporary (thread
1621
  // safe) map. This is a common occurrence in the first power iteration when
1622
  // the source region has already been visited already by some other ray. We
1623
  // begin by locking the temporary map before any operations are performed. The
1624
  // lock is not global over the full data structure -- it will be dependent on
1625
  // which key is used.
1626
  discovered_source_regions_.lock(sr_key);
56,961,980✔
1627

1628
  // If the key is found in the temporary map, then we return a handle to the
1629
  // source region that is stored in the temporary map.
1630
  if (discovered_source_regions_.contains(sr_key)) {
56,961,980✔
1631
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
54,551,375✔
1632
    discovered_source_regions_.unlock(sr_key);
54,551,375✔
1633
    return handle;
54,551,375✔
1634
  }
1635

1636
  // Case 3: The source region key is not present anywhere, but it is only due
1637
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1638
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1639
  // result in the ray tracer detecting an additional (very short) segment
1640
  // though a mesh bin that is actually past the physical source region
1641
  // boundary. This is a result of the the multi-level ray tracing treatment in
1642
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1643
  // result in the wrong surface being selected as the nearest. This can happen
1644
  // in a lattice when there are two directions that both are very close in
1645
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1646
  // treated as being equivalent so alternative logic is used. However, when we
1647
  // go and ray trace on this with the mesh tracer we may go past the surface
1648
  // bounding the current source region.
1649
  //
1650
  // To filter out this case, before we create the new source region, we double
1651
  // check that the actual starting point of this segment (r) is still in the
1652
  // same geometry source region that we started in. If an artifact is detected,
1653
  // we discard the segment (and attenuation through it) as it is not really a
1654
  // valid source region and will have only an infinitessimally small cell
1655
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1656
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1657
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1658
  // consequences for the general ray tracer, so for now we do the below sanity
1659
  // checks before generating phantom source regions. A significant extra cost
1660
  // is incurred in instantiating the GeometryState object and doing a cell
1661
  // lookup, but again, this is going to be an extremely rare thing to check
1662
  // after the first power iteration has completed.
1663

1664
  // Sanity check on source region id
1665
  GeometryState gs;
2,410,605✔
1666
  gs.r() = r + TINY_BIT * u;
2,410,605✔
1667
  gs.u() = {1.0, 0.0, 0.0};
2,410,605✔
1668
  exhaustive_find_cell(gs);
2,410,605✔
1669
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,410,605✔
1670
  if (sr_found != sr_key.base_source_region_id) {
2,410,605✔
1671
    discovered_source_regions_.unlock(sr_key);
858✔
1672
    SourceRegionHandle handle;
858✔
1673
    handle.is_numerical_fp_artifact_ = true;
858✔
1674
    return handle;
858✔
1675
  }
1676

1677
  // Sanity check on mesh bin
1678
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,409,747✔
1679
  if (mesh_idx == C_NONE) {
2,409,747✔
1680
    if (sr_key.mesh_bin != 0) {
369,908!
1681
      discovered_source_regions_.unlock(sr_key);
×
1682
      SourceRegionHandle handle;
×
1683
      handle.is_numerical_fp_artifact_ = true;
×
1684
      return handle;
×
1685
    }
1686
  } else {
1687
    Mesh* mesh = model::meshes[mesh_idx].get();
2,039,839✔
1688
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
2,039,839✔
1689
    if (bin_found != sr_key.mesh_bin) {
2,039,839!
1690
      discovered_source_regions_.unlock(sr_key);
×
1691
      SourceRegionHandle handle;
×
1692
      handle.is_numerical_fp_artifact_ = true;
×
1693
      return handle;
×
1694
    }
1695
  }
1696

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

1704
  // Call the basic constructor for the source region and store in the parallel
1705
  // map.
1706
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,409,747✔
1707
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
2,409,747✔
1708
    sr_key, {negroups_, ndgroups_, is_linear});
1709
  SourceRegionHandle handle {*sr_ptr};
2,409,747✔
1710

1711
  // Determine the material
1712
  int gs_i_cell = gs.lowest_coord().cell();
2,409,747✔
1713
  Cell& cell = *model::cells[gs_i_cell];
2,409,747✔
1714
  int material = cell.material(gs.cell_instance());
2,409,747✔
1715

1716
  // If material total XS is extremely low, just set it to void to avoid
1717
  // problems with 1/Sigma_t
1718
  for (int g = 0; g < negroups_; g++) {
5,634,179✔
1719
    double sigma_t = sigma_t_[material * negroups_ + g];
3,246,584✔
1720
    if (sigma_t < MINIMUM_MACRO_XS) {
3,246,584✔
1721
      material = MATERIAL_VOID;
22,152✔
1722
      break;
22,152✔
1723
    }
1724
  }
1725

1726
  if (settings::kinetic_simulation && material == MATERIAL_VOID) {
2,409,747!
NEW
1727
    fatal_error("Explicit void treatment for kinetic simulations "
×
1728
                " is not currently supported.");
1729
  }
1730

1731
  handle.material() = material;
2,409,747✔
1732

1733
  // Store the mesh index (if any) assigned to this source region
1734
  handle.mesh() = mesh_idx;
2,409,747✔
1735

1736
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,409,747✔
1737
    // Determine if there are any volumetric sources, and apply them.
1738
    // Volumetric sources are specifc only to the base SR idx.
1739
    auto it_vol =
1740
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,291,750✔
1741
    if (it_vol != external_volumetric_source_map_.end()) {
2,291,750✔
1742
      const vector<int>& vol_sources = it_vol->second;
4,488✔
1743
      for (int src_idx : vol_sources) {
8,976✔
1744
        apply_external_source_to_source_region(src_idx, handle);
4,488✔
1745
      }
1746
    }
1747

1748
    // Determine if there are any point sources, and apply them.
1749
    // Point sources are specific to the source region key.
1750
    auto it_point = external_point_source_map_.find(sr_key);
2,291,750✔
1751
    if (it_point != external_point_source_map_.end()) {
2,291,750✔
1752
      const vector<int>& point_sources = it_point->second;
12✔
1753
      for (int src_idx : point_sources) {
24✔
1754
        apply_external_source_to_source_region(src_idx, handle);
12✔
1755
      }
1756
    }
1757

1758
    // Divide external source term by sigma_t
1759
    if (material != C_NONE) {
2,291,750✔
1760
      for (int g = 0; g < negroups_; g++) {
4,585,155✔
1761
        double sigma_t = sigma_t_[material * negroups_ + g];
2,315,557✔
1762
        handle.external_source(g) /= sigma_t;
2,315,557✔
1763
      }
1764
    }
1765
  }
1766

1767
  // Compute the combined source term
1768
  update_single_neutron_source(handle);
2,409,747✔
1769
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
2,409,747!
NEW
1770
    if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
×
NEW
1771
      compute_single_phi_prime(handle);
×
NEW
1772
    else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
×
NEW
1773
      compute_single_T1(handle);
×
1774
  }
1775

1776
  // Unlock the parallel map. Note: we may be tempted to release
1777
  // this lock earlier, and then just use the source region's lock to protect
1778
  // the flux/source initialization stages above. However, the rest of the code
1779
  // only protects updates to the new flux and volume fields, and assumes that
1780
  // the source is constant for the duration of transport. Thus, using just the
1781
  // source region's lock by itself would result in other threads potentially
1782
  // reading from the source before it is computed, as they won't use the lock
1783
  // when only reading from the SR's source. It would be expensive to protect
1784
  // those operations, whereas generating the SR is only done once, so we just
1785
  // hold the map's bucket lock until the source region is fully initialized.
1786
  discovered_source_regions_.unlock(sr_key);
2,409,747✔
1787

1788
  return handle;
2,409,747✔
1789
}
2,410,605✔
1790

1791
void FlatSourceDomain::finalize_discovered_source_regions()
143,012✔
1792
{
1793
  // Extract keys for entries with a valid volume.
1794
  vector<SourceRegionKey> keys;
143,012✔
1795
  for (const auto& pair : discovered_source_regions_) {
2,552,759✔
1796
    if (pair.second.volume_ > 0.0) {
2,409,747✔
1797
      keys.push_back(pair.first);
2,308,283✔
1798
    }
1799
  }
1800

1801
  if (!keys.empty()) {
143,012✔
1802
    // Sort the keys, so as to ensure reproducible ordering given that source
1803
    // regions may have been added to discovered_source_regions_ in an arbitrary
1804
    // order due to shared memory threading.
1805
    std::sort(keys.begin(), keys.end());
925✔
1806

1807
    // Remember the index of the first new source region
1808
    int64_t start_sr_id = source_regions_.n_source_regions();
925✔
1809

1810
    // Append the source regions in the sorted key order.
1811
    for (const auto& key : keys) {
2,309,208✔
1812
      const SourceRegion& sr = discovered_source_regions_[key];
2,308,283✔
1813
      source_region_map_[key] = source_regions_.n_source_regions();
2,308,283✔
1814
      source_regions_.push_back(sr);
2,308,283✔
1815
    }
1816

1817
    // Map all new source regions to tallies
1818
    convert_source_regions_to_tallies(start_sr_id);
925✔
1819
  }
1820

1821
  discovered_source_regions_.clear();
143,012✔
1822
}
143,012✔
1823

1824
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1825
//
1826
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1827
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1828
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1829
// https://doi.org/10.1016/j.anucene.2018.10.036.
1830
void FlatSourceDomain::apply_transport_stabilization()
143,012✔
1831
{
1832
  // Don't do anything if all in-group scattering
1833
  // cross sections are positive
1834
  if (!is_transport_stabilization_needed_) {
143,012✔
1835
    return;
139,712✔
1836
  }
1837

1838
  // Apply the stabilization factor to all source elements
1839
#pragma omp parallel for
1,800✔
1840
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
148,300✔
1841
    int material = source_regions_.material(sr);
146,800✔
1842
    if (material == MATERIAL_VOID) {
146,800!
1843
      continue;
1844
    }
1845
    for (int g = 0; g < negroups_; g++) {
10,422,800✔
1846
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1847
      // negative
1848
      double sigma_s =
1849
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g];
10,276,000✔
1850
      if (sigma_s < 0.0) {
10,276,000✔
1851
        double sigma_t = sigma_t_[material * negroups_ + g];
3,550,000✔
1852
        double phi_new = source_regions_.scalar_flux_new(sr, g);
3,550,000✔
1853
        double phi_old = source_regions_.scalar_flux_old(sr, g);
3,550,000✔
1854

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

1865
        // Equation 16 in the above Gunow et al. 2019 paper
1866
        source_regions_.scalar_flux_new(sr, g) =
3,550,000✔
1867
          (phi_new - D * phi_old) / (1.0 - D);
3,550,000✔
1868
      }
1869
    }
1870
  }
1871
}
1872

1873
// Determines the base source region index (i.e., a material filled cell
1874
// instance) that corresponds to a particular location in the geometry. Requires
1875
// that the "gs" object passed in has already been initialized and has called
1876
// find_cell etc.
1877
int64_t FlatSourceDomain::lookup_base_source_region_idx(
2,147,483,647✔
1878
  const GeometryState& gs) const
1879
{
1880
  int i_cell = gs.lowest_coord().cell();
2,147,483,647✔
1881
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
2,147,483,647✔
1882
  return sr;
2,147,483,647✔
1883
}
1884

1885
// Determines the index of the mesh (if any) that has been applied
1886
// to a particular base source region index.
1887
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
2,147,483,647✔
1888
{
1889
  int mesh_idx = C_NONE;
2,147,483,647✔
1890
  auto mesh_it = mesh_map_.find(sr);
2,147,483,647✔
1891
  if (mesh_it != mesh_map_.end()) {
2,147,483,647✔
1892
    mesh_idx = mesh_it->second;
2,147,483,647✔
1893
  }
1894
  return mesh_idx;
2,147,483,647✔
1895
}
1896

1897
// Determines the source region key that corresponds to a particular location in
1898
// the geometry. This takes into account both the base source region index as
1899
// well as the mesh bin if a mesh is applied to this source region for
1900
// subdivision.
1901
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
15,001,437✔
1902
  const GeometryState& gs) const
1903
{
1904
  int64_t sr = lookup_base_source_region_idx(gs);
15,001,437✔
1905
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
15,001,437✔
1906
  return SourceRegionKey {sr, mesh_bin};
15,001,437✔
1907
}
1908

1909
// Determines the mesh bin that corresponds to a particular base source region
1910
// index and position.
1911
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
15,001,437✔
1912
{
1913
  int mesh_idx = lookup_mesh_idx(sr);
15,001,437✔
1914
  int mesh_bin = 0;
15,001,437✔
1915
  if (mesh_idx != C_NONE) {
15,001,437✔
1916
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
8,119,837✔
1917
  }
1918
  return mesh_bin;
15,001,437✔
1919
}
1920

1921
//------------------------------------------------------------------------------
1922
// Methods for kinetic simulations
1923

1924
// Generates new estimate of k_dynamic based on the fraction between this
1925
// timestep's estimate of neutron production and loss. (previous timestep
1926
// fission vs current timestep fission?)
1927
// TODO: implement compute_k_dynamic
1928

1929
// Compute new estimate of scattering + fission (+ precursor decay for
1930
// kinetic simulations) sources in each source region based on the flux
1931
// estimate from the previous iteration.
1932

1933
void FlatSourceDomain::compute_single_phi_prime(SourceRegionHandle& srh)
119,262,000✔
1934
{
1935
  double A0 =
1936
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
119,262,000✔
1937
  int material = srh.material();
119,262,000✔
1938
  for (int g = 0; g < negroups_; g++) {
960,445,200✔
1939
    double inverse_vbar = inverse_vbar_[material * negroups_ + g];
841,183,200✔
1940
    // TODO: add support for explicit void
1941
    double sigma_t = sigma_t_[material * negroups_ + g];
841,183,200✔
1942

1943
    double scalar_flux_time_derivative =
1944
      A0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd(g);
841,183,200✔
1945
    srh.phi_prime(g) = scalar_flux_time_derivative * inverse_vbar / sigma_t;
841,183,200✔
1946
  }
1947
}
119,262,000✔
1948

1949
// T1 calculation
1950
void FlatSourceDomain::compute_single_T1(SourceRegionHandle& srh)
286,000✔
1951
{
1952
  double A0 =
1953
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
286,000✔
1954
  double B0 = (bd_coefficients_second_order_.at(RandomRay::bd_order_))[0] /
286,000✔
1955
              (settings::dt * settings::dt);
286,000✔
1956
  int material = srh.material();
286,000✔
1957
  for (int g = 0; g < negroups_; g++) {
8,637,200✔
1958
    double inverse_vbar = inverse_vbar_[material * negroups_ + g];
8,351,200✔
1959
    // TODO: add support for explicit void
1960
    double sigma_t = sigma_t_[material * negroups_ + g];
8,351,200✔
1961

1962
    // Multiply out sigma_t to correctly compute the derivative term
1963
    float source_time_derivative =
1964
      A0 * srh.source(g) * sigma_t + srh.source_rhs_bd(g);
8,351,200✔
1965

1966
    double scalar_flux_time_derivative_2 =
1967
      B0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd_2(g);
8,351,200✔
1968
    scalar_flux_time_derivative_2 *= inverse_vbar;
8,351,200✔
1969

1970
    // Divide by sigma_t to save time during transport
1971
    srh.T1(g) =
8,351,200✔
1972
      (source_time_derivative - scalar_flux_time_derivative_2) / sigma_t;
8,351,200✔
1973
  }
1974
}
286,000✔
1975

1976
void FlatSourceDomain::compute_single_delayed_fission_source(
167,367,156✔
1977
  SourceRegionHandle& srh)
1978
{
1979

1980
  // Reset all delayed fission sources to zero (important for void regions)
1981
  for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
1982
    srh.delayed_fission_source(dg) = 0.0;
1,337,335,648✔
1983
  }
1984

1985
  int material = srh.material();
167,367,156✔
1986
  if (material != MATERIAL_VOID) {
167,367,156!
1987
    double inverse_k_eff = 1.0 / k_eff_;
167,367,156✔
1988
    for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
1989
      // We cannot have delayed neutrons if there is no delayed data
1990
      double lambda = lambda_[material * ndgroups_ + dg];
1,337,335,648✔
1991
      if (lambda != 0.0) {
1,337,335,648✔
1992
        for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
1993
          double scalar_flux = scalar_flux = srh.scalar_flux_new(g);
2,147,483,647✔
1994
          double nu_d_sigma_f = nu_d_sigma_f_[material * negroups_ * ndgroups_ +
2,147,483,647✔
1995
                                              dg * negroups_ + g];
2,147,483,647✔
1996
          srh.delayed_fission_source(dg) += nu_d_sigma_f * scalar_flux;
2,147,483,647✔
1997
        }
1998
        srh.delayed_fission_source(dg) *= inverse_k_eff;
616,492,624✔
1999
      }
2000
    }
2001
  }
2002
}
167,367,156✔
2003

2004
void FlatSourceDomain::compute_single_precursors(SourceRegionHandle& srh)
167,367,156✔
2005
{
2006
  // Reset all precursors to zero (important for void regions)
2007
  for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2008
    srh.precursors_new(dg) = 0.0;
1,337,335,648✔
2009
  }
2010

2011
  int material = srh.material();
167,367,156✔
2012
  if (material != MATERIAL_VOID) {
167,367,156!
2013
    for (int dg = 0; dg < ndgroups_; dg++) {
1,504,702,804✔
2014
      double lambda = lambda_[material * ndgroups_ + dg];
1,337,335,648✔
2015
      if (lambda != 0.0) {
1,337,335,648✔
2016
        double delayed_fission_source = srh.delayed_fission_source(dg);
616,492,624✔
2017
        if (simulation::is_initial_condition) {
616,492,624✔
2018
          srh.precursors_new(dg) = delayed_fission_source / lambda;
176,140,624✔
2019
        } else {
2020
          double precursor_rhs_bd = srh.precursors_rhs_bd(dg);
440,352,000✔
2021
          srh.precursors_new(dg) = delayed_fission_source - precursor_rhs_bd;
440,352,000✔
2022
          double A0 =
2023
            (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] /
440,352,000✔
2024
            settings::dt;
440,352,000✔
2025
          srh.precursors_new(dg) /= A0 + lambda;
440,352,000✔
2026
        }
2027
      }
2028
    }
2029
  }
2030
}
167,367,156✔
2031

2032
void FlatSourceDomain::compute_all_precursors()
130,900✔
2033
{
2034
  simulation::time_compute_precursors.start();
130,900✔
2035

2036
#pragma omp parallel for
71,400✔
2037
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
76,135,480✔
2038
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
76,075,980✔
2039
    compute_single_delayed_fission_source(srh);
76,075,980✔
2040
    compute_single_precursors(srh);
76,075,980✔
2041
  }
2042

2043
  simulation::time_compute_precursors.start();
130,900✔
2044
}
130,900✔
2045

NEW
2046
void FlatSourceDomain::serialize_final_precursors(vector<double>& precursors)
×
2047
{
2048
  // Ensure array is correct size
NEW
2049
  precursors.resize(n_source_regions() * ndgroups_);
×
2050
// Serialize the precursors for output
2051
#pragma omp parallel for
2052
  for (int64_t de = 0; de < n_delay_elements(); de++) {
×
2053
    precursors[de] = source_regions_.precursors_final(de);
2054
  }
NEW
2055
}
×
2056

2057
void FlatSourceDomain::precursors_swap()
130,900✔
2058
{
2059
  source_regions_.precursors_swap();
130,900✔
2060
}
130,900✔
2061

2062
void FlatSourceDomain::accumulate_iteration_quantities()
69,636✔
2063
{
2064
  accumulate_iteration_flux();
69,636✔
2065
  if (settings::kinetic_simulation) {
69,636✔
2066
#pragma omp parallel for
35,280✔
2067
    for (int64_t sr = 0; sr < n_source_regions(); sr++) {
38,031,000✔
2068
      for (int g = 0; g < negroups_; g++) {
305,760,000✔
2069
        if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
267,758,400✔
2070
          source_regions_.source_final(sr, g) += source_regions_.source(sr, g);
1,383,200✔
2071
        }
2072
      }
2073
      if (settings::create_delayed_neutrons) {
38,001,600!
2074
        for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
2075
          source_regions_.precursors_final(sr, dg) +=
303,721,600✔
2076
            source_regions_.precursors_new(sr, dg);
303,721,600✔
2077
        }
2078
      }
2079
    }
2080
  }
2081
}
69,636✔
2082

2083
void FlatSourceDomain::normalize_final_quantities()
2,066✔
2084
{
2085
  double normalization_factor =
2,066✔
2086
    1.0 / (settings::n_batches - settings::n_inactive);
2,066✔
2087
  double source_normalization_factor;
2088
  if (!settings::kinetic_simulation ||
2,066!
2089
      settings::kinetic_simulation &&
1,344✔
2090
        simulation::current_timestep == settings::n_timesteps)
1,344✔
2091
    source_normalization_factor =
914✔
2092
      compute_fixed_source_normalization_factor() * normalization_factor;
914✔
2093
  else
2094
    // The source normalization should only be applied to internal quantities at
2095
    // the end of time stepping in preparation for an adjoint solve.
2096
    source_normalization_factor = 1.0 * normalization_factor;
1,152✔
2097

2098
#pragma omp parallel for
1,163✔
2099
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,394,023✔
2100
    for (int g = 0; g < negroups_; g++) {
4,596,560✔
2101
      source_regions_.scalar_flux_final(sr, g) *= source_normalization_factor;
3,203,440✔
2102
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
3,203,440✔
2103
        source_regions_.source_final(sr, g) *= source_normalization_factor;
276,640✔
2104
      }
2105
    }
2106
    if (settings::kinetic_simulation) {
1,393,120✔
2107
      for (int dg = 0; dg < ndgroups_; dg++) {
1,907,360✔
2108
        source_regions_.precursors_final(sr, dg) *= source_normalization_factor;
1,688,960✔
2109
      }
2110
    }
2111
  }
2112
}
2,066✔
2113

2114
void FlatSourceDomain::propagate_final_quantities()
1,152✔
2115
{
2116
#pragma omp parallel for
648✔
2117
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,704✔
2118
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2119
      source_regions_.scalar_flux_old(sr, g) =
1,609,920✔
2120
        source_regions_.scalar_flux_final(sr, g);
1,609,920✔
2121
    }
2122
    if (settings::create_delayed_neutrons) {
187,200!
2123
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2124
        source_regions_.precursors_old(sr, dg) =
1,447,680✔
2125
          source_regions_.precursors_final(sr, dg);
1,447,680✔
2126
      }
2127
    }
2128
  }
2129
}
1,152✔
2130

2131
void FlatSourceDomain::store_time_step_quantities(bool increment_not_initialize)
1,152✔
2132
{
2133
#pragma omp parallel for
648✔
2134
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,704✔
2135
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2136
      int j = 0;
1,609,920✔
2137
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
1,609,920✔
2138
        j = 1;
237,120✔
2139
      add_value_to_bd_vector(source_regions_.scalar_flux_bd(sr, g),
1,609,920✔
2140
        source_regions_.scalar_flux_final(sr, g), increment_not_initialize,
2141
        RandomRay::bd_order_ + j);
2142
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,609,920✔
2143
        // Multiply out sigma_t to store the base source
2144
        int material = source_regions_.material(sr);
237,120✔
2145
        // TODO: add support for explicit void regions
2146
        double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g];
237,120✔
2147
        float source = source_regions_.source_final(sr, g) * sigma_t;
237,120✔
2148
        add_value_to_bd_vector(source_regions_.source_bd(sr, g), source,
237,120✔
2149
          increment_not_initialize, RandomRay::bd_order_);
2150
      }
2151
    }
2152
    if (settings::create_delayed_neutrons) {
187,200!
2153
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2154
        add_value_to_bd_vector(source_regions_.precursors_bd(sr, dg),
1,447,680✔
2155
          source_regions_.precursors_final(sr, dg), increment_not_initialize,
2156
          RandomRay::bd_order_);
2157
      }
2158
    }
2159
  }
2160
}
1,152✔
2161

2162
void FlatSourceDomain::compute_rhs_bd_quantities()
960✔
2163
{
2164
#pragma omp parallel for
540✔
2165
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
156,420✔
2166
    for (int g = 0; g < negroups_; g++) {
1,497,600✔
2167
      source_regions_.scalar_flux_rhs_bd(sr, g) =
2,683,200✔
2168
        rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
1,341,600✔
2169
          RandomRay::bd_order_, settings::dt);
2170

2171
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,341,600✔
2172
        source_regions_.source_rhs_bd(sr, g) = rhs_backwards_difference(
197,600✔
2173
          source_regions_.source_bd(sr, g), RandomRay::bd_order_, settings::dt);
2174

2175
        source_regions_.scalar_flux_rhs_bd_2(sr, g) =
197,600✔
2176
          rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
197,600✔
2177
            RandomRay::bd_order_, settings::dt, 2);
2178
      }
2179
    }
2180
    if (settings::create_delayed_neutrons) {
156,000!
2181
      for (int dg = 0; dg < ndgroups_; dg++) {
1,362,400✔
2182
        source_regions_.precursors_rhs_bd(sr, dg) =
1,206,400✔
2183
          rhs_backwards_difference(source_regions_.precursors_bd(sr, dg),
1,206,400✔
2184
            RandomRay::bd_order_, settings::dt);
2185
      }
2186
    }
2187
  }
2188
}
960✔
2189

2190
// Update material density and cross sections
2191
void FlatSourceDomain::update_material_density(int i)
960✔
2192
{
2193
#pragma omp parallel for
540✔
2194
  for (int j = 0; j < model::materials.size(); j++) {
1,610✔
2195
    auto& mat {model::materials[j]};
1,190✔
2196
    if (mat->density_timeseries_.size() != 0) {
1,190✔
2197
      double density_factor = mat->density_timeseries_[i] / mat->density_;
420✔
2198
      mat->density_ = mat->density_timeseries_[i];
420✔
2199
      for (int g_out = 0; g_out < negroups_; g_out++) {
6,720✔
2200
        for (int dg = 0; dg < ndgroups_; dg++) {
46,060✔
2201
          nu_d_sigma_f_[j * negroups_ * ndgroups_ + dg * negroups_ + g_out] *=
39,760✔
2202
            density_factor;
2203
        }
2204
        nu_p_sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2205
        sigma_t_[j * negroups_ + g_out] *= density_factor;
6,300✔
2206
        nu_sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2207
        sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2208
        for (int g_in = 0; g_in < negroups_; g_in++) {
357,000✔
2209
          sigma_s_[j * negroups_ * negroups_ + g_out * negroups_ + g_in] *=
350,700✔
2210
            density_factor;
2211
        }
2212
      }
2213
    }
2214
  }
2215
}
960✔
2216

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