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

openmc-dev / openmc / 21296750350

23 Jan 2026 06:25PM UTC coverage: 81.966% (-0.01%) from 81.977%
21296750350

Pull #3702

github

web-flow
Merge 8d3c7acd6 into 3e2f1f521
Pull Request #3702: Random Ray Kinetic Simulation Mode

17712 of 24657 branches covered (71.83%)

Branch coverage included in aggregate %.

902 of 1043 new or added lines in 20 files covered. (86.48%)

83 existing lines in 10 files now uncovered.

56501 of 65884 relevant lines covered (85.76%)

52138177.9 hits per line

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

75.5
/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()
799✔
41
  : negroups_(data::mg.num_energy_groups_),
799✔
42
    ndgroups_(data::mg.num_delayed_groups_)
799✔
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;
799✔
50
  for (const auto& c : model::cells) {
6,402✔
51
    if (c->type_ != Fill::MATERIAL) {
5,603✔
52
      source_region_offsets_.push_back(-1);
2,507✔
53
    } else {
54
      source_region_offsets_.push_back(base_source_regions);
3,096✔
55
      base_source_regions += c->n_instances();
3,096✔
56
    }
57
  }
58

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

63
  // Initialize tally volumes
64
  if (volume_normalized_flux_tallies_) {
799✔
65
    tally_volumes_.resize(model::tallies.size());
476✔
66
    for (int i = 0; i < model::tallies.size(); i++) {
1,694✔
67
      //  Get the shape of the 3D result tensor
68
      auto shape = model::tallies[i]->results().shape();
1,218✔
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,218✔
73
        xt::xtensor<double, 2>::from_shape({shape[0], shape[1]});
2,436✔
74
    }
75
  }
76

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

85
void FlatSourceDomain::batch_reset()
130,512✔
86
{
87
// Reset scalar fluxes and iteration volume tallies to zero
88
#pragma omp parallel for
65,262✔
89
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,518,845✔
90
    source_regions_.volume(sr) = 0.0;
108,453,595✔
91
    source_regions_.volume_sq(sr) = 0.0;
108,453,595✔
92
  }
93

94
#pragma omp parallel for
65,262✔
95
  for (int64_t se = 0; se < n_source_elements(); se++) {
577,129,285✔
96
    source_regions_.scalar_flux_new(se) = 0.0;
577,064,035✔
97
  }
98

99
  if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
130,512!
100
#pragma omp parallel for
59,500✔
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
}
130,512✔
105

106
void FlatSourceDomain::accumulate_iteration_flux()
63,556✔
107
{
108
#pragma omp parallel for
31,781✔
109
  for (int64_t se = 0; se < n_source_elements(); se++) {
284,520,075✔
110
    source_regions_.scalar_flux_final(se) +=
284,488,300✔
111
      source_regions_.scalar_flux_new(se);
284,488,300✔
112
  }
113
}
63,556✔
114

115
void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh)
192,174,044✔
116
{
117
  // Reset all source regions to zero (important for void regions)
118
  for (int g = 0; g < negroups_; g++) {
1,318,358,152✔
119
    srh.source(g) = 0.0;
1,126,184,108✔
120
  }
121

122
  // Add scattering + fission source
123
  int material = srh.material();
192,174,044✔
124
  double density_mult = srh.density_mult();
192,174,044✔
125
  if (material != MATERIAL_VOID) {
192,174,044✔
126
    double inverse_k_eff = 1.0 / k_eff_;
191,772,476✔
127
    for (int g_out = 0; g_out < negroups_; g_out++) {
1,317,554,248✔
128
      double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult;
1,125,781,772✔
129
      double scatter_source = 0.0;
1,125,781,772✔
130
      double fission_source = 0.0;
1,125,781,772✔
131
      double total_source = 0.0;
1,125,781,772✔
132

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

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

173
  // Add external source if in fixed source mode
174
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
192,174,044✔
175
    for (int g = 0; g < negroups_; g++) {
81,777,592✔
176
      srh.source(g) += srh.external_source(g);
42,194,318✔
177
    }
178
  }
179
}
192,174,044✔
180

181
// Compute new estimate of scattering + fission sources in each source region
182
// based on the flux estimate from the previous iteration.
183
void FlatSourceDomain::update_all_neutron_sources()
130,512✔
184
{
185
  simulation::time_update_src.start();
130,512✔
186

187
#pragma omp parallel for
65,262✔
188
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
108,518,845✔
189
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
108,453,595✔
190
    update_single_neutron_source(srh);
108,453,595✔
191
    if (settings::kinetic_simulation && !simulation::is_initial_condition) {
108,453,595✔
192
      if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
54,340,000✔
193
        compute_single_phi_prime(srh);
54,210,000✔
194
      else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
130,000!
195
        compute_single_T1(srh);
130,000✔
196
    }
197
  }
198

199
  simulation::time_update_src.stop();
130,512✔
200
}
130,512✔
201

202
// Normalizes flux and updates simulation-averaged volume estimate
203
void FlatSourceDomain::normalize_scalar_flux_and_volumes(
123,962✔
204
  double total_active_distance_per_iteration)
205
{
206
  double normalization_factor = 1.0 / total_active_distance_per_iteration;
123,962✔
207
  double volume_normalization_factor =
123,962✔
208
    1.0 / (total_active_distance_per_iteration * simulation::current_batch);
123,962✔
209

210
// Normalize scalar flux to total distance travelled by all rays this
211
// iteration
212
#pragma omp parallel for
61,987✔
213
  for (int64_t se = 0; se < n_source_elements(); se++) {
563,121,845✔
214
    source_regions_.scalar_flux_new(se) *= normalization_factor;
563,059,870✔
215
  }
216

217
// Accumulate cell-wise ray length tallies collected this iteration, then
218
// update the simulation-averaged cell-wise volume estimates
219
#pragma omp parallel for
61,987✔
220
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
96,117,425✔
221
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
96,055,450✔
222
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
96,055,450✔
223
    source_regions_.volume_naive(sr) =
192,110,900✔
224
      source_regions_.volume(sr) * normalization_factor;
96,055,450✔
225
    source_regions_.volume_sq(sr) =
192,110,900✔
226
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
96,055,450✔
227
    source_regions_.volume(sr) =
96,055,450✔
228
      source_regions_.volume_t(sr) * volume_normalization_factor;
96,055,450✔
229
  }
230
}
123,962✔
231

232
void FlatSourceDomain::set_flux_to_flux_plus_source(
1,120,686,154✔
233
  int64_t sr, double volume, int g)
234
{
235
  int material = source_regions_.material(sr);
1,120,686,154✔
236
  double density_mult = source_regions_.density_mult(sr);
1,120,686,154✔
237
  if (material == MATERIAL_VOID) {
1,120,686,154✔
238
    source_regions_.scalar_flux_new(sr, g) /= volume;
802,336✔
239
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
802,336!
240
      source_regions_.scalar_flux_new(sr, g) +=
802,336✔
241
        0.5f * source_regions_.external_source(sr, g) *
802,336✔
242
        source_regions_.volume_sq(sr);
802,336✔
243
    }
244
  } else {
245
    double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
1,119,883,818✔
246
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
1,119,883,818✔
247
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
1,119,883,818✔
248
  }
249
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
1,120,686,154✔
250
    double inverse_vbar = inverse_vbar_[material * negroups_ + g];
772,054,800✔
251
    double scalar_flux_rhs_bd = source_regions_.scalar_flux_rhs_bd(sr, g);
772,054,800✔
252
    double A0 =
253
      (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
772,054,800✔
254
    // TODO: Add support for expicit void regions
255
    double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
772,054,800✔
256
    source_regions_.scalar_flux_new(sr, g) -=
772,054,800✔
257
      scalar_flux_rhs_bd * inverse_vbar / sigma_t;
772,054,800✔
258
    source_regions_.scalar_flux_new(sr, g) /= 1 + A0 * inverse_vbar / sigma_t;
772,054,800✔
259
  }
260
}
1,120,686,154✔
261

262
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
1,402✔
263
{
264
  source_regions_.scalar_flux_new(sr, g) =
2,804✔
265
    source_regions_.scalar_flux_old(sr, g);
1,402✔
266
}
1,402✔
267

268
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
8,462,872✔
269
{
270
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
8,462,872✔
271
}
8,462,872✔
272

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

280
#pragma omp parallel for reduction(+ : n_hits)
65,262✔
281
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
109,578,065✔
282

283
    double volume_simulation_avg = source_regions_.volume(sr);
109,512,815✔
284
    double volume_iteration = source_regions_.volume_naive(sr);
109,512,815✔
285

286
    // Increment the number of hits if cell was hit this iteration
287
    if (volume_iteration) {
109,512,815✔
288
      n_hits++;
105,428,760✔
289
    }
290

291
    // Set the SR to small status if its expected number of hits
292
    // per iteration is less than 1.5
293
    if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) {
109,512,815✔
294
      source_regions_.is_small(sr) = 1;
6,813,175✔
295
    } else {
296
      source_regions_.is_small(sr) = 0;
102,699,640✔
297
    }
298

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

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

358
  // Return the number of source regions that were hit this iteration
359
  return n_hits;
130,512✔
360
}
361

362
// Generates new estimate of k_eff based on the differences between this
363
// iteration's estimate of the scalar flux and the last iteration's estimate.
364
void FlatSourceDomain::compute_k_eff()
36,700✔
365
{
366
  double fission_rate_old = 0;
36,700✔
367
  double fission_rate_new = 0;
36,700✔
368

369
  // Vector for gathering fission source terms for Shannon entropy calculation
370
  vector<float> p(n_source_regions(), 0.0f);
36,700✔
371

372
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
18,350✔
373
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,078,520✔
374

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

381
    int material = source_regions_.material(sr);
22,060,150✔
382
    if (material == MATERIAL_VOID) {
22,060,150!
383
      continue;
384
    }
385

386
    double sr_fission_source_old = 0;
22,060,150✔
387
    double sr_fission_source_new = 0;
22,060,150✔
388

389
    for (int g = 0; g < negroups_; g++) {
178,696,400✔
390
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g] *
156,636,250✔
391
                          source_regions_.density_mult(sr);
156,636,250✔
392
      sr_fission_source_old +=
156,636,250✔
393
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
156,636,250✔
394
      sr_fission_source_new +=
156,636,250✔
395
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
156,636,250✔
396
    }
397

398
    // Compute total fission rates in FSR
399
    sr_fission_source_old *= volume;
22,060,150✔
400
    sr_fission_source_new *= volume;
22,060,150✔
401

402
    // Accumulate totals
403
    fission_rate_old += sr_fission_source_old;
22,060,150✔
404
    fission_rate_new += sr_fission_source_new;
22,060,150✔
405

406
    // Store total fission rate in the FSR for Shannon calculation
407
    p[sr] = sr_fission_source_new;
22,060,150✔
408
  }
409

410
  double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old);
36,700✔
411

412
  double H = 0.0;
36,700✔
413
  // defining an inverse sum for better performance
414
  double inverse_sum = 1 / fission_rate_new;
36,700✔
415

416
#pragma omp parallel for reduction(+ : H)
18,350✔
417
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
22,078,520✔
418
    // Only if FSR has non-negative and non-zero fission source
419
    if (p[sr] > 0.0f) {
22,060,170✔
420
      // Normalize to total weight of bank sites. p_i for better performance
421
      float p_i = p[sr] * inverse_sum;
10,151,955✔
422
      // Sum values to obtain Shannon entropy.
423
      H -= p_i * std::log2(p_i);
10,151,955✔
424
    }
425
  }
426

427
  // Adds entropy value to shared entropy vector in openmc namespace.
428
  simulation::entropy.push_back(H);
36,700✔
429

430
  fission_rate_ = fission_rate_new;
36,700✔
431
  k_eff_ = k_eff_new;
36,700✔
432
}
36,700✔
433

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

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

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

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

472
void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id)
891✔
473
{
474
  openmc::simulation::time_tallies.start();
891✔
475

476
  // Tracks if we've generated a mapping yet for all source regions.
477
  bool all_source_regions_mapped = true;
891✔
478

479
// Attempt to generate mapping for all source regions
480
#pragma omp parallel for
446✔
481
  for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) {
1,059,665✔
482

483
    // If this source region has not been hit by a ray yet, then
484
    // we aren't going to be able to map it, so skip it.
485
    if (!source_regions_.position_recorded(sr)) {
1,059,220!
486
      all_source_regions_mapped = false;
487
      continue;
488
    }
489

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

499
    // Loop over energy groups (so as to support energy filters)
500
    for (int g = 0; g < negroups_; g++) {
2,506,220✔
501

502
      // Set particle to the current energy
503
      p.g() = g;
1,447,000✔
504
      p.g_last() = g;
1,447,000✔
505
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,447,000✔
506
      p.E_last() = p.E();
1,447,000✔
507

508
      int64_t source_element = sr * negroups_ + g;
1,447,000✔
509

510
      // If this task has already been populated, we don't need to do
511
      // it again.
512
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,447,000!
513
        continue;
514
      }
515

516
      // Loop over all active tallies. This logic is essentially identical
517
      // to what happens when scanning for applicable tallies during
518
      // MC transport.
519
      for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) {
5,143,520✔
520
        Tally& tally {*model::tallies[i_tally]};
3,696,520✔
521

522
        // Initialize an iterator over valid filter bin combinations.
523
        // If there are no valid combinations, use a continue statement
524
        // to ensure we skip the assume_separate break below.
525
        auto filter_iter = FilterBinIter(tally, p);
3,696,520✔
526
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,696,520✔
527
        if (filter_iter == end)
3,696,520✔
528
          continue;
2,001,680✔
529

530
        // Loop over filter bins.
531
        for (; filter_iter != end; ++filter_iter) {
3,389,680✔
532
          auto filter_index = filter_iter.index_;
1,694,840✔
533
          auto filter_weight = filter_iter.weight_;
1,694,840✔
534

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

547
            // Also add this task to the list of volume tasks for this source
548
            // region.
549
            source_regions_.volume_task(sr).insert(task);
2,221,240✔
550
          }
551
        }
552
      }
553
      // Reset all the filter matches for the next tally event.
554
      for (auto& match : p.filter_matches())
5,788,400✔
555
        match.bins_present_ = false;
4,341,400✔
556
    }
557
    // Loop over delayed groups (so as to support tallying delayed quantities)
558
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
1,059,220!
559
      for (int dg = 0; dg < ndgroups_; dg++) {
272,480✔
560

561
        // Set particle to the current delay group
562
        p.delayed_group() = dg;
241,280✔
563

564
        int64_t delay_element = sr * ndgroups_ + dg;
241,280✔
565

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

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

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

587
          // Loop over filter bins.
588
          for (; filter_iter != end; ++filter_iter) {
865,280✔
589
            auto filter_index = filter_iter.index_;
432,640✔
590
            auto filter_weight = filter_iter.weight_;
432,640✔
591

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

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

617
  mapped_all_tallies_ = all_source_regions_mapped;
891✔
618
}
891✔
619

620
// Set the volume accumulators to zero for all tallies
621
void FlatSourceDomain::reset_tally_volumes()
63,556✔
622
{
623
  if (volume_normalized_flux_tallies_) {
63,556✔
624
#pragma omp parallel for
29,800✔
625
    for (int i = 0; i < tally_volumes_.size(); i++) {
90,750✔
626
      auto& tensor = tally_volumes_[i];
60,950✔
627
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
60,950✔
628
    }
629
  }
630
}
63,556✔
631

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

660
  // If we are in adjoint mode of a fixed source problem, the external
661
  // source is already normalized, such that all resulting fluxes are
662
  // also normalized.
663
  if (adjoint_) {
3,878✔
664
    return 1.0;
360✔
665
  }
666

667
  // Fixed source mode normalization
668

669
  // Step 1 is to sum over all source regions and energy groups to get the
670
  // total external source strength in the simulation.
671
  double simulation_external_source_strength = 0.0;
3,518✔
672
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
1,761✔
673
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
14,678,157✔
674
    int material = source_regions_.material(sr);
14,676,400✔
675
    double volume = source_regions_.volume(sr) * simulation_volume_;
14,676,400✔
676
    for (int g = 0; g < negroups_; g++) {
29,895,680✔
677
      // For non-void regions, we store the external source pre-divided by
678
      // sigma_t. We need to multiply non-void regions back up by sigma_t
679
      // to get the total source strength in the expected units.
680
      double sigma_t = 1.0;
15,219,280✔
681
      if (material != MATERIAL_VOID) {
15,219,280✔
682
        sigma_t =
15,009,040✔
683
          sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr);
15,009,040✔
684
      }
685
      simulation_external_source_strength +=
15,219,280✔
686
        source_regions_.external_source(sr, g) * sigma_t * volume;
15,219,280✔
687
    }
688
  }
689

690
  // Step 2 is to determine the total user-specified external source strength
691
  double user_external_source_strength = 0.0;
3,518✔
692
  for (auto& ext_source : model::external_sources) {
7,036✔
693
    user_external_source_strength += ext_source->strength();
3,518✔
694
  }
695

696
  // The correction factor is the ratio of the user-specified external source
697
  // strength to the simulation external source strength.
698
  double source_normalization_factor =
3,518✔
699
    user_external_source_strength / simulation_external_source_strength;
700

701
  return source_normalization_factor;
3,518✔
702
}
703

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

713
// TODO: Add support for prompt and delayed nu fission tallies
714
void FlatSourceDomain::random_ray_tally()
63,556✔
715
{
716
  openmc::simulation::time_tallies.start();
63,556✔
717

718
  // Reset our tally volumes to zero
719
  reset_tally_volumes();
63,556✔
720

721
  double source_normalization_factor =
722
    compute_fixed_source_normalization_factor();
63,556✔
723

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

740
    int material = source_regions_.material(sr);
53,304,400✔
741
    double density_mult = source_regions_.density_mult(sr);
53,304,400✔
742

743
    for (int g = 0; g < negroups_; g++) {
337,792,700✔
744
      double flux =
745
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
284,488,300✔
746

747
      // Determine numerical score value
748
      for (auto& task : source_regions_.tally_task(sr, g)) {
1,102,256,800✔
749
        double score = 0.0;
817,768,500✔
750
        switch (task.score_type) {
817,768,500!
751

752
        case SCORE_FLUX:
284,526,100✔
753
          score = flux * volume;
284,526,100✔
754
          break;
284,526,100✔
755

756
        case SCORE_TOTAL:
757
          if (material != MATERIAL_VOID) {
×
758
            score =
759
              flux * volume * sigma_t_[material * negroups_ + g] * density_mult;
760
          }
761
          break;
762

763
        case SCORE_FISSION:
266,620,900✔
764
          if (material != MATERIAL_VOID) {
266,620,900!
765
            score =
266,620,900✔
766
              flux * volume * sigma_f_[material * negroups_ + g] * density_mult;
266,620,900✔
767
          }
768
          break;
266,620,900✔
769

770
        case SCORE_NU_FISSION:
266,620,900✔
771
          if (material != MATERIAL_VOID) {
266,620,900!
772
            score = flux * volume * nu_sigma_f_[material * negroups_ + g] *
266,620,900✔
773
                    density_mult;
774
          }
775
          break;
266,620,900✔
776

777
        case SCORE_EVENTS:
778
          score = 1.0;
779
          break;
780

781
        case SCORE_KAPPA_FISSION:
600✔
782
          score = flux * volume * kappa_fission_[material * negroups_ + g] *
600✔
783
                  density_mult;
784
          break;
600✔
785
            
786
        case SCORE_PRECURSORS:
787
          // Score precursors in tally_delay_tasks
788
          if (settings::kinetic_simulation &&
×
789
              settings::create_delayed_neutrons) {
790
            break;
791
          } else {
792
            fatal_error("Invalid score specified in tallies.xml. Precursors "
793
                        "are only supported in random ray mode for kinetic "
794
                        "simulations when delayed neutrons are turned on.");
795
          }
796

797
        default:
798
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
799
                      "total, fission, nu-fission, kappa-fission, and events "
800
                      "are supported in random ray mode (precursors are "
801
                      "supported in kinetic simulations when delayed neutrons "
802
                      "are turned on).");
803
          break;
804
        }
805
        // Apply score to the appropriate tally bin
806
        Tally& tally {*model::tallies[task.tally_idx]};
817,768,500✔
807
#pragma omp atomic
808
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
817,768,500✔
809
          score;
810
      }
811
    }
812
    if (settings::kinetic_simulation && settings::create_delayed_neutrons) {
53,304,400!
813
      for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
814
        // Determine numerical score value
815
        for (auto& task : source_regions_.tally_delay_task(sr, dg)) {
606,569,600✔
816
          double score = 0.0;
302,848,000✔
817
          switch (task.score_type) {
302,848,000!
818

819
          // Certain scores already tallied
820
          case SCORE_FLUX:
821
          case SCORE_TOTAL:
822
          case SCORE_FISSION:
823
          case SCORE_NU_FISSION:
824
          case SCORE_EVENTS:
825
            break;
826

827
          case SCORE_PRECURSORS:
302,848,000✔
828
            score = source_regions_.precursors_new(sr, dg) *
302,848,000✔
829
                    source_normalization_factor * volume;
830
            break;
302,848,000✔
831

832
          default:
833
            fatal_error(
834
              "Invalid score specified in tallies.xml. Only flux, "
835
              "total, fission, nu-fission, and events are supported in "
836
              "random ray mode (precursors are supported in kinetic "
837
              "simulations when delayed neutrons are turned on).");
838
            break;
839
          }
840

841
          // Apply score to the appropriate tally bin
842
          Tally& tally {*model::tallies[task.tally_idx]};
302,848,000✔
843
#pragma omp atomic
844
          tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
302,848,000✔
845
            score;
846
        }
847
      }
848
    }
849

850
    // For flux and precursor tallies, the total volume of the spatial region is
851
    // needed for normalizing the tally. We store this volume in a separate
852
    // tensor. We only contribute to each volume tally bin once per FSR.
853
    if (volume_normalized_flux_tallies_) {
53,304,400✔
854
      for (const auto& task : source_regions_.volume_task(sr)) {
1,159,662,200✔
855
        if (task.score_type == SCORE_FLUX ||
1,106,649,700✔
856
            task.score_type == SCORE_PRECURSORS) {
826,807,800✔
857
#pragma omp atomic
858
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
582,689,900✔
859
            volume;
860
        }
861
      }
862
    }
863
  } // end FSR loop
864

865
  // Normalize any flux or precursor scores by the total volume of the FSRs
866
  // scoring to that bin. To do this, we loop over all tallies, and then all
867
  // filter bins, and then scores. For each score, we check the tally data
868
  // structure to see what index that score corresponds to. If that score is a
869
  // flux or precursor score, then we divide it by volume.
870
  if (volume_normalized_flux_tallies_) {
63,556✔
871
    for (int i = 0; i < model::tallies.size(); i++) {
181,500✔
872
      Tally& tally {*model::tallies[i]};
121,900✔
873
#pragma omp parallel for
60,950✔
874
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
1,726,900✔
875
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
4,163,500✔
876
          auto score_type = tally.scores_[score_idx];
2,497,550✔
877
          if (score_type == SCORE_FLUX || score_type == SCORE_PRECURSORS) {
2,497,550✔
878
            double vol = tally_volumes_[i](bin, score_idx);
1,665,950✔
879
            if (vol > 0.0) {
1,665,950!
880
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
1,665,950✔
881
            }
882
          }
883
        }
884
      }
885
    }
886
  }
887

888
  openmc::simulation::time_tallies.stop();
63,556✔
889
}
63,556✔
890

891
double FlatSourceDomain::evaluate_flux_at_point(
×
892
  Position r, int64_t sr, int g) const
893
{
894
  return source_regions_.scalar_flux_final(sr, g) /
×
895
         (settings::n_batches - settings::n_inactive);
×
896
}
897

898
// Outputs all basic material, FSR ID, multigroup flux, and
899
// fission source data to .vtk file that can be directly
900
// loaded and displayed by Paraview. Note that .vtk binary
901
// files require big endian byte ordering, so endianness
902
// is checked and flipped if necessary.
903
void FlatSourceDomain::output_to_vtk() const
×
904
{
905
  // Rename .h5 plot filename(s) to .vtk filenames
906
  for (int p = 0; p < model::plots.size(); p++) {
×
907
    PlottableInterface* plot = model::plots[p].get();
×
908
    plot->path_plot() =
×
909
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
910
  }
911

912
  // Print header information
913
  print_plot();
×
914

915
  // Outer loop over plots
916
  for (int plt = 0; plt < model::plots.size(); plt++) {
×
917

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

921
    // Random ray plots only support voxel plots
922
    if (!openmc_plot) {
×
923
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
924
                          "is allowed in random ray mode.",
925
        plt));
926
      continue;
×
927
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
928
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
929
                          "is allowed in random ray mode.",
930
        plt));
931
      continue;
×
932
    }
933

934
    int Nx = openmc_plot->pixels_[0];
×
935
    int Ny = openmc_plot->pixels_[1];
×
936
    int Nz = openmc_plot->pixels_[2];
×
937
    Position origin = openmc_plot->origin_;
×
938
    Position width = openmc_plot->width_;
×
939
    Position ll = origin - width / 2.0;
×
940
    double x_delta = width.x / Nx;
×
941
    double y_delta = width.y / Ny;
×
942
    double z_delta = width.z / Nz;
×
943
    std::string filename = openmc_plot->path_plot();
×
944

945
    // Perform sanity checks on file size
946
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
947
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
948
      openmc_plot->id(), filename, bytes / 1.0e6);
×
949
    if (bytes / 1.0e9 > 1.0) {
×
950
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
951
              "slow.");
952
    } else if (bytes / 1.0e9 > 100.0) {
×
953
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
954
    }
955

956
    // Relate voxel spatial locations to random ray source regions
957
    vector<int> voxel_indices(Nx * Ny * Nz);
×
958
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
959
    vector<double> weight_windows(Nx * Ny * Nz);
×
960
    float min_weight = 1e20;
×
961
#pragma omp parallel for collapse(3) reduction(min : min_weight)
962
    for (int z = 0; z < Nz; z++) {
×
963
      for (int y = 0; y < Ny; y++) {
×
964
        for (int x = 0; x < Nx; x++) {
×
965
          Position sample;
966
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
967
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
968
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
969
          Particle p;
×
970
          p.r() = sample;
971
          p.r_last() = sample;
972
          p.E() = 1.0;
973
          p.E_last() = 1.0;
974
          p.u() = {1.0, 0.0, 0.0};
975

976
          bool found = exhaustive_find_cell(p);
×
977
          if (!found) {
×
978
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
979
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
980
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
981
            continue;
982
          }
983

984
          SourceRegionKey sr_key = lookup_source_region_key(p);
×
985
          int64_t sr = -1;
986
          auto it = source_region_map_.find(sr_key);
×
987
          if (it != source_region_map_.end()) {
×
988
            sr = it->second;
989
          }
990

991
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
992
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
993

994
          if (variance_reduction::weight_windows.size() == 1) {
×
995
            WeightWindow ww =
996
              variance_reduction::weight_windows[0]->get_weight_window(p);
×
997
            float weight = ww.lower_weight;
998
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
999
            if (weight < min_weight)
×
1000
              min_weight = weight;
1001
          }
1002
        }
×
1003
      }
1004
    }
1005

1006
    double source_normalization_factor =
1007
      compute_fixed_source_normalization_factor();
×
1008

1009
    // Open file for writing
1010
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
1011

1012
    // Write vtk metadata
1013
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
1014
    std::fprintf(plot, "Dataset File\n");
×
1015
    std::fprintf(plot, "BINARY\n");
×
1016
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
1017
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
1018
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
1019
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
1020
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
1021

1022
    int64_t num_neg = 0;
×
1023
    int64_t num_samples = 0;
×
1024
    float min_flux = 0.0;
×
1025
    float max_flux = -1.0e20;
×
1026
    // Plot multigroup flux data
1027
    for (int g = 0; g < negroups_; g++) {
×
1028
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
1029
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1030
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1031
        int64_t fsr = voxel_indices[i];
×
1032
        int64_t source_element = fsr * negroups_ + g;
×
1033
        float flux = 0;
×
1034
        if (fsr >= 0) {
×
1035
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
1036
          if (flux < 0.0)
×
1037
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
1038
              voxel_positions[i], fsr, g);
×
1039
        }
1040
        if (flux < 0.0) {
×
1041
          num_neg++;
×
1042
          if (flux < min_flux) {
×
1043
            min_flux = flux;
×
1044
          }
1045
        }
1046
        if (flux > max_flux)
×
1047
          max_flux = flux;
×
1048
        num_samples++;
×
1049
        flux = convert_to_big_endian<float>(flux);
×
1050
        std::fwrite(&flux, sizeof(float), 1, plot);
×
1051
      }
1052
    }
1053

1054
    // Slightly negative fluxes can be normal when sampling corners of linear
1055
    // source regions. However, very common and high magnitude negative fluxes
1056
    // may indicate numerical instability.
1057
    if (num_neg > 0) {
×
1058
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
1059
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
1060
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
1061
    }
1062

1063
    // Plot FSRs
1064
    std::fprintf(plot, "SCALARS FSRs float\n");
×
1065
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1066
    for (int fsr : voxel_indices) {
×
1067
      float value = future_prn(10, fsr);
×
1068
      value = convert_to_big_endian<float>(value);
×
1069
      std::fwrite(&value, sizeof(float), 1, plot);
×
1070
    }
1071

1072
    // Plot Materials
1073
    std::fprintf(plot, "SCALARS Materials int\n");
×
1074
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1075
    for (int fsr : voxel_indices) {
×
1076
      int mat = -1;
×
1077
      if (fsr >= 0)
×
1078
        mat = source_regions_.material(fsr);
×
1079
      mat = convert_to_big_endian<int>(mat);
×
1080
      std::fwrite(&mat, sizeof(int), 1, plot);
×
1081
    }
1082

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

1129
    // Plot weight window data
1130
    if (variance_reduction::weight_windows.size() == 1) {
×
1131
      std::fprintf(plot, "SCALARS weight_window_lower float\n");
×
1132
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
1133
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
1134
        float weight = weight_windows[i];
×
1135
        if (weight == 0.0)
×
1136
          weight = min_weight;
×
1137
        weight = convert_to_big_endian<float>(weight);
×
1138
        std::fwrite(&weight, sizeof(float), 1, plot);
×
1139
      }
1140
    }
1141

1142
    std::fclose(plot);
×
1143
  }
×
1144
}
×
1145

1146
void FlatSourceDomain::apply_external_source_to_source_region(
4,171✔
1147
  int src_idx, SourceRegionHandle& srh)
1148
{
1149
  auto s = model::external_sources[src_idx].get();
4,171✔
1150
  auto is = dynamic_cast<IndependentSource*>(s);
4,171!
1151
  auto discrete = dynamic_cast<Discrete*>(is->energy());
4,171!
1152
  double strength_factor = is->strength();
4,171✔
1153
  const auto& discrete_energies = discrete->x();
4,171✔
1154
  const auto& discrete_probs = discrete->prob();
4,171✔
1155

1156
  srh.external_source_present() = 1;
4,171✔
1157

1158
  for (int i = 0; i < discrete_energies.size(); i++) {
8,462✔
1159
    int g = data::mg.get_group_index(discrete_energies[i]);
4,291✔
1160
    srh.external_source(g) += discrete_probs[i] * strength_factor;
4,291✔
1161
  }
1162
}
4,171✔
1163

1164
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
378✔
1165
  int src_idx, int target_material_id, const vector<int32_t>& instances)
1166
{
1167
  Cell& cell = *model::cells[i_cell];
378✔
1168

1169
  if (cell.type_ != Fill::MATERIAL)
378!
1170
    return;
×
1171

1172
  for (int j : instances) {
27,062✔
1173
    int cell_material_idx = cell.material(j);
26,684✔
1174
    int cell_material_id;
1175
    if (cell_material_idx == MATERIAL_VOID) {
26,684✔
1176
      cell_material_id = MATERIAL_VOID;
224✔
1177
    } else {
1178
      cell_material_id = model::materials[cell_material_idx]->id();
26,460✔
1179
    }
1180
    if (target_material_id == C_NONE ||
26,684✔
1181
        cell_material_id == target_material_id) {
1182
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,604✔
1183
      external_volumetric_source_map_[source_region].push_back(src_idx);
2,604✔
1184
    }
1185
  }
1186
}
1187

1188
void FlatSourceDomain::apply_external_source_to_cell_and_children(
406✔
1189
  int32_t i_cell, int src_idx, int32_t target_material_id)
1190
{
1191
  Cell& cell = *model::cells[i_cell];
406✔
1192

1193
  if (cell.type_ == Fill::MATERIAL) {
406✔
1194
    vector<int> instances(cell.n_instances());
378✔
1195
    std::iota(instances.begin(), instances.end(), 0);
378✔
1196
    apply_external_source_to_cell_instances(
378✔
1197
      i_cell, src_idx, target_material_id, instances);
1198
  } else if (target_material_id == C_NONE) {
406!
1199
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1200
      cell.get_contained_cells(0, nullptr);
×
1201
    for (const auto& pair : cell_instance_list) {
×
1202
      int32_t i_child_cell = pair.first;
×
1203
      apply_external_source_to_cell_instances(
×
1204
        i_child_cell, src_idx, target_material_id, pair.second);
×
1205
    }
1206
  }
×
1207
}
406✔
1208

1209
void FlatSourceDomain::count_external_source_regions()
2,243✔
1210
{
1211
  n_external_source_regions_ = 0;
2,243✔
1212
#pragma omp parallel for reduction(+ : n_external_source_regions_)
1,123✔
1213
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,404,280✔
1214
    if (source_regions_.external_source_present(sr)) {
1,403,160✔
1215
      n_external_source_regions_++;
157,250✔
1216
    }
1217
  }
1218
}
2,243✔
1219

1220
void FlatSourceDomain::convert_external_sources()
365✔
1221
{
1222
  // Loop over external sources
1223
  for (int es = 0; es < model::external_sources.size(); es++) {
730✔
1224

1225
    // Extract source information
1226
    Source* s = model::external_sources[es].get();
365✔
1227
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
365!
1228
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
365!
1229
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
365✔
1230
    double strength_factor = is->strength();
365✔
1231

1232
    // If there is no domain constraint specified, then this must be a point
1233
    // source. In this case, we need to find the source region that contains the
1234
    // point source and apply or relate it to the external source.
1235
    if (is->domain_ids().size() == 0) {
365✔
1236

1237
      // Extract the point source coordinate and find the base source region at
1238
      // that point
1239
      auto sp = dynamic_cast<SpatialPoint*>(is->space());
15!
1240
      GeometryState gs;
15✔
1241
      gs.r() = sp->r();
15✔
1242
      gs.r_last() = sp->r();
15✔
1243
      gs.u() = {1.0, 0.0, 0.0};
15✔
1244
      bool found = exhaustive_find_cell(gs);
15✔
1245
      if (!found) {
15!
1246
        fatal_error(fmt::format("Could not find cell containing external "
×
1247
                                "point source at {}",
1248
          sp->r()));
×
1249
      }
1250
      SourceRegionKey key = lookup_source_region_key(gs);
15✔
1251

1252
      // With the source region and mesh bin known, we can use the
1253
      // accompanying SourceRegionKey as a key into a map that stores the
1254
      // corresponding external source index for the point source. Notably, we
1255
      // do not actually apply the external source to any source regions here,
1256
      // as if mesh subdivision is enabled, they haven't actually been
1257
      // discovered & initilized yet. When discovered, they will read from the
1258
      // external_source_map to determine if there are any external source
1259
      // terms that should be applied.
1260
      external_point_source_map_[key].push_back(es);
15✔
1261

1262
    } else {
15✔
1263
      // If not a point source, then use the volumetric domain constraints to
1264
      // determine which source regions to apply the external source to.
1265
      if (is->domain_type() == Source::DomainType::MATERIAL) {
350✔
1266
        for (int32_t material_id : domain_ids) {
28✔
1267
          for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
84✔
1268
            apply_external_source_to_cell_and_children(i_cell, es, material_id);
70✔
1269
          }
1270
        }
1271
      } else if (is->domain_type() == Source::DomainType::CELL) {
336✔
1272
        for (int32_t cell_id : domain_ids) {
28✔
1273
          int32_t i_cell = model::cell_map[cell_id];
14✔
1274
          apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
14✔
1275
        }
1276
      } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
322!
1277
        for (int32_t universe_id : domain_ids) {
644✔
1278
          int32_t i_universe = model::universe_map[universe_id];
322✔
1279
          Universe& universe = *model::universes[i_universe];
322✔
1280
          for (int32_t i_cell : universe.cells_) {
644✔
1281
            apply_external_source_to_cell_and_children(i_cell, es, C_NONE);
322✔
1282
          }
1283
        }
1284
      }
1285
    }
1286
  } // End loop over external sources
1287
}
365✔
1288

1289
void FlatSourceDomain::flux_swap()
130,512✔
1290
{
1291
  source_regions_.flux_swap();
130,512✔
1292
}
130,512✔
1293

1294
void FlatSourceDomain::flatten_xs()
799✔
1295
{
1296
  // Temperature and angle indices, if using multiple temperature
1297
  // data sets and/or anisotropic data sets.
1298
  // TODO: Currently assumes we are only using single temp/single angle data.
1299
  const int t = 0;
799✔
1300
  const int a = 0;
799✔
1301

1302
  n_materials_ = data::mg.macro_xs_.size();
799✔
1303
  for (int i = 0; i < n_materials_; i++) {
3,012✔
1304
    auto& m = data::mg.macro_xs_[i];
2,213✔
1305
    for (int g_out = 0; g_out < negroups_; g_out++) {
17,153✔
1306
      if (m.exists_in_model) {
14,940✔
1307
        double sigma_t =
1308
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
14,884✔
1309
        sigma_t_.push_back(sigma_t);
14,884✔
1310

1311
        if (sigma_t < MINIMUM_MACRO_XS) {
14,884✔
1312
          Material* mat = model::materials[i].get();
14✔
1313
          warning(fmt::format(
14✔
1314
            "Material \"{}\" (id: {}) has a group {} total cross section "
1315
            "({:.3e}) below the minimum threshold "
1316
            "({:.3e}). Material will be treated as pure void.",
1317
            mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS));
28✔
1318
        }
1319

1320
        double nu_sigma_f =
1321
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
14,884✔
1322
        nu_sigma_f_.push_back(nu_sigma_f);
14,884✔
1323

1324
        double sigma_f =
1325
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
14,884✔
1326
        sigma_f_.push_back(sigma_f);
14,884✔
1327

1328
        double chi = m.get_xs(MgxsType::CHI, g_out, &g_out, NULL, NULL, t, a);
14,884✔
1329
        if (!std::isfinite(chi)) {
14,884!
1330
          // MGXS interface may return NaN in some cases, such as when material
1331
          // is fissionable but has very small sigma_f.
UNCOV
1332
          chi = 0.0;
×
1333
        }
1334
        chi_.push_back(chi);
14,884✔
1335

1336
        double kappa_fission =
1337
          m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a);
14,884✔
1338
        kappa_fission_.push_back(kappa_fission);
14,884✔
1339

1340
        for (int g_in = 0; g_in < negroups_; g_in++) {
665,230✔
1341
          double sigma_s =
1342
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
650,346✔
1343
          sigma_s_.push_back(sigma_s);
650,346✔
1344
          // For transport corrected XS data, diagonal elements may be negative.
1345
          // In this case, set a flag to enable transport stabilization for the
1346
          // simulation.
1347
          if (g_out == g_in && sigma_s < 0.0)
650,346✔
1348
            is_transport_stabilization_needed_ = true;
2,254✔
1349
        }
1350
        // Prompt cross-sections for kinetic simulations
1351
        if (settings::kinetic_simulation) {
14,884✔
1352
          double chi_p =
1353
            m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
7,364✔
1354
          if (!std::isfinite(chi_p)) {
7,364!
1355
            // MGXS interface may return NaN in some cases, such as when
1356
            // material is fissionable but has very small sigma_f.
NEW
1357
            chi_p = 0.0;
×
1358
          }
1359
          chi_p_.push_back(chi_p);
7,364✔
1360

1361
          double inverse_vbar =
1362
            m.get_xs(MgxsType::INVERSE_VELOCITY, g_out, NULL, NULL, NULL, t, a);
7,364✔
1363
          inverse_vbar_.push_back(inverse_vbar);
7,364✔
1364

1365
          double nu_p_Sigma_f = m.get_xs(
7,364✔
1366
            MgxsType::PROMPT_NU_FISSION, g_out, NULL, NULL, NULL, t, a);
7,364✔
1367
          nu_p_sigma_f_.push_back(nu_p_Sigma_f);
7,364✔
1368
        }
1369
      } else {
1370
        sigma_t_.push_back(0);
56✔
1371
        nu_sigma_f_.push_back(0);
56✔
1372
        sigma_f_.push_back(0);
56✔
1373
        chi_.push_back(0);
56✔
1374
        kappa_fission_.push_back(0);
56✔
1375
        for (int g_in = 0; g_in < negroups_; g_in++) {
112✔
1376
          sigma_s_.push_back(0);
56✔
1377
        }
1378
        if (settings::kinetic_simulation) {
56!
NEW
1379
          chi_p_.push_back(0);
×
NEW
1380
          inverse_vbar_.push_back(0);
×
NEW
1381
          nu_p_sigma_f_.push_back(0);
×
1382
        }
1383
      }
1384
    }
1385
    // Delayed cross sections for time-dependent simulations
1386
    if (settings::kinetic_simulation) {
2,213✔
1387
      for (int dg = 0; dg < ndgroups_; dg++) {
3,612✔
1388
        if (m.exists_in_model) {
3,136!
1389
          double lambda =
1390
            m.get_xs(MgxsType::DECAY_RATE, 0, NULL, NULL, &dg, t, a);
3,136✔
1391
          lambda_.push_back(lambda);
3,136✔
1392
          for (int g_out = 0; g_out < negroups_; g_out++) {
49,280✔
1393
            double nu_d_Sigma_f = m.get_xs(
46,144✔
1394
              MgxsType::DELAYED_NU_FISSION, g_out, NULL, NULL, &dg, t, a);
46,144✔
1395
            nu_d_sigma_f_.push_back(nu_d_Sigma_f);
46,144✔
1396
            double chi_d =
1397
              m.get_xs(MgxsType::CHI_DELAYED, g_out, &g_out, NULL, &dg, t, a);
46,144✔
1398
            if (!std::isfinite(chi_d)) {
46,144!
1399
              // MGXS interface may return NaN in some cases, such as when
1400
              // material is fissionable but has very small sigma_f.
NEW
1401
              chi_d = 0.0;
×
1402
            }
1403
            chi_d_.push_back(chi_d);
46,144✔
1404
          }
1405
        } else {
NEW
1406
          lambda_.push_back(0);
×
NEW
1407
          for (int g_out = 0; g_out < negroups_; g_out++) {
×
NEW
1408
            nu_d_sigma_f_.push_back(0);
×
NEW
1409
            chi_d_.push_back(0);
×
1410
          }
1411
        }
1412
      }
1413
    }
1414
  }
1415
}
799✔
1416

1417
void FlatSourceDomain::set_adjoint_sources()
57✔
1418
{
1419
  // Set the adjoint external source to 1/forward_flux. If the forward flux is
1420
  // negative, zero, or extremely close to zero, set the adjoint source to zero,
1421
  // as this is likely a very small source region that we don't need to bother
1422
  // trying to vector particles towards. In the case of flux "being extremely
1423
  // close to zero", we define this as being a fixed fraction of the maximum
1424
  // forward flux, below which we assume the flux would be physically
1425
  // undetectable.
1426

1427
  // First, find the maximum forward flux value
1428
  double max_flux = 0.0;
57✔
1429
#pragma omp parallel for reduction(max : max_flux)
29✔
1430
  for (int64_t se = 0; se < n_source_elements(); se++) {
155,548✔
1431
    double flux = source_regions_.scalar_flux_final(se);
155,520✔
1432
    if (flux > max_flux) {
155,520✔
1433
      max_flux = flux;
25✔
1434
    }
1435
  }
1436

1437
  // Then, compute the adjoint source for each source region
1438
#pragma omp parallel for
29✔
1439
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1440
    for (int g = 0; g < negroups_; g++) {
311,040✔
1441
      double flux = source_regions_.scalar_flux_final(sr, g);
155,520✔
1442
      if (flux <= ZERO_FLUX_CUTOFF * max_flux) {
155,520✔
1443
        source_regions_.external_source(sr, g) = 0.0;
325✔
1444
      } else {
1445
        source_regions_.external_source(sr, g) = 1.0 / flux;
155,195✔
1446
      }
1447
      if (flux > 0.0) {
155,520✔
1448
        source_regions_.external_source_present(sr) = 1;
155,195✔
1449
      }
1450
      source_regions_.scalar_flux_final(sr, g) = 0.0;
155,520✔
1451
    }
1452
  }
1453

1454
  // "Small" source regions in OpenMC are defined as those that are hit by
1455
  // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have
1456
  // very small volumes combined with a low aspect ratio, and are often
1457
  // generated when applying a source region mesh that clips the edge of a
1458
  // curved surface. As perhaps only a few rays will visit these regions over
1459
  // the entire forward simulation, the forward flux estimates are extremely
1460
  // noisy and unreliable. In some cases, the noise may make the forward fluxes
1461
  // extremely low, leading to unphysically large adjoint source terms,
1462
  // resulting in weight windows that aggressively try to drive particles
1463
  // towards these regions. To fix this, we simply filter out any "small" source
1464
  // regions from consideration. If a source region is "small", we
1465
  // set its adjoint source to zero. This adds negligible bias to the adjoint
1466
  // flux solution, as the true total adjoint source contribution from small
1467
  // regions is likely to be negligible.
1468
#pragma omp parallel for
29✔
1469
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1470
    if (source_regions_.is_small(sr)) {
155,520!
1471
      for (int g = 0; g < negroups_; g++) {
×
1472
        source_regions_.external_source(sr, g) = 0.0;
1473
      }
1474
      source_regions_.external_source_present(sr) = 0;
1475
    }
1476
  }
1477
  // Divide the fixed source term by sigma t (to save time when applying each
1478
  // iteration)
1479
#pragma omp parallel for
29✔
1480
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
155,548✔
1481
    int material = source_regions_.material(sr);
155,520✔
1482
    double density_mult = source_regions_.density_mult(sr);
155,520✔
1483
    if (material == MATERIAL_VOID) {
155,520!
1484
      continue;
1485
    }
1486
    for (int g = 0; g < negroups_; g++) {
311,040✔
1487
      double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
155,520✔
1488
      source_regions_.external_source(sr, g) /= sigma_t;
155,520✔
1489
    }
1490
  }
1491
}
57✔
1492

1493
void FlatSourceDomain::transpose_scattering_matrix()
71✔
1494
{
1495
  // Transpose the inner two dimensions for each material
1496
  for (int m = 0; m < n_materials_; ++m) {
268✔
1497
    int material_offset = m * negroups_ * negroups_;
197✔
1498
    for (int i = 0; i < negroups_; ++i) {
563✔
1499
      for (int j = i + 1; j < negroups_; ++j) {
955✔
1500
        // Calculate indices of the elements to swap
1501
        int idx1 = material_offset + i * negroups_ + j;
589✔
1502
        int idx2 = material_offset + j * negroups_ + i;
589✔
1503

1504
        // Swap the elements to transpose the matrix
1505
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
589✔
1506
      }
1507
    }
1508
  }
1509
}
71✔
1510

1511
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
×
1512
{
1513
  // Ensure array is correct size
1514
  flux.resize(n_source_regions() * negroups_);
×
1515
// Serialize the final fluxes for output
1516
#pragma omp parallel for
1517
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1518
    flux[se] = source_regions_.scalar_flux_final(se);
1519
  }
1520
}
×
1521

NEW
1522
void FlatSourceDomain::serialize_final_sources(vector<double>& source)
×
1523
{
1524
  // Ensure array is correct size
NEW
1525
  source.resize(n_source_regions() * negroups_);
×
1526
  // Serialize the final sources for output
1527
#pragma omp parallel for
1528
  for (int64_t se = 0; se < n_source_elements(); se++) {
×
1529
    source[se] = source_regions_.source_final(se);
1530
  }
NEW
1531
}
×
1532

1533
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
1,360✔
1534
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1535
  bool is_target_void)
1536
{
1537
  Cell& cell = *model::cells[i_cell];
1,360✔
1538
  if (cell.type_ != Fill::MATERIAL)
1,360!
1539
    return;
×
1540
  for (int32_t j : instances) {
161,368✔
1541
    int cell_material_idx = cell.material(j);
160,008✔
1542
    int cell_material_id = (cell_material_idx == C_NONE)
1543
                             ? C_NONE
160,008✔
1544
                             : model::materials[cell_material_idx]->id();
160,007✔
1545

1546
    if ((target_material_id == C_NONE && !is_target_void) ||
160,008!
1547
        cell_material_id == target_material_id) {
1548
      int64_t sr = source_region_offsets_[i_cell] + j;
132,008✔
1549
      // Check if the key is already present in the mesh_map_
1550
      if (mesh_map_.find(sr) != mesh_map_.end()) {
132,008!
1551
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1552
                                "applied, but trying to apply mesh idx {}",
1553
          sr, mesh_map_[sr], mesh_idx));
×
1554
      }
1555
      // If the SR has not already been assigned, then we can write to it
1556
      mesh_map_[sr] = mesh_idx;
132,008✔
1557
    }
1558
  }
1559
}
1560

1561
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
1,079✔
1562
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1563
{
1564
  Cell& cell = *model::cells[i_cell];
1,079✔
1565

1566
  if (cell.type_ == Fill::MATERIAL) {
1,079✔
1567
    vector<int> instances(cell.n_instances());
938✔
1568
    std::iota(instances.begin(), instances.end(), 0);
938✔
1569
    apply_mesh_to_cell_instances(
938✔
1570
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1571
  } else if (target_material_id == C_NONE && !is_target_void) {
1,079!
1572
    for (int j = 0; j < cell.n_instances(); j++) {
170✔
1573
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1574
        cell.get_contained_cells(j, nullptr);
85✔
1575
      for (const auto& pair : cell_instance_list) {
507✔
1576
        int32_t i_child_cell = pair.first;
422✔
1577
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
422✔
1578
          pair.second, is_target_void);
422✔
1579
      }
1580
    }
85✔
1581
  }
1582
}
1,079✔
1583

1584
void FlatSourceDomain::apply_meshes()
799✔
1585
{
1586
  // Skip if there are no mappings between mesh IDs and domains
1587
  if (mesh_domain_map_.empty())
799✔
1588
    return;
420✔
1589

1590
  // Loop over meshes
1591
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
856✔
1592
    Mesh* mesh = model::meshes[mesh_idx].get();
477✔
1593
    int mesh_id = mesh->id();
477✔
1594

1595
    // Skip if mesh id is not present in the map
1596
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
477✔
1597
      continue;
42✔
1598

1599
    // Loop over domains associated with the mesh
1600
    for (auto& domain : mesh_domain_map_[mesh_id]) {
870✔
1601
      Source::DomainType domain_type = domain.first;
435✔
1602
      int domain_id = domain.second;
435✔
1603

1604
      if (domain_type == Source::DomainType::MATERIAL) {
435✔
1605
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
168✔
1606
          if (domain_id == C_NONE) {
140!
1607
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1608
          } else {
1609
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
140✔
1610
          }
1611
        }
1612
      } else if (domain_type == Source::DomainType::CELL) {
407✔
1613
        int32_t i_cell = model::cell_map[domain_id];
28✔
1614
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
28✔
1615
      } else if (domain_type == Source::DomainType::UNIVERSE) {
379!
1616
        int32_t i_universe = model::universe_map[domain_id];
379✔
1617
        Universe& universe = *model::universes[i_universe];
379✔
1618
        for (int32_t i_cell : universe.cells_) {
1,290✔
1619
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
911✔
1620
        }
1621
      }
1622
    }
1623
  }
1624
}
1625

1626
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
2,147,483,647✔
1627
  SourceRegionKey sr_key, Position r, Direction u)
1628
{
1629
  // Case 1: Check if the source region key is already present in the permanent
1630
  // map. This is the most common condition, as any source region visited in a
1631
  // previous power iteration will already be present in the permanent map. If
1632
  // the source region key is found, we translate the key into a specific 1D
1633
  // source region index and return a handle its position in the
1634
  // source_regions_ vector.
1635
  auto it = source_region_map_.find(sr_key);
2,147,483,647✔
1636
  if (it != source_region_map_.end()) {
2,147,483,647✔
1637
    int64_t sr = it->second;
2,147,483,647✔
1638
    return source_regions_.get_source_region_handle(sr);
2,147,483,647✔
1639
  }
1640

1641
  // Case 2: Check if the source region key is present in the temporary (thread
1642
  // safe) map. This is a common occurrence in the first power iteration when
1643
  // the source region has already been visited already by some other ray. We
1644
  // begin by locking the temporary map before any operations are performed. The
1645
  // lock is not global over the full data structure -- it will be dependent on
1646
  // which key is used.
1647
  discovered_source_regions_.lock(sr_key);
54,765,673✔
1648

1649
  // If the key is found in the temporary map, then we return a handle to the
1650
  // source region that is stored in the temporary map.
1651
  if (discovered_source_regions_.contains(sr_key)) {
54,765,673✔
1652
    SourceRegionHandle handle {discovered_source_regions_[sr_key]};
52,554,126✔
1653
    discovered_source_regions_.unlock(sr_key);
52,554,126✔
1654
    return handle;
52,554,126✔
1655
  }
1656

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

1685
  // Sanity check on source region id
1686
  GeometryState gs;
2,211,547✔
1687
  gs.r() = r + TINY_BIT * u;
2,211,547✔
1688
  gs.u() = {1.0, 0.0, 0.0};
2,211,547✔
1689
  exhaustive_find_cell(gs);
2,211,547✔
1690
  int64_t sr_found = lookup_base_source_region_idx(gs);
2,211,547✔
1691
  if (sr_found != sr_key.base_source_region_id) {
2,211,547✔
1692
    discovered_source_regions_.unlock(sr_key);
780✔
1693
    SourceRegionHandle handle;
780✔
1694
    handle.is_numerical_fp_artifact_ = true;
780✔
1695
    return handle;
780✔
1696
  }
1697

1698
  // Sanity check on mesh bin
1699
  int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id);
2,210,767✔
1700
  if (mesh_idx == C_NONE) {
2,210,767✔
1701
    if (sr_key.mesh_bin != 0) {
356,000!
1702
      discovered_source_regions_.unlock(sr_key);
×
1703
      SourceRegionHandle handle;
×
1704
      handle.is_numerical_fp_artifact_ = true;
×
1705
      return handle;
×
1706
    }
1707
  } else {
1708
    Mesh* mesh = model::meshes[mesh_idx].get();
1,854,767✔
1709
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,854,767✔
1710
    if (bin_found != sr_key.mesh_bin) {
1,854,767!
1711
      discovered_source_regions_.unlock(sr_key);
×
1712
      SourceRegionHandle handle;
×
1713
      handle.is_numerical_fp_artifact_ = true;
×
1714
      return handle;
×
1715
    }
1716
  }
1717

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

1725
  // Call the basic constructor for the source region and store in the parallel
1726
  // map.
1727
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
2,210,767✔
1728
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
2,210,767✔
1729
    sr_key, {negroups_, ndgroups_, is_linear});
1730
  SourceRegionHandle handle {*sr_ptr};
2,210,767✔
1731

1732
  // Determine the material
1733
  int gs_i_cell = gs.lowest_coord().cell();
2,210,767✔
1734
  Cell& cell = *model::cells[gs_i_cell];
2,210,767✔
1735
  int material = cell.material(gs.cell_instance());
2,210,767✔
1736

1737
  // If material total XS is extremely low, just set it to void to avoid
1738
  // problems with 1/Sigma_t
1739
  for (int g = 0; g < negroups_; g++) {
5,177,217✔
1740
    double sigma_t = sigma_t_[material * negroups_ + g];
2,986,594✔
1741
    if (sigma_t < MINIMUM_MACRO_XS) {
2,986,594✔
1742
      material = MATERIAL_VOID;
20,144✔
1743
      break;
20,144✔
1744
    }
1745
  }
1746

1747
  if (settings::kinetic_simulation && material == MATERIAL_VOID) {
2,210,767!
NEW
1748
    fatal_error("Explicit void treatment for kinetic simulations "
×
1749
                " is not currently supported.");
1750
  }
1751

1752
  handle.material() = material;
2,210,767✔
1753

1754
  handle.density_mult() = cell.density_mult(gs.cell_instance());
2,210,767✔
1755

1756
  // Store the mesh index (if any) assigned to this source region
1757
  handle.mesh() = mesh_idx;
2,210,767✔
1758

1759
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
2,210,767✔
1760
    // Determine if there are any volumetric sources, and apply them.
1761
    // Volumetric sources are specifc only to the base SR idx.
1762
    auto it_vol =
1763
      external_volumetric_source_map_.find(sr_key.base_source_region_id);
2,100,697✔
1764
    if (it_vol != external_volumetric_source_map_.end()) {
2,100,697✔
1765
      const vector<int>& vol_sources = it_vol->second;
4,160✔
1766
      for (int src_idx : vol_sources) {
8,320✔
1767
        apply_external_source_to_source_region(src_idx, handle);
4,160✔
1768
      }
1769
    }
1770

1771
    // Determine if there are any point sources, and apply them.
1772
    // Point sources are specific to the source region key.
1773
    auto it_point = external_point_source_map_.find(sr_key);
2,100,697✔
1774
    if (it_point != external_point_source_map_.end()) {
2,100,697✔
1775
      const vector<int>& point_sources = it_point->second;
11✔
1776
      for (int src_idx : point_sources) {
22✔
1777
        apply_external_source_to_source_region(src_idx, handle);
11✔
1778
      }
1779
    }
1780

1781
    // Divide external source term by sigma_t
1782
    if (material != C_NONE) {
2,100,697✔
1783
      for (int g = 0; g < negroups_; g++) {
4,202,889✔
1784
        double sigma_t =
1785
          sigma_t_[material * negroups_ + g] * handle.density_mult();
2,122,336✔
1786
        handle.external_source(g) /= sigma_t;
2,122,336✔
1787
      }
1788
    }
1789
  }
1790

1791
  // Compute the combined source term
1792
  update_single_neutron_source(handle);
2,210,767✔
1793
  if (settings::kinetic_simulation && !simulation::is_initial_condition) {
2,210,767!
NEW
1794
    if (RandomRay::time_method_ == RandomRayTimeMethod::ISOTROPIC)
×
NEW
1795
      compute_single_phi_prime(handle);
×
NEW
1796
    else if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
×
NEW
1797
      compute_single_T1(handle);
×
1798
  }
1799

1800
  // Unlock the parallel map. Note: we may be tempted to release
1801
  // this lock earlier, and then just use the source region's lock to protect
1802
  // the flux/source initialization stages above. However, the rest of the code
1803
  // only protects updates to the new flux and volume fields, and assumes that
1804
  // the source is constant for the duration of transport. Thus, using just the
1805
  // source region's lock by itself would result in other threads potentially
1806
  // reading from the source before it is computed, as they won't use the lock
1807
  // when only reading from the SR's source. It would be expensive to protect
1808
  // those operations, whereas generating the SR is only done once, so we just
1809
  // hold the map's bucket lock until the source region is fully initialized.
1810
  discovered_source_regions_.unlock(sr_key);
2,210,767✔
1811

1812
  return handle;
2,210,767✔
1813
}
2,211,547✔
1814

1815
void FlatSourceDomain::finalize_discovered_source_regions()
130,512✔
1816
{
1817
  // Extract keys for entries with a valid volume.
1818
  vector<SourceRegionKey> keys;
130,512✔
1819
  for (const auto& pair : discovered_source_regions_) {
2,341,279✔
1820
    if (pair.second.volume_ > 0.0) {
2,210,767✔
1821
      keys.push_back(pair.first);
2,118,527✔
1822
    }
1823
  }
1824

1825
  if (!keys.empty()) {
130,512✔
1826
    // Sort the keys, so as to ensure reproducible ordering given that source
1827
    // regions may have been added to discovered_source_regions_ in an arbitrary
1828
    // order due to shared memory threading.
1829
    std::sort(keys.begin(), keys.end());
891✔
1830

1831
    // Remember the index of the first new source region
1832
    int64_t start_sr_id = source_regions_.n_source_regions();
891✔
1833

1834
    // Append the source regions in the sorted key order.
1835
    for (const auto& key : keys) {
2,119,418✔
1836
      const SourceRegion& sr = discovered_source_regions_[key];
2,118,527✔
1837
      source_region_map_[key] = source_regions_.n_source_regions();
2,118,527✔
1838
      source_regions_.push_back(sr);
2,118,527✔
1839
    }
1840

1841
    // Map all new source regions to tallies
1842
    convert_source_regions_to_tallies(start_sr_id);
891✔
1843
  }
1844

1845
  discovered_source_regions_.clear();
130,512✔
1846
}
130,512✔
1847

1848
// This is the "diagonal stabilization" technique developed by Gunow et al. in:
1849
//
1850
// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group
1851
// neutron transport with transport-corrected cross-sections, Annals of Nuclear
1852
// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549,
1853
// https://doi.org/10.1016/j.anucene.2018.10.036.
1854
void FlatSourceDomain::apply_transport_stabilization()
130,512✔
1855
{
1856
  // Don't do anything if all in-group scattering
1857
  // cross sections are positive
1858
  if (!is_transport_stabilization_needed_) {
130,512✔
1859
    return;
127,512✔
1860
  }
1861

1862
  // Apply the stabilization factor to all source elements
1863
#pragma omp parallel for
1,500✔
1864
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
148,300✔
1865
    int material = source_regions_.material(sr);
146,800✔
1866
    double density_mult = source_regions_.density_mult(sr);
146,800✔
1867
    if (material == MATERIAL_VOID) {
146,800!
1868
      continue;
1869
    }
1870
    for (int g = 0; g < negroups_; g++) {
10,422,800✔
1871
      // Only apply stabilization if the diagonal (in-group) scattering XS is
1872
      // negative
1873
      double sigma_s =
1874
        sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g] *
10,276,000✔
1875
        density_mult;
10,276,000✔
1876
      if (sigma_s < 0.0) {
10,276,000✔
1877
        double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
3,550,000✔
1878
        double phi_new = source_regions_.scalar_flux_new(sr, g);
3,550,000✔
1879
        double phi_old = source_regions_.scalar_flux_old(sr, g);
3,550,000✔
1880

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

1891
        // Equation 16 in the above Gunow et al. 2019 paper
1892
        source_regions_.scalar_flux_new(sr, g) =
3,550,000✔
1893
          (phi_new - D * phi_old) / (1.0 - D);
3,550,000✔
1894
      }
1895
    }
1896
  }
1897
}
1898

1899
// Determines the base source region index (i.e., a material filled cell
1900
// instance) that corresponds to a particular location in the geometry. Requires
1901
// that the "gs" object passed in has already been initialized and has called
1902
// find_cell etc.
1903
int64_t FlatSourceDomain::lookup_base_source_region_idx(
2,147,483,647✔
1904
  const GeometryState& gs) const
1905
{
1906
  int i_cell = gs.lowest_coord().cell();
2,147,483,647✔
1907
  int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance();
2,147,483,647✔
1908
  return sr;
2,147,483,647✔
1909
}
1910

1911
// Determines the index of the mesh (if any) that has been applied
1912
// to a particular base source region index.
1913
int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const
2,147,483,647✔
1914
{
1915
  int mesh_idx = C_NONE;
2,147,483,647✔
1916
  auto mesh_it = mesh_map_.find(sr);
2,147,483,647✔
1917
  if (mesh_it != mesh_map_.end()) {
2,147,483,647✔
1918
    mesh_idx = mesh_it->second;
2,147,483,647✔
1919
  }
1920
  return mesh_idx;
2,147,483,647✔
1921
}
1922

1923
// Determines the source region key that corresponds to a particular location in
1924
// the geometry. This takes into account both the base source region index as
1925
// well as the mesh bin if a mesh is applied to this source region for
1926
// subdivision.
1927
SourceRegionKey FlatSourceDomain::lookup_source_region_key(
13,686,735✔
1928
  const GeometryState& gs) const
1929
{
1930
  int64_t sr = lookup_base_source_region_idx(gs);
13,686,735✔
1931
  int64_t mesh_bin = lookup_mesh_bin(sr, gs.r());
13,686,735✔
1932
  return SourceRegionKey {sr, mesh_bin};
13,686,735✔
1933
}
1934

1935
// Determines the mesh bin that corresponds to a particular base source region
1936
// index and position.
1937
int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const
13,686,735✔
1938
{
1939
  int mesh_idx = lookup_mesh_idx(sr);
13,686,735✔
1940
  int mesh_bin = 0;
13,686,735✔
1941
  if (mesh_idx != C_NONE) {
13,686,735✔
1942
    mesh_bin = model::meshes[mesh_idx]->get_bin(r);
7,411,735✔
1943
  }
1944
  return mesh_bin;
13,686,735✔
1945
}
1946

1947
//------------------------------------------------------------------------------
1948
// Methods for kinetic simulations
1949

1950
// Generates new estimate of k_dynamic based on the fraction between this
1951
// timestep's estimate of neutron production and loss. (previous timestep
1952
// fission vs current timestep fission?)
1953
// TODO: implement compute_k_dynamic
1954

1955
// Compute new estimate of scattering + fission (+ precursor decay for
1956
// kinetic simulations) sources in each source region based on the flux
1957
// estimate from the previous iteration.
1958

1959
void FlatSourceDomain::compute_single_phi_prime(SourceRegionHandle& srh)
108,420,000✔
1960
{
1961
  double A0 =
1962
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
108,420,000✔
1963
  int material = srh.material();
108,420,000✔
1964
  double density_mult = srh.density_mult();
108,420,000✔
1965
  for (int g = 0; g < negroups_; g++) {
873,132,000✔
1966
    double inverse_vbar = inverse_vbar_[material * negroups_ + g];
764,712,000✔
1967
    // TODO: add support for explicit void
1968
    double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
764,712,000✔
1969
    ;
1970

1971
    double scalar_flux_time_derivative =
1972
      A0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd(g);
764,712,000✔
1973
    srh.phi_prime(g) = scalar_flux_time_derivative * inverse_vbar / sigma_t;
764,712,000✔
1974
  }
1975
}
108,420,000✔
1976

1977
// T1 calculation
1978
void FlatSourceDomain::compute_single_T1(SourceRegionHandle& srh)
260,000✔
1979
{
1980
  double A0 =
1981
    (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] / settings::dt;
260,000✔
1982
  double B0 = (bd_coefficients_second_order_.at(RandomRay::bd_order_))[0] /
260,000✔
1983
              (settings::dt * settings::dt);
260,000✔
1984
  int material = srh.material();
260,000✔
1985
  double density_mult = srh.density_mult();
260,000✔
1986
  for (int g = 0; g < negroups_; g++) {
7,852,000✔
1987
    double inverse_vbar = inverse_vbar_[material * negroups_ + g];
7,592,000✔
1988
    // TODO: add support for explicit void
1989
    double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
7,592,000✔
1990

1991
    // Multiply out sigma_t to correctly compute the derivative term
1992
    float source_time_derivative =
1993
      A0 * srh.source(g) * sigma_t + srh.source_rhs_bd(g);
7,592,000✔
1994

1995
    double scalar_flux_time_derivative_2 =
1996
      B0 * srh.scalar_flux_old(g) + srh.scalar_flux_rhs_bd_2(g);
7,592,000✔
1997
    scalar_flux_time_derivative_2 *= inverse_vbar;
7,592,000✔
1998

1999
    // Divide by sigma_t to save time during transport
2000
    srh.T1(g) =
7,592,000✔
2001
      (source_time_derivative - scalar_flux_time_derivative_2) / sigma_t;
7,592,000✔
2002
  }
2003
}
260,000✔
2004

2005
void FlatSourceDomain::compute_single_delayed_fission_source(
152,151,960✔
2006
  SourceRegionHandle& srh)
2007
{
2008

2009
  // Reset all delayed fission sources to zero (important for void regions)
2010
  for (int dg = 0; dg < ndgroups_; dg++) {
1,367,911,640✔
2011
    srh.delayed_fission_source(dg) = 0.0;
1,215,759,680✔
2012
  }
2013

2014
  int material = srh.material();
152,151,960✔
2015
  double density_mult = srh.density_mult();
152,151,960✔
2016
  if (material != MATERIAL_VOID) {
152,151,960!
2017
    double inverse_k_eff = 1.0 / k_eff_;
152,151,960✔
2018
    for (int dg = 0; dg < ndgroups_; dg++) {
1,367,911,640✔
2019
      // We cannot have delayed neutrons if there is no delayed data
2020
      double lambda = lambda_[material * ndgroups_ + dg];
1,215,759,680✔
2021
      if (lambda != 0.0) {
1,215,759,680✔
2022
        for (int g = 0; g < negroups_; g++) {
2,147,483,647✔
2023
          double scalar_flux = scalar_flux = srh.scalar_flux_new(g);
2,147,483,647✔
2024
          double nu_d_sigma_f = nu_d_sigma_f_[material * negroups_ * ndgroups_ +
2,147,483,647✔
2025
                                              dg * negroups_ + g] *
2,147,483,647✔
2026
                                density_mult;
2,147,483,647✔
2027
          srh.delayed_fission_source(dg) += nu_d_sigma_f * scalar_flux;
2,147,483,647✔
2028
        }
2029
        srh.delayed_fission_source(dg) *= inverse_k_eff;
560,447,840✔
2030
      }
2031
    }
2032
  }
2033
}
152,151,960✔
2034

2035
void FlatSourceDomain::compute_single_precursors(SourceRegionHandle& srh)
152,151,960✔
2036
{
2037
  // Reset all precursors to zero (important for void regions)
2038
  for (int dg = 0; dg < ndgroups_; dg++) {
1,367,911,640✔
2039
    srh.precursors_new(dg) = 0.0;
1,215,759,680✔
2040
  }
2041

2042
  int material = srh.material();
152,151,960✔
2043
  if (material != MATERIAL_VOID) {
152,151,960!
2044
    for (int dg = 0; dg < ndgroups_; dg++) {
1,367,911,640✔
2045
      double lambda = lambda_[material * ndgroups_ + dg];
1,215,759,680✔
2046
      if (lambda != 0.0) {
1,215,759,680✔
2047
        double delayed_fission_source = srh.delayed_fission_source(dg);
560,447,840✔
2048
        if (simulation::is_initial_condition) {
560,447,840✔
2049
          srh.precursors_new(dg) = delayed_fission_source / lambda;
160,127,840✔
2050
        } else {
2051
          double precursor_rhs_bd = srh.precursors_rhs_bd(dg);
400,320,000✔
2052
          srh.precursors_new(dg) = delayed_fission_source - precursor_rhs_bd;
400,320,000✔
2053
          double A0 =
2054
            (bd_coefficients_first_order_.at(RandomRay::bd_order_))[0] /
400,320,000✔
2055
            settings::dt;
400,320,000✔
2056
          srh.precursors_new(dg) /= A0 + lambda;
400,320,000✔
2057
        }
2058
      }
2059
    }
2060
  }
2061
}
152,151,960✔
2062

2063
void FlatSourceDomain::compute_all_precursors()
119,000✔
2064
{
2065
  simulation::time_compute_precursors.start();
119,000✔
2066

2067
#pragma omp parallel for
59,500✔
2068
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
76,135,480✔
2069
    SourceRegionHandle srh = source_regions_.get_source_region_handle(sr);
76,075,980✔
2070
    compute_single_delayed_fission_source(srh);
76,075,980✔
2071
    compute_single_precursors(srh);
76,075,980✔
2072
  }
2073

2074
  simulation::time_compute_precursors.start();
119,000✔
2075
}
119,000✔
2076

NEW
2077
void FlatSourceDomain::serialize_final_precursors(vector<double>& precursors)
×
2078
{
2079
  // Ensure array is correct size
NEW
2080
  precursors.resize(n_source_regions() * ndgroups_);
×
2081
// Serialize the precursors for output
2082
#pragma omp parallel for
2083
  for (int64_t de = 0; de < n_delay_elements(); de++) {
×
2084
    precursors[de] = source_regions_.precursors_final(de);
2085
  }
NEW
2086
}
×
2087

2088
void FlatSourceDomain::precursors_swap()
119,000✔
2089
{
2090
  source_regions_.precursors_swap();
119,000✔
2091
}
119,000✔
2092

2093
void FlatSourceDomain::accumulate_iteration_quantities()
63,556✔
2094
{
2095
  accumulate_iteration_flux();
63,556✔
2096
  if (settings::kinetic_simulation) {
63,556✔
2097
#pragma omp parallel for
29,400✔
2098
    for (int64_t sr = 0; sr < n_source_regions(); sr++) {
38,031,000✔
2099
      for (int g = 0; g < negroups_; g++) {
305,760,000✔
2100
        if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
267,758,400✔
2101
          source_regions_.source_final(sr, g) += source_regions_.source(sr, g);
1,383,200✔
2102
        }
2103
      }
2104
      if (settings::create_delayed_neutrons) {
38,001,600!
2105
        for (int dg = 0; dg < ndgroups_; dg++) {
341,723,200✔
2106
          source_regions_.precursors_final(sr, dg) +=
303,721,600✔
2107
            source_regions_.precursors_new(sr, dg);
303,721,600✔
2108
        }
2109
      }
2110
    }
2111
  }
2112
}
63,556✔
2113

2114
void FlatSourceDomain::normalize_final_quantities()
1,878✔
2115
{
2116
  double normalization_factor =
1,878✔
2117
    1.0 / (settings::n_batches - settings::n_inactive);
1,878✔
2118
  double source_normalization_factor;
2119
  if (!settings::kinetic_simulation ||
1,878!
2120
      settings::kinetic_simulation &&
1,176✔
2121
        simulation::current_timestep == settings::n_timesteps)
1,176✔
2122
    source_normalization_factor =
870✔
2123
      compute_fixed_source_normalization_factor() * normalization_factor;
870✔
2124
  else
2125
    // The source normalization should only be applied to internal quantities at
2126
    // the end of time stepping in preparation for an adjoint solve.
2127
    source_normalization_factor = 1.0 * normalization_factor;
1,008✔
2128

2129
#pragma omp parallel for
940✔
2130
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
1,404,098✔
2131
    for (int g = 0; g < negroups_; g++) {
4,624,140✔
2132
      source_regions_.scalar_flux_final(sr, g) *= source_normalization_factor;
3,220,980✔
2133
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
3,220,980✔
2134
        source_regions_.source_final(sr, g) *= source_normalization_factor;
276,640✔
2135
      }
2136
    }
2137
    if (settings::kinetic_simulation) {
1,403,160✔
2138
      for (int dg = 0; dg < ndgroups_; dg++) {
1,907,360✔
2139
        source_regions_.precursors_final(sr, dg) *= source_normalization_factor;
1,688,960✔
2140
      }
2141
    }
2142
  }
2143
}
1,878✔
2144

2145
void FlatSourceDomain::propagate_final_quantities()
1,008✔
2146
{
2147
#pragma omp parallel for
504✔
2148
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,704✔
2149
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2150
      source_regions_.scalar_flux_old(sr, g) =
1,609,920✔
2151
        source_regions_.scalar_flux_final(sr, g);
1,609,920✔
2152
    }
2153
    if (settings::create_delayed_neutrons) {
187,200!
2154
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2155
        source_regions_.precursors_old(sr, dg) =
1,447,680✔
2156
          source_regions_.precursors_final(sr, dg);
1,447,680✔
2157
      }
2158
    }
2159
  }
2160
}
1,008✔
2161

2162
void FlatSourceDomain::store_time_step_quantities(bool increment_not_initialize)
1,008✔
2163
{
2164
#pragma omp parallel for
504✔
2165
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
187,704✔
2166
    for (int g = 0; g < negroups_; g++) {
1,797,120✔
2167
      int j = 0;
1,609,920✔
2168
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION)
1,609,920✔
2169
        j = 1;
237,120✔
2170
      add_value_to_bd_vector(source_regions_.scalar_flux_bd(sr, g),
1,609,920✔
2171
        source_regions_.scalar_flux_final(sr, g), increment_not_initialize,
2172
        RandomRay::bd_order_ + j);
2173
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,609,920✔
2174
        // Multiply out sigma_t to store the base source
2175
        int material = source_regions_.material(sr);
237,120✔
2176
        double density_mult = source_regions_.density_mult(sr);
237,120✔
2177
        // TODO: add support for explicit void regions
2178
        double sigma_t = sigma_t_[material * negroups_ + g] * density_mult;
237,120✔
2179
        float source = source_regions_.source_final(sr, g) * sigma_t;
237,120✔
2180
        add_value_to_bd_vector(source_regions_.source_bd(sr, g), source,
237,120✔
2181
          increment_not_initialize, RandomRay::bd_order_);
2182
      }
2183
    }
2184
    if (settings::create_delayed_neutrons) {
187,200!
2185
      for (int dg = 0; dg < ndgroups_; dg++) {
1,634,880✔
2186
        add_value_to_bd_vector(source_regions_.precursors_bd(sr, dg),
1,447,680✔
2187
          source_regions_.precursors_final(sr, dg), increment_not_initialize,
2188
          RandomRay::bd_order_);
2189
      }
2190
    }
2191
  }
2192
}
1,008✔
2193

2194
void FlatSourceDomain::compute_rhs_bd_quantities()
840✔
2195
{
2196
#pragma omp parallel for
420✔
2197
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
156,420✔
2198
    for (int g = 0; g < negroups_; g++) {
1,497,600✔
2199
      source_regions_.scalar_flux_rhs_bd(sr, g) =
2,683,200✔
2200
        rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
1,341,600✔
2201
          RandomRay::bd_order_, settings::dt);
2202

2203
      if (RandomRay::time_method_ == RandomRayTimeMethod::PROPAGATION) {
1,341,600✔
2204
        source_regions_.source_rhs_bd(sr, g) = rhs_backwards_difference(
197,600✔
2205
          source_regions_.source_bd(sr, g), RandomRay::bd_order_, settings::dt);
2206

2207
        source_regions_.scalar_flux_rhs_bd_2(sr, g) =
197,600✔
2208
          rhs_backwards_difference(source_regions_.scalar_flux_bd(sr, g),
197,600✔
2209
            RandomRay::bd_order_, settings::dt, 2);
2210
      }
2211
    }
2212
    if (settings::create_delayed_neutrons) {
156,000!
2213
      for (int dg = 0; dg < ndgroups_; dg++) {
1,362,400✔
2214
        source_regions_.precursors_rhs_bd(sr, dg) =
1,206,400✔
2215
          rhs_backwards_difference(source_regions_.precursors_bd(sr, dg),
1,206,400✔
2216
            RandomRay::bd_order_, settings::dt);
2217
      }
2218
    }
2219
  }
2220
}
840✔
2221

2222
// Update material density and cross sections
2223
void FlatSourceDomain::update_material_density(int i)
840✔
2224
{
2225
#pragma omp parallel for
420✔
2226
  for (int j = 0; j < model::materials.size(); j++) {
1,610✔
2227
    auto& mat {model::materials[j]};
1,190✔
2228
    if (mat->density_timeseries_.size() != 0) {
1,190✔
2229
      double density_factor = mat->density_timeseries_[i] / mat->density_;
420✔
2230
      mat->density_ = mat->density_timeseries_[i];
420✔
2231
      for (int g_out = 0; g_out < negroups_; g_out++) {
6,720✔
2232
        for (int dg = 0; dg < ndgroups_; dg++) {
46,060✔
2233
          nu_d_sigma_f_[j * negroups_ * ndgroups_ + dg * negroups_ + g_out] *=
39,760✔
2234
            density_factor;
2235
        }
2236
        nu_p_sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2237
        sigma_t_[j * negroups_ + g_out] *= density_factor;
6,300✔
2238
        nu_sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2239
        sigma_f_[j * negroups_ + g_out] *= density_factor;
6,300✔
2240
        for (int g_in = 0; g_in < negroups_; g_in++) {
357,000✔
2241
          sigma_s_[j * negroups_ * negroups_ + g_out * negroups_ + g_in] *=
350,700✔
2242
            density_factor;
2243
        }
2244
      }
2245
    }
2246
  }
2247
}
840✔
2248

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