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

openmc-dev / openmc / 13688481331

06 Mar 2025 12:35AM UTC coverage: 85.002% (-0.04%) from 85.042%
13688481331

Pull #3333

github

web-flow
Merge 5654c92ef into ced892912
Pull Request #3333: Random Ray Source Region Mesh Subdivision (Cell-Under-Voxel Geometry)

560 of 646 new or added lines in 11 files covered. (86.69%)

361 existing lines in 16 files now uncovered.

51473 of 60555 relevant lines covered (85.0%)

36077311.57 hits per line

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

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

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

19
#include <cstdio>
20

21
namespace openmc {
22

23
//==============================================================================
24
// FlatSourceDomain implementation
25
//==============================================================================
26

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

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

51
  // Initialize source regions.
52
  bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT;
560✔
53
  source_regions_ = SourceRegionContainer(negroups_, is_linear);
560✔
54
  source_regions_.assign(
560✔
55
    base_source_regions, SourceRegion(negroups_, is_linear));
1,120✔
56

57
  // Initialize materials
58
  int64_t source_region_id = 0;
560✔
59
  for (int i = 0; i < model::cells.size(); i++) {
5,104✔
60
    Cell& cell = *model::cells[i];
4,544✔
61
    if (cell.type_ == Fill::MATERIAL) {
4,544✔
62
      for (int j = 0; j < cell.n_instances_; j++) {
740,112✔
63
        source_regions_.material(source_region_id++) = cell.material(j);
737,856✔
64
      }
65
    }
66
  }
67

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

73
  // Initialize tally volumes
74
  if (volume_normalized_flux_tallies_) {
560✔
75
    tally_volumes_.resize(model::tallies.size());
496✔
76
    for (int i = 0; i < model::tallies.size(); i++) {
1,856✔
77
      //  Get the shape of the 3D result tensor
78
      auto shape = model::tallies[i]->results().shape();
1,360✔
79

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

87
  // Compute simulation domain volume based on ray source
88
  auto* is = dynamic_cast<IndependentSource*>(RandomRay::ray_source_.get());
560✔
89
  SpatialDistribution* space_dist = is->space();
560✔
90
  SpatialBox* sb = dynamic_cast<SpatialBox*>(space_dist);
560✔
91
  Position dims = sb->upper_right() - sb->lower_left();
560✔
92
  simulation_volume_ = dims.x * dims.y * dims.z;
560✔
93
}
560✔
94

95
void FlatSourceDomain::batch_reset()
10,670✔
96
{
97
// Reset scalar fluxes and iteration volume tallies to zero
98
#pragma omp parallel for
5,820✔
99
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
25,742,790✔
100
    source_regions_.volume(sr) = 0.0;
25,737,940✔
101
    source_regions_.volume_sq(sr) = 0.0;
25,737,940✔
102
  }
103

104
#pragma omp parallel for
5,820✔
105
  for (int64_t se = 0; se < n_source_elements(); se++) {
29,955,570✔
106
    source_regions_.scalar_flux_new(se) = 0.0;
29,950,720✔
107
  }
108
}
10,670✔
109

110
void FlatSourceDomain::accumulate_iteration_flux()
4,290✔
111
{
112
#pragma omp parallel for
2,340✔
113
  for (int64_t se = 0; se < n_source_elements(); se++) {
13,069,350✔
114
    source_regions_.scalar_flux_final(se) +=
13,067,400✔
115
      source_regions_.scalar_flux_new(se);
13,067,400✔
116
  }
117
}
4,290✔
118

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

125
  double inverse_k_eff = 1.0 / k_eff;
4,785✔
126

127
// Reset all source regions to zero (important for void regions)
128
#pragma omp parallel for
2,610✔
129
  for (int64_t se = 0; se < n_source_elements(); se++) {
14,896,490✔
130
    source_regions_.source(se) = 0.0;
14,894,315✔
131
  }
132

133
  // Add scattering + fission source
134
#pragma omp parallel for
2,610✔
135
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
12,574,310✔
136
    int material = source_regions_.material(sr);
12,572,135✔
137
    if (material == MATERIAL_VOID) {
12,572,135✔
138
      continue;
200,000✔
139
    }
140
    for (int g_out = 0; g_out < negroups_; g_out++) {
27,066,450✔
141
      double sigma_t = sigma_t_[material * negroups_ + g_out];
14,694,315✔
142
      double scatter_source = 0.0;
14,694,315✔
143
      double fission_source = 0.0;
14,694,315✔
144

145
      for (int g_in = 0; g_in < negroups_; g_in++) {
45,643,890✔
146
        double scalar_flux = source_regions_.scalar_flux_old(sr, g_in);
30,949,575✔
147
        double sigma_s =
148
          sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in];
30,949,575✔
149
        double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in];
30,949,575✔
150
        double chi = chi_[material * negroups_ + g_out];
30,949,575✔
151

152
        scatter_source += sigma_s * scalar_flux;
30,949,575✔
153
        fission_source += nu_sigma_f * scalar_flux * chi;
30,949,575✔
154
      }
155
      source_regions_.source(sr, g_out) =
14,694,315✔
156
        (scatter_source + fission_source * inverse_k_eff) / sigma_t;
14,694,315✔
157
    }
158
  }
159

160
  // Add external source if in fixed source mode
161
  if (settings::run_mode == RunMode::FIXED_SOURCE) {
4,785✔
162
#pragma omp parallel for
2,250✔
163
    for (int64_t se = 0; se < n_source_elements(); se++) {
13,683,880✔
164
      source_regions_.source(se) += source_regions_.external_source(se);
13,682,005✔
165
    }
166
  }
167

168
  simulation::time_update_src.stop();
4,785✔
169
}
4,785✔
170

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

179
// Normalize scalar flux to total distance travelled by all rays this
180
// iteration
181
#pragma omp parallel for
2,610✔
182
  for (int64_t se = 0; se < n_source_elements(); se++) {
15,265,270✔
183
    source_regions_.scalar_flux_new(se) *= normalization_factor;
15,263,095✔
184
  }
185

186
// Accumulate cell-wise ray length tallies collected this iteration, then
187
// update the simulation-averaged cell-wise volume estimates
188
#pragma omp parallel for
2,610✔
189
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
12,870,730✔
190
    source_regions_.volume_t(sr) += source_regions_.volume(sr);
12,868,555✔
191
    source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr);
12,868,555✔
192
    source_regions_.volume_naive(sr) =
25,737,110✔
193
      source_regions_.volume(sr) * normalization_factor;
12,868,555✔
194
    source_regions_.volume_sq(sr) =
25,737,110✔
195
      source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr);
12,868,555✔
196
    source_regions_.volume(sr) =
12,868,555✔
197
      source_regions_.volume_t(sr) * volume_normalization_factor;
12,868,555✔
198
  }
199
}
4,785✔
200

201
void FlatSourceDomain::set_flux_to_flux_plus_source(
31,123,708✔
202
  int64_t sr, double volume, int g)
203
{
204
  int material = source_regions_.material(sr);
31,123,708✔
205
  if (material == MATERIAL_VOID) {
31,123,708✔
206
    source_regions_.scalar_flux_new(sr, g) /= volume;
880,000✔
207
    source_regions_.scalar_flux_new(sr, g) +=
880,000✔
208
      0.5f * source_regions_.external_source(sr, g) *
880,000✔
209
      source_regions_.volume_sq(sr);
880,000✔
210
  } else {
211
    double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g];
30,243,708✔
212
    source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume);
30,243,708✔
213
    source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g);
30,243,708✔
214
  }
215
}
31,123,708✔
216

217
void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g)
990✔
218
{
219
  source_regions_.scalar_flux_new(sr, g) =
1,980✔
220
    source_regions_.scalar_flux_old(sr, g);
990✔
221
}
990✔
222

223
void FlatSourceDomain::set_flux_to_source(int64_t sr, int g)
5,787,793✔
224
{
225
  source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g);
5,787,793✔
226
}
5,787,793✔
227

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

235
#pragma omp parallel for reduction(+ : n_hits)
5,820✔
236
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
26,323,570✔
237

238
    double volume_simulation_avg = source_regions_.volume(sr);
26,318,720✔
239
    double volume_iteration = source_regions_.volume_naive(sr);
26,318,720✔
240

241
    // Increment the number of hits if cell was hit this iteration
242
    if (volume_iteration) {
26,318,720✔
243
      n_hits++;
23,687,145✔
244
    }
245

246
    // Set the SR to small status if its expected number of hits
247
    // per iteration is less than 1.5
248
    const double min_hits_per_batch = 1.5;
26,318,720✔
249
    if (source_regions_.n_hits(sr) * inverse_batch < min_hits_per_batch) {
26,318,720✔
250
      source_regions_.is_small(sr) = 1;
4,172,940✔
251
    } else {
252
      source_regions_.is_small(sr) = 0;
22,145,780✔
253
    }
254

255
    // The volume treatment depends on the volume estimator type
256
    // and whether or not an external source is present in the cell.
257
    double volume;
258
    switch (volume_estimator_) {
26,318,720✔
259
    case RandomRayVolumeEstimator::NAIVE:
777,600✔
260
      volume = volume_iteration;
777,600✔
261
      break;
777,600✔
262
    case RandomRayVolumeEstimator::SIMULATION_AVERAGED:
432,000✔
263
      volume = volume_simulation_avg;
432,000✔
264
      break;
432,000✔
265
    case RandomRayVolumeEstimator::HYBRID:
25,109,120✔
266
      if (source_regions_.external_source_present(sr) ||
46,030,780✔
267
          source_regions_.is_small(sr)) {
20,921,660✔
268
        volume = volume_iteration;
8,360,160✔
269
      } else {
270
        volume = volume_simulation_avg;
16,748,960✔
271
      }
272
      break;
25,109,120✔
273
    default:
274
      fatal_error("Invalid volume estimator type");
275
    }
276

277
    for (int g = 0; g < negroups_; g++) {
56,922,580✔
278
      // There are three scenarios we need to consider:
279
      if (volume_iteration > 0.0) {
30,603,860✔
280
        // 1. If the FSR was hit this iteration, then the new flux is equal to
281
        // the flat source from the previous iteration plus the contributions
282
        // from rays passing through the source region (computed during the
283
        // transport sweep)
284
        set_flux_to_flux_plus_source(sr, volume, g);
27,972,135✔
285
      } else if (volume_simulation_avg > 0.0) {
2,631,725✔
286
        // 2. If the FSR was not hit this iteration, but has been hit some
287
        // previous iteration, then we need to make a choice about what
288
        // to do. Naively we will usually want to set the flux to be equal
289
        // to the reduced source. However, in fixed source problems where
290
        // there is a strong external source present in the cell, and where
291
        // the cell has a very low cross section, this approximation will
292
        // cause a huge upward bias in the flux estimate of the cell (in these
293
        // conditions, the flux estimate can be orders of magnitude too large).
294
        // Thus, to avoid this bias, if any external source is present
295
        // in the cell we will use the previous iteration's flux estimate. This
296
        // injects a small degree of correlation into the simulation, but this
297
        // is going to be trivial when the miss rate is a few percent or less.
298
        if (source_regions_.external_source_present(sr) && !adjoint_) {
2,631,715✔
299
          set_flux_to_old_flux(sr, g);
900✔
300
        } else {
301
          set_flux_to_source(sr, g);
2,630,815✔
302
        }
303
      }
304
    }
305
  }
306

307
  // Return the number of source regions that were hit this iteration
308
  return n_hits;
10,670✔
309
}
310

311
// Generates new estimate of k_eff based on the differences between this
312
// iteration's estimate of the scalar flux and the last iteration's estimate.
313
double FlatSourceDomain::compute_k_eff(double k_eff_old) const
1,540✔
314
{
315
  double fission_rate_old = 0;
1,540✔
316
  double fission_rate_new = 0;
1,540✔
317

318
  // Vector for gathering fission source terms for Shannon entropy calculation
319
  vector<float> p(n_source_regions(), 0.0f);
1,540✔
320

321
#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new)
840✔
322
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
305,490✔
323

324
    // If simulation averaged volume is zero, don't include this cell
325
    double volume = source_regions_.volume(sr);
304,790✔
326
    if (volume == 0.0) {
304,790✔
327
      continue;
328
    }
329

330
    int material = source_regions_.material(sr);
304,790✔
331
    if (material == MATERIAL_VOID) {
304,790✔
332
      continue;
333
    }
334

335
    double sr_fission_source_old = 0;
304,790✔
336
    double sr_fission_source_new = 0;
304,790✔
337

338
    for (int g = 0; g < negroups_; g++) {
2,284,720✔
339
      double nu_sigma_f = nu_sigma_f_[material * negroups_ + g];
1,979,930✔
340
      sr_fission_source_old +=
1,979,930✔
341
        nu_sigma_f * source_regions_.scalar_flux_old(sr, g);
1,979,930✔
342
      sr_fission_source_new +=
1,979,930✔
343
        nu_sigma_f * source_regions_.scalar_flux_new(sr, g);
1,979,930✔
344
    }
345

346
    // Compute total fission rates in FSR
347
    sr_fission_source_old *= volume;
304,790✔
348
    sr_fission_source_new *= volume;
304,790✔
349

350
    // Accumulate totals
351
    fission_rate_old += sr_fission_source_old;
304,790✔
352
    fission_rate_new += sr_fission_source_new;
304,790✔
353

354
    // Store total fission rate in the FSR for Shannon calculation
355
    p[sr] = sr_fission_source_new;
304,790✔
356
  }
357

358
  double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old);
1,540✔
359

360
  double H = 0.0;
1,540✔
361
  // defining an inverse sum for better performance
362
  double inverse_sum = 1 / fission_rate_new;
1,540✔
363

364
#pragma omp parallel for reduction(+ : H)
840✔
365
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
305,490✔
366
    // Only if FSR has non-negative and non-zero fission source
367
    if (p[sr] > 0.0f) {
304,790✔
368
      // Normalize to total weight of bank sites. p_i for better performance
369
      float p_i = p[sr] * inverse_sum;
129,995✔
370
      // Sum values to obtain Shannon entropy.
371
      H -= p_i * std::log2(p_i);
129,995✔
372
    }
373
  }
374

375
  // Adds entropy value to shared entropy vector in openmc namespace.
376
  simulation::entropy.push_back(H);
1,540✔
377

378
  return k_eff_new;
1,540✔
379
}
1,540✔
380

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

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

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

414
void FlatSourceDomain::convert_source_regions_to_tallies()
385✔
415
{
416
  openmc::simulation::time_tallies.start();
385✔
417

418
  // Tracks if we've generated a mapping yet for all source regions.
419
  bool all_source_regions_mapped = true;
385✔
420

421
// Attempt to generate mapping for all source regions
422
#pragma omp parallel for
210✔
423
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
932,495✔
424

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

432
    // A particle located at the recorded midpoint of a ray
433
    // crossing through this source region is used to estabilish
434
    // the spatial location of the source region
435
    Particle p;
932,320✔
436
    p.r() = source_regions_.position(sr);
932,320✔
437
    p.r_last() = source_regions_.position(sr);
932,320✔
438
    bool found = exhaustive_find_cell(p);
932,320✔
439

440
    // Loop over energy groups (so as to support energy filters)
441
    for (int g = 0; g < negroups_; g++) {
2,009,120✔
442

443
      // Set particle to the current energy
444
      p.g() = g;
1,076,800✔
445
      p.g_last() = g;
1,076,800✔
446
      p.E() = data::mg.energy_bin_avg_[p.g()];
1,076,800✔
447
      p.E_last() = p.E();
1,076,800✔
448

449
      int64_t source_element = sr * negroups_ + g;
1,076,800✔
450

451
      // If this task has already been populated, we don't need to do
452
      // it again.
453
      if (source_regions_.tally_task(sr, g).size() > 0) {
1,076,800✔
454
        continue;
455
      }
456

457
      // Loop over all active tallies. This logic is essentially identical
458
      // to what happens when scanning for applicable tallies during
459
      // MC transport.
460
      for (auto i_tally : model::active_tallies) {
4,256,160✔
461
        Tally& tally {*model::tallies[i_tally]};
3,179,360✔
462

463
        // Initialize an iterator over valid filter bin combinations.
464
        // If there are no valid combinations, use a continue statement
465
        // to ensure we skip the assume_separate break below.
466
        auto filter_iter = FilterBinIter(tally, p);
3,179,360✔
467
        auto end = FilterBinIter(tally, true, &p.filter_matches());
3,179,360✔
468
        if (filter_iter == end)
3,179,360✔
469
          continue;
1,811,360✔
470

471
        // Loop over filter bins.
472
        for (; filter_iter != end; ++filter_iter) {
2,736,000✔
473
          auto filter_index = filter_iter.index_;
1,368,000✔
474
          auto filter_weight = filter_iter.weight_;
1,368,000✔
475

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

484
            // Also add this task to the list of volume tasks for this source
485
            // region.
486
            source_regions_.volume_task(sr).insert(task);
1,705,120✔
487
          }
488
        }
489
      }
490
      // Reset all the filter matches for the next tally event.
491
      for (auto& match : p.filter_matches())
5,012,240✔
492
        match.bins_present_ = false;
3,935,440✔
493
    }
494
  }
932,320✔
495
  openmc::simulation::time_tallies.stop();
385✔
496

497
  mapped_all_tallies_ = all_source_regions_mapped;
385✔
498
}
385✔
499

500
// Set the volume accumulators to zero for all tallies
501
void FlatSourceDomain::reset_tally_volumes()
4,290✔
502
{
503
  if (volume_normalized_flux_tallies_) {
4,290✔
504
#pragma omp parallel for
1,980✔
505
    for (int i = 0; i < tally_volumes_.size(); i++) {
6,200✔
506
      auto& tensor = tally_volumes_[i];
4,550✔
507
      tensor.fill(0.0); // Set all elements of the tensor to 0.0
4,550✔
508
    }
509
  }
510
}
4,290✔
511

512
// In fixed source mode, due to the way that volumetric fixed sources are
513
// converted and applied as volumetric sources in one or more source regions,
514
// we need to perform an additional normalization step to ensure that the
515
// reported scalar fluxes are in units per source neutron. This allows for
516
// direct comparison of reported tallies to Monte Carlo flux results.
517
// This factor needs to be computed at each iteration, as it is based on the
518
// volume estimate of each FSR, which improves over the course of the
519
// simulation
520
double FlatSourceDomain::compute_fixed_source_normalization_factor() const
4,770✔
521
{
522
  // If we are not in fixed source mode, then there are no external sources
523
  // so no normalization is needed.
524
  if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) {
4,770✔
525
    return 1.0;
1,212✔
526
  }
527

528
  // Step 1 is to sum over all source regions and energy groups to get the
529
  // total external source strength in the simulation.
530
  double simulation_external_source_strength = 0.0;
3,558✔
531
#pragma omp parallel for reduction(+ : simulation_external_source_strength)
1,947✔
532
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
10,915,819✔
533
    int material = source_regions_.material(sr);
10,914,208✔
534
    double volume = source_regions_.volume(sr) * simulation_volume_;
10,914,208✔
535
    for (int g = 0; g < negroups_; g++) {
22,379,648✔
536
      // For non-void regions, we store the external source pre-divided by
537
      // sigma_t. We need to multiply non-void regions back up by sigma_t
538
      // to get the total source strength in the expected units.
539
      double sigma_t = 1.0;
11,465,440✔
540
      if (material != MATERIAL_VOID) {
11,465,440✔
541
        sigma_t = sigma_t_[material * negroups_ + g];
11,251,440✔
542
      }
543
      simulation_external_source_strength +=
11,465,440✔
544
        source_regions_.external_source(sr, g) * sigma_t * volume;
11,465,440✔
545
    }
546
  }
547

548
  // Step 2 is to determine the total user-specified external source strength
549
  double user_external_source_strength = 0.0;
3,558✔
550
  for (auto& ext_source : model::external_sources) {
7,116✔
551
    user_external_source_strength += ext_source->strength();
3,558✔
552
  }
553

554
  // The correction factor is the ratio of the user-specified external source
555
  // strength to the simulation external source strength.
556
  double source_normalization_factor =
3,558✔
557
    user_external_source_strength / simulation_external_source_strength;
558

559
  return source_normalization_factor;
3,558✔
560
}
561

562
// Tallying in random ray is not done directly during transport, rather,
563
// it is done only once after each power iteration. This is made possible
564
// by way of a mapping data structure that relates spatial source regions
565
// (FSRs) to tally/filter/score combinations. The mechanism by which the
566
// mapping is done (and the limitations incurred) is documented in the
567
// "convert_source_regions_to_tallies()" function comments above. The present
568
// tally function simply traverses the mapping data structure and executes
569
// the scoring operations to OpenMC's native tally result arrays.
570

571
void FlatSourceDomain::random_ray_tally()
4,290✔
572
{
573
  openmc::simulation::time_tallies.start();
4,290✔
574

575
  // Reset our tally volumes to zero
576
  reset_tally_volumes();
4,290✔
577

578
  double source_normalization_factor =
579
    compute_fixed_source_normalization_factor();
4,290✔
580

581
// We loop over all source regions and energy groups. For each
582
// element, we check if there are any scores needed and apply
583
// them.
584
#pragma omp parallel for
2,340✔
585
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
11,709,750✔
586
    // The fsr.volume_ is the unitless fractional simulation averaged volume
587
    // (i.e., it is the FSR's fraction of the overall simulation volume). The
588
    // simulation_volume_ is the total 3D physical volume in cm^3 of the
589
    // entire global simulation domain (as defined by the ray source box).
590
    // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying
591
    // its fraction of the total volume by the total volume. Not important in
592
    // eigenvalue solves, but useful in fixed source solves for returning the
593
    // flux shape with a magnitude that makes sense relative to the fixed
594
    // source strength.
595
    double volume = source_regions_.volume(sr) * simulation_volume_;
11,707,800✔
596

597
    double material = source_regions_.material(sr);
11,707,800✔
598
    for (int g = 0; g < negroups_; g++) {
24,775,200✔
599
      double flux =
600
        source_regions_.scalar_flux_new(sr, g) * source_normalization_factor;
13,067,400✔
601

602
      // Determine numerical score value
603
      for (auto& task : source_regions_.tally_task(sr, g)) {
32,145,600✔
604
        double score = 0.0;
19,078,200✔
605
        switch (task.score_type) {
19,078,200✔
606

607
        case SCORE_FLUX:
15,905,800✔
608
          score = flux * volume;
15,905,800✔
609
          break;
15,905,800✔
610

611
        case SCORE_TOTAL:
612
          if (material != MATERIAL_VOID) {
613
            score = flux * volume * sigma_t_[material * negroups_ + g];
614
          }
615
          break;
616

617
        case SCORE_FISSION:
1,586,200✔
618
          if (material != MATERIAL_VOID) {
1,586,200✔
619
            score = flux * volume * sigma_f_[material * negroups_ + g];
1,586,200✔
620
          }
621
          break;
1,586,200✔
622

623
        case SCORE_NU_FISSION:
1,586,200✔
624
          if (material != MATERIAL_VOID) {
1,586,200✔
625
            score = flux * volume * nu_sigma_f_[material * negroups_ + g];
1,586,200✔
626
          }
627
          break;
1,586,200✔
628

629
        case SCORE_EVENTS:
630
          score = 1.0;
631
          break;
632

633
        default:
634
          fatal_error("Invalid score specified in tallies.xml. Only flux, "
635
                      "total, fission, nu-fission, and events are supported in "
636
                      "random ray mode.");
637
          break;
638
        }
639

640
        // Apply score to the appropriate tally bin
641
        Tally& tally {*model::tallies[task.tally_idx]};
19,078,200✔
642
#pragma omp atomic
643
        tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) +=
19,078,200✔
644
          score;
645
      }
646
    }
647

648
    // For flux tallies, the total volume of the spatial region is needed
649
    // for normalizing the flux. We store this volume in a separate tensor.
650
    // We only contribute to each volume tally bin once per FSR.
651
    if (volume_normalized_flux_tallies_) {
11,707,800✔
652
      for (const auto& task : source_regions_.volume_task(sr)) {
28,772,800✔
653
        if (task.score_type == SCORE_FLUX) {
17,208,000✔
654
#pragma omp atomic
655
          tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) +=
15,253,600✔
656
            volume;
657
        }
658
      }
659
    }
660
  } // end FSR loop
661

662
  // Normalize any flux scores by the total volume of the FSRs scoring to that
663
  // bin. To do this, we loop over all tallies, and then all filter bins,
664
  // and then scores. For each score, we check the tally data structure to
665
  // see what index that score corresponds to. If that score is a flux score,
666
  // then we divide it by volume.
667
  if (volume_normalized_flux_tallies_) {
4,290✔
668
    for (int i = 0; i < model::tallies.size(); i++) {
13,640✔
669
      Tally& tally {*model::tallies[i]};
10,010✔
670
#pragma omp parallel for
5,460✔
671
      for (int bin = 0; bin < tally.n_filter_bins(); bin++) {
703,425✔
672
        for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) {
1,415,950✔
673
          auto score_type = tally.scores_[score_idx];
717,075✔
674
          if (score_type == SCORE_FLUX) {
717,075✔
675
            double vol = tally_volumes_[i](bin, score_idx);
698,875✔
676
            if (vol > 0.0) {
698,875✔
677
              tally.results_(bin, score_idx, TallyResult::VALUE) /= vol;
698,875✔
678
            }
679
          }
680
        }
681
      }
682
    }
683
  }
684

685
  openmc::simulation::time_tallies.stop();
4,290✔
686
}
4,290✔
687

UNCOV
688
double FlatSourceDomain::evaluate_flux_at_point(
×
689
  Position r, int64_t sr, int g) const
690
{
691
  return source_regions_.scalar_flux_final(sr, g) /
×
692
         (settings::n_batches - settings::n_inactive);
×
693
}
694

695
// Outputs all basic material, FSR ID, multigroup flux, and
696
// fission source data to .vtk file that can be directly
697
// loaded and displayed by Paraview. Note that .vtk binary
698
// files require big endian byte ordering, so endianness
699
// is checked and flipped if necessary.
700
void FlatSourceDomain::output_to_vtk() const
×
701
{
702
  // Rename .h5 plot filename(s) to .vtk filenames
703
  for (int p = 0; p < model::plots.size(); p++) {
×
704
    PlottableInterface* plot = model::plots[p].get();
×
705
    plot->path_plot() =
×
706
      plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk";
×
707
  }
708

709
  // Print header information
710
  print_plot();
×
711

712
  // Outer loop over plots
713
  for (int p = 0; p < model::plots.size(); p++) {
×
714

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

718
    // Random ray plots only support voxel plots
719
    if (!openmc_plot) {
×
720
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
721
                          "is allowed in random ray mode.",
722
        p));
723
      continue;
×
724
    } else if (openmc_plot->type_ != Plot::PlotType::voxel) {
×
725
      warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting "
×
726
                          "is allowed in random ray mode.",
727
        p));
728
      continue;
×
729
    }
730

731
    int Nx = openmc_plot->pixels_[0];
×
732
    int Ny = openmc_plot->pixels_[1];
×
733
    int Nz = openmc_plot->pixels_[2];
×
734
    Position origin = openmc_plot->origin_;
×
735
    Position width = openmc_plot->width_;
×
736
    Position ll = origin - width / 2.0;
×
737
    double x_delta = width.x / Nx;
×
738
    double y_delta = width.y / Ny;
×
739
    double z_delta = width.z / Nz;
×
740
    std::string filename = openmc_plot->path_plot();
×
741

742
    // Perform sanity checks on file size
743
    uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float);
×
744
    write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)",
×
745
      openmc_plot->id(), filename, bytes / 1.0e6);
×
746
    if (bytes / 1.0e9 > 1.0) {
×
747
      warning("Voxel plot specification is very large (>1 GB). Plotting may be "
×
748
              "slow.");
749
    } else if (bytes / 1.0e9 > 100.0) {
×
750
      fatal_error("Voxel plot specification is too large (>100 GB). Exiting.");
×
751
    }
752

753
    // Relate voxel spatial locations to random ray source regions
754
    vector<int> voxel_indices(Nx * Ny * Nz);
×
755
    vector<Position> voxel_positions(Nx * Ny * Nz);
×
NEW
756
    vector<double> weight_windows(Nx * Ny * Nz);
×
NEW
757
    float min_weight = 1e20;
×
758
#pragma omp parallel for collapse(3) reduction(min : min_weight)
759
    for (int z = 0; z < Nz; z++) {
760
      for (int y = 0; y < Ny; y++) {
761
        for (int x = 0; x < Nx; x++) {
762
          Position sample;
763
          sample.z = ll.z + z_delta / 2.0 + z * z_delta;
764
          sample.y = ll.y + y_delta / 2.0 + y * y_delta;
765
          sample.x = ll.x + x_delta / 2.0 + x * x_delta;
766
          Particle p;
767
          p.r() = sample;
768
          p.r_last() = sample;
769
          p.E() = 1.0;
770
          p.E_last() = 1.0;
771

772
          bool found = exhaustive_find_cell(p);
773
          if (!found) {
774
            voxel_indices[z * Ny * Nx + y * Nx + x] = -1;
775
            voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
776
            weight_windows[z * Ny * Nx + y * Nx + x] = 0.0;
777
            continue;
778
          }
779

780
          int i_cell = p.lowest_coord().cell;
781
          int64_t sr = source_region_offsets_[i_cell] + p.cell_instance();
782
          if (RandomRay::mesh_subdivision_enabled_) {
783
            int mesh_idx = base_source_regions_.mesh(sr);
784
            int mesh_bin;
785
            if (mesh_idx == C_NONE) {
786
              mesh_bin = 0;
787
            } else {
788
              mesh_bin = model::meshes[mesh_idx]->get_bin(p.r());
789
            }
790
            SourceRegionKey sr_key {sr, mesh_bin};
791
            auto it = source_region_map_.find(sr_key);
792
            if (it != source_region_map_.end()) {
793
              sr = it->second;
794
            } else {
795
              sr = -1;
796
            }
797
          }
798

799
          voxel_indices[z * Ny * Nx + y * Nx + x] = sr;
800
          voxel_positions[z * Ny * Nx + y * Nx + x] = sample;
801

802
          if (variance_reduction::weight_windows.size() == 1) {
803
            WeightWindow ww =
804
              variance_reduction::weight_windows[0]->get_weight_window(p);
805
            float weight = ww.lower_weight;
806
            weight_windows[z * Ny * Nx + y * Nx + x] = weight;
807
            if (weight < min_weight)
808
              min_weight = weight;
809
          }
810
        }
811
      }
812
    }
813

814
    double source_normalization_factor =
815
      compute_fixed_source_normalization_factor();
×
816

817
    // Open file for writing
818
    std::FILE* plot = std::fopen(filename.c_str(), "wb");
×
819

820
    // Write vtk metadata
821
    std::fprintf(plot, "# vtk DataFile Version 2.0\n");
×
822
    std::fprintf(plot, "Dataset File\n");
×
823
    std::fprintf(plot, "BINARY\n");
×
824
    std::fprintf(plot, "DATASET STRUCTURED_POINTS\n");
×
825
    std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz);
×
NEW
826
    std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z);
×
827
    std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta);
×
828
    std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz);
×
829

NEW
830
    int64_t num_neg = 0;
×
NEW
831
    int64_t num_samples = 0;
×
NEW
832
    float min_flux = 0.0;
×
NEW
833
    float max_flux = -1.0e20;
×
834
    // Plot multigroup flux data
835
    for (int g = 0; g < negroups_; g++) {
×
836
      std::fprintf(plot, "SCALARS flux_group_%d float\n", g);
×
837
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
838
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
839
        int64_t fsr = voxel_indices[i];
×
840
        int64_t source_element = fsr * negroups_ + g;
×
NEW
841
        float flux = 0;
×
NEW
842
        if (fsr >= 0) {
×
NEW
843
          flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
NEW
844
          if (flux < 0.0)
×
NEW
845
            flux = FlatSourceDomain::evaluate_flux_at_point(
×
NEW
846
              voxel_positions[i], fsr, g);
×
847
        }
NEW
848
        if (flux < 0.0) {
×
NEW
849
          num_neg++;
×
NEW
850
          if (flux < min_flux) {
×
NEW
851
            min_flux = flux;
×
852
          }
853
        }
NEW
854
        if (flux > max_flux)
×
NEW
855
          max_flux = flux;
×
NEW
856
        num_samples++;
×
857
        flux = convert_to_big_endian<float>(flux);
×
858
        std::fwrite(&flux, sizeof(float), 1, plot);
×
859
      }
860
    }
861

862
    // Slightly negative fluxes can be normal when sampling corners of linear
863
    // source regions. However, very common and high magnitude negative fluxes
864
    // may indicate numerical instability.
NEW
865
    if (num_neg > 0) {
×
NEW
866
      warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes "
×
867
                          "(minumum found = {:.2e} maximum_found = {:.2e})",
NEW
868
        num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux));
×
869
    }
870

871
    // Plot FSRs
872
    std::fprintf(plot, "SCALARS FSRs float\n");
×
873
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
874
    for (int fsr : voxel_indices) {
×
875
      float value = future_prn(10, fsr);
×
876
      value = convert_to_big_endian<float>(value);
×
877
      std::fwrite(&value, sizeof(float), 1, plot);
×
878
    }
879

880
    // Plot Materials
881
    std::fprintf(plot, "SCALARS Materials int\n");
×
882
    std::fprintf(plot, "LOOKUP_TABLE default\n");
×
883
    for (int fsr : voxel_indices) {
×
NEW
884
      int mat = -1;
×
NEW
885
      if (fsr >= 0)
×
NEW
886
        mat = source_regions_.material(fsr);
×
887
      mat = convert_to_big_endian<int>(mat);
×
888
      std::fwrite(&mat, sizeof(int), 1, plot);
×
889
    }
890

891
    // Plot fission source
892
    if (settings::run_mode == RunMode::EIGENVALUE) {
×
893
      std::fprintf(plot, "SCALARS total_fission_source float\n");
×
894
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
895
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
896
        int64_t fsr = voxel_indices[i];
×
UNCOV
897
        float total_fission = 0.0;
×
NEW
898
        if (fsr >= 0) {
×
NEW
899
          int mat = source_regions_.material(fsr);
×
NEW
900
          if (mat != MATERIAL_VOID) {
×
NEW
901
            for (int g = 0; g < negroups_; g++) {
×
NEW
902
              int64_t source_element = fsr * negroups_ + g;
×
NEW
903
              float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g);
×
NEW
904
              double sigma_f = sigma_f_[mat * negroups_ + g];
×
NEW
905
              total_fission += sigma_f * flux;
×
906
            }
907
          }
908
        }
909
        total_fission = convert_to_big_endian<float>(total_fission);
×
910
        std::fwrite(&total_fission, sizeof(float), 1, plot);
×
911
      }
912
    } else {
913
      std::fprintf(plot, "SCALARS external_source float\n");
×
914
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
915
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
916
        int64_t fsr = voxel_indices[i];
×
917
        float total_external = 0.0f;
×
NEW
918
        if (fsr >= 0) {
×
NEW
919
          for (int g = 0; g < negroups_; g++) {
×
NEW
920
            total_external += source_regions_.external_source(fsr, g);
×
921
          }
922
        }
923
        total_external = convert_to_big_endian<float>(total_external);
×
924
        std::fwrite(&total_external, sizeof(float), 1, plot);
×
925
      }
926
    }
927

928
    // Plot weight window data
NEW
929
    float min = 1.0e20;
×
NEW
930
    float max = -1.0e20;
×
931

NEW
932
    if (variance_reduction::weight_windows.size() == 1) {
×
NEW
933
      std::fprintf(plot, "SCALARS ww_lower float\n");
×
NEW
934
      std::fprintf(plot, "LOOKUP_TABLE default\n");
×
NEW
935
      for (int i = 0; i < Nx * Ny * Nz; i++) {
×
NEW
936
        float weight = weight_windows[i];
×
NEW
937
        if (weight == 0.0)
×
NEW
938
          weight = min_weight;
×
NEW
939
        if (weight < min)
×
NEW
940
          min = weight;
×
NEW
941
        if (weight > max)
×
NEW
942
          max = weight;
×
NEW
943
        weight = convert_to_big_endian<float>(weight);
×
NEW
944
        std::fwrite(&weight, sizeof(float), 1, plot);
×
945
      }
NEW
946
      fmt::print("Min ww = {} Max ww = {}\n", min, max);
×
947
    }
948

UNCOV
949
    std::fclose(plot);
×
950
  }
951
}
952

953
void FlatSourceDomain::apply_external_source_to_source_region(
2,720✔
954
  Discrete* discrete, double strength_factor, int64_t sr)
955
{
956
  source_regions_.external_source_present(sr) = 1;
2,720✔
957

958
  const auto& discrete_energies = discrete->x();
2,720✔
959
  const auto& discrete_probs = discrete->prob();
2,720✔
960

961
  for (int i = 0; i < discrete_energies.size(); i++) {
5,632✔
962
    int g = data::mg.get_group_index(discrete_energies[i]);
2,912✔
963
    source_regions_.external_source(sr, g) +=
2,912✔
964
      discrete_probs[i] * strength_factor;
2,912✔
965
  }
966
}
2,720✔
967

968
void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell,
400✔
969
  Discrete* discrete, double strength_factor, int target_material_id,
970
  const vector<int32_t>& instances)
971
{
972
  Cell& cell = *model::cells[i_cell];
400✔
973

974
  if (cell.type_ != Fill::MATERIAL)
400✔
975
    return;
×
976

977
  for (int j : instances) {
30,640✔
978
    int cell_material_idx = cell.material(j);
30,240✔
979
    int cell_material_id;
980
    if (cell_material_idx == MATERIAL_VOID) {
30,240✔
981
      cell_material_id = MATERIAL_VOID;
256✔
982
    } else {
983
      cell_material_id = model::materials[cell_material_idx]->id();
29,984✔
984
    }
985
    if (target_material_id == C_NONE ||
30,240✔
986
        cell_material_id == target_material_id) {
987
      int64_t source_region = source_region_offsets_[i_cell] + j;
2,720✔
988
      apply_external_source_to_source_region(
2,720✔
989
        discrete, strength_factor, source_region);
990
    }
991
  }
992
}
993

994
void FlatSourceDomain::apply_external_source_to_cell_and_children(
432✔
995
  int32_t i_cell, Discrete* discrete, double strength_factor,
996
  int32_t target_material_id)
997
{
998
  Cell& cell = *model::cells[i_cell];
432✔
999

1000
  if (cell.type_ == Fill::MATERIAL) {
432✔
1001
    vector<int> instances(cell.n_instances_);
400✔
1002
    std::iota(instances.begin(), instances.end(), 0);
400✔
1003
    apply_external_source_to_cell_instances(
400✔
1004
      i_cell, discrete, strength_factor, target_material_id, instances);
1005
  } else if (target_material_id == C_NONE) {
432✔
1006
    std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1007
      cell.get_contained_cells(0, nullptr);
×
1008
    for (const auto& pair : cell_instance_list) {
×
1009
      int32_t i_child_cell = pair.first;
×
1010
      apply_external_source_to_cell_instances(i_child_cell, discrete,
×
1011
        strength_factor, target_material_id, pair.second);
×
1012
    }
1013
  }
1014
}
432✔
1015

1016
void FlatSourceDomain::count_external_source_regions()
368✔
1017
{
1018
  n_external_source_regions_ = 0;
368✔
1019
#pragma omp parallel for reduction(+ : n_external_source_regions_)
207✔
1020
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
259,049✔
1021
    if (source_regions_.external_source_present(sr)) {
258,888✔
1022
      n_external_source_regions_++;
1,190✔
1023
    }
1024
  }
1025
}
368✔
1026

1027
void FlatSourceDomain::convert_external_sources()
368✔
1028
{
1029
  // Loop over external sources
1030
  for (int es = 0; es < model::external_sources.size(); es++) {
736✔
1031
    Source* s = model::external_sources[es].get();
368✔
1032
    IndependentSource* is = dynamic_cast<IndependentSource*>(s);
368✔
1033
    Discrete* energy = dynamic_cast<Discrete*>(is->energy());
368✔
1034
    const std::unordered_set<int32_t>& domain_ids = is->domain_ids();
368✔
1035

1036
    double strength_factor = is->strength();
368✔
1037

1038
    if (is->domain_type() == Source::DomainType::MATERIAL) {
368✔
1039
      for (int32_t material_id : domain_ids) {
32✔
1040
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
96✔
1041
          apply_external_source_to_cell_and_children(
80✔
1042
            i_cell, energy, strength_factor, material_id);
1043
        }
1044
      }
1045
    } else if (is->domain_type() == Source::DomainType::CELL) {
352✔
1046
      for (int32_t cell_id : domain_ids) {
32✔
1047
        int32_t i_cell = model::cell_map[cell_id];
16✔
1048
        apply_external_source_to_cell_and_children(
16✔
1049
          i_cell, energy, strength_factor, C_NONE);
1050
      }
1051
    } else if (is->domain_type() == Source::DomainType::UNIVERSE) {
336✔
1052
      for (int32_t universe_id : domain_ids) {
672✔
1053
        int32_t i_universe = model::universe_map[universe_id];
336✔
1054
        Universe& universe = *model::universes[i_universe];
336✔
1055
        for (int32_t i_cell : universe.cells_) {
672✔
1056
          apply_external_source_to_cell_and_children(
336✔
1057
            i_cell, energy, strength_factor, C_NONE);
1058
        }
1059
      }
1060
    }
1061
  } // End loop over external sources
1062

1063
// Divide the fixed source term by sigma t (to save time when applying each
1064
// iteration)
1065
#pragma omp parallel for
207✔
1066
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
259,049✔
1067
    int material = source_regions_.material(sr);
258,888✔
1068
    if (material == MATERIAL_VOID) {
258,888✔
1069
      continue;
14,000✔
1070
    }
1071
    for (int g = 0; g < negroups_; g++) {
519,008✔
1072
      double sigma_t = sigma_t_[material * negroups_ + g];
274,120✔
1073
      source_regions_.external_source(sr, g) /= sigma_t;
274,120✔
1074
    }
1075
  }
1076
}
368✔
1077

1078
void FlatSourceDomain::flux_swap()
10,670✔
1079
{
1080
  source_regions_.flux_swap();
10,670✔
1081
}
10,670✔
1082

1083
void FlatSourceDomain::flatten_xs()
560✔
1084
{
1085
  // Temperature and angle indices, if using multiple temperature
1086
  // data sets and/or anisotropic data sets.
1087
  // TODO: Currently assumes we are only using single temp/single angle data.
1088
  const int t = 0;
560✔
1089
  const int a = 0;
560✔
1090

1091
  n_materials_ = data::mg.macro_xs_.size();
560✔
1092
  for (auto& m : data::mg.macro_xs_) {
2,064✔
1093
    for (int g_out = 0; g_out < negroups_; g_out++) {
4,736✔
1094
      if (m.exists_in_model) {
3,232✔
1095
        double sigma_t =
1096
          m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a);
3,168✔
1097
        sigma_t_.push_back(sigma_t);
3,168✔
1098

1099
        double nu_Sigma_f =
1100
          m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a);
3,168✔
1101
        nu_sigma_f_.push_back(nu_Sigma_f);
3,168✔
1102

1103
        double sigma_f =
1104
          m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a);
3,168✔
1105
        sigma_f_.push_back(sigma_f);
3,168✔
1106

1107
        double chi =
1108
          m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a);
3,168✔
1109
        chi_.push_back(chi);
3,168✔
1110

1111
        for (int g_in = 0; g_in < negroups_; g_in++) {
18,432✔
1112
          double sigma_s =
1113
            m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a);
15,264✔
1114
          sigma_s_.push_back(sigma_s);
15,264✔
1115
        }
1116
      } else {
1117
        sigma_t_.push_back(0);
64✔
1118
        nu_sigma_f_.push_back(0);
64✔
1119
        sigma_f_.push_back(0);
64✔
1120
        chi_.push_back(0);
64✔
1121
        for (int g_in = 0; g_in < negroups_; g_in++) {
128✔
1122
          sigma_s_.push_back(0);
64✔
1123
        }
1124
      }
1125
    }
1126
  }
1127
}
560✔
1128

1129
void FlatSourceDomain::set_adjoint_sources(const vector<double>& forward_flux)
64✔
1130
{
1131
  // Set the external source to 1/forward_flux. If the forward flux is negative
1132
  // or zero, set the adjoint source to zero, as this is likely a very small
1133
  // source region that we don't need to bother trying to vector particles
1134
  // towards. Flux negativity in random ray is not related to the flux being
1135
  // small in magnitude, but rather due to the source region being physically
1136
  // small in volume and thus having a noisy flux estimate.
1137
#pragma omp parallel for
36✔
1138
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
169,372✔
1139
    for (int g = 0; g < negroups_; g++) {
338,688✔
1140
      double flux = forward_flux[sr * negroups_ + g];
169,344✔
1141
      if (flux <= 0.0) {
169,344✔
1142
        source_regions_.external_source(sr, g) = 0.0;
325✔
1143
      } else {
1144
        source_regions_.external_source(sr, g) = 1.0 / flux;
169,019✔
1145
      }
1146
      if (flux > 0.0) {
169,344✔
1147
        source_regions_.external_source_present(sr) = 1;
155,195✔
1148
      }
1149
    }
1150
  }
1151

1152
  // Divide the fixed source term by sigma t (to save time when applying each
1153
  // iteration)
1154
#pragma omp parallel for
36✔
1155
  for (int64_t sr = 0; sr < n_source_regions(); sr++) {
169,372✔
1156
    int material = source_regions_.material(sr);
169,344✔
1157
    if (material == MATERIAL_VOID) {
169,344✔
1158
      continue;
1159
    }
1160
    for (int g = 0; g < negroups_; g++) {
338,688✔
1161
      double sigma_t = sigma_t_[material * negroups_ + g];
169,344✔
1162
      source_regions_.external_source(sr, g) /= sigma_t;
169,344✔
1163
    }
1164
  }
1165
}
64✔
1166

1167
void FlatSourceDomain::transpose_scattering_matrix()
80✔
1168
{
1169
  // Transpose the inner two dimensions for each material
1170
  for (int m = 0; m < n_materials_; ++m) {
304✔
1171
    int material_offset = m * negroups_ * negroups_;
224✔
1172
    for (int i = 0; i < negroups_; ++i) {
640✔
1173
      for (int j = i + 1; j < negroups_; ++j) {
1,088✔
1174
        // Calculate indices of the elements to swap
1175
        int idx1 = material_offset + i * negroups_ + j;
672✔
1176
        int idx2 = material_offset + j * negroups_ + i;
672✔
1177

1178
        // Swap the elements to transpose the matrix
1179
        std::swap(sigma_s_[idx1], sigma_s_[idx2]);
672✔
1180
      }
1181
    }
1182
  }
1183
}
80✔
1184

1185
void FlatSourceDomain::serialize_final_fluxes(vector<double>& flux)
480✔
1186
{
1187
  // Ensure array is correct size
1188
  flux.resize(n_source_regions() * negroups_);
480✔
1189
// Serialize the final fluxes for output
1190
#pragma omp parallel for
270✔
1191
  for (int64_t se = 0; se < n_source_elements(); se++) {
1,016,790✔
1192
    flux[se] = source_regions_.scalar_flux_final(se);
1,016,580✔
1193
  }
1194
}
480✔
1195

1196
void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell,
368✔
1197
  int32_t mesh_idx, int target_material_id, const vector<int32_t>& instances,
1198
  bool is_target_void)
1199
{
1200
  Cell& cell = *model::cells[i_cell];
368✔
1201
  if (cell.type_ != Fill::MATERIAL)
368✔
NEW
1202
    return;
×
1203
  for (int32_t j : instances) {
146,864✔
1204
    int cell_material_idx = cell.material(j);
146,496✔
1205
    int cell_material_id = (cell_material_idx == C_NONE)
1206
                             ? C_NONE
146,496✔
1207
                             : model::materials[cell_material_idx]->id();
146,496✔
1208

1209
    if ((target_material_id == C_NONE && !is_target_void) ||
146,496✔
1210
        cell_material_id == target_material_id) {
1211
      int64_t sr = source_region_offsets_[i_cell] + j;
114,496✔
1212
      if (source_regions_.mesh(sr) != C_NONE) {
114,496✔
1213
        // print out the source region that is broken:
NEW
1214
        fatal_error(fmt::format("Source region {} already has mesh idx {} "
×
1215
                                "applied, but trying to apply mesh idx {}",
1216
          sr, source_regions_.mesh(sr), mesh_idx));
1217
      }
1218
      source_regions_.mesh(sr) = mesh_idx;
114,496✔
1219
    }
1220
  }
1221
}
1222

1223
void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell,
272✔
1224
  int32_t mesh_idx, int32_t target_material_id, bool is_target_void)
1225
{
1226
  Cell& cell = *model::cells[i_cell];
272✔
1227

1228
  if (cell.type_ == Fill::MATERIAL) {
272✔
1229
    vector<int> instances(cell.n_instances_);
160✔
1230
    std::iota(instances.begin(), instances.end(), 0);
160✔
1231
    apply_mesh_to_cell_instances(
160✔
1232
      i_cell, mesh_idx, target_material_id, instances, is_target_void);
1233
  } else if (target_material_id == C_NONE && !is_target_void) {
272✔
1234
    for (int j = 0; j < cell.n_instances_; j++) {
96✔
1235
      std::unordered_map<int32_t, vector<int32_t>> cell_instance_list =
1236
        cell.get_contained_cells(j, nullptr);
48✔
1237
      for (const auto& pair : cell_instance_list) {
256✔
1238
        int32_t i_child_cell = pair.first;
208✔
1239
        apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id,
208✔
1240
          pair.second, is_target_void);
208✔
1241
      }
1242
    }
48✔
1243
  }
1244
}
272✔
1245

1246
// Problem: my logic breaks if I am trying to apply things to a void region,
1247
// where the target material ID is C_NONE. Might be worth making a different
1248
// argument to pass through that indicates the presence of void.
1249
void FlatSourceDomain::apply_meshes()
480✔
1250
{
1251
  // Skip if there are no mappings between mesh IDs and domains
1252
  if (mesh_domain_map_.empty())
480✔
1253
    return;
400✔
1254

1255
  // Loop over meshes
1256
  for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) {
240✔
1257
    Mesh* mesh = model::meshes[mesh_idx].get();
160✔
1258
    int mesh_id = mesh->id();
160✔
1259

1260
    // Skip if mesh id is not present in the map
1261
    if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end())
160✔
1262
      continue;
16✔
1263

1264
    // Loop over domains associated with the mesh
1265
    for (auto& domain : mesh_domain_map_[mesh_id]) {
288✔
1266
      Source::DomainType domain_type = domain.first;
144✔
1267
      int domain_id = domain.second;
144✔
1268

1269
      if (domain_type == Source::DomainType::MATERIAL) {
144✔
1270
        for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) {
192✔
1271
          if (domain_id == C_NONE) {
160✔
NEW
1272
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true);
×
1273
          } else {
1274
            apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false);
160✔
1275
          }
1276
        }
1277
      } else if (domain_type == Source::DomainType::CELL) {
112✔
1278
        int32_t i_cell = model::cell_map[domain_id];
32✔
1279
        apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
32✔
1280
      } else if (domain_type == Source::DomainType::UNIVERSE) {
80✔
1281
        int32_t i_universe = model::universe_map[domain_id];
80✔
1282
        Universe& universe = *model::universes[i_universe];
80✔
1283
        for (int32_t i_cell : universe.cells_) {
160✔
1284
          apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false);
80✔
1285
        }
1286
      }
1287
    }
1288
  }
1289
}
1290

1291
void FlatSourceDomain::prepare_base_source_regions()
55✔
1292
{
1293
  std::swap(source_regions_, base_source_regions_);
55✔
1294
  source_regions_.negroups() = base_source_regions_.negroups();
55✔
1295
  source_regions_.is_linear() = base_source_regions_.is_linear();
55✔
1296
}
55✔
1297

1298
SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle(
806,904,131✔
1299
  int64_t sr, int mesh_bin, Position r, double dist, Direction u)
1300
{
1301
  SourceRegionKey sr_key {sr, mesh_bin};
806,904,131✔
1302

1303
  // Case 1: Check if the source region key is already present in the permanent
1304
  // map. This is the most common condition, as any source region visited in a
1305
  // previous power iteration will already be present in the permanent map. If
1306
  // the source region key is found, we translate the key into a specific 1D
1307
  // source region index and return a handle its position in the
1308
  // source_regions_ vector.
1309
  auto it = source_region_map_.find(sr_key);
806,904,131✔
1310
  if (it != source_region_map_.end()) {
806,904,131✔
1311
    int64_t sr = it->second;
789,751,963✔
1312
    return source_regions_.get_source_region_handle(sr);
789,751,963✔
1313
  }
1314

1315
  // Case 2: Check if the source region key is present in the temporary (thread
1316
  // safe) map. This is a common occurrence in the first power iteration when
1317
  // the source region has already been visited already by some other ray. We
1318
  // begin by locking the temporary map before any operations are performed. The
1319
  // lock is not global over the full data structure -- it will be dependent on
1320
  // which key is used.
1321
  discovered_source_regions_.lock(sr_key);
17,152,168✔
1322

1323
  // If the key is found in the temporary map, then we return a handle to the
1324
  // source region that is stored in the temporary map.
1325
  if (discovered_source_regions_.contains(sr_key)) {
17,152,168✔
1326
    SourceRegionHandle handle =
1327
      discovered_source_regions_[sr_key].get_source_region_handle();
15,729,329✔
1328
    discovered_source_regions_.unlock(sr_key);
15,729,329✔
1329
    return handle;
15,729,329✔
1330
  }
1331

1332
  // Case 3: The source region key is not present anywhere, but it is only due
1333
  // to floating point artifacts. These artifacts occur when the overlaid mesh
1334
  // overlaps with actual geometry surfaces. In these cases, roundoff error may
1335
  // result in the ray tracer detecting an additional (very short) segment
1336
  // though a mesh bin that is actually past the physical source region
1337
  // boundary. This is a result of the the multi-level ray tracing treatment in
1338
  // OpenMC, which depending on the number of universes in the hierarchy etc can
1339
  // result in the wrong surface being selected as the nearest. This can happen
1340
  // in a lattice when there are two directions that both are very close in
1341
  // distance, within the tolerance of FP_REL_PRECISION, and the are thus
1342
  // treated as being equivalent so alternative logic is used. However, when we
1343
  // go and ray trace on this with the mesh tracer we may go past the surface
1344
  // bounding the current source region.
1345
  //
1346
  // To filter out this case, before we create the new source region, we double
1347
  // check that the actual starting point of this segment (r) is still in the
1348
  // same geometry source region that we started in. If an artifact is detected,
1349
  // we discard the segment (and attenuation through it) as it is not really a
1350
  // valid source region and will have only an infinitessimally small cell
1351
  // combined with the mesh bin. Thankfully, this is a fairly rare condition,
1352
  // and only triggers for very short ray lengths. It can be fixed by decreasing
1353
  // the value of FP_REL_PRECISION in constants.h, but this may have unknown
1354
  // consequences for the general ray tracer, so for now we do the below sanity
1355
  // checks before generating phantom source regions. A significant extra cost
1356
  // is incurred in instantiating the GeometryState object and doing a cell
1357
  // lookup, but again, this is going to be an extremely rare thing to check
1358
  // after the first power iteration has completed.
1359

1360
  // Sanity check on source region id
1361
  GeometryState gs;
1,422,839✔
1362
  gs.r() = r + TINY_BIT * u;
1,422,839✔
1363
  exhaustive_find_cell(gs);
1,422,839✔
1364
  int gs_i_cell = gs.lowest_coord().cell;
1,422,839✔
1365
  int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance();
1,422,839✔
1366
  if (sr_found != sr) {
1,422,839✔
1367
    discovered_source_regions_.unlock(sr_key);
88✔
1368
    SourceRegionHandle handle;
88✔
1369
    handle.is_numerical_fp_artifact_ = true;
88✔
1370
    return handle;
88✔
1371
  }
1372

1373
  // Sanity check on mesh bin
1374
  int mesh_idx = base_source_regions_.mesh(sr);
1,422,751✔
1375
  if (mesh_idx == C_NONE) {
1,422,751✔
NEW
1376
    if (mesh_bin != 0) {
×
NEW
1377
      discovered_source_regions_.unlock(sr_key);
×
NEW
1378
      SourceRegionHandle handle;
×
NEW
1379
      handle.is_numerical_fp_artifact_ = true;
×
NEW
1380
      return handle;
×
1381
    }
1382
  } else {
1383
    Mesh* mesh = model::meshes[mesh_idx].get();
1,422,751✔
1384
    int bin_found = mesh->get_bin(r + TINY_BIT * u);
1,422,751✔
1385
    if (bin_found != mesh_bin) {
1,422,751✔
NEW
1386
      discovered_source_regions_.unlock(sr_key);
×
NEW
1387
      SourceRegionHandle handle;
×
NEW
1388
      handle.is_numerical_fp_artifact_ = true;
×
NEW
1389
      return handle;
×
1390
    }
1391
  }
1392

1393
  // Case 4: The source region key is valid, but is not present anywhere. This
1394
  // condition only occurs the first time the source region is discovered
1395
  // (typically in the first power iteration). In this case, we need to handle
1396
  // creation of the new source region and its storage into the parallel map.
1397
  // The new source region is created by copying the base source region, so as
1398
  // to inherit material, external source, and some flux properties etc. We
1399
  // also pass the base source region id to allow the new source region to
1400
  // know which base source region it is derived from.
1401
  SourceRegion* sr_ptr = discovered_source_regions_.emplace(
1,422,751✔
1402
    sr_key, {base_source_regions_.get_source_region_handle(sr), sr});
1,422,751✔
1403
  // print out all elements of the sr_ptr
1404
  discovered_source_regions_.unlock(sr_key);
1,422,751✔
1405
  return sr_ptr->get_source_region_handle();
1,422,751✔
1406
}
1,422,839✔
1407

1408
void FlatSourceDomain::finalize_discovered_source_regions()
2,090✔
1409
{
1410
  // Extract keys for entries with a valid volume.
1411
  vector<SourceRegionKey> keys;
2,090✔
1412
  for (const auto& pair : discovered_source_regions_) {
1,424,841✔
1413
    if (pair.second.volume_ > 0.0) {
1,422,751✔
1414
      keys.push_back(pair.first);
1,356,432✔
1415
    }
1416
  }
1417

1418
  if (!keys.empty()) {
2,090✔
1419
    // Sort the keys, so as to ensure reproducible ordering given that source
1420
    // regions may have been added to discovered_source_regions_ in an arbitrary
1421
    // order due to shared memory threading.
1422
    std::sort(keys.begin(), keys.end());
286✔
1423

1424
    // Append the source regions in the sorted key order.
1425
    for (const auto& key : keys) {
1,356,718✔
1426
      const SourceRegion& sr = discovered_source_regions_[key];
1,356,432✔
1427
      source_region_map_[key] = source_regions_.n_source_regions();
1,356,432✔
1428
      source_regions_.push_back(sr);
1,356,432✔
1429
    }
1430

1431
    // If any new source regions were discovered, we need to update the
1432
    // tally mapping between source regions and tally bins.
1433
    mapped_all_tallies_ = false;
286✔
1434
  }
1435

1436
  discovered_source_regions_.clear();
2,090✔
1437
}
2,090✔
1438

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

© 2025 Coveralls, Inc