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

openmc-dev / openmc / 21489819490

29 Jan 2026 06:21PM UTC coverage: 80.077% (-1.9%) from 81.953%
21489819490

Pull #3757

github

web-flow
Merge d08626053 into f7a734189
Pull Request #3757: Testing point detectors

16004 of 22621 branches covered (70.75%)

Branch coverage included in aggregate %.

94 of 518 new or added lines in 26 files covered. (18.15%)

1021 existing lines in 52 files now uncovered.

53779 of 64524 relevant lines covered (83.35%)

8016833.26 hits per line

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

70.54
/src/tallies/tally_scoring.cpp
1
#include "openmc/tallies/tally_scoring.h"
2

3
#include "openmc/bank.h"
4
#include "openmc/capi.h"
5
#include "openmc/constants.h"
6
#include "openmc/error.h"
7
#include "openmc/ifp.h"
8
#include "openmc/material.h"
9
#include "openmc/mgxs_interface.h"
10
#include "openmc/nuclide.h"
11
#include "openmc/photon.h"
12
#include "openmc/reaction_product.h"
13
#include "openmc/search.h"
14
#include "openmc/settings.h"
15
#include "openmc/simulation.h"
16
#include "openmc/source.h"
17
#include "openmc/string_utils.h"
18
#include "openmc/tallies/derivative.h"
19
#include "openmc/tallies/filter.h"
20
#include "openmc/tallies/filter_cell.h"
21
#include "openmc/tallies/filter_delayedgroup.h"
22
#include "openmc/tallies/filter_energy.h"
23

24
#include <string>
25

26
namespace openmc {
27

28
namespace simulation {
29
thread_local Particle pseudoparticle;
30
thread_local int i_det;
31
} // namespace simulation
32

33
//==============================================================================
34
// FilterBinIter implementation
35
//==============================================================================
36

37
FilterBinIter::FilterBinIter(const Tally& tally, Particle& p)
368,187,292✔
38
  : filter_matches_ {p.filter_matches()}, tally_ {tally}
368,187,292✔
39
{
40
  // Find all valid bins in each relevant filter if they have not already been
41
  // found for this event.
42
  for (auto i_filt : tally_.filters()) {
813,544,707✔
43
    auto& match {filter_matches_[i_filt]};
582,315,331✔
44
    if (!match.bins_present_) {
582,315,331✔
45
      match.bins_.clear();
375,697,518✔
46
      match.weights_.clear();
375,697,518✔
47
      model::tally_filters[i_filt]->get_all_bins(p, tally_.estimator_, match);
375,697,518✔
48
      match.bins_present_ = true;
375,697,518✔
49
    }
50

51
    // If there are no valid bins for this filter, then there are no valid
52
    // filter bin combinations so all iterators are end iterators.
53
    if (match.bins_.size() == 0) {
582,315,331✔
54
      index_ = -1;
136,957,916✔
55
      return;
136,957,916✔
56
    }
57

58
    // Set the index of the bin used in the first filter combination
59
    match.i_bin_ = 0;
445,357,415✔
60
  }
61

62
  // Compute the initial index and weight.
63
  this->compute_index_weight();
231,229,376✔
64
}
65

66
FilterBinIter::FilterBinIter(
368,190,672✔
67
  const Tally& tally, bool end, vector<FilterMatch>* particle_filter_matches)
368,190,672✔
68
  : filter_matches_ {*particle_filter_matches}, tally_ {tally}
368,190,672✔
69
{
70
  // Handle the special case for an iterator that points to the end.
71
  if (end) {
368,190,672✔
72
    index_ = -1;
368,188,982✔
73
    return;
368,188,982✔
74
  }
75

76
  for (auto i_filt : tally_.filters()) {
4,913✔
77
    auto& match {filter_matches_[i_filt]};
3,223✔
78
    if (!match.bins_present_) {
3,223!
79
      match.bins_.clear();
3,223✔
80
      match.weights_.clear();
3,223✔
81
      for (auto i = 0; i < model::tally_filters[i_filt]->n_bins(); ++i) {
494,126✔
82
        match.bins_.push_back(i);
490,903✔
83
        match.weights_.push_back(1.0);
490,903✔
84
      }
85
      match.bins_present_ = true;
3,223✔
86
    }
87

88
    if (match.bins_.size() == 0) {
3,223!
89
      index_ = -1;
×
90
      return;
×
91
    }
92

93
    match.i_bin_ = 0;
3,223✔
94
  }
95

96
  // Compute the initial index and weight.
97
  this->compute_index_weight();
1,690✔
98
}
99

100
FilterBinIter& FilterBinIter::operator++()
283,466,067✔
101
{
102
  // Find the next valid combination of filter bins.  To do this, we search
103
  // backwards through the filters until we find the first filter whose bins
104
  // can be incremented.
105
  bool visited_all_combinations = true;
283,466,067✔
106
  for (int i = tally_.filters().size() - 1; i >= 0; --i) {
735,435,904✔
107
    auto i_filt = tally_.filters(i);
504,204,838✔
108
    auto& match {filter_matches_[i_filt]};
504,204,838✔
109
    if (match.i_bin_ < match.bins_.size() - 1) {
504,204,838✔
110
      // The bin for this filter can be incremented.  Increment it and do not
111
      // touch any of the remaining filters.
112
      ++match.i_bin_;
52,235,001✔
113
      visited_all_combinations = false;
52,235,001✔
114
      break;
52,235,001✔
115
    } else {
116
      // This bin cannot be incremented so reset it and continue to the next
117
      // filter.
118
      match.i_bin_ = 0;
451,969,837✔
119
    }
120
  }
121

122
  if (visited_all_combinations) {
283,466,067✔
123
    // We have visited every valid combination.  All done!
124
    index_ = -1;
231,231,066✔
125
  } else {
126
    // The loop found a new valid combination.  Compute the corresponding
127
    // index and weight.
128
    compute_index_weight();
52,235,001✔
129
  }
130

131
  return *this;
283,466,067✔
132
}
133

134
void FilterBinIter::compute_index_weight()
283,466,067✔
135
{
136
  index_ = 0;
283,466,067✔
137
  weight_ = 1.;
283,466,067✔
138
  for (auto i = 0; i < tally_.filters().size(); ++i) {
799,458,192✔
139
    auto i_filt = tally_.filters(i);
515,992,125✔
140
    auto& match {filter_matches_[i_filt]};
515,992,125✔
141
    auto i_bin = match.i_bin_;
515,992,125✔
142
    index_ += match.bins_[i_bin] * tally_.strides(i);
515,992,125✔
143
    weight_ *= match.weights_[i_bin];
515,992,125✔
144
  }
145
}
283,466,067✔
146

147
//==============================================================================
148
// Non-member functions
149
//==============================================================================
150

151
//! Helper function used to increment tallies with a delayed group filter.
152

153
void score_fission_delayed_dg(int i_tally, int d_bin, double score,
9,511,892✔
154
  int score_index, vector<FilterMatch>& filter_matches)
155
{
156
  // Save the original delayed group bin
157
  auto& tally {*model::tallies[i_tally]};
9,511,892✔
158
  auto i_filt = tally.filters(tally.delayedgroup_filter_);
9,511,892✔
159
  auto& dg_match {filter_matches[i_filt]};
9,511,892✔
160
  auto i_bin = dg_match.i_bin_;
9,511,892✔
161
  auto original_bin = dg_match.bins_[i_bin];
9,511,892✔
162
  dg_match.bins_[i_bin] = d_bin;
9,511,892✔
163

164
  // Determine the filter scoring index
165
  auto filter_index = 0;
9,511,892✔
166
  double filter_weight = 1.;
9,511,892✔
167
  for (auto i = 0; i < tally.filters().size(); ++i) {
29,100,493✔
168
    auto i_filt = tally.filters(i);
19,588,601✔
169
    auto& match {filter_matches[i_filt]};
19,588,601✔
170
    auto i_bin = match.i_bin_;
19,588,601✔
171
    filter_index += match.bins_[i_bin] * tally.strides(i);
19,588,601✔
172
    filter_weight *= match.weights_[i_bin];
19,588,601✔
173
  }
174

175
// Update the tally result
176
#pragma omp atomic
177
  tally.results_(filter_index, score_index, TallyResult::VALUE) +=
9,511,892✔
178
    score * filter_weight;
9,511,892✔
179

180
  // Reset the original delayed group bin
181
  dg_match.bins_[i_bin] = original_bin;
9,511,892✔
182
}
9,511,892✔
183

184
//! Helper function to retrieve fission q value from a nuclide
185

186
double get_nuc_fission_q(const Nuclide& nuc, const Particle& p, int score_bin)
1,347,648✔
187
{
188
  if (score_bin == SCORE_FISS_Q_PROMPT) {
1,347,648✔
189
    if (nuc.fission_q_prompt_) {
673,824✔
190
      return (*nuc.fission_q_prompt_)(p.E_last());
394,548✔
191
    }
192
  } else if (score_bin == SCORE_FISS_Q_RECOV) {
673,824!
193
    if (nuc.fission_q_recov_) {
673,824✔
194
      return (*nuc.fission_q_recov_)(p.E_last());
394,548✔
195
    }
196
  }
197
  return 0.0;
558,552✔
198
}
199

200
//! Helper function to score fission energy
201
//
202
//! Pulled out to support both the fission_q scores and energy deposition
203
//! score
204

205
double score_fission_q(const Particle& p, int score_bin, const Tally& tally,
503,344✔
206
  double flux, int i_nuclide, double atom_density)
207
{
208
  if (tally.estimator_ == TallyEstimator::ANALOG) {
503,344✔
209
    const Nuclide& nuc {*data::nuclides[p.event_nuclide()]};
56,704✔
210
    if (settings::survival_biasing) {
56,704!
211
      // No fission events occur if survival biasing is on -- need to
212
      // calculate fraction of absorptions that would have resulted in
213
      // fission scaled by the Q-value
214
      if (p.neutron_xs(p.event_nuclide()).total > 0) {
×
215
        return p.wgt_last() * get_nuc_fission_q(nuc, p, score_bin) *
×
216
               p.neutron_xs(p.event_nuclide()).fission * flux /
×
217
               p.neutron_xs(p.event_nuclide()).total;
×
218
      }
219
    } else {
220
      // Skip any non-absorption events
221
      if (p.event() == TallyEvent::SCATTER)
56,704✔
222
        return 0.0;
48,976✔
223
      // All fission events will contribute, so again we can use particle's
224
      // weight entering the collision as the estimate for the fission
225
      // reaction rate
226
      if (p.neutron_xs(p.event_nuclide()).absorption > 0) {
7,728!
227
        return p.wgt_last() * get_nuc_fission_q(nuc, p, score_bin) *
7,728✔
228
               p.neutron_xs(p.event_nuclide()).fission * flux /
7,728✔
229
               p.neutron_xs(p.event_nuclide()).absorption;
7,728✔
230
      }
231
    }
232
  } else {
233
    if (i_nuclide >= 0) {
446,640✔
234
      const Nuclide& nuc {*data::nuclides[i_nuclide]};
223,320✔
235
      return get_nuc_fission_q(nuc, p, score_bin) * atom_density * flux *
223,320✔
236
             p.neutron_xs(i_nuclide).fission;
223,320✔
237
    } else if (p.material() != MATERIAL_VOID) {
223,320!
238
      const Material& material {*model::materials[p.material()]};
223,320✔
239
      double score {0.0};
223,320✔
240
      for (auto i = 0; i < material.nuclide_.size(); ++i) {
1,339,920✔
241
        auto j_nuclide = material.nuclide_[i];
1,116,600✔
242
        auto atom_density = material.atom_density(i, p.density_mult());
1,116,600✔
243
        const Nuclide& nuc {*data::nuclides[j_nuclide]};
1,116,600✔
244
        score += get_nuc_fission_q(nuc, p, score_bin) * atom_density *
1,116,600✔
245
                 p.neutron_xs(j_nuclide).fission;
1,116,600✔
246
      }
247
      return score * flux;
223,320✔
248
    }
249
  }
250
  return 0.0;
×
251
}
252

253
//! Helper function to obtain the kerma coefficient for a given nuclide
254

255
double get_nuclide_neutron_heating(
2,477,217✔
256
  const Particle& p, const Nuclide& nuc, int rxn_index, int i_nuclide)
257
{
258
  size_t mt = nuc.reaction_index_[rxn_index];
2,477,217✔
259
  if (mt == C_NONE)
2,477,217!
260
    return 0.0;
×
261

262
  const auto& micro = p.neutron_xs(i_nuclide);
2,477,217✔
263
  auto i_temp = micro.index_temp;
2,477,217✔
264
  if (i_temp < 0)
2,477,217!
265
    return 0.0; // Can be true due to multipole
×
266

267
  // Determine total kerma
268
  const auto& rx {*nuc.reactions_[mt]};
2,477,217✔
269
  double kerma = rx.xs(micro);
2,477,217✔
270
  if (kerma == 0.0)
2,477,217!
271
    return 0.0;
×
272

273
  if (settings::run_mode == RunMode::EIGENVALUE) {
2,477,217✔
274
    // Determine kerma for fission as (EFR + EB)*sigma_f
275
    double kerma_fission =
276
      nuc.fragments_
277
        ? ((*nuc.fragments_)(p.E_last()) + (*nuc.betas_)(p.E_last())) *
2,375,238✔
278
            p.neutron_xs(i_nuclide).fission
338,221✔
279
        : 0.0;
2,375,238✔
280

281
    // Determine non-fission kerma as difference
282
    double kerma_non_fission = kerma - kerma_fission;
2,375,238✔
283

284
    // Re-weight non-fission kerma by keff to properly balance energy release
285
    // and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H. Stedry,
286
    // "Self-consistent energy normalization for quasistatic reactor
287
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
288
    kerma = simulation::keff * kerma_non_fission + kerma_fission;
2,375,238✔
289
  }
290
  return kerma;
2,477,217✔
291
}
292

293
//! Helper function to obtain neutron heating [eV]
294

295
double score_neutron_heating(const Particle& p, const Tally& tally, double flux,
996,579✔
296
  int rxn_bin, int i_nuclide, double atom_density)
297
{
298
  // Get heating macroscopic "cross section"
299
  double heating_xs;
300
  if (i_nuclide >= 0) {
996,579✔
301
    const Nuclide& nuc {*data::nuclides[i_nuclide]};
469,664✔
302
    heating_xs = get_nuclide_neutron_heating(p, nuc, rxn_bin, i_nuclide);
469,664✔
303
    if (tally.estimator_ == TallyEstimator::ANALOG) {
469,664✔
304
      heating_xs /= p.neutron_xs(i_nuclide).total;
43,112✔
305
    } else {
306
      heating_xs *= atom_density;
426,552✔
307
    }
308
  } else {
309
    if (p.material() != MATERIAL_VOID) {
526,915!
310
      heating_xs = 0.0;
526,915✔
311
      const Material& material {*model::materials[p.material()]};
526,915✔
312
      for (auto i = 0; i < material.nuclide_.size(); ++i) {
2,534,468✔
313
        int j_nuclide = material.nuclide_[i];
2,007,553✔
314
        double atom_density {material.atom_density_(i)};
2,007,553✔
315
        const Nuclide& nuc {*data::nuclides[j_nuclide]};
2,007,553✔
316
        heating_xs += atom_density *
2,007,553✔
317
                      get_nuclide_neutron_heating(p, nuc, rxn_bin, j_nuclide);
2,007,553✔
318
      }
319
      if (tally.estimator_ == TallyEstimator::ANALOG) {
526,915✔
320
        heating_xs /= p.macro_xs().total;
43,112✔
321
      }
322
    }
323
  }
324
  double score = heating_xs * flux;
996,579✔
325
  if (tally.estimator_ == TallyEstimator::ANALOG) {
996,579✔
326
    // All events score to a heating tally bin. We actually use a
327
    // collision estimator in place of an analog one since there is no
328
    // reaction-wise heating cross section
329
    score *= p.wgt_last();
86,224✔
330
  }
331
  return score;
996,579✔
332
}
333

334
//! Helper function to obtain reaction Q value for photons and charged particles
335
double get_reaction_q_value(const Particle& p)
1,940,188✔
336
{
337
  if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) {
1,940,188✔
338
    // pair production
339
    return -2 * MASS_ELECTRON_EV;
7,517✔
340
  } else if (p.type() == ParticleType::positron) {
1,932,671✔
341
    // positron annihilation
342
    return 2 * MASS_ELECTRON_EV;
891✔
343
  } else {
344
    return 0.0;
1,931,780✔
345
  }
346
}
347

348
//! Helper function to obtain particle heating [eV]
349

350
double score_particle_heating(const Particle& p, const Tally& tally,
3,037,106✔
351
  double flux, int rxn_bin, int i_nuclide, double atom_density)
352
{
353
  if (p.type() == ParticleType::neutron)
3,037,106✔
354
    return score_neutron_heating(
996,579✔
355
      p, tally, flux, rxn_bin, i_nuclide, atom_density);
996,579✔
356
  if (i_nuclide == -1 || i_nuclide == p.event_nuclide() ||
2,237,728✔
357
      p.event_nuclide() == -1) {
197,201✔
358
    // For pair production and positron annihilation, we need to account for the
359
    // reaction Q value
360
    double Q = get_reaction_q_value(p);
1,940,188✔
361

362
    // Get the pre-collision energy of the particle.
363
    auto E = p.E_last();
1,940,188✔
364

365
    // The energy deposited is the sum of the incident energy and the reaction
366
    // Q-value less the energy of any outgoing particles
367
    double score = E + Q - p.E() - p.bank_second_E();
1,940,188✔
368

369
    score *= p.wgt_last();
1,940,188✔
370

371
    // if no event_nuclide (charged particle) scale energy deposition by
372
    // fractional charge density
373
    if (i_nuclide != -1 && p.event_nuclide() == -1) {
1,940,188✔
374
      const auto& mat {model::materials[p.material()]};
96,862✔
375
      int z = data::nuclides[i_nuclide]->Z_;
96,862✔
376
      auto i = mat->mat_nuclide_index_[i_nuclide];
96,862✔
377
      score *= (z * mat->atom_density_[i] / mat->charge_density());
96,862✔
378
    }
379
    return score;
1,940,188✔
380
  }
381
  return 0.0;
100,339✔
382
}
383

384
//! Helper function for nu-fission tallies with energyout filters.
385
//
386
//! In this case, we may need to score to multiple bins if there were multiple
387
//! neutrons produced with different energies.
388

389
void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin)
96,193✔
390
{
391
  auto& tally {*model::tallies[i_tally]};
96,193✔
392
  auto i_eout_filt = tally.filters()[tally.energyout_filter_];
96,193✔
393
  auto i_bin = p.filter_matches(i_eout_filt).i_bin_;
96,193✔
394
  auto bin_energyout = p.filter_matches(i_eout_filt).bins_[i_bin];
96,193✔
395

396
  const EnergyoutFilter& eo_filt {
397
    *dynamic_cast<EnergyoutFilter*>(model::tally_filters[i_eout_filt].get())};
96,193!
398

399
  // Note that the score below is weighted by keff. Since the creation of
400
  // fission sites is weighted such that it is expected to create n_particles
401
  // sites, we need to multiply the score by keff to get the true nu-fission
402
  // rate. Otherwise, the sum of all nu-fission rates would be ~1.0.
403

404
  // loop over number of particles banked
405
  for (auto i = 0; i < p.n_bank(); ++i) {
223,277✔
406
    const auto& bank = p.nu_bank(i);
127,084✔
407

408
    // get the delayed group
409
    auto g = bank.delayed_group;
127,084✔
410

411
    // determine score based on bank site weight and keff
412
    double score = simulation::keff * bank.wgt;
127,084✔
413

414
    // Add derivative information for differential tallies.  Note that the
415
    // i_nuclide and atom_density arguments do not matter since this is an
416
    // analog estimator.
417
    if (tally.deriv_ != C_NONE)
127,084✔
418
      apply_derivative_to_score(p, i_tally, 0, 0., SCORE_NU_FISSION, score);
2,148✔
419

420
    if (!settings::run_CE && eo_filt.matches_transport_groups()) {
127,084✔
421

422
      // determine outgoing energy group from fission bank
423
      auto g_out = static_cast<int>(bank.E);
9,962✔
424

425
      // modify the value so that g_out = 0 corresponds to the highest energy
426
      // bin
427
      g_out = eo_filt.n_bins() - g_out - 1;
9,962✔
428

429
      // change outgoing energy bin
430
      p.filter_matches(i_eout_filt).bins_[i_bin] = g_out;
9,962✔
431

432
    } else {
433

434
      double E_out;
435
      if (settings::run_CE) {
117,122✔
436
        E_out = bank.E;
107,160✔
437
      } else {
438
        E_out = data::mg.energy_bin_avg_[static_cast<int>(bank.E)];
9,962✔
439
      }
440

441
      // Set EnergyoutFilter bin index
442
      if (E_out < eo_filt.bins().front() || E_out > eo_filt.bins().back()) {
117,122!
443
        continue;
4✔
444
      } else {
445
        auto i_match = lower_bound_index(
234,236✔
446
          eo_filt.bins().begin(), eo_filt.bins().end(), E_out);
234,236✔
447
        p.filter_matches(i_eout_filt).bins_[i_bin] = i_match;
117,118✔
448
      }
449
    }
450

451
    // Case for tallying prompt neutrons
452
    if (score_bin == SCORE_NU_FISSION ||
127,080✔
453
        (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) {
15,560✔
454

455
      // Find the filter scoring index for this filter combination
456
      int filter_index = 0;
114,134✔
457
      double filter_weight = 1.0;
114,134✔
458
      for (auto j = 0; j < tally.filters().size(); ++j) {
415,324✔
459
        auto i_filt = tally.filters(j);
301,190✔
460
        auto& match {p.filter_matches(i_filt)};
301,190✔
461
        auto i_bin = match.i_bin_;
301,190✔
462
        filter_index += match.bins_[i_bin] * tally.strides(j);
301,190✔
463
        filter_weight *= match.weights_[i_bin];
301,190✔
464
      }
465

466
// Update tally results
467
#pragma omp atomic
468
      tally.results_(filter_index, i_score, TallyResult::VALUE) +=
114,134✔
469
        score * filter_weight;
114,134✔
470

471
    } else if (score_bin == SCORE_DELAYED_NU_FISSION && g != 0) {
127,080✔
472

473
      // If the delayed group filter is present, tally to corresponding delayed
474
      // group bin if it exists
475
      if (tally.delayedgroup_filter_ >= 0) {
78!
476

477
        // Get the index of the delayed group filter
478
        auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
78✔
479

480
        const DelayedGroupFilter& dg_filt {*dynamic_cast<DelayedGroupFilter*>(
78!
481
          model::tally_filters[i_dg_filt].get())};
78✔
482

483
        // Loop over delayed group bins until the corresponding bin is found
484
        for (auto d_bin = 0; d_bin < dg_filt.n_bins(); ++d_bin) {
546✔
485
          if (dg_filt.groups()[d_bin] == g) {
468✔
486
            // Find the filter index and weight for this filter combination
487
            double filter_weight = 1.;
78✔
488
            for (auto j = 0; j < tally.filters().size(); ++j) {
351✔
489
              auto i_filt = tally.filters(j);
273✔
490
              auto& match {p.filter_matches(i_filt)};
273✔
491
              auto i_bin = match.i_bin_;
273✔
492
              filter_weight *= match.weights_[i_bin];
273✔
493
            }
494

495
            score_fission_delayed_dg(i_tally, d_bin, score * filter_weight,
78✔
496
              i_score, p.filter_matches());
497
          }
498
        }
499

500
        // If the delayed group filter is not present, add score to tally
501
      } else {
502

503
        // Find the filter index and weight for this filter combination
504
        int filter_index = 0;
×
505
        double filter_weight = 1.;
×
506
        for (auto j = 0; j < tally.filters().size(); ++j) {
×
507
          auto i_filt = tally.filters(j);
×
508
          auto& match {p.filter_matches(i_filt)};
×
509
          auto i_bin = match.i_bin_;
×
510
          filter_index += match.bins_[i_bin] * tally.strides(j);
×
511
          filter_weight *= match.weights_[i_bin];
×
512
        }
513

514
// Update tally results
515
#pragma omp atomic
516
        tally.results_(filter_index, i_score, TallyResult::VALUE) +=
×
517
          score * filter_weight;
×
518
      }
519
    }
520
  }
521

522
  // Reset outgoing energy bin and score index
523
  p.filter_matches(i_eout_filt).bins_[i_bin] = bin_energyout;
96,193✔
524
}
96,193✔
525

526
double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin)
63,672,590✔
527
{
528
  const auto& nuc {*data::nuclides[i_nuclide]};
63,672,590✔
529

530
  // Get reaction object, or return 0 if reaction is not present
531
  auto m = nuc.reaction_index_[score_bin];
63,672,590✔
532
  if (m == C_NONE)
63,672,590✔
533
    return 0.0;
13,151,181✔
534
  const auto& rx {*nuc.reactions_[m]};
50,521,409✔
535
  const auto& micro {p.neutron_xs(i_nuclide)};
50,521,409✔
536

537
  // In the URR, the (n,gamma) cross section is sampled randomly from
538
  // probability tables. Make sure we use the sampled value (which is equal to
539
  // absorption - fission) rather than the dilute average value
540
  if (micro.use_ptable && score_bin == N_GAMMA) {
50,521,409✔
541
    return micro.absorption - micro.fission;
341,111✔
542
  }
543

544
  auto i_temp = micro.index_temp;
50,180,298✔
545
  if (i_temp >= 0) { // Can be false due to multipole
50,180,298✔
546
    // Get index on energy grid and interpolation factor
547
    auto i_grid = micro.index_grid;
47,588,725✔
548
    auto f = micro.interp_factor;
47,588,725✔
549

550
    // Calculate interpolated cross section
551
    double xs = rx.xs(micro);
47,588,725✔
552

553
    if (settings::run_mode == RunMode::EIGENVALUE &&
47,588,725!
554
        score_bin == HEATING_LOCAL) {
555
      // Determine kerma for fission as (EFR + EGP + EGD + EB)*sigma_f
556
      double kerma_fission =
557
        nuc.fragments_
558
          ? ((*nuc.fragments_)(p.E_last()) + (*nuc.betas_)(p.E_last()) +
×
559
              (*nuc.prompt_photons_)(p.E_last()) +
×
560
              (*nuc.delayed_photons_)(p.E_last())) *
×
561
              micro.fission
×
562
          : 0.0;
×
563

564
      // Determine non-fission kerma as difference
565
      double kerma_non_fission = xs - kerma_fission;
×
566

567
      // Re-weight non-fission kerma by keff to properly balance energy release
568
      // and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
569
      // Stedry, "Self-consistent energy normalization for quasistatic reactor
570
      // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
571
      xs = simulation::keff * kerma_non_fission + kerma_fission;
×
572
    }
573
    return xs;
47,588,725✔
574
  } else {
575
    // For multipole, calculate (n,gamma) from other reactions
576
    return rx.mt_ == N_GAMMA ? micro.absorption - micro.fission : 0.0;
2,591,573✔
577
  }
578
  return 0.0;
579
}
580

581
//! Update tally results for continuous-energy tallies with a tracklength or
582
//! collision estimator.
583

584
void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
238,490,316✔
585
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
586
  double flux)
587
{
588
  Tally& tally {*model::tallies[i_tally]};
238,490,316✔
589

590
  // Get the pre-collision energy of the particle.
591
  auto E = p.E_last();
238,490,316✔
592

593
  using Type = ParticleType;
594

595
  for (auto i = 0; i < tally.scores_.size(); ++i) {
756,877,427✔
596
    auto score_bin = tally.scores_[i];
518,387,111✔
597
    auto score_index = start_index + i;
518,387,111✔
598
    double score = 0.0;
518,387,111✔
599

600
    switch (score_bin) {
518,387,111✔
601
    case SCORE_FLUX:
111,733,800✔
602
      score = flux;
111,733,800✔
603
      break;
111,733,800✔
604

605
    case SCORE_TOTAL:
51,914,287✔
606
      if (i_nuclide >= 0) {
51,914,287✔
607
        if (p.type() == Type::neutron) {
12,609,625✔
608
          score = p.neutron_xs(i_nuclide).total * atom_density * flux;
12,558,003✔
609
        } else if (p.type() == Type::photon) {
51,622✔
610
          score = p.photon_xs(i_nuclide).total * atom_density * flux;
28,982✔
611
        }
612
      } else {
613
        score = p.macro_xs().total * flux;
39,304,662✔
614
      }
615
      break;
51,914,287✔
616

617
    case SCORE_INVERSE_VELOCITY:
2,044,469✔
618
      if (p.type() != Type::neutron)
2,044,469!
619
        continue;
115,473,122✔
620

621
      // Score inverse velocity in units of s/cm.
622
      score = flux / p.speed();
2,044,469✔
623
      break;
2,044,469✔
624

625
    case SCORE_SCATTER:
42,581,225✔
626
      if (p.type() != Type::neutron && p.type() != Type::photon)
42,581,225!
627
        continue;
×
628

629
      if (i_nuclide >= 0) {
42,581,225✔
630
        if (p.type() == Type::neutron) {
5,797,570!
631
          const auto& micro = p.neutron_xs(i_nuclide);
5,797,570✔
632
          score = (micro.total - micro.absorption) * atom_density * flux;
5,797,570✔
633
        } else {
634
          const auto& micro = p.photon_xs(i_nuclide);
×
635
          score = (micro.coherent + micro.incoherent) * atom_density * flux;
×
636
        }
637
      } else {
638
        if (p.type() == Type::neutron) {
36,783,655!
639
          score = (p.macro_xs().total - p.macro_xs().absorption) * flux;
36,783,655✔
640
        } else {
641
          score = (p.macro_xs().coherent + p.macro_xs().incoherent) * flux;
×
642
        }
643
      }
644
      break;
42,581,225✔
645

646
    case SCORE_ABSORPTION:
27,605,708✔
647
      if (p.type() != Type::neutron && p.type() != Type::photon)
27,605,708!
648
        continue;
×
649

650
      if (i_nuclide >= 0) {
27,605,708✔
651
        if (p.type() == Type::neutron) {
2,181,203!
652
          score = p.neutron_xs(i_nuclide).absorption * atom_density * flux;
2,181,203✔
653
        } else {
654
          const auto& xs = p.photon_xs(i_nuclide);
×
655
          score =
×
656
            (xs.total - xs.coherent - xs.incoherent) * atom_density * flux;
×
657
        }
658
      } else {
659
        if (p.type() == Type::neutron) {
25,424,505!
660
          score = p.macro_xs().absorption * flux;
25,424,505✔
661
        } else {
662
          score =
×
663
            (p.macro_xs().photoelectric + p.macro_xs().pair_production) * flux;
×
664
        }
665
      }
666
      break;
27,605,708✔
667

668
    case SCORE_FISSION:
105,624,739✔
669
      if (p.macro_xs().fission == 0)
105,624,739✔
670
        continue;
57,522,227✔
671

672
      if (i_nuclide >= 0) {
48,102,512✔
673
        score = p.neutron_xs(i_nuclide).fission * atom_density * flux;
32,127,633✔
674
      } else {
675
        score = p.macro_xs().fission * flux;
15,974,879✔
676
      }
677
      break;
48,102,512✔
678

679
    case SCORE_NU_FISSION:
30,417,712✔
680
      if (p.macro_xs().fission == 0)
30,417,712✔
681
        continue;
24,380,109✔
682

683
      if (i_nuclide >= 0) {
6,037,603✔
684
        score = p.neutron_xs(i_nuclide).nu_fission * atom_density * flux;
1,999,424✔
685
      } else {
686
        score = p.macro_xs().nu_fission * flux;
4,038,179✔
687
      }
688
      break;
6,037,603✔
689

690
    case SCORE_PROMPT_NU_FISSION:
2,044,469✔
691
      if (p.macro_xs().fission == 0)
2,044,469✔
692
        continue;
1,664,187✔
693
      if (i_nuclide >= 0) {
380,282✔
694
        score = p.neutron_xs(i_nuclide).fission *
169,620✔
695
                data::nuclides[i_nuclide]->nu(
169,620✔
696
                  E, ReactionProduct::EmissionMode::prompt) *
169,620✔
697
                atom_density * flux;
169,620✔
698
      } else {
699
        score = 0.;
210,662✔
700
        // Add up contributions from each nuclide in the material.
701
        if (p.material() != MATERIAL_VOID) {
210,662!
702
          const Material& material {*model::materials[p.material()]};
210,662✔
703
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
1,104,511✔
704
            auto j_nuclide = material.nuclide_[i];
893,849✔
705
            auto atom_density = material.atom_density(i, p.density_mult());
893,849✔
706
            score += p.neutron_xs(j_nuclide).fission *
893,849✔
707
                     data::nuclides[j_nuclide]->nu(
893,849✔
708
                       E, ReactionProduct::EmissionMode::prompt) *
893,849✔
709
                     atom_density * flux;
893,849✔
710
          }
711
        }
712
      }
713
      break;
380,282✔
714

715
    case SCORE_DELAYED_NU_FISSION:
3,075,175✔
716
      if (p.macro_xs().fission == 0)
3,075,175✔
717
        continue;
2,455,990✔
718
      if (i_nuclide >= 0) {
619,185✔
719
        if (tally.delayedgroup_filter_ != C_NONE) {
200,750✔
720
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
89,090✔
721
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
89,090!
722
            model::tally_filters[i_dg_filt].get())};
89,090✔
723
          // Tally each delayed group bin individually
724
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
623,630✔
725
            auto d = filt.groups()[d_bin];
534,540✔
726
            auto yield = data::nuclides[i_nuclide]->nu(
534,540✔
727
              E, ReactionProduct::EmissionMode::delayed, d);
728
            score =
534,540✔
729
              p.neutron_xs(i_nuclide).fission * yield * atom_density * flux;
534,540✔
730
            score_fission_delayed_dg(
534,540✔
731
              i_tally, d_bin, score, score_index, p.filter_matches());
732
          }
733
          continue;
89,090✔
734
        } else {
89,090✔
735
          // If the delayed group filter is not present, compute the score
736
          // by multiplying the delayed-nu-fission macro xs by the flux
737
          score = p.neutron_xs(i_nuclide).fission *
111,660✔
738
                  data::nuclides[i_nuclide]->nu(
111,660✔
739
                    E, ReactionProduct::EmissionMode::delayed) *
111,660✔
740
                  atom_density * flux;
111,660✔
741
        }
742
      } else {
743
        // Need to add up contributions for each nuclide
744
        if (tally.delayedgroup_filter_ != C_NONE) {
418,435✔
745
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
306,775✔
746
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
306,775!
747
            model::tally_filters[i_dg_filt].get())};
306,775✔
748
          if (p.material() != MATERIAL_VOID) {
306,775!
749
            const Material& material {*model::materials[p.material()]};
306,775✔
750
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
1,397,043✔
751
              auto j_nuclide = material.nuclide_[i];
1,090,268✔
752
              auto atom_density = material.atom_density(i, p.density_mult());
1,090,268✔
753
              // Tally each delayed group bin individually
754
              for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
7,631,876✔
755
                auto d = filt.groups()[d_bin];
6,541,608✔
756
                auto yield = data::nuclides[j_nuclide]->nu(
6,541,608✔
757
                  E, ReactionProduct::EmissionMode::delayed, d);
758
                score =
6,541,608✔
759
                  p.neutron_xs(j_nuclide).fission * yield * atom_density * flux;
6,541,608✔
760
                score_fission_delayed_dg(
6,541,608✔
761
                  i_tally, d_bin, score, score_index, p.filter_matches());
762
              }
763
            }
764
          }
765
          continue;
306,775✔
766
        } else {
306,775✔
767
          score = 0.;
111,660✔
768
          if (p.material() != MATERIAL_VOID) {
111,660!
769
            const Material& material {*model::materials[p.material()]};
111,660✔
770
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
669,960✔
771
              auto j_nuclide = material.nuclide_[i];
558,300✔
772
              auto atom_density = material.atom_density(i, p.density_mult());
558,300✔
773
              score += p.neutron_xs(j_nuclide).fission *
558,300✔
774
                       data::nuclides[j_nuclide]->nu(
558,300✔
775
                         E, ReactionProduct::EmissionMode::delayed) *
558,300✔
776
                       atom_density * flux;
558,300✔
777
            }
778
          }
779
        }
780
      }
781
      break;
223,320✔
782

783
    case SCORE_DECAY_RATE:
2,483,207✔
784
      if (p.macro_xs().fission == 0)
2,483,207✔
785
        continue;
2,038,842✔
786
      if (i_nuclide >= 0) {
444,365✔
787
        const auto& nuc {*data::nuclides[i_nuclide]};
200,750✔
788
        if (!nuc.fissionable_)
200,750✔
789
          continue;
100,375✔
790
        const auto& rxn {*nuc.fission_rx_[0]};
100,375✔
791
        if (tally.delayedgroup_filter_ != C_NONE) {
100,375✔
792
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
44,545✔
793
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
44,545!
794
            model::tally_filters[i_dg_filt].get())};
44,545✔
795
          // Tally each delayed group bin individually
796
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
311,815✔
797
            auto d = filt.groups()[d_bin];
267,270✔
798
            auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
267,270✔
799
            auto rate = rxn.products_[d].decay_rate_;
267,270✔
800
            score = p.neutron_xs(i_nuclide).fission * yield * flux *
267,270✔
801
                    atom_density * rate;
267,270✔
802
            score_fission_delayed_dg(
267,270✔
803
              i_tally, d_bin, score, score_index, p.filter_matches());
804
          }
805
          continue;
44,545✔
806
        } else {
44,545✔
807
          score = 0.;
55,830✔
808
          // We need to be careful not to overshoot the number of
809
          // delayed groups since this could cause the range of the
810
          // rxn.products_ array to be exceeded. Hence, we use the size
811
          // of this array and not the MAX_DELAYED_GROUPS constant for
812
          // this loop.
813
          for (auto d = 1; d < rxn.products_.size(); ++d) {
446,640✔
814
            const auto& product = rxn.products_[d];
390,810✔
815
            if (product.particle_ != Type::neutron)
390,810✔
816
              continue;
55,830✔
817

818
            auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
334,980✔
819
            auto rate = product.decay_rate_;
334,980✔
820
            score += p.neutron_xs(i_nuclide).fission * flux * yield *
334,980✔
821
                     atom_density * rate;
334,980✔
822
          }
823
        }
824
      } else {
825
        if (tally.delayedgroup_filter_ != C_NONE) {
243,615✔
826
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
131,955✔
827
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
131,955!
828
            model::tally_filters[i_dg_filt].get())};
131,955✔
829
          if (p.material() != MATERIAL_VOID) {
131,955!
830
            const Material& material {*model::materials[p.material()]};
131,955✔
831
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
643,861✔
832
              auto j_nuclide = material.nuclide_[i];
511,906✔
833
              auto atom_density = material.atom_density(i, p.density_mult());
511,906✔
834
              const auto& nuc {*data::nuclides[j_nuclide]};
511,906✔
835
              if (nuc.fissionable_) {
511,906✔
836
                const auto& rxn {*nuc.fission_rx_[0]};
355,559✔
837
                // Tally each delayed group bin individually
838
                for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
2,488,913✔
839
                  auto d = filt.groups()[d_bin];
2,133,354✔
840
                  auto yield =
841
                    nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
2,133,354✔
842
                  auto rate = rxn.products_[d].decay_rate_;
2,133,354✔
843
                  score = p.neutron_xs(j_nuclide).fission * yield * flux *
2,133,354✔
844
                          atom_density * rate;
2,133,354✔
845
                  score_fission_delayed_dg(
2,133,354✔
846
                    i_tally, d_bin, score, score_index, p.filter_matches());
847
                }
848
              }
849
            }
850
          }
851
          continue;
131,955✔
852
        } else {
131,955✔
853
          score = 0.;
111,660✔
854
          if (p.material() != MATERIAL_VOID) {
111,660!
855
            const Material& material {*model::materials[p.material()]};
111,660✔
856
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
669,960✔
857
              auto j_nuclide = material.nuclide_[i];
558,300✔
858
              auto atom_density = material.atom_density(i, p.density_mult());
558,300✔
859
              const auto& nuc {*data::nuclides[j_nuclide]};
558,300✔
860
              if (nuc.fissionable_) {
558,300✔
861
                const auto& rxn {*nuc.fission_rx_[0]};
334,980✔
862
                // We need to be careful not to overshoot the number of
863
                // delayed groups since this could cause the range of the
864
                // rxn.products_ array to be exceeded. Hence, we use the size
865
                // of this array and not the MAX_DELAYED_GROUPS constant for
866
                // this loop.
867
                for (auto d = 1; d < rxn.products_.size(); ++d) {
2,568,180✔
868
                  const auto& product = rxn.products_[d];
2,233,200✔
869
                  if (product.particle_ != Type::neutron)
2,233,200✔
870
                    continue;
223,320✔
871

872
                  auto yield =
873
                    nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
2,009,880✔
874
                  auto rate = product.decay_rate_;
2,009,880✔
875
                  score += p.neutron_xs(j_nuclide).fission * yield *
2,009,880✔
876
                           atom_density * flux * rate;
2,009,880✔
877
                }
878
              }
879
            }
880
          }
881
        }
882
      }
883
      break;
167,490✔
884

885
    case SCORE_KAPPA_FISSION:
25,335,567✔
886
      if (p.macro_xs().fission == 0.)
25,335,567✔
887
        continue;
21,408,897✔
888
      score = 0.;
3,926,670✔
889
      // Kappa-fission values are determined from the Q-value listed for the
890
      // fission cross section.
891
      if (i_nuclide >= 0) {
3,926,670✔
892
        const auto& nuc {*data::nuclides[i_nuclide]};
169,620✔
893
        if (nuc.fissionable_) {
169,620✔
894
          const auto& rxn {*nuc.fission_rx_[0]};
102,198✔
895
          score = rxn.q_value_ * p.neutron_xs(i_nuclide).fission *
102,198✔
896
                  atom_density * flux;
102,198✔
897
        }
898
      } else if (p.material() != MATERIAL_VOID) {
3,757,050!
899
        const Material& material {*model::materials[p.material()]};
3,757,050✔
900
        for (auto i = 0; i < material.nuclide_.size(); ++i) {
18,836,451✔
901
          auto j_nuclide = material.nuclide_[i];
15,079,401✔
902
          auto atom_density = material.atom_density(i, p.density_mult());
15,079,401✔
903
          const auto& nuc {*data::nuclides[j_nuclide]};
15,079,401✔
904
          if (nuc.fissionable_) {
15,079,401✔
905
            const auto& rxn {*nuc.fission_rx_[0]};
11,230,844✔
906
            score += rxn.q_value_ * p.neutron_xs(j_nuclide).fission *
11,230,844✔
907
                     atom_density * flux;
11,230,844✔
908
          }
909
        }
910
      }
911
      break;
3,926,670✔
912

913
    case SCORE_EVENTS:
1,312,108✔
914
// Simply count the number of scoring events
915
#pragma omp atomic
916
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
1,312,108✔
917
      continue;
1,312,108✔
918

919
    case ELASTIC:
11,151,862✔
920
      if (p.type() != Type::neutron)
11,151,862!
921
        continue;
×
922

923
      if (i_nuclide >= 0) {
11,151,862✔
924
        if (p.neutron_xs(i_nuclide).elastic == CACHE_INVALID)
9,839,608✔
925
          data::nuclides[i_nuclide]->calculate_elastic_xs(p);
3,960,136✔
926
        score = p.neutron_xs(i_nuclide).elastic * atom_density * flux;
9,839,608✔
927
      } else {
928
        score = 0.;
1,312,254✔
929
        if (p.material() != MATERIAL_VOID) {
1,312,254!
930
          const Material& material {*model::materials[p.material()]};
1,312,254✔
931
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
5,823,954✔
932
            auto j_nuclide = material.nuclide_[i];
4,511,700✔
933
            auto atom_density = material.atom_density(i, p.density_mult());
4,511,700✔
934
            if (p.neutron_xs(j_nuclide).elastic == CACHE_INVALID)
4,511,700✔
935
              data::nuclides[j_nuclide]->calculate_elastic_xs(p);
453,252✔
936
            score += p.neutron_xs(j_nuclide).elastic * atom_density * flux;
4,511,700✔
937
          }
938
        }
939
      }
940
      break;
11,151,862✔
941

942
    case SCORE_FISS_Q_PROMPT:
2,624,216✔
943
    case SCORE_FISS_Q_RECOV:
944
      if (p.macro_xs().fission == 0.)
2,624,216✔
945
        continue;
2,177,576✔
946
      score =
446,640✔
947
        score_fission_q(p, score_bin, tally, flux, i_nuclide, atom_density);
446,640✔
948
      break;
446,640✔
949

950
    case SCORE_IFP_TIME_NUM:
349,862✔
951
      if (settings::ifp_on) {
349,862!
952
        if ((p.type() == Type::neutron) && (p.fission())) {
349,862!
953
          if (is_generation_time_or_both()) {
89,184!
954
            const auto& lifetimes =
955
              simulation::ifp_source_lifetime_bank[p.current_work() - 1];
89,184✔
956
            if (lifetimes.size() == settings::ifp_n_generation) {
89,184!
957
              score = lifetimes[0] * p.wgt_last();
89,184✔
958
            }
959
          }
960
        }
961
      }
962
      break;
349,862✔
963

964
    case SCORE_IFP_BETA_NUM:
349,862✔
965
      if (settings::ifp_on) {
349,862!
966
        if ((p.type() == Type::neutron) && (p.fission())) {
349,862!
967
          if (is_beta_effective_or_both()) {
89,184!
968
            const auto& delayed_groups =
969
              simulation::ifp_source_delayed_group_bank[p.current_work() - 1];
89,184✔
970
            if (delayed_groups.size() == settings::ifp_n_generation) {
89,184!
971
              if (delayed_groups[0] > 0) {
89,184✔
972
                score = p.wgt_last();
544✔
973
                if (tally.delayedgroup_filter_ != C_NONE) {
544✔
974
                  auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
272✔
975
                  const DelayedGroupFilter& filt {
976
                    *dynamic_cast<DelayedGroupFilter*>(
272!
977
                      model::tally_filters[i_dg_filt].get())};
272✔
978
                  score_fission_delayed_dg(i_tally, delayed_groups[0] - 1,
272✔
979
                    score, score_index, p.filter_matches());
980
                  continue;
272✔
981
                }
272✔
982
              }
983
            }
984
          }
985
        }
986
      }
987
      break;
349,590✔
988

989
    case SCORE_IFP_DENOM:
349,862✔
990
      if (settings::ifp_on) {
349,862!
991
        if ((p.type() == Type::neutron) && (p.fission())) {
349,862!
992
          int ifp_data_size;
993
          if (is_beta_effective_or_both()) {
89,184!
994
            ifp_data_size = static_cast<int>(
89,184✔
995
              simulation::ifp_source_delayed_group_bank[p.current_work() - 1]
89,184✔
996
                .size());
89,184✔
997
          } else {
998
            ifp_data_size = static_cast<int>(
×
999
              simulation::ifp_source_lifetime_bank[p.current_work() - 1]
×
1000
                .size());
×
1001
          }
1002
          if (ifp_data_size == settings::ifp_n_generation) {
89,184!
1003
            score = p.wgt_last();
89,184✔
1004
          }
1005
        }
1006
      }
1007
      break;
349,862✔
1008

1009
    case N_2N:
86,717,892✔
1010
    case N_3N:
1011
    case N_4N:
1012
    case N_GAMMA:
1013
    case N_P:
1014
    case N_A:
1015
      // This case block only works if cross sections for these reactions have
1016
      // been precalculated. When they are not, we revert to the default case,
1017
      // which looks up cross sections
1018
      if (!simulation::need_depletion_rx)
86,717,892✔
1019
        goto default_case;
36,061,852✔
1020

1021
      if (p.type() != Type::neutron)
50,656,040!
1022
        continue;
×
1023

1024
      int m;
1025
      switch (score_bin) {
1026
        // clang-format off
1027
      case N_GAMMA: m = 0; break;
50,656,040✔
1028
      case N_P:     m = 1; break;
×
1029
      case N_A:     m = 2; break;
×
1030
      case N_2N:    m = 3; break;
×
1031
      case N_3N:    m = 4; break;
×
1032
      case N_4N:    m = 5; break;
×
1033
        // clang-format on
1034
      }
1035
      if (i_nuclide >= 0) {
50,656,040✔
1036
        score = p.neutron_xs(i_nuclide).reaction[m] * atom_density * flux;
50,614,078✔
1037
      } else {
1038
        score = 0.;
41,962✔
1039
        if (p.material() != MATERIAL_VOID) {
41,962!
1040
          const Material& material {*model::materials[p.material()]};
41,962✔
1041
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
83,924✔
1042
            auto j_nuclide = material.nuclide_[i];
41,962✔
1043
            auto atom_density = material.atom_density(i, p.density_mult());
41,962✔
1044
            score += p.neutron_xs(j_nuclide).reaction[m] * atom_density * flux;
41,962✔
1045
          }
1046
        }
1047
      }
1048
      break;
50,656,040✔
1049

1050
    case COHERENT:
628,841✔
1051
    case INCOHERENT:
1052
    case PHOTOELECTRIC:
1053
    case PAIR_PROD:
1054
      if (p.type() != Type::photon)
628,841!
1055
        continue;
628,841✔
1056

1057
      if (i_nuclide >= 0) {
×
1058
        const auto& micro = p.photon_xs(i_nuclide);
×
1059
        double xs = (score_bin == COHERENT)        ? micro.coherent
×
1060
                    : (score_bin == INCOHERENT)    ? micro.incoherent
1061
                    : (score_bin == PHOTOELECTRIC) ? micro.photoelectric
1062
                                                   : micro.pair_production;
1063
        score = xs * atom_density * flux;
×
1064
      } else {
1065
        double xs = (score_bin == COHERENT)     ? p.macro_xs().coherent
×
1066
                    : (score_bin == INCOHERENT) ? p.macro_xs().incoherent
×
1067
                    : (score_bin == PHOTOELECTRIC)
1068
                      ? p.macro_xs().photoelectric
×
1069
                      : p.macro_xs().pair_production;
×
1070
        score = xs * flux;
×
1071
      }
1072
      break;
×
1073

1074
    case HEATING:
2,509,072✔
1075
      score = score_particle_heating(
2,509,072✔
1076
        p, tally, flux, HEATING, i_nuclide, atom_density);
1077
      break;
2,509,072✔
1078

1079
    default:
1080
    default_case:
43,595,028✔
1081

1082
      // The default block is really only meant for redundant neutron reactions
1083
      // (e.g. 444, 901)
1084
      if (p.type() != Type::neutron)
43,595,028✔
1085
        continue;
1,211,333✔
1086

1087
      // Any other cross section has to be calculated on-the-fly
1088
      if (score_bin < 2)
42,383,695!
1089
        fatal_error("Invalid score type on tally " + std::to_string(tally.id_));
×
1090
      score = 0.;
42,383,695✔
1091
      if (i_nuclide >= 0) {
42,383,695✔
1092
        score = get_nuclide_xs(p, i_nuclide, score_bin) * atom_density * flux;
34,263,156✔
1093
      } else if (p.material() != MATERIAL_VOID) {
8,120,539✔
1094
        const Material& material {*model::materials[p.material()]};
8,100,151✔
1095
        for (auto i = 0; i < material.nuclide_.size(); ++i) {
37,509,585✔
1096
          auto j_nuclide = material.nuclide_[i];
29,409,434✔
1097
          auto atom_density = material.atom_density(i, p.density_mult());
29,409,434✔
1098
          score +=
29,409,434✔
1099
            get_nuclide_xs(p, j_nuclide, score_bin) * atom_density * flux;
29,409,434✔
1100
        }
1101
      }
1102
    }
1,312,108✔
1103

1104
    // Add derivative information on score for differential tallies.
1105
    if (tally.deriv_ != C_NONE)
402,913,989✔
1106
      apply_derivative_to_score(
240,770✔
1107
        p, i_tally, i_nuclide, atom_density, score_bin, score);
1108

1109
// Update tally results
1110
#pragma omp atomic
1111
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
402,913,989✔
1112
      score * filter_weight;
402,913,989✔
1113
  }
1114
}
238,490,316✔
1115

1116
//! Update tally results for continuous-energy tallies with an analog estimator.
1117
//
1118
//! For analog tallies, the flux estimate depends on the score type so the flux
1119
//! argument is really just used for filter weights. The atom_density argument
1120
//! is not used for analog tallies.
1121

1122
void score_general_ce_analog(Particle& p, int i_tally, int start_index,
104,549,578✔
1123
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
1124
  double flux)
1125
{
1126
  Tally& tally {*model::tallies[i_tally]};
104,549,578✔
1127

1128
  // Get the pre-collision energy of the particle.
1129
  auto E = p.E_last();
104,549,578✔
1130

1131
  // Determine how much weight was absorbed due to survival biasing
1132
  double wgt_absorb = settings::survival_biasing
1133
                        ? p.wgt_last() *
104,549,578✔
1134
                            p.neutron_xs(p.event_nuclide()).absorption /
21,129✔
1135
                            p.neutron_xs(p.event_nuclide()).total
21,129✔
1136
                        : 0.0;
104,549,578✔
1137

1138
  using Type = ParticleType;
1139

1140
  for (auto i = 0; i < tally.scores_.size(); ++i) {
247,546,273✔
1141
    auto score_bin = tally.scores_[i];
142,996,695✔
1142
    auto score_index = start_index + i;
142,996,695✔
1143
    double score = 0.0;
142,996,695✔
1144

1145
    switch (score_bin) {
142,996,695!
1146
    case SCORE_FLUX:
8,531,649✔
1147
      // All events score to a flux bin. We actually use a collision estimator
1148
      // in place of an analog one since there is no way to count 'events'
1149
      // exactly for the flux
1150
      if (p.type() == Type::neutron || p.type() == Type::photon) {
8,531,649!
1151
        score = flux * p.wgt_last() / p.macro_xs().total;
8,531,649✔
1152
      } else {
1153
        score = 0.;
×
1154
      }
1155
      break;
8,531,649✔
1156

1157
    case SCORE_TOTAL:
7,421,252✔
1158
      // All events will score to the total reaction rate. We can just use
1159
      // use the weight of the particle entering the collision as the score
1160
      score = p.wgt_last() * flux;
7,421,252✔
1161
      break;
7,421,252✔
1162

1163
    case SCORE_INVERSE_VELOCITY:
120,835✔
1164
      if (p.type() != Type::neutron)
120,835!
1165
        continue;
47,530,507✔
1166

1167
      // All events score to an inverse velocity bin. We actually use a
1168
      // collision estimator in place of an analog one since there is no way
1169
      // to count 'events' exactly for the inverse velocity
1170
      score = flux * p.wgt_last() / (p.macro_xs().total * p.speed());
120,835✔
1171
      break;
120,835✔
1172

1173
    case SCORE_SCATTER:
52,305,564✔
1174
      if (p.type() != Type::neutron && p.type() != Type::photon)
52,305,564!
1175
        continue;
×
1176

1177
      // Skip any event where the particle didn't scatter
1178
      if (p.event() != TallyEvent::SCATTER)
52,305,564✔
1179
        continue;
1,011,591✔
1180
      // Since only scattering events make it here, again we can use the
1181
      // weight entering the collision as the estimator for the reaction rate
1182
      score = (p.wgt_last() - wgt_absorb) * flux;
51,293,973✔
1183
      break;
51,293,973✔
1184

1185
    case SCORE_NU_SCATTER:
27,540,287✔
1186
      if (p.type() != Type::neutron)
27,540,287!
1187
        continue;
×
1188

1189
      // Only analog estimators are available.
1190
      // Skip any event where the particle didn't scatter
1191
      if (p.event() != TallyEvent::SCATTER)
27,540,287✔
1192
        continue;
552,546✔
1193
      // For scattering production, we need to use the pre-collision weight
1194
      // times the yield as the estimate for the number of neutrons exiting a
1195
      // reaction with neutrons in the exit channel
1196
      score = (p.wgt_last() - wgt_absorb) * flux;
26,987,741✔
1197

1198
      // Don't waste time on very common reactions we know have multiplicities
1199
      // of one.
1200
      if (p.event_mt() != ELASTIC && p.event_mt() != N_LEVEL &&
27,570,491!
1201
          !(p.event_mt() >= N_N1 && p.event_mt() <= N_NC)) {
582,750!
1202
        // Get yield and apply to score
1203
        auto m =
1204
          data::nuclides[p.event_nuclide()]->reaction_index_[p.event_mt()];
3,454✔
1205
        const auto& rxn {*data::nuclides[p.event_nuclide()]->reactions_[m]};
3,454✔
1206
        score *= (*rxn.products_[0].yield_)(E);
3,454✔
1207
      }
1208
      break;
26,987,741✔
1209

1210
    case SCORE_ABSORPTION:
171,234✔
1211
      if (p.type() != Type::neutron && p.type() != Type::photon)
171,234!
1212
        continue;
×
1213

1214
      if (settings::survival_biasing) {
171,234✔
1215
        // No absorption events actually occur if survival biasing is on --
1216
        // just use weight absorbed in survival biasing
1217
        score = wgt_absorb * flux;
21,129✔
1218
      } else {
1219
        // Skip any event where the particle wasn't absorbed
1220
        if (p.event() == TallyEvent::SCATTER)
150,105✔
1221
          continue;
144,141✔
1222
        // All fission and absorption events will contribute here, so we
1223
        // can just use the particle's weight entering the collision
1224
        score = p.wgt_last() * flux;
5,964✔
1225
      }
1226
      break;
27,093✔
1227

1228
    case SCORE_FISSION:
599,294✔
1229
      if (p.macro_xs().fission == 0)
599,294✔
1230
        continue;
513,785✔
1231
      if (settings::survival_biasing) {
85,509✔
1232
        // No fission events occur if survival biasing is on -- use collision
1233
        // estimator instead
1234
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
21,129!
1235
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission /
21,129✔
1236
                  p.neutron_xs(p.event_nuclide()).total * flux;
21,129✔
1237
        } else {
1238
          score = 0.;
×
1239
        }
1240
      } else {
1241
        // Skip any non-absorption events
1242
        if (p.event() == TallyEvent::SCATTER)
64,380✔
1243
          continue;
54,496✔
1244
        // All fission events will contribute, so again we can use particle's
1245
        // weight entering the collision as the estimate for the fission
1246
        // reaction rate
1247
        score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission /
9,884✔
1248
                p.neutron_xs(p.event_nuclide()).absorption * flux;
9,884✔
1249
      }
1250
      break;
31,013✔
1251

1252
    case SCORE_NU_FISSION:
43,206,913✔
1253
      if (p.macro_xs().fission == 0)
43,206,913✔
1254
        continue;
33,411,704✔
1255
      if (settings::survival_biasing || p.fission()) {
9,795,209✔
1256
        if (tally.energyout_filter_ != C_NONE) {
341,318✔
1257
          // Fission has multiple outgoing neutrons so this helper function
1258
          // is used to handle scoring the multiple filter bins.
1259
          score_fission_eout(p, i_tally, score_index, score_bin);
51,281✔
1260
          continue;
51,281✔
1261
        }
1262
      }
1263
      if (settings::survival_biasing) {
9,743,928✔
1264
        // No fission events occur if survival biasing is on -- use collision
1265
        // estimator instead
1266
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
21,129!
1267
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).nu_fission /
21,129✔
1268
                  p.neutron_xs(p.event_nuclide()).total * flux;
21,129✔
1269
        } else {
1270
          score = 0.;
×
1271
        }
1272
      } else {
1273
        // Skip any non-fission events
1274
        if (!p.fission())
9,722,799✔
1275
          continue;
9,453,891✔
1276
        // If there is no outgoing energy filter, than we only need to score
1277
        // to one bin. For the score to be 'analog', we need to score the
1278
        // number of particles that were banked in the fission bank. Since
1279
        // this was weighted by 1/keff, we multiply by keff to get the proper
1280
        // score.
1281
        score = simulation::keff * p.wgt_bank() * flux;
268,908✔
1282
      }
1283
      break;
290,037✔
1284

1285
    case SCORE_PROMPT_NU_FISSION:
433,660✔
1286
      if (p.macro_xs().fission == 0)
433,660✔
1287
        continue;
321,170✔
1288
      if (settings::survival_biasing || p.fission()) {
112,490!
1289
        if (tally.energyout_filter_ != C_NONE) {
23,205✔
1290
          // Fission has multiple outgoing neutrons so this helper function
1291
          // is used to handle scoring the multiple filter bins.
1292
          score_fission_eout(p, i_tally, score_index, score_bin);
13,398✔
1293
          continue;
13,398✔
1294
        }
1295
      }
1296
      if (settings::survival_biasing) {
99,092!
1297
        // No fission events occur if survival biasing is on -- need to
1298
        // calculate fraction of absorptions that would have resulted in
1299
        // prompt-nu-fission
1300
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
×
1301
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission *
×
1302
                  data::nuclides[p.event_nuclide()]->nu(
×
1303
                    E, ReactionProduct::EmissionMode::prompt) /
×
1304
                  p.neutron_xs(p.event_nuclide()).total * flux;
×
1305
        } else {
1306
          score = 0.;
×
1307
        }
1308
      } else {
1309
        // Skip any non-fission events
1310
        if (!p.fission())
99,092✔
1311
          continue;
89,285✔
1312
        // If there is no outgoing energy filter, than we only need to score
1313
        // to one bin. For the score to be 'analog', we need to score the
1314
        // number of particles that were banked in the fission bank. Since
1315
        // this was weighted by 1/keff, we multiply by keff to get the proper
1316
        // score.
1317
        auto n_delayed = std::accumulate(
9,807✔
1318
          p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
9,807✔
1319
        auto prompt_frac = 1. - n_delayed / static_cast<double>(p.n_bank());
9,807✔
1320
        score = simulation::keff * p.wgt_bank() * prompt_frac * flux;
9,807✔
1321
      }
1322
      break;
9,807✔
1323

1324
    case SCORE_DELAYED_NU_FISSION:
328,402✔
1325
      if (p.macro_xs().fission == 0)
328,402✔
1326
        continue;
214,079✔
1327
      if (settings::survival_biasing || p.fission()) {
114,323✔
1328
        if (tally.energyout_filter_ != C_NONE) {
41,622✔
1329
          // Fission has multiple outgoing neutrons so this helper function
1330
          // is used to handle scoring the multiple filter bins.
1331
          score_fission_eout(p, i_tally, score_index, score_bin);
11,590✔
1332
          continue;
11,590✔
1333
        }
1334
      }
1335
      if (settings::survival_biasing) {
102,733✔
1336
        // No fission events occur if survival biasing is on -- need to
1337
        // calculate fraction of absorptions that would have resulted in
1338
        // delayed-nu-fission
1339
        if (p.neutron_xs(p.event_nuclide()).total > 0 &&
42,258!
1340
            data::nuclides[p.event_nuclide()]->fissionable_) {
21,129!
1341
          if (tally.delayedgroup_filter_ != C_NONE) {
21,129!
1342
            auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
1343
            const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
1344
              model::tally_filters[i_dg_filt].get())};
×
1345
            // Tally each delayed group bin individually
1346
            for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
1347
              auto dg = filt.groups()[d_bin];
×
1348
              auto yield = data::nuclides[p.event_nuclide()]->nu(
×
1349
                E, ReactionProduct::EmissionMode::delayed, dg);
1350
              score = p.wgt_last() * yield *
×
1351
                      p.neutron_xs(p.event_nuclide()).fission /
×
1352
                      p.neutron_xs(p.event_nuclide()).total * flux;
×
1353
              score_fission_delayed_dg(
×
1354
                i_tally, d_bin, score, score_index, p.filter_matches());
1355
            }
1356
            continue;
×
1357
          } else {
×
1358
            // If the delayed group filter is not present, compute the score
1359
            // by multiplying the absorbed weight by the fraction of the
1360
            // delayed-nu-fission xs to the absorption xs
1361
            score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission *
21,129✔
1362
                    data::nuclides[p.event_nuclide()]->nu(
21,129✔
1363
                      E, ReactionProduct::EmissionMode::delayed) /
21,129✔
1364
                    p.neutron_xs(p.event_nuclide()).total * flux;
21,129✔
1365
          }
1366
        }
1367
      } else {
1368
        // Skip any non-fission events
1369
        if (!p.fission())
81,604✔
1370
          continue;
72,701✔
1371
        // If there is no outgoing energy filter, than we only need to score
1372
        // to one bin. For the score to be 'analog', we need to score the
1373
        // number of particles that were banked in the fission bank. Since
1374
        // this was weighted by 1/keff, we multiply by keff to get the proper
1375
        // score. Loop over the neutrons produced from fission and check which
1376
        // ones are delayed. If a delayed neutron is encountered, add its
1377
        // contribution to the fission bank to the score.
1378
        if (tally.delayedgroup_filter_ != C_NONE) {
8,903✔
1379
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
5,795✔
1380
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
5,795!
1381
            model::tally_filters[i_dg_filt].get())};
5,795✔
1382
          // Tally each delayed group bin individually
1383
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
40,565✔
1384
            auto d = filt.groups()[d_bin];
34,770✔
1385
            score = simulation::keff * p.wgt_bank() / p.n_bank() *
34,770✔
1386
                    p.n_delayed_bank(d - 1) * flux;
34,770✔
1387
            score_fission_delayed_dg(
34,770✔
1388
              i_tally, d_bin, score, score_index, p.filter_matches());
1389
          }
1390
          continue;
5,795✔
1391
        } else {
5,795✔
1392
          // Add the contribution from all delayed groups
1393
          auto n_delayed = std::accumulate(
3,108✔
1394
            p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
3,108✔
1395
          score =
3,108✔
1396
            simulation::keff * p.wgt_bank() / p.n_bank() * n_delayed * flux;
3,108✔
1397
        }
1398
      }
1399
      break;
24,237✔
1400

1401
    case SCORE_DECAY_RATE:
120,835✔
1402
      if (p.macro_xs().fission == 0)
120,835✔
1403
        continue;
92,483✔
1404
      if (settings::survival_biasing) {
28,352!
1405
        // No fission events occur if survival biasing is on -- need to
1406
        // calculate fraction of absorptions that would have resulted in
1407
        // delayed-nu-fission
1408
        const auto& nuc {*data::nuclides[p.event_nuclide()]};
×
1409
        if (p.neutron_xs(p.event_nuclide()).total > 0 && nuc.fissionable_) {
×
1410
          const auto& rxn {*nuc.fission_rx_[0]};
×
1411
          if (tally.delayedgroup_filter_ != C_NONE) {
×
1412
            auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
1413
            const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
1414
              model::tally_filters[i_dg_filt].get())};
×
1415
            // Tally each delayed group bin individually
1416
            for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
1417
              auto d = filt.groups()[d_bin];
×
1418
              auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
×
1419
              auto rate = rxn.products_[d].decay_rate_;
×
1420
              score = p.wgt_last() * yield *
×
1421
                      p.neutron_xs(p.event_nuclide()).fission /
×
1422
                      p.neutron_xs(p.event_nuclide()).total * rate * flux;
×
1423
              score_fission_delayed_dg(
×
1424
                i_tally, d_bin, score, score_index, p.filter_matches());
1425
            }
1426
            continue;
×
1427
          } else {
×
1428
            // If the delayed group filter is not present, compute the score
1429
            // by multiplying the absorbed weight by the fraction of the
1430
            // delayed-nu-fission xs to the absorption xs for all delayed
1431
            // groups
1432
            score = 0.;
×
1433
            // We need to be careful not to overshoot the number of
1434
            // delayed groups since this could cause the range of the
1435
            // rxn.products_ array to be exceeded. Hence, we use the size
1436
            // of this array and not the MAX_DELAYED_GROUPS constant for
1437
            // this loop.
1438
            for (auto d = 1; d < rxn.products_.size(); ++d) {
×
1439
              const auto& product = rxn.products_[d];
×
1440
              if (product.particle_ != Type::neutron)
×
1441
                continue;
×
1442

1443
              auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
×
1444
              auto rate = product.decay_rate_;
×
1445
              score += rate * p.wgt_last() *
×
1446
                       p.neutron_xs(p.event_nuclide()).fission * yield /
×
1447
                       p.neutron_xs(p.event_nuclide()).total * flux;
×
1448
            }
1449
          }
1450
        }
1451
      } else {
1452
        // Skip any non-fission events
1453
        if (!p.fission())
28,352✔
1454
          continue;
25,244✔
1455
        // If there is no outgoing energy filter, than we only need to score
1456
        // to one bin. For the score to be 'analog', we need to score the
1457
        // number of particles that were banked in the fission bank. Since
1458
        // this was weighted by 1/keff, we multiply by keff to get the proper
1459
        // score. Loop over the neutrons produced from fission and check which
1460
        // ones are delayed. If a delayed neutron is encountered, add its
1461
        // contribution to the fission bank to the score.
1462
        score = 0.;
3,108✔
1463
        for (auto i = 0; i < p.n_bank(); ++i) {
9,144✔
1464
          const auto& bank = p.nu_bank(i);
6,036✔
1465
          auto g = bank.delayed_group;
6,036✔
1466
          if (g != 0) {
6,036✔
1467
            const auto& nuc {*data::nuclides[p.event_nuclide()]};
28✔
1468
            const auto& rxn {*nuc.fission_rx_[0]};
28✔
1469
            auto rate = rxn.products_[g].decay_rate_;
28✔
1470
            score += simulation::keff * bank.wgt * rate * flux;
28✔
1471
            if (tally.delayedgroup_filter_ != C_NONE) {
28!
1472
              auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
1473
              const DelayedGroupFilter& filt {
1474
                *dynamic_cast<DelayedGroupFilter*>(
×
1475
                  model::tally_filters[i_dg_filt].get())};
×
1476
              // Find the corresponding filter bin and then score
1477
              for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
1478
                auto d = filt.groups()[d_bin];
×
1479
                if (d == g)
×
1480
                  score_fission_delayed_dg(
×
1481
                    i_tally, d_bin, score, score_index, p.filter_matches());
1482
              }
1483
              score = 0.;
×
1484
            }
1485
          }
1486
        }
1487
      }
1488
      break;
3,108✔
1489

1490
    case SCORE_KAPPA_FISSION:
141,964✔
1491
      if (p.macro_xs().fission == 0.)
141,964✔
1492
        continue;
92,483✔
1493
      score = 0.;
49,481✔
1494
      // Kappa-fission values are determined from the Q-value listed for the
1495
      // fission cross section.
1496
      if (settings::survival_biasing) {
49,481✔
1497
        // No fission events occur if survival biasing is on -- need to
1498
        // calculate fraction of absorptions that would have resulted in
1499
        // fission scaled by the Q-value
1500
        const auto& nuc {*data::nuclides[p.event_nuclide()]};
21,129✔
1501
        if (p.neutron_xs(p.event_nuclide()).total > 0 && nuc.fissionable_) {
21,129!
1502
          const auto& rxn {*nuc.fission_rx_[0]};
21,129✔
1503
          score = p.wgt_last() * rxn.q_value_ *
21,129✔
1504
                  p.neutron_xs(p.event_nuclide()).fission /
21,129✔
1505
                  p.neutron_xs(p.event_nuclide()).total * flux;
21,129✔
1506
        }
1507
      } else {
1508
        // Skip any non-absorption events
1509
        if (p.event() == TallyEvent::SCATTER)
28,352✔
1510
          continue;
24,488✔
1511
        // All fission events will contribute, so again we can use particle's
1512
        // weight entering the collision as the estimate for the fission
1513
        // reaction rate
1514
        const auto& nuc {*data::nuclides[p.event_nuclide()]};
3,864✔
1515
        if (p.neutron_xs(p.event_nuclide()).absorption > 0 &&
7,728!
1516
            nuc.fissionable_) {
3,864✔
1517
          const auto& rxn {*nuc.fission_rx_[0]};
3,738✔
1518
          score = p.wgt_last() * rxn.q_value_ *
3,738✔
1519
                  p.neutron_xs(p.event_nuclide()).fission /
3,738✔
1520
                  p.neutron_xs(p.event_nuclide()).absorption * flux;
3,738✔
1521
        }
1522
      }
1523
      break;
24,993✔
1524

1525
    case SCORE_EVENTS:
120,835✔
1526
// Simply count the number of scoring events
1527
#pragma omp atomic
1528
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
120,835✔
1529
      continue;
120,835✔
1530

1531
    case ELASTIC:
120,835✔
1532
      if (p.type() != Type::neutron)
120,835!
1533
        continue;
×
1534

1535
      // Check if event MT matches
1536
      if (p.event_mt() != ELASTIC)
120,835✔
1537
        continue;
6,024✔
1538
      score = (p.wgt_last() - wgt_absorb) * flux;
114,811✔
1539
      break;
114,811✔
1540

1541
    case SCORE_FISS_Q_PROMPT:
241,670✔
1542
    case SCORE_FISS_Q_RECOV:
1543
      if (p.macro_xs().fission == 0.)
241,670✔
1544
        continue;
184,966✔
1545
      score =
56,704✔
1546
        score_fission_q(p, score_bin, tally, flux, i_nuclide, atom_density);
56,704✔
1547
      break;
56,704✔
1548

1549
    case N_2N:
370,005✔
1550
    case N_3N:
1551
    case N_4N:
1552
    case N_GAMMA:
1553
    case N_P:
1554
    case N_A:
1555
      // This case block only works if cross sections for these reactions have
1556
      // been precalculated. When they are not, we revert to the default case,
1557
      // which looks up cross sections
1558
      if (!simulation::need_depletion_rx)
370,005!
1559
        goto default_case;
370,005✔
1560

1561
      if (p.type() != Type::neutron)
×
1562
        continue;
×
1563

1564
      // Check if the event MT matches
1565
      if (p.event_mt() != score_bin)
×
1566
        continue;
×
1567
      score = (p.wgt_last() - wgt_absorb) * flux;
×
1568
      break;
×
1569

1570
    case COHERENT:
×
1571
    case INCOHERENT:
1572
    case PHOTOELECTRIC:
1573
    case PAIR_PROD:
1574
      if (p.type() != Type::photon)
×
1575
        continue;
×
1576

1577
      if (score_bin == PHOTOELECTRIC) {
×
1578
        // Photoelectric events are assigned an MT value corresponding to the
1579
        // shell cross section. Also, photons below the energy cutoff are
1580
        // assumed to have been absorbed via photoelectric absorption
1581
        if ((p.event_mt() < 534 || p.event_mt() > 572) &&
×
1582
            p.event_mt() != REACTION_NONE)
×
1583
          continue;
×
1584
      } else {
1585
        if (p.event_mt() != score_bin)
×
1586
          continue;
×
1587
      }
1588
      score = p.wgt_last() * flux;
×
1589
      break;
×
1590

1591
    case HEATING:
528,034✔
1592
      score = score_particle_heating(
528,034✔
1593
        p, tally, flux, HEATING, i_nuclide, atom_density);
1594
      break;
528,034✔
1595

1596
    default:
1597
    default_case:
1,063,432✔
1598

1599
      // The default block is really only meant for redundant neutron reactions
1600
      // (e.g. 444, 901)
1601
      if (p.type() != Type::neutron)
1,063,432✔
1602
        continue;
441,810✔
1603

1604
      // Any other score is assumed to be a MT number. Thus, we just need
1605
      // to check if it matches the MT number of the event
1606
      if (p.event_mt() != score_bin)
621,622✔
1607
        continue;
620,721✔
1608
      score = (p.wgt_last() - wgt_absorb) * flux;
901✔
1609
    }
120,835✔
1610

1611
    // Add derivative information on score for differential tallies.
1612
    if (tally.deriv_ != C_NONE)
95,466,188✔
1613
      apply_derivative_to_score(
12,588✔
1614
        p, i_tally, i_nuclide, atom_density, score_bin, score);
1615

1616
// Update tally results
1617
#pragma omp atomic
1618
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
95,466,188✔
1619
      score * filter_weight;
95,466,188✔
1620
  }
1621
}
104,549,578✔
1622

1623
//! Update tally results for multigroup tallies with any estimator.
1624
//
1625
//! For analog tallies, the flux estimate depends on the score type so the flux
1626
//! argument is really just used for filter weights.
1627

1628
void score_general_mg(Particle& p, int i_tally, int start_index,
5,514,993✔
1629
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
1630
  double flux)
1631
{
1632
  auto& tally {*model::tallies[i_tally]};
5,514,993✔
1633

1634
  // Set the direction and group to use with get_xs
1635
  Direction p_u;
5,514,993✔
1636
  int p_g;
1637
  double wgt_absorb = 0.0;
5,514,993✔
1638
  if (tally.estimator_ == TallyEstimator::ANALOG ||
5,514,993✔
1639
      tally.estimator_ == TallyEstimator::COLLISION) {
4,589,699✔
1640
    if (settings::survival_biasing) {
1,364,662!
1641
      // Determine weight that was absorbed
1642
      wgt_absorb = p.wgt_last() * p.macro_xs().absorption / p.macro_xs().total;
×
1643

1644
      // Then we either are alive and had a scatter (and so g changed),
1645
      // or are dead and g did not change
1646
      if (p.alive()) {
×
1647
        p_u = p.u_last();
×
1648
        p_g = p.g_last();
×
1649
      } else {
1650
        p_u = p.u_local();
×
1651
        p_g = p.g();
×
1652
      }
1653
    } else if (p.event() == TallyEvent::SCATTER) {
1,364,662✔
1654

1655
      // Then the energy group has been changed by the scattering routine
1656
      // meaning gin is now in p % last_g
1657
      p_u = p.u_last();
1,302,554✔
1658
      p_g = p.g_last();
1,302,554✔
1659
    } else {
1660

1661
      // No scatter, no change in g.
1662
      p_u = p.u_local();
62,108✔
1663
      p_g = p.g();
62,108✔
1664
    }
1665
  } else {
1666

1667
    // No actual collision so g has not changed.
1668
    p_u = p.u_local();
4,150,331✔
1669
    p_g = p.g();
4,150,331✔
1670
  }
1671

1672
  // For shorthand, assign pointers to the material and nuclide xs set
1673
  auto& nuc_xs = (i_nuclide >= 0) ? data::mg.nuclides_[i_nuclide]
925,765✔
1674
                                  : data::mg.macro_xs_[p.material()];
5,514,993✔
1675
  auto& macro_xs = data::mg.macro_xs_[p.material()];
5,514,993✔
1676

1677
  // Find the temperature and angle indices of interest
1678
  int macro_t = p.mg_xs_cache().t;
5,514,993✔
1679
  int macro_a = macro_xs.get_angle_index(p_u);
5,514,993✔
1680
  int nuc_t = 0;
5,514,993✔
1681
  int nuc_a = 0;
5,514,993✔
1682
  if (i_nuclide >= 0) {
5,514,993✔
1683
    nuc_t = nuc_xs.get_temperature_index(p.sqrtkT());
925,765✔
1684
    nuc_a = nuc_xs.get_angle_index(p_u);
925,765✔
1685
  }
1686

1687
  for (auto i = 0; i < tally.scores_.size(); ++i) {
26,202,997✔
1688
    auto score_bin = tally.scores_[i];
20,688,004✔
1689
    auto score_index = start_index + i;
20,688,004✔
1690

1691
    double score;
1692

1693
    switch (score_bin) {
20,688,004!
1694

1695
    case SCORE_FLUX:
4,369,544✔
1696
      if (tally.estimator_ == TallyEstimator::ANALOG) {
4,369,544✔
1697
        // All events score to a flux bin. We actually use a collision estimator
1698
        // in place of an analog one since there is no way to count 'events'
1699
        // exactly for the flux
1700
        score = flux * p.wgt_last() / p.macro_xs().total;
242,963✔
1701
      } else {
1702
        score = flux;
4,126,581✔
1703
      }
1704
      break;
4,369,544✔
1705

1706
    case SCORE_TOTAL:
1,412,162✔
1707
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
1708
        // All events will score to the total reaction rate. We can just use
1709
        // use the weight of the particle entering the collision as the score
1710
        score = flux * p.wgt_last();
485,926✔
1711
        if (i_nuclide >= 0) {
485,926✔
1712
          score *= atom_density *
485,926✔
1713
                   nuc_xs.get_xs(MgxsType::TOTAL, p_g, nuc_t, nuc_a) /
242,963✔
1714
                   macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
242,963✔
1715
        }
1716
      } else {
1717
        if (i_nuclide >= 0) {
926,236✔
1718
          score = atom_density * flux *
926,236✔
1719
                  nuc_xs.get_xs(MgxsType::TOTAL, p_g, nuc_t, nuc_a);
463,118✔
1720
        } else {
1721
          score = p.macro_xs().total * flux;
463,118✔
1722
        }
1723
      }
1724
      break;
1,412,162✔
1725

1726
    case SCORE_INVERSE_VELOCITY:
1,412,162✔
1727
      if (tally.estimator_ == TallyEstimator::ANALOG ||
1,412,162✔
1728
          tally.estimator_ == TallyEstimator::COLLISION) {
926,236✔
1729
        // All events score to an inverse velocity bin. We actually use a
1730
        // collision estimator in place of an analog one since there is no way
1731
        // to count 'events' exactly for the inverse velocity
1732
        score = flux * p.wgt_last();
925,294✔
1733
        if (i_nuclide >= 0) {
925,294✔
1734
          score *=
462,647✔
1735
            nuc_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, nuc_t, nuc_a) /
462,647✔
1736
            macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
462,647✔
1737
        } else {
1738
          score *=
462,647✔
1739
            macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, macro_t, macro_a) /
462,647✔
1740
            macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
462,647✔
1741
        }
1742
      } else {
1743
        if (i_nuclide >= 0) {
486,868✔
1744
          score =
243,434✔
1745
            flux * nuc_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, nuc_t, nuc_a);
243,434✔
1746
        } else {
1747
          score = flux * macro_xs.get_xs(
243,434✔
1748
                           MgxsType::INVERSE_VELOCITY, p_g, macro_t, macro_a);
1749
        }
1750
      }
1751
      break;
1,412,162✔
1752

1753
    case SCORE_SCATTER:
878,736✔
1754
      if (tally.estimator_ == TallyEstimator::ANALOG) {
878,736!
1755
        // Skip any event where the particle didn't scatter
1756
        if (p.event() != TallyEvent::SCATTER)
878,736✔
1757
          continue;
39,944✔
1758
        // Since only scattering events make it here, again we can use the
1759
        // weight entering the collision as the estimator for the reaction rate
1760
        score = (p.wgt_last() - wgt_absorb) * flux;
838,792✔
1761
        if (i_nuclide >= 0) {
838,792✔
1762
          score *= atom_density *
838,792✔
1763
                   nuc_xs.get_xs(MgxsType::SCATTER_FMU, p.g_last(), &p.g(),
419,396✔
1764
                     &p.mu(), nullptr, nuc_t, nuc_a) /
419,396✔
1765
                   macro_xs.get_xs(MgxsType::SCATTER_FMU, p.g_last(), &p.g(),
419,396✔
1766
                     &p.mu(), nullptr, macro_t, macro_a);
419,396✔
1767
        }
1768
      } else {
1769
        if (i_nuclide >= 0) {
×
1770
          score = atom_density * flux *
×
1771
                  nuc_xs.get_xs(MgxsType::SCATTER, p_g, nullptr, &p.mu(),
×
1772
                    nullptr, nuc_t, nuc_a);
1773
        } else {
1774
          score = flux * macro_xs.get_xs(MgxsType::SCATTER, p_g, nullptr,
×
1775
                           &p.mu(), nullptr, macro_t, macro_a);
×
1776
        }
1777
      }
1778
      break;
838,792✔
1779

1780
    case SCORE_NU_SCATTER:
878,736✔
1781
      if (tally.estimator_ == TallyEstimator::ANALOG) {
878,736!
1782
        // Skip any event where the particle didn't scatter
1783
        if (p.event() != TallyEvent::SCATTER)
878,736✔
1784
          continue;
39,944✔
1785
        // For scattering production, we need to use the pre-collision weight
1786
        // times the multiplicity as the estimate for the number of neutrons
1787
        // exiting a reaction with neutrons in the exit channel
1788
        score = (p.wgt_last() - wgt_absorb) * flux;
838,792✔
1789
        // Since we transport based on material data, the angle selected
1790
        // was not selected from the f(mu) for the nuclide.  Therefore
1791
        // adjust the score by the actual probability for that nuclide.
1792
        if (i_nuclide >= 0) {
838,792✔
1793
          score *= atom_density *
838,792✔
1794
                   nuc_xs.get_xs(MgxsType::NU_SCATTER_FMU, p.g_last(), &p.g(),
419,396✔
1795
                     &p.mu(), nullptr, nuc_t, nuc_a) /
419,396✔
1796
                   macro_xs.get_xs(MgxsType::NU_SCATTER_FMU, p.g_last(), &p.g(),
419,396✔
1797
                     &p.mu(), nullptr, macro_t, macro_a);
419,396✔
1798
        }
1799
      } else {
1800
        if (i_nuclide >= 0) {
×
1801
          score = atom_density * flux *
×
1802
                  nuc_xs.get_xs(MgxsType::NU_SCATTER, p_g, nuc_t, nuc_a);
×
1803
        } else {
1804
          score =
×
1805
            flux * macro_xs.get_xs(MgxsType::NU_SCATTER, p_g, macro_t, macro_a);
×
1806
        }
1807
      }
1808
      break;
838,792✔
1809

1810
    case SCORE_ABSORPTION:
1,412,162✔
1811
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
1812
        if (settings::survival_biasing) {
485,926!
1813
          // No absorption events actually occur if survival biasing is on --
1814
          // just use weight absorbed in survival biasing
1815
          score = wgt_absorb * flux;
×
1816
        } else {
1817
          // Skip any event where the particle wasn't absorbed
1818
          if (p.event() == TallyEvent::SCATTER)
485,926✔
1819
            continue;
463,762✔
1820
          // All fission and absorption events will contribute here, so we
1821
          // can just use the particle's weight entering the collision
1822
          score = p.wgt_last() * flux;
22,164✔
1823
        }
1824
        if (i_nuclide >= 0) {
22,164✔
1825
          score *= atom_density *
22,164✔
1826
                   nuc_xs.get_xs(MgxsType::ABSORPTION, p_g, nuc_t, nuc_a) /
11,082✔
1827
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
11,082✔
1828
        }
1829
      } else {
1830
        if (i_nuclide >= 0) {
926,236✔
1831
          score = atom_density * flux *
926,236✔
1832
                  nuc_xs.get_xs(MgxsType::ABSORPTION, p_g, nuc_t, nuc_a);
463,118✔
1833
        } else {
1834
          score = p.macro_xs().absorption * flux;
463,118✔
1835
        }
1836
      }
1837
      break;
948,400✔
1838

1839
    case SCORE_FISSION:
1,412,162✔
1840
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
1841
        if (settings::survival_biasing) {
485,926!
1842
          // No fission events occur if survival biasing is on -- need to
1843
          // calculate fraction of absorptions that would have resulted in
1844
          // fission
1845
          score = wgt_absorb * flux;
×
1846
        } else {
1847
          // Skip any non-absorption events
1848
          if (p.event() == TallyEvent::SCATTER)
485,926✔
1849
            continue;
463,762✔
1850
          // All fission events will contribute, so again we can use particle's
1851
          // weight entering the collision as the estimate for the fission
1852
          // reaction rate
1853
          score = p.wgt_last() * flux;
22,164✔
1854
        }
1855
        if (i_nuclide >= 0) {
22,164✔
1856
          score *= atom_density *
22,164✔
1857
                   nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
11,082✔
1858
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
11,082✔
1859
        } else {
1860
          score *= macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a) /
11,082✔
1861
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
11,082✔
1862
        }
1863
      } else {
1864
        if (i_nuclide >= 0) {
926,236✔
1865
          score = atom_density * flux *
926,236✔
1866
                  nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a);
463,118✔
1867
        } else {
1868
          score =
463,118✔
1869
            flux * macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
463,118✔
1870
        }
1871
      }
1872
      break;
948,400✔
1873

1874
    case SCORE_NU_FISSION:
1,851,530✔
1875
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,851,530✔
1876
        if (settings::survival_biasing || p.fission()) {
925,294!
1877
          if (tally.energyout_filter_ != C_NONE) {
41,964✔
1878
            // Fission has multiple outgoing neutrons so this helper function
1879
            // is used to handle scoring the multiple filter bins.
1880
            score_fission_eout(p, i_tally, score_index, score_bin);
19,924✔
1881
            continue;
19,924✔
1882
          }
1883
        }
1884
        if (settings::survival_biasing) {
905,370!
1885
          // No fission events occur if survival biasing is on -- need to
1886
          // calculate fraction of absorptions that would have resulted in
1887
          // nu-fission
1888
          score = wgt_absorb * flux;
×
1889
          if (i_nuclide >= 0) {
×
1890
            score *=
×
1891
              atom_density *
×
1892
              nuc_xs.get_xs(MgxsType::NU_FISSION, p_g, nuc_t, nuc_a) /
×
1893
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1894
          } else {
1895
            score *=
×
1896
              macro_xs.get_xs(MgxsType::NU_FISSION, p_g, macro_t, macro_a) /
×
1897
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1898
          }
1899
        } else {
1900
          // Skip any non-fission events
1901
          if (!p.fission())
905,370✔
1902
            continue;
883,330✔
1903
          // If there is no outgoing energy filter, than we only need to score
1904
          // to one bin. For the score to be 'analog', we need to score the
1905
          // number of particles that were banked in the fission bank. Since
1906
          // this was weighted by 1/keff, we multiply by keff to get the proper
1907
          // score.
1908
          score = simulation::keff * p.wgt_bank() * flux;
22,040✔
1909
          if (i_nuclide >= 0) {
22,040✔
1910
            score *= atom_density *
22,040✔
1911
                     nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
11,020✔
1912
                     macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
11,020✔
1913
          }
1914
        }
1915
      } else {
1916
        if (i_nuclide >= 0) {
926,236✔
1917
          score = atom_density * flux *
926,236✔
1918
                  nuc_xs.get_xs(MgxsType::NU_FISSION, p_g, nuc_t, nuc_a);
463,118✔
1919
        } else {
1920
          score =
463,118✔
1921
            flux * macro_xs.get_xs(MgxsType::NU_FISSION, p_g, macro_t, macro_a);
463,118✔
1922
        }
1923
      }
1924
      break;
948,276✔
1925

1926
    case SCORE_PROMPT_NU_FISSION:
1,412,162✔
1927
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
1928
        if (settings::survival_biasing || p.fission()) {
485,926!
1929
          if (tally.energyout_filter_ != C_NONE) {
22,040!
1930
            // Fission has multiple outgoing neutrons so this helper function
1931
            // is used to handle scoring the multiple filter bins.
1932
            score_fission_eout(p, i_tally, score_index, score_bin);
×
1933
            continue;
×
1934
          }
1935
        }
1936
        if (settings::survival_biasing) {
485,926!
1937
          // No fission events occur if survival biasing is on -- need to
1938
          // calculate fraction of absorptions that would have resulted in
1939
          // prompt-nu-fission
1940
          score = wgt_absorb * flux;
×
1941
          if (i_nuclide >= 0) {
×
1942
            score *=
×
1943
              atom_density *
×
1944
              nuc_xs.get_xs(MgxsType::PROMPT_NU_FISSION, p_g, nuc_t, nuc_a) /
×
1945
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1946
          } else {
1947
            score *=
×
1948
              macro_xs.get_xs(
×
1949
                MgxsType::PROMPT_NU_FISSION, p_g, macro_t, macro_a) /
×
1950
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1951
          }
1952
        } else {
1953
          // Skip any non-fission events
1954
          if (!p.fission())
485,926✔
1955
            continue;
463,886✔
1956
          // If there is no outgoing energy filter, than we only need to score
1957
          // to one bin. For the score to be 'analog', we need to score the
1958
          // number of particles that were banked in the fission bank. Since
1959
          // this was weighted by 1/keff, we multiply by keff to get the proper
1960
          // score.
1961
          auto n_delayed = std::accumulate(
22,040✔
1962
            p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
22,040✔
1963
          auto prompt_frac = 1. - n_delayed / static_cast<double>(p.n_bank());
22,040✔
1964
          score = simulation::keff * p.wgt_bank() * prompt_frac * flux;
22,040✔
1965
          if (i_nuclide >= 0) {
22,040✔
1966
            score *= atom_density *
22,040✔
1967
                     nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
11,020✔
1968
                     macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
11,020✔
1969
          }
1970
        }
1971
      } else {
1972
        if (i_nuclide >= 0) {
926,236✔
1973
          score = atom_density * flux *
926,236✔
1974
                  nuc_xs.get_xs(MgxsType::PROMPT_NU_FISSION, p_g, nuc_t, nuc_a);
463,118✔
1975
        } else {
1976
          score = flux * macro_xs.get_xs(
463,118✔
1977
                           MgxsType::PROMPT_NU_FISSION, p_g, macro_t, macro_a);
1978
        }
1979
      }
1980
      break;
948,276✔
1981

1982
    case SCORE_DELAYED_NU_FISSION:
1,412,162✔
1983
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
1984
        if (settings::survival_biasing || p.fission()) {
485,926!
1985
          if (tally.energyout_filter_ != C_NONE) {
22,040!
1986
            // Fission has multiple outgoing neutrons so this helper function
1987
            // is used to handle scoring the multiple filter bins.
1988
            score_fission_eout(p, i_tally, score_index, score_bin);
×
1989
            continue;
×
1990
          }
1991
        }
1992
        if (settings::survival_biasing) {
485,926!
1993
          // No fission events occur if survival biasing is on -- need to
1994
          // calculate fraction of absorptions that would have resulted in
1995
          // delayed-nu-fission
1996
          double abs_xs =
1997
            macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1998
          if (abs_xs > 0.) {
×
1999
            if (tally.delayedgroup_filter_ != C_NONE) {
×
2000
              auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2001
              const DelayedGroupFilter& filt {
2002
                *dynamic_cast<DelayedGroupFilter*>(
×
2003
                  model::tally_filters[i_dg_filt].get())};
×
2004
              // Tally each delayed group bin individually
2005
              for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2006
                auto d = filt.groups()[d_bin] - 1;
×
2007
                score = wgt_absorb * flux;
×
2008
                if (i_nuclide >= 0) {
×
2009
                  score *= nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2010
                             nullptr, nullptr, &d, nuc_t, nuc_a) /
×
2011
                           abs_xs;
2012
                } else {
2013
                  score *= macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2014
                             nullptr, nullptr, &d, macro_t, macro_a) /
×
2015
                           abs_xs;
2016
                }
2017
                score_fission_delayed_dg(
×
2018
                  i_tally, d_bin, score, score_index, p.filter_matches());
2019
              }
2020
              continue;
×
2021
            } else {
×
2022
              // If the delayed group filter is not present, compute the score
2023
              // by multiplying the absorbed weight by the fraction of the
2024
              // delayed-nu-fission xs to the absorption xs
2025
              score = wgt_absorb * flux;
×
2026
              if (i_nuclide >= 0) {
×
2027
                score *= nuc_xs.get_xs(
×
2028
                           MgxsType::DELAYED_NU_FISSION, p_g, nuc_t, nuc_a) /
×
2029
                         abs_xs;
2030
              } else {
2031
                score *= macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2032
                           macro_t, macro_a) /
×
2033
                         abs_xs;
2034
              }
2035
            }
2036
          }
2037
        } else {
2038
          // Skip any non-fission events
2039
          if (!p.fission())
485,926✔
2040
            continue;
463,886✔
2041
          // If there is no outgoing energy filter, than we only need to score
2042
          // to one bin. For the score to be 'analog', we need to score the
2043
          // number of particles that were banked in the fission bank. Since
2044
          // this was weighted by 1/keff, we multiply by keff to get the proper
2045
          // score. Loop over the neutrons produced from fission and check which
2046
          // ones are delayed. If a delayed neutron is encountered, add its
2047
          // contribution to the fission bank to the score.
2048
          if (tally.delayedgroup_filter_ != C_NONE) {
22,040!
2049
            auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2050
            const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
2051
              model::tally_filters[i_dg_filt].get())};
×
2052
            // Tally each delayed group bin individually
2053
            for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2054
              auto d = filt.groups()[d_bin];
×
2055
              score = simulation::keff * p.wgt_bank() / p.n_bank() *
×
2056
                      p.n_delayed_bank(d - 1) * flux;
×
2057
              if (i_nuclide >= 0) {
×
2058
                score *=
×
2059
                  atom_density *
×
2060
                  nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
×
2061
                  macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
×
2062
              }
2063
              score_fission_delayed_dg(
×
2064
                i_tally, d_bin, score, score_index, p.filter_matches());
2065
            }
2066
            continue;
×
2067
          } else {
×
2068
            // Add the contribution from all delayed groups
2069
            auto n_delayed = std::accumulate(
22,040✔
2070
              p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
22,040✔
2071
            score =
22,040✔
2072
              simulation::keff * p.wgt_bank() / p.n_bank() * n_delayed * flux;
22,040✔
2073
            if (i_nuclide >= 0) {
22,040✔
2074
              score *=
11,020✔
2075
                atom_density *
11,020✔
2076
                nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
11,020✔
2077
                macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
11,020✔
2078
            }
2079
          }
2080
        }
2081
      } else {
2082
        if (tally.delayedgroup_filter_ != C_NONE) {
926,236!
2083
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2084
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
2085
            model::tally_filters[i_dg_filt].get())};
×
2086
          // Tally each delayed group bin individually
2087
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2088
            auto d = filt.groups()[d_bin] - 1;
×
2089
            if (i_nuclide >= 0) {
×
2090
              score = flux * atom_density *
×
2091
                      nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g, nullptr,
×
2092
                        nullptr, &d, nuc_t, nuc_a);
2093
            } else {
2094
              score = flux * macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2095
                               nullptr, nullptr, &d, macro_t, macro_a);
2096
            }
2097
            score_fission_delayed_dg(
×
2098
              i_tally, d_bin, score, score_index, p.filter_matches());
2099
          }
2100
          continue;
×
2101
        } else {
×
2102
          if (i_nuclide >= 0) {
926,236✔
2103
            score =
463,118✔
2104
              flux * atom_density *
463,118✔
2105
              nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g, nuc_t, nuc_a);
463,118✔
2106
          } else {
2107
            score = flux * macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
463,118✔
2108
                             macro_t, macro_a);
2109
          }
2110
        }
2111
      }
2112
      break;
948,276✔
2113

2114
    case SCORE_DECAY_RATE:
1,412,162✔
2115
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
2116
        if (settings::survival_biasing) {
485,926!
2117
          // No fission events occur if survival biasing is on -- need to
2118
          // calculate fraction of absorptions that would have resulted in
2119
          // delayed-nu-fission
2120
          double abs_xs =
2121
            macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
2122
          if (abs_xs > 0) {
×
2123
            if (tally.delayedgroup_filter_ != C_NONE) {
×
2124
              auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2125
              const DelayedGroupFilter& filt {
2126
                *dynamic_cast<DelayedGroupFilter*>(
×
2127
                  model::tally_filters[i_dg_filt].get())};
×
2128
              // Tally each delayed group bin individually
2129
              for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2130
                auto d = filt.groups()[d_bin] - 1;
×
2131
                score = wgt_absorb * flux;
×
2132
                if (i_nuclide >= 0) {
×
2133
                  score *= nuc_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
×
2134
                             nullptr, &d, nuc_t, nuc_a) *
×
2135
                           nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2136
                             nullptr, nullptr, &d, nuc_t, nuc_a) /
×
2137
                           abs_xs;
2138
                } else {
2139
                  score *= macro_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
×
2140
                             nullptr, &d, macro_t, macro_a) *
×
2141
                           macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2142
                             nullptr, nullptr, &d, macro_t, macro_a) /
×
2143
                           abs_xs;
2144
                }
2145
                score_fission_delayed_dg(
×
2146
                  i_tally, d_bin, score, score_index, p.filter_matches());
2147
              }
2148
              continue;
×
2149
            } else {
×
2150
              // If the delayed group filter is not present, compute the score
2151
              // by multiplying the absorbed weight by the fraction of the
2152
              // delayed-nu-fission xs to the absorption xs for all delayed
2153
              // groups
2154
              score = 0.;
×
2155
              for (auto d = 0; d < data::mg.num_delayed_groups_; ++d) {
×
2156
                if (i_nuclide >= 0) {
×
2157
                  score += wgt_absorb * flux *
×
2158
                           nuc_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
×
2159
                             nullptr, &d, nuc_t, nuc_a) *
×
2160
                           nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2161
                             nullptr, nullptr, &d, nuc_t, nuc_a) /
×
2162
                           abs_xs;
2163
                } else {
2164
                  score += wgt_absorb * flux *
×
2165
                           macro_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
×
2166
                             nullptr, &d, macro_t, macro_a) *
×
2167
                           macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2168
                             nullptr, nullptr, &d, macro_t, macro_a) /
×
2169
                           abs_xs;
2170
                }
2171
              }
2172
            }
2173
          }
2174
        } else {
2175
          // Skip any non-fission events
2176
          if (!p.fission())
485,926✔
2177
            continue;
463,886✔
2178
          // If there is no outgoing energy filter, than we only need to score
2179
          // to one bin. For the score to be 'analog', we need to score the
2180
          // number of particles that were banked in the fission bank. Since
2181
          // this was weighted by 1/keff, we multiply by keff to get the proper
2182
          // score. Loop over the neutrons produced from fission and check which
2183
          // ones are delayed. If a delayed neutron is encountered, add its
2184
          // contribution to the fission bank to the score.
2185
          score = 0.;
22,040✔
2186
          for (auto i = 0; i < p.n_bank(); ++i) {
44,080✔
2187
            const auto& bank = p.nu_bank(i);
22,040✔
2188
            auto d = bank.delayed_group - 1;
22,040✔
2189
            if (d != -1) {
22,040✔
2190
              if (i_nuclide >= 0) {
156✔
2191
                score +=
78✔
2192
                  simulation::keff * atom_density * bank.wgt * flux *
156✔
2193
                  nuc_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr, nullptr, &d,
78✔
2194
                    nuc_t, nuc_a) *
78✔
2195
                  nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
78✔
2196
                  macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
78✔
2197
              } else {
2198
                score += simulation::keff * bank.wgt * flux *
156✔
2199
                         macro_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
78✔
2200
                           nullptr, &d, macro_t, macro_a);
2201
              }
2202
              if (tally.delayedgroup_filter_ != C_NONE) {
156!
2203
                auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2204
                const DelayedGroupFilter& filt {
2205
                  *dynamic_cast<DelayedGroupFilter*>(
×
2206
                    model::tally_filters[i_dg_filt].get())};
×
2207
                // Find the corresponding filter bin and then score
2208
                for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2209
                  auto dg = filt.groups()[d_bin];
×
2210
                  if (dg == d + 1)
×
2211
                    score_fission_delayed_dg(
×
2212
                      i_tally, d_bin, score, score_index, p.filter_matches());
2213
                }
2214
                score = 0.;
×
2215
              }
2216
            }
2217
          }
2218
          if (tally.delayedgroup_filter_ != C_NONE)
22,040!
2219
            continue;
×
2220
        }
2221
      } else {
2222
        if (tally.delayedgroup_filter_ != C_NONE) {
926,236!
2223
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
2224
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
2225
            model::tally_filters[i_dg_filt].get())};
×
2226
          // Tally each delayed group bin individually
2227
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
2228
            auto d = filt.groups()[d_bin] - 1;
×
2229
            if (i_nuclide >= 0) {
×
2230
              score = atom_density * flux *
×
2231
                      nuc_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr, nullptr,
×
2232
                        &d, nuc_t, nuc_a) *
2233
                      nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g, nullptr,
×
2234
                        nullptr, &d, nuc_t, nuc_a);
2235
            } else {
2236
              score = flux *
×
2237
                      macro_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
×
2238
                        nullptr, &d, macro_t, macro_a) *
2239
                      macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
×
2240
                        nullptr, nullptr, &d, macro_t, macro_a);
2241
            }
2242
            score_fission_delayed_dg(
×
2243
              i_tally, d_bin, score, score_index, p.filter_matches());
2244
          }
2245
          continue;
×
2246
        } else {
×
2247
          score = 0.;
926,236✔
2248
          for (auto d = 0; d < data::mg.num_delayed_groups_; ++d) {
6,483,652✔
2249
            if (i_nuclide >= 0) {
5,557,416✔
2250
              score += atom_density * flux *
8,336,124✔
2251
                       nuc_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
2,778,708✔
2252
                         nullptr, &d, nuc_t, nuc_a) *
2,778,708✔
2253
                       nuc_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g, nullptr,
2,778,708✔
2254
                         nullptr, &d, nuc_t, nuc_a);
2255
            } else {
2256
              score += flux *
5,557,416✔
2257
                       macro_xs.get_xs(MgxsType::DECAY_RATE, p_g, nullptr,
2,778,708✔
2258
                         nullptr, &d, macro_t, macro_a) *
2,778,708✔
2259
                       macro_xs.get_xs(MgxsType::DELAYED_NU_FISSION, p_g,
2,778,708✔
2260
                         nullptr, nullptr, &d, macro_t, macro_a);
2261
            }
2262
          }
2263
        }
2264
      }
2265
      break;
948,276✔
2266

2267
    case SCORE_KAPPA_FISSION:
1,412,162✔
2268
      if (tally.estimator_ == TallyEstimator::ANALOG) {
1,412,162✔
2269
        if (settings::survival_biasing) {
485,926!
2270
          // No fission events occur if survival biasing is on -- need to
2271
          // calculate fraction of absorptions that would have resulted in
2272
          // fission scaled by the Q-value
2273
          score = wgt_absorb * flux;
×
2274
        } else {
2275
          // Skip any non-absorption events
2276
          if (p.event() == TallyEvent::SCATTER)
485,926✔
2277
            continue;
463,762✔
2278
          // All fission events will contribute, so again we can use particle's
2279
          // weight entering the collision as the estimate for the fission
2280
          // reaction rate
2281
          score = p.wgt_last() * flux;
22,164✔
2282
        }
2283
        if (i_nuclide >= 0) {
22,164✔
2284
          score *= atom_density *
22,164✔
2285
                   nuc_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, nuc_t, nuc_a) /
11,082✔
2286
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
11,082✔
2287
        } else {
2288
          score *=
11,082✔
2289
            macro_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, macro_t, macro_a) /
11,082✔
2290
            macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
11,082✔
2291
        }
2292
      } else {
2293
        if (i_nuclide >= 0) {
926,236✔
2294
          score = atom_density * flux *
926,236✔
2295
                  nuc_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, nuc_t, nuc_a);
463,118✔
2296
        } else {
2297
          score = flux * macro_xs.get_xs(
463,118✔
2298
                           MgxsType::KAPPA_FISSION, p_g, macro_t, macro_a);
2299
        }
2300
      }
2301
      break;
948,400✔
2302

2303
    case SCORE_EVENTS:
1,412,162✔
2304
// Simply count the number of scoring events
2305
#pragma omp atomic
2306
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
1,412,162✔
2307
      continue;
1,412,162✔
2308

2309
    default:
×
2310
      continue;
×
2311
    }
1,412,162✔
2312

2313
// Update tally results
2314
#pragma omp atomic
2315
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
15,509,756✔
2316
      score * filter_weight;
15,509,756✔
2317
  }
2318
}
5,514,993✔
2319

2320
void score_analog_tally_ce(Particle& p)
26,398,040✔
2321
{
2322
  // Since electrons/positrons are not transported, we assign a flux of zero.
2323
  // Note that the heating score does NOT use the flux and will be non-zero for
2324
  // electrons/positrons.
2325
  double flux =
2326
    (p.type() == ParticleType::neutron || p.type() == ParticleType::photon)
28,295,708✔
2327
      ? 1.0
28,295,708✔
2328
      : 0.0;
26,398,040✔
2329

2330
  for (auto i_tally : model::active_analog_tallies) {
127,194,795✔
2331
    const Tally& tally {*model::tallies[i_tally]};
100,796,755✔
2332

2333
    // Initialize an iterator over valid filter bin combinations.  If there are
2334
    // no valid combinations, use a continue statement to ensure we skip the
2335
    // assume_separate break below.
2336
    auto filter_iter = FilterBinIter(tally, p);
100,796,755✔
2337
    auto end = FilterBinIter(tally, true, &p.filter_matches());
100,796,755✔
2338
    if (filter_iter == end)
100,796,755✔
2339
      continue;
5,592,225✔
2340

2341
    // Loop over filter bins.
2342
    for (; filter_iter != end; ++filter_iter) {
199,391,852✔
2343
      auto filter_index = filter_iter.index_;
104,187,322✔
2344
      auto filter_weight = filter_iter.weight_;
104,187,322✔
2345

2346
      // Loop over nuclide bins.
2347
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
211,049,328✔
2348
        auto i_nuclide = tally.nuclides_[i];
106,862,006✔
2349

2350
        // Tally this event in the present nuclide bin if that bin represents
2351
        // the event nuclide or the total material.  Note that the atomic
2352
        // density argument for score_general is not used for analog tallies.
2353
        if (i_nuclide == p.event_nuclide() || i_nuclide == -1)
106,862,006✔
2354
          score_general_ce_analog(p, i_tally, i * tally.scores_.size(),
104,549,578✔
2355
            filter_index, filter_weight, i_nuclide, -1.0, flux);
2356
      }
2357
    }
2358

2359
    // If the user has specified that we can assume all tallies are spatially
2360
    // separate, this implies that once a tally has been scored to, we needn't
2361
    // check the others. This cuts down on overhead when there are many
2362
    // tallies specified
2363
    if (settings::assume_separate)
95,204,530!
2364
      break;
×
2365
  }
2366

2367
  // Reset all the filter matches for the next tally event.
2368
  for (auto& match : p.filter_matches())
147,927,615✔
2369
    match.bins_present_ = false;
121,529,575✔
2370
}
26,398,040✔
2371

2372
void score_analog_tally_mg(Particle& p)
109,842✔
2373
{
2374
  for (auto i_tally : model::active_analog_tallies) {
1,208,262✔
2375
    const Tally& tally {*model::tallies[i_tally]};
1,098,420✔
2376

2377
    // Initialize an iterator over valid filter bin combinations.  If there are
2378
    // no valid combinations, use a continue statement to ensure we skip the
2379
    // assume_separate break below.
2380
    auto filter_iter = FilterBinIter(tally, p);
1,098,420✔
2381
    auto end = FilterBinIter(tally, true, &p.filter_matches());
1,098,420✔
2382
    if (filter_iter == end)
1,098,420✔
2383
      continue;
173,126✔
2384

2385
    // Loop over filter bins.
2386
    for (; filter_iter != end; ++filter_iter) {
1,850,588✔
2387
      auto filter_index = filter_iter.index_;
925,294✔
2388
      auto filter_weight = filter_iter.weight_;
925,294✔
2389

2390
      // Loop over nuclide bins.
2391
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
1,850,588✔
2392
        auto i_nuclide = tally.nuclides_[i];
925,294✔
2393

2394
        double atom_density = 0.;
925,294✔
2395
        if (i_nuclide >= 0) {
925,294✔
2396
          auto j =
2397
            model::materials[p.material()]->mat_nuclide_index_[i_nuclide];
462,647✔
2398
          if (j == C_NONE)
462,647!
2399
            continue;
×
2400
          atom_density =
2401
            model::materials[p.material()]->atom_density(j, p.density_mult());
462,647✔
2402
        }
2403

2404
        score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
925,294✔
2405
          filter_weight, i_nuclide, atom_density, 1.0);
2406
      }
2407
    }
2408

2409
    // If the user has specified that we can assume all tallies are spatially
2410
    // separate, this implies that once a tally has been scored to, we needn't
2411
    // check the others. This cuts down on overhead when there are many
2412
    // tallies specified
2413
    if (settings::assume_separate)
925,294!
2414
      break;
×
2415
  }
2416

2417
  // Reset all the filter matches for the next tally event.
2418
  for (auto& match : p.filter_matches())
768,894✔
2419
    match.bins_present_ = false;
659,052✔
2420
}
109,842✔
2421

2422
void score_tracklength_tally_general(
142,083,973✔
2423
  Particle& p, double flux, const vector<int>& tallies)
2424
{
2425
  // Set 'none' value for log union grid index
2426
  int i_log_union = C_NONE;
142,083,973✔
2427

2428
  for (auto i_tally : tallies) {
382,437,779✔
2429
    const Tally& tally {*model::tallies[i_tally]};
240,357,632✔
2430

2431
    // Initialize an iterator over valid filter bin combinations.  If there are
2432
    // no valid combinations, use a continue statement to ensure we skip the
2433
    // assume_separate break below.
2434
    auto filter_iter = FilterBinIter(tally, p);
240,357,632✔
2435
    auto end = FilterBinIter(tally, true, &p.filter_matches());
240,357,632✔
2436
    if (filter_iter == end)
240,357,632✔
2437
      continue;
115,211,545✔
2438

2439
    // Loop over filter bins.
2440
    for (; filter_iter != end; ++filter_iter) {
283,137,856✔
2441
      auto filter_index = filter_iter.index_;
157,991,769✔
2442
      auto filter_weight = filter_iter.weight_;
157,991,769✔
2443

2444
      // Loop over nuclide bins.
2445
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
387,756,081✔
2446
        auto i_nuclide = tally.nuclides_[i];
229,764,312✔
2447

2448
        double atom_density = 0.;
229,764,312✔
2449
        if (i_nuclide >= 0) {
229,764,312✔
2450
          if (p.material() != MATERIAL_VOID) {
90,988,545✔
2451
            const auto& mat = model::materials[p.material()];
90,961,215✔
2452
            auto j = mat->mat_nuclide_index_[i_nuclide];
90,961,215✔
2453
            if (j == C_NONE) {
90,961,215✔
2454
              // Determine log union grid index
2455
              if (i_log_union == C_NONE) {
48,319,111✔
2456
                int neutron = static_cast<int>(ParticleType::neutron);
13,419,617✔
2457
                i_log_union = std::log(p.E() / data::energy_min[neutron]) /
13,419,617✔
2458
                              simulation::log_spacing;
2459
              }
2460

2461
              // Update micro xs cache
2462
              if (!tally.multiply_density()) {
48,319,111✔
2463
                p.update_neutron_xs(i_nuclide, i_log_union);
43,068,181✔
2464
                atom_density = 1.0;
43,068,181✔
2465
              }
2466
            } else {
2467
              atom_density = tally.multiply_density()
85,284,208✔
2468
                               ? mat->atom_density(j, p.density_mult())
42,642,104✔
2469
                               : 1.0;
2470
            }
2471
          }
2472
        }
2473

2474
        // TODO: consider replacing this "if" with pointers or templates
2475
        if (settings::run_CE) {
229,764,312✔
2476
          score_general_ce_nonanalog(p, i_tally, i * tally.scores_.size(),
225,613,981✔
2477
            filter_index, filter_weight, i_nuclide, atom_density, flux);
2478
        } else {
2479
          score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
4,150,331✔
2480
            filter_weight, i_nuclide, atom_density, flux);
2481
        }
2482
      }
2483
    }
2484

2485
    // If the user has specified that we can assume all tallies are spatially
2486
    // separate, this implies that once a tally has been scored to, we needn't
2487
    // check the others. This cuts down on overhead when there are many
2488
    // tallies specified
2489
    if (settings::assume_separate)
125,146,087✔
2490
      break;
3,826✔
2491
  }
2492

2493
  // Reset all the filter matches for the next tally event.
2494
  for (auto& match : p.filter_matches())
502,287,614✔
2495
    match.bins_present_ = false;
360,203,641✔
2496
}
142,083,973✔
2497

2498
void score_timed_tracklength_tally(Particle& p, double total_distance)
329,847✔
2499
{
2500
  double speed = p.speed();
329,847✔
2501
  double total_dt = total_distance / speed;
329,847✔
2502

2503
  // save particle last state
2504
  auto time_last = p.time_last();
329,847✔
2505
  auto r_last = p.r_last();
329,847✔
2506

2507
  // move particle back
2508
  p.move_distance(-total_distance);
329,847✔
2509
  p.time() -= total_dt;
329,847✔
2510
  p.lifetime() -= total_dt;
329,847✔
2511

2512
  double distance_traveled = 0.0;
329,847✔
2513
  while (distance_traveled < total_distance) {
1,037,760✔
2514

2515
    double distance = std::min(distance_to_time_boundary(p.time(), speed),
707,913✔
2516
      total_distance - distance_traveled);
707,913✔
2517
    double dt = distance / speed;
707,913✔
2518

2519
    // Save particle last state for tracklength tallies
2520
    p.time_last() = p.time();
707,913✔
2521
    p.r_last() = p.r();
707,913✔
2522

2523
    // Advance particle in space and time
2524
    p.move_distance(distance);
707,913✔
2525
    p.time() += dt;
707,913✔
2526
    p.lifetime() += dt;
707,913✔
2527

2528
    // Determine the tracklength estimate of the flux
2529
    double flux = p.wgt() * distance;
707,913✔
2530

2531
    score_tracklength_tally_general(
707,913✔
2532
      p, flux, model::active_timed_tracklength_tallies);
2533
    distance_traveled += distance;
707,913✔
2534
  }
2535

2536
  p.time_last() = time_last;
329,847✔
2537
  p.r_last() = r_last;
329,847✔
2538
}
329,847✔
2539

2540
void score_tracklength_tally(Particle& p, double distance)
141,376,060✔
2541
{
2542

2543
  // Determine the tracklength estimate of the flux
2544
  double flux = p.wgt() * distance;
141,376,060✔
2545

2546
  score_tracklength_tally_general(p, flux, model::active_tracklength_tallies);
141,376,060✔
2547
}
141,376,060✔
2548

2549
void score_collision_tally(Particle& p)
9,425,164✔
2550
{
2551
  // Determine the collision estimate of the flux
2552
  double flux = 0.0;
9,425,164✔
2553
  if (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) {
9,425,164✔
2554
    flux = p.wgt_last() / p.macro_xs().total;
6,532,615✔
2555
  }
2556

2557
  // Set 'none value for log union grid index
2558
  int i_log_union = C_NONE;
9,425,164✔
2559

2560
  for (auto i_tally : model::active_collision_tallies) {
20,651,195✔
2561
    const Tally& tally {*model::tallies[i_tally]};
11,226,031✔
2562

2563
    // Initialize an iterator over valid filter bin combinations.  If there are
2564
    // no valid combinations, use a continue statement to ensure we skip the
2565
    // assume_separate break below.
2566
    auto filter_iter = FilterBinIter(tally, p);
11,226,031✔
2567
    auto end = FilterBinIter(tally, true, &p.filter_matches());
11,226,031✔
2568
    if (filter_iter == end)
11,226,031✔
2569
      continue;
4,944,937✔
2570

2571
    // Loop over filter bins.
2572
    for (; filter_iter != end; ++filter_iter) {
19,035,081✔
2573
      auto filter_index = filter_iter.index_;
12,753,987✔
2574
      auto filter_weight = filter_iter.weight_;
12,753,987✔
2575

2576
      // Loop over nuclide bins.
2577
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
26,069,690✔
2578
        auto i_nuclide = tally.nuclides_[i];
13,315,703✔
2579

2580
        double atom_density = 0.;
13,315,703✔
2581
        if (i_nuclide >= 0) {
13,315,703✔
2582
          const auto& mat = model::materials[p.material()];
932,815✔
2583
          auto j = mat->mat_nuclide_index_[i_nuclide];
932,815✔
2584
          if (j == C_NONE) {
932,815✔
2585
            // Determine log union grid index
2586
            if (i_log_union == C_NONE) {
67,025✔
2587
              int neutron = static_cast<int>(ParticleType::neutron);
27,736✔
2588
              i_log_union = std::log(p.E() / data::energy_min[neutron]) /
27,736✔
2589
                            simulation::log_spacing;
2590
            }
2591

2592
            // Update micro xs cache
2593
            if (!tally.multiply_density()) {
67,025!
2594
              p.update_neutron_xs(i_nuclide, i_log_union);
×
2595
              atom_density = 1.0;
×
2596
            }
2597
          } else {
2598
            atom_density = tally.multiply_density()
1,731,580✔
2599
                             ? mat->atom_density(j, p.density_mult())
865,790!
2600
                             : 1.0;
2601
          }
2602
        }
2603

2604
        // TODO: consider replacing this "if" with pointers or templates
2605
        if (settings::run_CE) {
13,315,703✔
2606
          score_general_ce_nonanalog(p, i_tally, i * tally.scores_.size(),
12,876,335✔
2607
            filter_index, filter_weight, i_nuclide, atom_density, flux);
2608
        } else {
2609
          score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
439,368✔
2610
            filter_weight, i_nuclide, atom_density, flux);
2611
        }
2612
      }
2613
    }
2614

2615
    // If the user has specified that we can assume all tallies are spatially
2616
    // separate, this implies that once a tally has been scored to, we needn't
2617
    // check the others. This cuts down on overhead when there are many
2618
    // tallies specified
2619
    if (settings::assume_separate)
6,281,094!
2620
      break;
×
2621
  }
2622

2623
  // Reset all the filter matches for the next tally event.
2624
  for (auto& match : p.filter_matches())
25,045,281✔
2625
    match.bins_present_ = false;
15,620,117✔
2626
}
9,425,164✔
2627

2628
void score_surface_tally(Particle& p, const vector<int>& tallies)
13,284,493✔
2629
{
2630
  double current = p.wgt_last();
13,284,493✔
2631

2632
  for (auto i_tally : tallies) {
27,328,855✔
2633
    auto& tally {*model::tallies[i_tally]};
14,044,362✔
2634

2635
    // Initialize an iterator over valid filter bin combinations.  If there are
2636
    // no valid combinations, use a continue statement to ensure we skip the
2637
    // assume_separate break below.
2638
    auto filter_iter = FilterBinIter(tally, p);
14,044,362✔
2639
    auto end = FilterBinIter(tally, true, &p.filter_matches());
14,044,362✔
2640
    if (filter_iter == end)
14,044,362✔
2641
      continue;
10,635,747✔
2642

2643
    // Loop over filter bins.
2644
    for (; filter_iter != end; ++filter_iter) {
9,913,563✔
2645
      auto filter_index = filter_iter.index_;
6,504,948✔
2646
      auto filter_weight = filter_iter.weight_;
6,504,948✔
2647

2648
      // Loop over scores.
2649
      // There is only one score type for current tallies so there is no need
2650
      // for a further scoring function.
2651
      double score = current * filter_weight;
6,504,948✔
2652
      for (auto score_index = 0; score_index < tally.scores_.size();
13,009,896✔
2653
           ++score_index) {
2654
#pragma omp atomic
2655
        tally.results_(filter_index, score_index, TallyResult::VALUE) += score;
6,504,948✔
2656
      }
2657
    }
2658

2659
    // If the user has specified that we can assume all tallies are spatially
2660
    // separate, this implies that once a tally has been scored to, we needn't
2661
    // check the others. This cuts down on overhead when there are many
2662
    // tallies specified
2663
    if (settings::assume_separate)
3,408,615!
2664
      break;
×
2665
  }
2666

2667
  // Reset all the filter matches for the next tally event.
2668
  for (auto& match : p.filter_matches())
68,213,279✔
2669
    match.bins_present_ = false;
54,928,786✔
2670
}
13,284,493✔
2671

2672
void score_pulse_height_tally(Particle& p, const vector<int>& tallies)
500✔
2673
{
2674
  // The pulse height tally in OpenMC hijacks the logic of CellFilter and
2675
  // EnergyFilter to score specific quantities related to particle pulse height.
2676
  // This is achieved by setting the pulse-height cell of the tally to the cell
2677
  // of the particle being scored, and the energy to the particle's last
2678
  // recorded energy (E_last()). After the tally is scored, the values are reset
2679
  // to ensure proper accounting and avoid interference with subsequent
2680
  // calculations or tallies.
2681

2682
  // Save original cell/energy information
2683
  int orig_n_coord = p.n_coord();
500✔
2684
  int orig_cell = p.coord(0).cell();
500✔
2685
  double orig_E_last = p.E_last();
500✔
2686

2687
  for (auto i_tally : tallies) {
1,000✔
2688
    auto& tally {*model::tallies[i_tally]};
500✔
2689

2690
    // Determine all CellFilter in the tally
2691
    for (const auto& filter : tally.filters()) {
1,500✔
2692
      auto cell_filter =
2693
        dynamic_cast<CellFilter*>(model::tally_filters[filter].get());
1,000!
2694
      if (cell_filter != nullptr) {
1,000✔
2695

2696
        const auto& cells = cell_filter->cells();
500✔
2697
        // Loop over all cells in the CellFilter
2698
        for (auto cell_index = 0; cell_index < cells.size(); ++cell_index) {
1,000✔
2699
          int cell_id = cells[cell_index];
500✔
2700

2701
          // Temporarily change cell of particle
2702
          p.n_coord() = 1;
500✔
2703
          p.coord(0).cell() = cell_id;
500✔
2704

2705
          // Determine index of cell in model::pulse_height_cells
2706
          auto it = std::find(model::pulse_height_cells.begin(),
500✔
2707
            model::pulse_height_cells.end(), cell_id);
2708
          int index = std::distance(model::pulse_height_cells.begin(), it);
500✔
2709

2710
          // Temporarily change energy of particle to pulse-height value
2711
          p.E_last() = p.pht_storage()[index];
500✔
2712

2713
          // Initialize an iterator over valid filter bin combinations. If
2714
          // there are no valid combinations, use a continue statement to ensure
2715
          // we skip the assume_separate break below.
2716
          auto filter_iter = FilterBinIter(tally, p);
500✔
2717
          auto end = FilterBinIter(tally, true, &p.filter_matches());
500✔
2718
          if (filter_iter == end)
500!
2719
            continue;
×
2720

2721
          // Loop over filter bins.
2722
          for (; filter_iter != end; ++filter_iter) {
1,000✔
2723
            auto filter_index = filter_iter.index_;
500✔
2724
            auto filter_weight = filter_iter.weight_;
500✔
2725

2726
            // Loop over scores.
2727
            for (auto score_index = 0; score_index < tally.scores_.size();
1,000✔
2728
                 ++score_index) {
2729
#pragma omp atomic
2730
              tally.results_(filter_index, score_index, TallyResult::VALUE) +=
500✔
2731
                filter_weight;
2732
            }
2733
          }
2734

2735
          // Reset all the filter matches for the next tally event.
2736
          for (auto& match : p.filter_matches())
1,500✔
2737
            match.bins_present_ = false;
1,000✔
2738
        }
2739
      }
2740
    }
2741
    // Restore cell/energy
2742
    p.n_coord() = orig_n_coord;
500✔
2743
    p.coord(0).cell() = orig_cell;
500✔
2744
    p.E_last() = orig_E_last;
500✔
2745
  }
2746
}
500✔
2747

NEW
2748
void score_pseudoparticle_tally(Particle& p, double mfp, double pdf)
×
2749
{
NEW
2750
  double attenuation = std::exp(-mfp);
×
2751

2752
  // Save the attenuation for point filter handling
NEW
2753
  p.wgt_last() = p.wgt();
×
NEW
2754
  p.wgt() *= attenuation;
×
2755

NEW
2756
  double flux = p.wgt_last() * pdf;
×
2757

NEW
2758
  score_tracklength_tally_general(p, flux, model::active_point_tallies);
×
NEW
2759
}
×
2760

NEW
2761
void score_point_tally(
×
2762
  Particle& p, int i_nuclide, const Reaction& rx, int i_product, Direction* v_t)
2763
{
NEW
2764
  const auto& nuc {data::nuclides[i_nuclide]};
×
NEW
2765
  double awr = nuc->awr_;
×
NEW
2766
  double E_in = p.E();
×
NEW
2767
  double vel = std::sqrt(E_in);
×
NEW
2768
  Direction v_n = vel * p.u();
×
NEW
2769
  Direction v_cm = v_n / (awr + 1.0);
×
NEW
2770
  if (v_t != nullptr)
×
NEW
2771
    v_cm += awr / (awr + 1.0) * (*v_t);
×
NEW
2772
  Direction u_cm = (v_n - v_cm);
×
NEW
2773
  u_cm /= u_cm.norm();
×
2774
  double mfp;
2775

NEW
2776
  auto old_stream = p.stream();
×
NEW
2777
  p.stream() = STREAM_NEXT_EVENT;
×
2778

NEW
2779
  simulation::i_det = -1;
×
NEW
2780
  for (auto& det : model::active_point_detectors) {
×
NEW
2781
    ++simulation::i_det;
×
2782

NEW
2783
    auto u = (det - p.r());
×
NEW
2784
    double distance = u.norm();
×
NEW
2785
    u /= distance;
×
2786

2787
    double mu;
NEW
2788
    if (rx.scatter_in_cm_) {
×
NEW
2789
      mu = u.dot(u_cm);
×
2790
    } else {
NEW
2791
      mu = u.dot(p.u());
×
2792
    }
2793
    double E_out;
NEW
2794
    double pdf = rx.products_[i_product].sample_energy_and_pdf(
×
NEW
2795
      p.E(), mu, E_out, p.current_seed());
×
NEW
2796
    if (rx.scatter_in_cm_) {
×
NEW
2797
      double E_cm = E_out;
×
NEW
2798
      Direction v_out = std::sqrt(E_cm) * u_cm;
×
NEW
2799
      v_out += v_cm;
×
NEW
2800
      E_out = std::pow(v_out.norm(), 2);
×
NEW
2801
      pdf *= std::sqrt(E_out / E_cm) /
×
NEW
2802
             (1 - mu / (awr + 1) * std::sqrt(E_in / E_out));
×
2803
    }
NEW
2804
    simulation::pseudoparticle.initialize_pseudoparticle(p, u, E_out);
×
NEW
2805
    mfp = 0.0;
×
NEW
2806
    transport_pseudoparticle(simulation::pseudoparticle, distance, mfp);
×
NEW
2807
    if (!simulation::pseudoparticle.alive())
×
NEW
2808
      continue;
×
NEW
2809
    score_pseudoparticle_tally(simulation::pseudoparticle, mfp, pdf);
×
2810
  }
NEW
2811
  p.stream() = old_stream;
×
NEW
2812
}
×
2813

NEW
2814
void score_point_tally(Particle& p, int i_nuclide, const ThermalData& sab,
×
2815
  const NuclideMicroXS& micro)
2816
{
NEW
2817
  const auto& nuc {data::nuclides[i_nuclide]};
×
NEW
2818
  double awr = nuc->awr_;
×
NEW
2819
  double E_in = p.E();
×
NEW
2820
  double vel = std::sqrt(E_in);
×
NEW
2821
  Direction v_n = vel * p.u();
×
NEW
2822
  Direction v_cm = v_n / (awr + 1.0);
×
NEW
2823
  Direction u_cm = (v_n - v_cm);
×
NEW
2824
  u_cm /= u_cm.norm();
×
2825
  double mfp;
2826

NEW
2827
  auto old_stream = p.stream();
×
NEW
2828
  p.stream() = STREAM_NEXT_EVENT;
×
2829

NEW
2830
  simulation::i_det = -1;
×
NEW
2831
  for (auto& det : model::active_point_detectors) {
×
NEW
2832
    ++simulation::i_det;
×
2833

NEW
2834
    auto u = (det - p.r());
×
NEW
2835
    double distance = u.norm();
×
NEW
2836
    u /= distance;
×
2837

NEW
2838
    double mu = u.dot(p.u());
×
2839
    double E_out;
2840
    double pdf =
NEW
2841
      sab.sample_energy_and_pdf(micro, p.E(), mu, E_out, p.current_seed());
×
NEW
2842
    simulation::pseudoparticle.initialize_pseudoparticle(p, u, E_out);
×
NEW
2843
    mfp = 0.0;
×
NEW
2844
    transport_pseudoparticle(simulation::pseudoparticle, distance, mfp);
×
NEW
2845
    if (!simulation::pseudoparticle.alive())
×
NEW
2846
      continue;
×
NEW
2847
    score_pseudoparticle_tally(simulation::pseudoparticle, mfp, pdf);
×
2848
  }
NEW
2849
  p.stream() = old_stream;
×
NEW
2850
}
×
2851

NEW
2852
void score_point_tally(SourceSite& site, int source_index)
×
2853
{
NEW
2854
  double E_out = site.E;
×
NEW
2855
  Position r = site.r;
×
NEW
2856
  auto src_ = model::external_sources[source_index].get();
×
NEW
2857
  auto src = dynamic_cast<IndependentSource*>(src_);
×
NEW
2858
  if (!src)
×
NEW
2859
    fatal_error("Only independent source is valid for point detectors.");
×
NEW
2860
  auto angle = src->angle();
×
2861

2862
  double mfp;
2863

NEW
2864
  simulation::i_det = -1;
×
NEW
2865
  for (auto& det : model::active_point_detectors) {
×
NEW
2866
    ++simulation::i_det;
×
2867

NEW
2868
    auto u = (det - r);
×
NEW
2869
    double distance = u.norm();
×
NEW
2870
    u /= distance;
×
2871

NEW
2872
    double pdf = angle->evaluate(u);
×
NEW
2873
    simulation::pseudoparticle.from_source(&site);
×
NEW
2874
    simulation::pseudoparticle.u() = u;
×
NEW
2875
    mfp = 0.0;
×
NEW
2876
    transport_pseudoparticle(simulation::pseudoparticle, distance, mfp);
×
NEW
2877
    if (!simulation::pseudoparticle.alive())
×
NEW
2878
      continue;
×
NEW
2879
    score_pseudoparticle_tally(simulation::pseudoparticle, mfp, pdf);
×
2880
  }
NEW
2881
}
×
2882

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