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

openmc-dev / openmc / 19058781736

04 Nov 2025 05:26AM UTC coverage: 82.008% (-3.1%) from 85.155%
19058781736

Pull #3252

github

web-flow
Merge b8a72730f into bd76fc056
Pull Request #3252: Adding vtkhdf option to write vtk data

16714 of 23236 branches covered (71.93%)

Branch coverage included in aggregate %.

61 of 66 new or added lines in 1 file covered. (92.42%)

3175 existing lines in 103 files now uncovered.

54243 of 63288 relevant lines covered (85.71%)

43393337.77 hits per line

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

76.12
/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/string_utils.h"
17
#include "openmc/tallies/derivative.h"
18
#include "openmc/tallies/filter.h"
19
#include "openmc/tallies/filter_cell.h"
20
#include "openmc/tallies/filter_delayedgroup.h"
21
#include "openmc/tallies/filter_energy.h"
22

23
#include <string>
24

25
namespace openmc {
26

27
//==============================================================================
28
// FilterBinIter implementation
29
//==============================================================================
30

31
FilterBinIter::FilterBinIter(const Tally& tally, Particle& p)
2,147,483,647✔
32
  : filter_matches_ {p.filter_matches()}, tally_ {tally}
2,147,483,647✔
33
{
34
  // Find all valid bins in each relevant filter if they have not already been
35
  // found for this event.
36
  for (auto i_filt : tally_.filters()) {
2,147,483,647✔
37
    auto& match {filter_matches_[i_filt]};
2,147,483,647✔
38
    if (!match.bins_present_) {
2,147,483,647✔
39
      match.bins_.clear();
2,147,483,647✔
40
      match.weights_.clear();
2,147,483,647✔
41
      model::tally_filters[i_filt]->get_all_bins(p, tally_.estimator_, match);
2,147,483,647✔
42
      match.bins_present_ = true;
2,147,483,647✔
43
    }
44

45
    // If there are no valid bins for this filter, then there are no valid
46
    // filter bin combinations so all iterators are end iterators.
47
    if (match.bins_.size() == 0) {
2,147,483,647✔
48
      index_ = -1;
1,682,222,284✔
49
      return;
1,682,222,284✔
50
    }
51

52
    // Set the index of the bin used in the first filter combination
53
    match.i_bin_ = 0;
2,147,483,647✔
54
  }
55

56
  // Compute the initial index and weight.
57
  this->compute_index_weight();
1,675,765,894✔
58
}
59

60
FilterBinIter::FilterBinIter(
2,147,483,647✔
61
  const Tally& tally, bool end, vector<FilterMatch>* particle_filter_matches)
2,147,483,647✔
62
  : filter_matches_ {*particle_filter_matches}, tally_ {tally}
2,147,483,647✔
63
{
64
  // Handle the special case for an iterator that points to the end.
65
  if (end) {
2,147,483,647✔
66
    index_ = -1;
2,147,483,647✔
67
    return;
2,147,483,647✔
68
  }
69

70
  for (auto i_filt : tally_.filters()) {
54,764✔
71
    auto& match {filter_matches_[i_filt]};
35,701✔
72
    if (!match.bins_present_) {
35,701!
73
      match.bins_.clear();
35,701✔
74
      match.weights_.clear();
35,701✔
75
      for (auto i = 0; i < model::tally_filters[i_filt]->n_bins(); ++i) {
5,820,171✔
76
        match.bins_.push_back(i);
5,784,470✔
77
        match.weights_.push_back(1.0);
5,784,470✔
78
      }
79
      match.bins_present_ = true;
35,701✔
80
    }
81

82
    if (match.bins_.size() == 0) {
35,701!
83
      index_ = -1;
×
84
      return;
×
85
    }
86

87
    match.i_bin_ = 0;
35,701✔
88
  }
89

90
  // Compute the initial index and weight.
91
  this->compute_index_weight();
19,063✔
92
}
93

94
FilterBinIter& FilterBinIter::operator++()
2,147,483,647✔
95
{
96
  // Find the next valid combination of filter bins.  To do this, we search
97
  // backwards through the filters until we find the first filter whose bins
98
  // can be incremented.
99
  bool visited_all_combinations = true;
2,147,483,647✔
100
  for (int i = tally_.filters().size() - 1; i >= 0; --i) {
2,147,483,647✔
101
    auto i_filt = tally_.filters(i);
2,147,483,647✔
102
    auto& match {filter_matches_[i_filt]};
2,147,483,647✔
103
    if (match.i_bin_ < match.bins_.size() - 1) {
2,147,483,647✔
104
      // The bin for this filter can be incremented.  Increment it and do not
105
      // touch any of the remaining filters.
106
      ++match.i_bin_;
609,089,900✔
107
      visited_all_combinations = false;
609,089,900✔
108
      break;
609,089,900✔
109
    } else {
110
      // This bin cannot be incremented so reset it and continue to the next
111
      // filter.
112
      match.i_bin_ = 0;
2,147,483,647✔
113
    }
114
  }
115

116
  if (visited_all_combinations) {
2,147,483,647✔
117
    // We have visited every valid combination.  All done!
118
    index_ = -1;
1,675,784,957✔
119
  } else {
120
    // The loop found a new valid combination.  Compute the corresponding
121
    // index and weight.
122
    compute_index_weight();
609,089,900✔
123
  }
124

125
  return *this;
2,147,483,647✔
126
}
127

128
void FilterBinIter::compute_index_weight()
2,147,483,647✔
129
{
130
  index_ = 0;
2,147,483,647✔
131
  weight_ = 1.;
2,147,483,647✔
132
  for (auto i = 0; i < tally_.filters().size(); ++i) {
2,147,483,647✔
133
    auto i_filt = tally_.filters(i);
2,147,483,647✔
134
    auto& match {filter_matches_[i_filt]};
2,147,483,647✔
135
    auto i_bin = match.i_bin_;
2,147,483,647✔
136
    index_ += match.bins_[i_bin] * tally_.strides(i);
2,147,483,647✔
137
    weight_ *= match.weights_[i_bin];
2,147,483,647✔
138
  }
139
}
2,147,483,647✔
140

141
//==============================================================================
142
// Non-member functions
143
//==============================================================================
144

145
//! Helper function used to increment tallies with a delayed group filter.
146

147
void score_fission_delayed_dg(int i_tally, int d_bin, double score,
104,630,812✔
148
  int score_index, vector<FilterMatch>& filter_matches)
149
{
150
  // Save the original delayed group bin
151
  auto& tally {*model::tallies[i_tally]};
104,630,812✔
152
  auto i_filt = tally.filters(tally.delayedgroup_filter_);
104,630,812✔
153
  auto& dg_match {filter_matches[i_filt]};
104,630,812✔
154
  auto i_bin = dg_match.i_bin_;
104,630,812✔
155
  auto original_bin = dg_match.bins_[i_bin];
104,630,812✔
156
  dg_match.bins_[i_bin] = d_bin;
104,630,812✔
157

158
  // Determine the filter scoring index
159
  auto filter_index = 0;
104,630,812✔
160
  double filter_weight = 1.;
104,630,812✔
161
  for (auto i = 0; i < tally.filters().size(); ++i) {
320,105,423✔
162
    auto i_filt = tally.filters(i);
215,474,611✔
163
    auto& match {filter_matches[i_filt]};
215,474,611✔
164
    auto i_bin = match.i_bin_;
215,474,611✔
165
    filter_index += match.bins_[i_bin] * tally.strides(i);
215,474,611✔
166
    filter_weight *= match.weights_[i_bin];
215,474,611✔
167
  }
168

169
// Update the tally result
170
#pragma omp atomic
57,077,523✔
171
  tally.results_(filter_index, score_index, TallyResult::VALUE) +=
104,630,812✔
172
    score * filter_weight;
104,630,812✔
173

174
  // Reset the original delayed group bin
175
  dg_match.bins_[i_bin] = original_bin;
104,630,812✔
176
}
104,630,812✔
177

178
//! Helper function to retrieve fission q value from a nuclide
179

180
double get_nuc_fission_q(const Nuclide& nuc, const Particle& p, int score_bin)
14,824,128✔
181
{
182
  if (score_bin == SCORE_FISS_Q_PROMPT) {
14,824,128✔
183
    if (nuc.fission_q_prompt_) {
7,412,064✔
184
      return (*nuc.fission_q_prompt_)(p.E_last());
4,340,028✔
185
    }
186
  } else if (score_bin == SCORE_FISS_Q_RECOV) {
7,412,064!
187
    if (nuc.fission_q_recov_) {
7,412,064✔
188
      return (*nuc.fission_q_recov_)(p.E_last());
4,340,028✔
189
    }
190
  }
191
  return 0.0;
6,144,072✔
192
}
193

194
//! Helper function to score fission energy
195
//
196
//! Pulled out to support both the fission_q scores and energy deposition
197
//! score
198

199
double score_fission_q(const Particle& p, int score_bin, const Tally& tally,
5,536,784✔
200
  double flux, int i_nuclide, double atom_density)
201
{
202
  if (tally.estimator_ == TallyEstimator::ANALOG) {
5,536,784✔
203
    const Nuclide& nuc {*data::nuclides[p.event_nuclide()]};
623,744✔
204
    if (settings::survival_biasing) {
623,744!
205
      // No fission events occur if survival biasing is on -- need to
206
      // calculate fraction of absorptions that would have resulted in
207
      // fission scaled by the Q-value
208
      if (p.neutron_xs(p.event_nuclide()).total > 0) {
×
209
        return p.wgt_last() * get_nuc_fission_q(nuc, p, score_bin) *
×
210
               p.neutron_xs(p.event_nuclide()).fission * flux /
×
211
               p.neutron_xs(p.event_nuclide()).total;
×
212
      }
213
    } else {
214
      // Skip any non-absorption events
215
      if (p.event() == TallyEvent::SCATTER)
623,744✔
216
        return 0.0;
538,736✔
217
      // All fission events will contribute, so again we can use particle's
218
      // weight entering the collision as the estimate for the fission
219
      // reaction rate
220
      if (p.neutron_xs(p.event_nuclide()).absorption > 0) {
85,008!
221
        return p.wgt_last() * get_nuc_fission_q(nuc, p, score_bin) *
85,008✔
222
               p.neutron_xs(p.event_nuclide()).fission * flux /
85,008✔
223
               p.neutron_xs(p.event_nuclide()).absorption;
85,008✔
224
      }
225
    }
226
  } else {
227
    if (i_nuclide >= 0) {
4,913,040✔
228
      const Nuclide& nuc {*data::nuclides[i_nuclide]};
2,456,520✔
229
      return get_nuc_fission_q(nuc, p, score_bin) * atom_density * flux *
2,456,520✔
230
             p.neutron_xs(i_nuclide).fission;
2,456,520✔
231
    } else if (p.material() != MATERIAL_VOID) {
2,456,520!
232
      const Material& material {*model::materials[p.material()]};
2,456,520✔
233
      double score {0.0};
2,456,520✔
234
      for (auto i = 0; i < material.nuclide_.size(); ++i) {
14,739,120✔
235
        auto j_nuclide = material.nuclide_[i];
12,282,600✔
236
        auto atom_density = material.atom_density(i, p.density_mult());
12,282,600✔
237
        const Nuclide& nuc {*data::nuclides[j_nuclide]};
12,282,600✔
238
        score += get_nuc_fission_q(nuc, p, score_bin) * atom_density *
12,282,600✔
239
                 p.neutron_xs(j_nuclide).fission;
12,282,600✔
240
      }
241
      return score * flux;
2,456,520✔
242
    }
243
  }
244
  return 0.0;
×
245
}
246

247
//! Helper function to obtain the kerma coefficient for a given nuclide
248

249
double get_nuclide_neutron_heating(
27,249,387✔
250
  const Particle& p, const Nuclide& nuc, int rxn_index, int i_nuclide)
251
{
252
  size_t mt = nuc.reaction_index_[rxn_index];
27,249,387✔
253
  if (mt == C_NONE)
27,249,387!
254
    return 0.0;
×
255

256
  const auto& micro = p.neutron_xs(i_nuclide);
27,249,387✔
257
  auto i_temp = micro.index_temp;
27,249,387✔
258
  if (i_temp < 0)
27,249,387!
259
    return 0.0; // Can be true due to multipole
×
260

261
  // Determine total kerma
262
  const auto& rx {*nuc.reactions_[mt]};
27,249,387✔
263
  double kerma = rx.xs(micro);
27,249,387✔
264
  if (kerma == 0.0)
27,249,387!
265
    return 0.0;
×
266

267
  if (settings::run_mode == RunMode::EIGENVALUE) {
27,249,387✔
268
    // Determine kerma for fission as (EFR + EB)*sigma_f
269
    double kerma_fission =
270
      nuc.fragments_
271
        ? ((*nuc.fragments_)(p.E_last()) + (*nuc.betas_)(p.E_last())) *
26,127,618✔
272
            p.neutron_xs(i_nuclide).fission
3,720,431✔
273
        : 0.0;
26,127,618✔
274

275
    // Determine non-fission kerma as difference
276
    double kerma_non_fission = kerma - kerma_fission;
26,127,618✔
277

278
    // Re-weight non-fission kerma by keff to properly balance energy release
279
    // and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H. Stedry,
280
    // "Self-consistent energy normalization for quasistatic reactor
281
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
282
    kerma = simulation::keff * kerma_non_fission + kerma_fission;
26,127,618✔
283
  }
284
  return kerma;
27,249,387✔
285
}
286

287
//! Helper function to obtain neutron heating [eV]
288

289
double score_neutron_heating(const Particle& p, const Tally& tally, double flux,
10,962,369✔
290
  int rxn_bin, int i_nuclide, double atom_density)
291
{
292
  // Get heating macroscopic "cross section"
293
  double heating_xs;
294
  if (i_nuclide >= 0) {
10,962,369✔
295
    const Nuclide& nuc {*data::nuclides[i_nuclide]};
5,166,304✔
296
    heating_xs = get_nuclide_neutron_heating(p, nuc, rxn_bin, i_nuclide);
5,166,304✔
297
    if (tally.estimator_ == TallyEstimator::ANALOG) {
5,166,304✔
298
      heating_xs /= p.neutron_xs(i_nuclide).total;
474,232✔
299
    } else {
300
      heating_xs *= atom_density;
4,692,072✔
301
    }
302
  } else {
303
    if (p.material() != MATERIAL_VOID) {
5,796,065!
304
      heating_xs = 0.0;
5,796,065✔
305
      const Material& material {*model::materials[p.material()]};
5,796,065✔
306
      for (auto i = 0; i < material.nuclide_.size(); ++i) {
27,879,148✔
307
        int j_nuclide = material.nuclide_[i];
22,083,083✔
308
        double atom_density {material.atom_density_(i)};
22,083,083✔
309
        const Nuclide& nuc {*data::nuclides[j_nuclide]};
22,083,083✔
310
        heating_xs += atom_density *
22,083,083✔
311
                      get_nuclide_neutron_heating(p, nuc, rxn_bin, j_nuclide);
22,083,083✔
312
      }
313
      if (tally.estimator_ == TallyEstimator::ANALOG) {
5,796,065✔
314
        heating_xs /= p.macro_xs().total;
474,232✔
315
      }
316
    }
317
  }
318
  double score = heating_xs * flux;
10,962,369✔
319
  if (tally.estimator_ == TallyEstimator::ANALOG) {
10,962,369✔
320
    // All events score to a heating tally bin. We actually use a
321
    // collision estimator in place of an analog one since there is no
322
    // reaction-wise heating cross section
323
    score *= p.wgt_last();
948,464✔
324
  }
325
  return score;
10,962,369✔
326
}
327

328
//! Helper function to obtain reaction Q value for photons and charged particles
329
double get_reaction_q_value(const Particle& p)
21,328,454✔
330
{
331
  if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) {
21,328,454✔
332
    // pair production
333
    return -2 * MASS_ELECTRON_EV;
82,688✔
334
  } else if (p.type() == ParticleType::positron) {
21,245,766✔
335
    // positron annihilation
336
    return 2 * MASS_ELECTRON_EV;
9,801✔
337
  } else {
338
    return 0.0;
21,235,965✔
339
  }
340
}
341

342
//! Helper function to obtain particle heating [eV]
343

344
double score_particle_heating(const Particle& p, const Tally& tally,
33,394,552✔
345
  double flux, int rxn_bin, int i_nuclide, double atom_density)
346
{
347
  if (p.type() == ParticleType::neutron)
33,394,552✔
348
    return score_neutron_heating(
10,962,369✔
349
      p, tally, flux, rxn_bin, i_nuclide, atom_density);
10,962,369✔
350
  if (i_nuclide == -1 || i_nuclide == p.event_nuclide() ||
24,601,394✔
351
      p.event_nuclide() == -1) {
2,169,211✔
352
    // For pair production and positron annihilation, we need to account for the
353
    // reaction Q value
354
    double Q = get_reaction_q_value(p);
21,328,454✔
355

356
    // Get the pre-collision energy of the particle.
357
    auto E = p.E_last();
21,328,454✔
358

359
    // The energy deposited is the sum of the incident energy and the reaction
360
    // Q-value less the energy of any outgoing particles
361
    double score = E + Q - p.E() - p.bank_second_E();
21,328,454✔
362

363
    score *= p.wgt_last();
21,328,454✔
364

365
    // if no event_nuclide (charged particle) scale energy deposition by
366
    // fractional charge density
367
    if (i_nuclide != -1 && p.event_nuclide() == -1) {
21,328,454✔
368
      const auto& mat {model::materials[p.material()]};
1,065,482✔
369
      int z = data::nuclides[i_nuclide]->Z_;
1,065,482✔
370
      auto i = mat->mat_nuclide_index_[i_nuclide];
1,065,482✔
371
      score *= (z * mat->atom_density_[i] / mat->charge_density());
1,065,482✔
372
    }
373
    return score;
21,328,454✔
374
  }
375
  return 0.0;
1,103,729✔
376
}
377

378
//! Helper function for nu-fission tallies with energyout filters.
379
//
380
//! In this case, we may need to score to multiple bins if there were multiple
381
//! neutrons produced with different energies.
382

383
void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin)
1,113,675✔
384
{
385
  auto& tally {*model::tallies[i_tally]};
1,113,675✔
386
  auto i_eout_filt = tally.filters()[tally.energyout_filter_];
1,113,675✔
387
  auto i_bin = p.filter_matches(i_eout_filt).i_bin_;
1,113,675✔
388
  auto bin_energyout = p.filter_matches(i_eout_filt).bins_[i_bin];
1,113,675✔
389

390
  const EnergyoutFilter& eo_filt {
391
    *dynamic_cast<EnergyoutFilter*>(model::tally_filters[i_eout_filt].get())};
1,113,675!
392

393
  // Note that the score below is weighted by keff. Since the creation of
394
  // fission sites is weighted such that it is expected to create n_particles
395
  // sites, we need to multiply the score by keff to get the true nu-fission
396
  // rate. Otherwise, the sum of all nu-fission rates would be ~1.0.
397

398
  // loop over number of particles banked
399
  for (auto i = 0; i < p.n_bank(); ++i) {
2,546,440✔
400
    const auto& bank = p.nu_bank(i);
1,432,765✔
401

402
    // get the delayed group
403
    auto g = bank.delayed_group;
1,432,765✔
404

405
    // determine score based on bank site weight and keff
406
    double score = simulation::keff * bank.wgt;
1,432,765✔
407

408
    // Add derivative information for differential tallies.  Note that the
409
    // i_nuclide and atom_density arguments do not matter since this is an
410
    // analog estimator.
411
    if (tally.deriv_ != C_NONE)
1,432,765✔
412
      apply_derivative_to_score(p, i_tally, 0, 0., SCORE_NU_FISSION, score);
23,628✔
413

414
    if (!settings::run_CE && eo_filt.matches_transport_groups()) {
1,432,765✔
415

416
      // determine outgoing energy group from fission bank
417
      auto g_out = static_cast<int>(bank.E);
109,582✔
418

419
      // modify the value so that g_out = 0 corresponds to the highest energy
420
      // bin
421
      g_out = eo_filt.n_bins() - g_out - 1;
109,582✔
422

423
      // change outgoing energy bin
424
      p.filter_matches(i_eout_filt).bins_[i_bin] = g_out;
109,582✔
425

426
    } else {
427

428
      double E_out;
429
      if (settings::run_CE) {
1,323,183✔
430
        E_out = bank.E;
1,213,601✔
431
      } else {
432
        E_out = data::mg.energy_bin_avg_[static_cast<int>(bank.E)];
109,582✔
433
      }
434

435
      // Set EnergyoutFilter bin index
436
      if (E_out < eo_filt.bins().front() || E_out > eo_filt.bins().back()) {
1,323,183!
437
        continue;
44✔
438
      } else {
439
        auto i_match = lower_bound_index(
2,646,278✔
440
          eo_filt.bins().begin(), eo_filt.bins().end(), E_out);
2,646,278✔
441
        p.filter_matches(i_eout_filt).bins_[i_bin] = i_match;
1,323,139✔
442
      }
443
    }
444

445
    // Case for tallying prompt neutrons
446
    if (score_bin == SCORE_NU_FISSION ||
1,432,721✔
447
        (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) {
171,160✔
448

449
      // Find the filter scoring index for this filter combination
450
      int filter_index = 0;
1,290,315✔
451
      double filter_weight = 1.0;
1,290,315✔
452
      for (auto j = 0; j < tally.filters().size(); ++j) {
4,705,068✔
453
        auto i_filt = tally.filters(j);
3,414,753✔
454
        auto& match {p.filter_matches(i_filt)};
3,414,753✔
455
        auto i_bin = match.i_bin_;
3,414,753✔
456
        filter_index += match.bins_[i_bin] * tally.strides(j);
3,414,753✔
457
        filter_weight *= match.weights_[i_bin];
3,414,753✔
458
      }
459

460
// Update tally results
461
#pragma omp atomic
718,346✔
462
      tally.results_(filter_index, i_score, TallyResult::VALUE) +=
1,290,315✔
463
        score * filter_weight;
1,290,315✔
464

465
    } else if (score_bin == SCORE_DELAYED_NU_FISSION && g != 0) {
1,432,721✔
466

467
      // If the delayed group filter is present, tally to corresponding delayed
468
      // group bin if it exists
469
      if (tally.delayedgroup_filter_ >= 0) {
858!
470

471
        // Get the index of the delayed group filter
472
        auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
858✔
473

474
        const DelayedGroupFilter& dg_filt {*dynamic_cast<DelayedGroupFilter*>(
858!
475
          model::tally_filters[i_dg_filt].get())};
858✔
476

477
        // Loop over delayed group bins until the corresponding bin is found
478
        for (auto d_bin = 0; d_bin < dg_filt.n_bins(); ++d_bin) {
6,006✔
479
          if (dg_filt.groups()[d_bin] == g) {
5,148✔
480
            // Find the filter index and weight for this filter combination
481
            double filter_weight = 1.;
858✔
482
            for (auto j = 0; j < tally.filters().size(); ++j) {
3,861✔
483
              auto i_filt = tally.filters(j);
3,003✔
484
              auto& match {p.filter_matches(i_filt)};
3,003✔
485
              auto i_bin = match.i_bin_;
3,003✔
486
              filter_weight *= match.weights_[i_bin];
3,003✔
487
            }
488

489
            score_fission_delayed_dg(i_tally, d_bin, score * filter_weight,
858✔
490
              i_score, p.filter_matches());
491
          }
492
        }
493

494
        // If the delayed group filter is not present, add score to tally
495
      } else {
496

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

508
// Update tally results
509
#pragma omp atomic
×
510
        tally.results_(filter_index, i_score, TallyResult::VALUE) +=
×
511
          score * filter_weight;
×
512
      }
513
    }
514
  }
515

516
  // Reset outgoing energy bin and score index
517
  p.filter_matches(i_eout_filt).bins_[i_bin] = bin_energyout;
1,113,675✔
518
}
1,113,675✔
519

520
double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin)
730,530,811✔
521
{
522
  const auto& nuc {*data::nuclides[i_nuclide]};
730,530,811✔
523

524
  // Get reaction object, or return 0 if reaction is not present
525
  auto m = nuc.reaction_index_[score_bin];
730,530,811✔
526
  if (m == C_NONE)
730,530,811✔
527
    return 0.0;
146,799,193✔
528
  const auto& rx {*nuc.reactions_[m]};
583,731,618✔
529
  const auto& micro {p.neutron_xs(i_nuclide)};
583,731,618✔
530

531
  // In the URR, the (n,gamma) cross section is sampled randomly from
532
  // probability tables. Make sure we use the sampled value (which is equal to
533
  // absorption - fission) rather than the dilute average value
534
  if (micro.use_ptable && score_bin == N_GAMMA) {
583,731,618✔
535
    return micro.absorption - micro.fission;
4,241,657✔
536
  }
537

538
  auto i_temp = micro.index_temp;
579,489,961✔
539
  if (i_temp >= 0) { // Can be false due to multipole
579,489,961✔
540
    // Get index on energy grid and interpolation factor
541
    auto i_grid = micro.index_grid;
550,974,516✔
542
    auto f = micro.interp_factor;
550,974,516✔
543

544
    // Calculate interpolated cross section
545
    double xs = rx.xs(micro);
550,974,516✔
546

547
    if (settings::run_mode == RunMode::EIGENVALUE &&
550,974,516!
548
        score_bin == HEATING_LOCAL) {
549
      // Determine kerma for fission as (EFR + EGP + EGD + EB)*sigma_f
550
      double kerma_fission =
551
        nuc.fragments_
552
          ? ((*nuc.fragments_)(p.E_last()) + (*nuc.betas_)(p.E_last()) +
×
553
              (*nuc.prompt_photons_)(p.E_last()) +
×
554
              (*nuc.delayed_photons_)(p.E_last())) *
×
555
              micro.fission
×
556
          : 0.0;
×
557

558
      // Determine non-fission kerma as difference
559
      double kerma_non_fission = xs - kerma_fission;
×
560

561
      // Re-weight non-fission kerma by keff to properly balance energy release
562
      // and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
563
      // Stedry, "Self-consistent energy normalization for quasistatic reactor
564
      // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
565
      xs = simulation::keff * kerma_non_fission + kerma_fission;
×
566
    }
567
    return xs;
550,974,516✔
568
  } else {
569
    // For multipole, calculate (n,gamma) from other reactions
570
    return rx.mt_ == N_GAMMA ? micro.absorption - micro.fission : 0.0;
28,515,445✔
571
  }
572
  return 0.0;
573
}
574

575
//! Update tally results for continuous-energy tallies with a tracklength or
576
//! collision estimator.
577

578
void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
2,147,483,647✔
579
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
580
  double flux)
581
{
582
  Tally& tally {*model::tallies[i_tally]};
2,147,483,647✔
583

584
  // Get the pre-collision energy of the particle.
585
  auto E = p.E_last();
2,147,483,647✔
586

587
  using Type = ParticleType;
588

589
  for (auto i = 0; i < tally.scores_.size(); ++i) {
2,147,483,647✔
590
    auto score_bin = tally.scores_[i];
2,147,483,647✔
591
    auto score_index = start_index + i;
2,147,483,647✔
592
    double score = 0.0;
2,147,483,647✔
593

594
    switch (score_bin) {
2,147,483,647✔
595
    case SCORE_FLUX:
1,061,785,897✔
596
      score = flux;
1,061,785,897✔
597
      break;
1,061,785,897✔
598

599
    case SCORE_TOTAL:
351,342,356✔
600
      if (i_nuclide >= 0) {
351,342,356✔
601
        if (p.type() == Type::neutron) {
139,590,667✔
602
          score = p.neutron_xs(i_nuclide).total * atom_density * flux;
139,022,825✔
603
        } else if (p.type() == Type::photon) {
567,842✔
604
          score = p.photon_xs(i_nuclide).total * atom_density * flux;
318,802✔
605
        }
606
      } else {
607
        score = p.macro_xs().total * flux;
211,751,689✔
608
      }
609
      break;
351,342,356✔
610

611
    case SCORE_INVERSE_VELOCITY:
22,489,159✔
612
      if (p.type() != Type::neutron)
22,489,159!
613
        continue;
568,567,879✔
614

615
      // Score inverse velocity in units of s/cm.
616
      score = flux / p.speed();
22,489,159✔
617
      break;
22,489,159✔
618

619
    case SCORE_SCATTER:
264,624,961✔
620
      if (p.type() != Type::neutron && p.type() != Type::photon)
264,624,961!
621
        continue;
×
622

623
      if (i_nuclide >= 0) {
264,624,961✔
624
        if (p.type() == Type::neutron) {
79,427,870!
625
          const auto& micro = p.neutron_xs(i_nuclide);
79,427,870✔
626
          score = (micro.total - micro.absorption) * atom_density * flux;
79,427,870✔
627
        } else {
628
          const auto& micro = p.photon_xs(i_nuclide);
×
629
          score = (micro.coherent + micro.incoherent) * atom_density * flux;
×
630
        }
631
      } else {
632
        if (p.type() == Type::neutron) {
185,197,091!
633
          score = (p.macro_xs().total - p.macro_xs().absorption) * flux;
185,197,091✔
634
        } else {
635
          score = (p.macro_xs().coherent + p.macro_xs().incoherent) * flux;
×
636
        }
637
      }
638
      break;
264,624,961✔
639

640
    case SCORE_ABSORPTION:
84,236,360✔
641
      if (p.type() != Type::neutron && p.type() != Type::photon)
84,236,360!
642
        continue;
×
643

644
      if (i_nuclide >= 0) {
84,236,360✔
645
        if (p.type() == Type::neutron) {
23,993,233!
646
          score = p.neutron_xs(i_nuclide).absorption * atom_density * flux;
23,993,233✔
647
        } else {
648
          const auto& xs = p.photon_xs(i_nuclide);
×
649
          score =
×
650
            (xs.total - xs.coherent - xs.incoherent) * atom_density * flux;
×
651
        }
652
      } else {
653
        if (p.type() == Type::neutron) {
60,243,127!
654
          score = p.macro_xs().absorption * flux;
60,243,127✔
655
        } else {
656
          score =
×
657
            (p.macro_xs().photoelectric + p.macro_xs().pair_production) * flux;
×
658
        }
659
      }
660
      break;
84,236,360✔
661

662
    case SCORE_FISSION:
829,396,319✔
663
      if (p.macro_xs().fission == 0)
829,396,319✔
664
        continue;
328,601,159✔
665

666
      if (i_nuclide >= 0) {
500,795,160✔
667
        score = p.neutron_xs(i_nuclide).fission * atom_density * flux;
317,649,219✔
668
      } else {
669
        score = p.macro_xs().fission * flux;
183,145,941✔
670
      }
671
      break;
500,795,160✔
672

673
    case SCORE_NU_FISSION:
130,834,620✔
674
      if (p.macro_xs().fission == 0)
130,834,620✔
675
        continue;
87,879,884✔
676

677
      if (i_nuclide >= 0) {
42,954,736✔
678
        score = p.neutron_xs(i_nuclide).nu_fission * atom_density * flux;
27,771,920✔
679
      } else {
680
        score = p.macro_xs().nu_fission * flux;
15,182,816✔
681
      }
682
      break;
42,954,736✔
683

684
    case SCORE_PROMPT_NU_FISSION:
22,489,159✔
685
      if (p.macro_xs().fission == 0)
22,489,159✔
686
        continue;
18,306,057✔
687
      if (i_nuclide >= 0) {
4,183,102✔
688
        score = p.neutron_xs(i_nuclide).fission *
1,865,820✔
689
                data::nuclides[i_nuclide]->nu(
1,865,820✔
690
                  E, ReactionProduct::EmissionMode::prompt) *
1,865,820✔
691
                atom_density * flux;
1,865,820✔
692
      } else {
693
        score = 0.;
2,317,282✔
694
        // Add up contributions from each nuclide in the material.
695
        if (p.material() != MATERIAL_VOID) {
2,317,282!
696
          const Material& material {*model::materials[p.material()]};
2,317,282✔
697
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
12,149,621✔
698
            auto j_nuclide = material.nuclide_[i];
9,832,339✔
699
            auto atom_density = material.atom_density(i, p.density_mult());
9,832,339✔
700
            score += p.neutron_xs(j_nuclide).fission *
9,832,339✔
701
                     data::nuclides[j_nuclide]->nu(
9,832,339✔
702
                       E, ReactionProduct::EmissionMode::prompt) *
9,832,339✔
703
                     atom_density * flux;
9,832,339✔
704
          }
705
        }
706
      }
707
      break;
4,183,102✔
708

709
    case SCORE_DELAYED_NU_FISSION:
33,826,925✔
710
      if (p.macro_xs().fission == 0)
33,826,925✔
711
        continue;
27,015,890✔
712
      if (i_nuclide >= 0) {
6,811,035✔
713
        if (tally.delayedgroup_filter_ != C_NONE) {
2,208,250✔
714
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
979,990✔
715
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
979,990!
716
            model::tally_filters[i_dg_filt].get())};
979,990✔
717
          // Tally each delayed group bin individually
718
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
6,859,930✔
719
            auto d = filt.groups()[d_bin];
5,879,940✔
720
            auto yield = data::nuclides[i_nuclide]->nu(
5,879,940✔
721
              E, ReactionProduct::EmissionMode::delayed, d);
722
            score =
5,879,940✔
723
              p.neutron_xs(i_nuclide).fission * yield * atom_density * flux;
5,879,940✔
724
            score_fission_delayed_dg(
5,879,940✔
725
              i_tally, d_bin, score, score_index, p.filter_matches());
726
          }
727
          continue;
979,990✔
728
        } else {
979,990✔
729
          // If the delayed group filter is not present, compute the score
730
          // by multiplying the delayed-nu-fission macro xs by the flux
731
          score = p.neutron_xs(i_nuclide).fission *
1,228,260✔
732
                  data::nuclides[i_nuclide]->nu(
1,228,260✔
733
                    E, ReactionProduct::EmissionMode::delayed) *
1,228,260✔
734
                  atom_density * flux;
1,228,260✔
735
        }
736
      } else {
737
        // Need to add up contributions for each nuclide
738
        if (tally.delayedgroup_filter_ != C_NONE) {
4,602,785✔
739
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
3,374,525✔
740
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
3,374,525!
741
            model::tally_filters[i_dg_filt].get())};
3,374,525✔
742
          if (p.material() != MATERIAL_VOID) {
3,374,525!
743
            const Material& material {*model::materials[p.material()]};
3,374,525✔
744
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
15,367,473✔
745
              auto j_nuclide = material.nuclide_[i];
11,992,948✔
746
              auto atom_density = material.atom_density(i, p.density_mult());
11,992,948✔
747
              // Tally each delayed group bin individually
748
              for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
83,950,636✔
749
                auto d = filt.groups()[d_bin];
71,957,688✔
750
                auto yield = data::nuclides[j_nuclide]->nu(
71,957,688✔
751
                  E, ReactionProduct::EmissionMode::delayed, d);
752
                score =
71,957,688✔
753
                  p.neutron_xs(j_nuclide).fission * yield * atom_density * flux;
71,957,688✔
754
                score_fission_delayed_dg(
71,957,688✔
755
                  i_tally, d_bin, score, score_index, p.filter_matches());
756
              }
757
            }
758
          }
759
          continue;
3,374,525✔
760
        } else {
3,374,525✔
761
          score = 0.;
1,228,260✔
762
          if (p.material() != MATERIAL_VOID) {
1,228,260!
763
            const Material& material {*model::materials[p.material()]};
1,228,260✔
764
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
7,369,560✔
765
              auto j_nuclide = material.nuclide_[i];
6,141,300✔
766
              auto atom_density = material.atom_density(i, p.density_mult());
6,141,300✔
767
              score += p.neutron_xs(j_nuclide).fission *
6,141,300✔
768
                       data::nuclides[j_nuclide]->nu(
6,141,300✔
769
                         E, ReactionProduct::EmissionMode::delayed) *
6,141,300✔
770
                       atom_density * flux;
6,141,300✔
771
            }
772
          }
773
        }
774
      }
775
      break;
2,456,520✔
776

777
    case SCORE_DECAY_RATE:
27,315,277✔
778
      if (p.macro_xs().fission == 0)
27,315,277✔
779
        continue;
22,427,262✔
780
      if (i_nuclide >= 0) {
4,888,015✔
781
        const auto& nuc {*data::nuclides[i_nuclide]};
2,208,250✔
782
        if (!nuc.fissionable_)
2,208,250✔
783
          continue;
1,104,125✔
784
        const auto& rxn {*nuc.fission_rx_[0]};
1,104,125✔
785
        if (tally.delayedgroup_filter_ != C_NONE) {
1,104,125✔
786
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
489,995✔
787
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
489,995!
788
            model::tally_filters[i_dg_filt].get())};
489,995✔
789
          // Tally each delayed group bin individually
790
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
3,429,965✔
791
            auto d = filt.groups()[d_bin];
2,939,970✔
792
            auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
2,939,970✔
793
            auto rate = rxn.products_[d].decay_rate_;
2,939,970✔
794
            score = p.neutron_xs(i_nuclide).fission * yield * flux *
2,939,970✔
795
                    atom_density * rate;
2,939,970✔
796
            score_fission_delayed_dg(
2,939,970✔
797
              i_tally, d_bin, score, score_index, p.filter_matches());
798
          }
799
          continue;
489,995✔
800
        } else {
489,995✔
801
          score = 0.;
614,130✔
802
          // We need to be careful not to overshoot the number of
803
          // delayed groups since this could cause the range of the
804
          // rxn.products_ array to be exceeded. Hence, we use the size
805
          // of this array and not the MAX_DELAYED_GROUPS constant for
806
          // this loop.
807
          for (auto d = 1; d < rxn.products_.size(); ++d) {
4,913,040✔
808
            const auto& product = rxn.products_[d];
4,298,910✔
809
            if (product.particle_ != Type::neutron)
4,298,910✔
810
              continue;
614,130✔
811

812
            auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
3,684,780✔
813
            auto rate = product.decay_rate_;
3,684,780✔
814
            score += p.neutron_xs(i_nuclide).fission * flux * yield *
3,684,780✔
815
                     atom_density * rate;
3,684,780✔
816
          }
817
        }
818
      } else {
819
        if (tally.delayedgroup_filter_ != C_NONE) {
2,679,765✔
820
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
1,451,505✔
821
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
1,451,505!
822
            model::tally_filters[i_dg_filt].get())};
1,451,505✔
823
          if (p.material() != MATERIAL_VOID) {
1,451,505!
824
            const Material& material {*model::materials[p.material()]};
1,451,505✔
825
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
7,082,471✔
826
              auto j_nuclide = material.nuclide_[i];
5,630,966✔
827
              auto atom_density = material.atom_density(i, p.density_mult());
5,630,966✔
828
              const auto& nuc {*data::nuclides[j_nuclide]};
5,630,966✔
829
              if (nuc.fissionable_) {
5,630,966✔
830
                const auto& rxn {*nuc.fission_rx_[0]};
3,911,149✔
831
                // Tally each delayed group bin individually
832
                for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
27,378,043✔
833
                  auto d = filt.groups()[d_bin];
23,466,894✔
834
                  auto yield =
835
                    nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
23,466,894✔
836
                  auto rate = rxn.products_[d].decay_rate_;
23,466,894✔
837
                  score = p.neutron_xs(j_nuclide).fission * yield * flux *
23,466,894✔
838
                          atom_density * rate;
23,466,894✔
839
                  score_fission_delayed_dg(
23,466,894✔
840
                    i_tally, d_bin, score, score_index, p.filter_matches());
841
                }
842
              }
843
            }
844
          }
845
          continue;
1,451,505✔
846
        } else {
1,451,505✔
847
          score = 0.;
1,228,260✔
848
          if (p.material() != MATERIAL_VOID) {
1,228,260!
849
            const Material& material {*model::materials[p.material()]};
1,228,260✔
850
            for (auto i = 0; i < material.nuclide_.size(); ++i) {
7,369,560✔
851
              auto j_nuclide = material.nuclide_[i];
6,141,300✔
852
              auto atom_density = material.atom_density(i, p.density_mult());
6,141,300✔
853
              const auto& nuc {*data::nuclides[j_nuclide]};
6,141,300✔
854
              if (nuc.fissionable_) {
6,141,300✔
855
                const auto& rxn {*nuc.fission_rx_[0]};
3,684,780✔
856
                // We need to be careful not to overshoot the number of
857
                // delayed groups since this could cause the range of the
858
                // rxn.products_ array to be exceeded. Hence, we use the size
859
                // of this array and not the MAX_DELAYED_GROUPS constant for
860
                // this loop.
861
                for (auto d = 1; d < rxn.products_.size(); ++d) {
28,249,980✔
862
                  const auto& product = rxn.products_[d];
24,565,200✔
863
                  if (product.particle_ != Type::neutron)
24,565,200✔
864
                    continue;
2,456,520✔
865

866
                  auto yield =
867
                    nuc.nu(E, ReactionProduct::EmissionMode::delayed, d);
22,108,680✔
868
                  auto rate = product.decay_rate_;
22,108,680✔
869
                  score += p.neutron_xs(j_nuclide).fission * yield *
22,108,680✔
870
                           atom_density * flux * rate;
22,108,680✔
871
                }
872
              }
873
            }
874
          }
875
        }
876
      }
877
      break;
1,842,390✔
878

879
    case SCORE_KAPPA_FISSION:
22,489,159✔
880
      if (p.macro_xs().fission == 0.)
22,489,159✔
881
        continue;
18,306,057✔
882
      score = 0.;
4,183,102✔
883
      // Kappa-fission values are determined from the Q-value listed for the
884
      // fission cross section.
885
      if (i_nuclide >= 0) {
4,183,102✔
886
        const auto& nuc {*data::nuclides[i_nuclide]};
1,865,820✔
887
        if (nuc.fissionable_) {
1,865,820✔
888
          const auto& rxn {*nuc.fission_rx_[0]};
1,124,178✔
889
          score = rxn.q_value_ * p.neutron_xs(i_nuclide).fission *
1,124,178✔
890
                  atom_density * flux;
1,124,178✔
891
        }
892
      } else if (p.material() != MATERIAL_VOID) {
2,317,282!
893
        const Material& material {*model::materials[p.material()]};
2,317,282✔
894
        for (auto i = 0; i < material.nuclide_.size(); ++i) {
12,149,621✔
895
          auto j_nuclide = material.nuclide_[i];
9,832,339✔
896
          auto atom_density = material.atom_density(i, p.density_mult());
9,832,339✔
897
          const auto& nuc {*data::nuclides[j_nuclide]};
9,832,339✔
898
          if (nuc.fissionable_) {
9,832,339✔
899
            const auto& rxn {*nuc.fission_rx_[0]};
6,508,480✔
900
            score += rxn.q_value_ * p.neutron_xs(j_nuclide).fission *
6,508,480✔
901
                     atom_density * flux;
6,508,480✔
902
          }
903
        }
904
      }
905
      break;
4,183,102✔
906

907
    case SCORE_EVENTS:
14,433,188✔
908
// Simply count the number of scoring events
909
#pragma omp atomic
7,873,350✔
910
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
14,433,188✔
911
      continue;
14,433,188✔
912

913
    case ELASTIC:
123,555,274✔
914
      if (p.type() != Type::neutron)
123,555,274!
915
        continue;
×
916

917
      if (i_nuclide >= 0) {
123,555,274✔
918
        if (p.neutron_xs(i_nuclide).elastic == CACHE_INVALID)
109,120,480✔
919
          data::nuclides[i_nuclide]->calculate_elastic_xs(p);
43,649,100✔
920
        score = p.neutron_xs(i_nuclide).elastic * atom_density * flux;
109,120,480✔
921
      } else {
922
        score = 0.;
14,434,794✔
923
        if (p.material() != MATERIAL_VOID) {
14,434,794!
924
          const Material& material {*model::materials[p.material()]};
14,434,794✔
925
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
64,063,494✔
926
            auto j_nuclide = material.nuclide_[i];
49,628,700✔
927
            auto atom_density = material.atom_density(i, p.density_mult());
49,628,700✔
928
            if (p.neutron_xs(j_nuclide).elastic == CACHE_INVALID)
49,628,700✔
929
              data::nuclides[j_nuclide]->calculate_elastic_xs(p);
4,985,772✔
930
            score += p.neutron_xs(j_nuclide).elastic * atom_density * flux;
49,628,700✔
931
          }
932
        }
933
      }
934
      break;
123,555,274✔
935

936
    case SCORE_FISS_Q_PROMPT:
28,866,376✔
937
    case SCORE_FISS_Q_RECOV:
938
      if (p.macro_xs().fission == 0.)
28,866,376✔
939
        continue;
23,953,336✔
940
      score =
4,913,040✔
941
        score_fission_q(p, score_bin, tally, flux, i_nuclide, atom_density);
4,913,040✔
942
      break;
4,913,040✔
943

944
    case SCORE_IFP_TIME_NUM:
3,848,482✔
945
      if (settings::ifp_on) {
3,848,482!
946
        if ((p.type() == Type::neutron) && (p.fission())) {
3,848,482!
947
          if (is_generation_time_or_both()) {
981,024!
948
            const auto& lifetimes =
949
              simulation::ifp_source_lifetime_bank[p.current_work() - 1];
981,024✔
950
            if (lifetimes.size() == settings::ifp_n_generation) {
981,024!
951
              score = lifetimes[0] * p.wgt_last();
981,024✔
952
            }
953
          }
954
        }
955
      }
956
      break;
3,848,482✔
957

958
    case SCORE_IFP_BETA_NUM:
3,848,482✔
959
      if (settings::ifp_on) {
3,848,482!
960
        if ((p.type() == Type::neutron) && (p.fission())) {
3,848,482!
961
          if (is_beta_effective_or_both()) {
981,024!
962
            const auto& delayed_groups =
963
              simulation::ifp_source_delayed_group_bank[p.current_work() - 1];
981,024✔
964
            if (delayed_groups.size() == settings::ifp_n_generation) {
981,024!
965
              if (delayed_groups[0] > 0) {
981,024✔
966
                score = p.wgt_last();
5,984✔
967
                if (tally.delayedgroup_filter_ != C_NONE) {
5,984✔
968
                  auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
2,992✔
969
                  const DelayedGroupFilter& filt {
970
                    *dynamic_cast<DelayedGroupFilter*>(
2,992!
971
                      model::tally_filters[i_dg_filt].get())};
2,992✔
972
                  score_fission_delayed_dg(i_tally, delayed_groups[0] - 1,
2,992✔
973
                    score, score_index, p.filter_matches());
974
                  continue;
2,992✔
975
                }
2,992✔
976
              }
977
            }
978
          }
979
        }
980
      }
981
      break;
3,845,490✔
982

983
    case SCORE_IFP_DENOM:
3,848,482✔
984
      if (settings::ifp_on) {
3,848,482!
985
        if ((p.type() == Type::neutron) && (p.fission())) {
3,848,482!
986
          int ifp_data_size;
987
          if (is_beta_effective_or_both()) {
981,024!
988
            ifp_data_size = static_cast<int>(
981,024✔
989
              simulation::ifp_source_delayed_group_bank[p.current_work() - 1]
981,024✔
990
                .size());
981,024✔
991
          } else {
UNCOV
992
            ifp_data_size = static_cast<int>(
×
UNCOV
993
              simulation::ifp_source_lifetime_bank[p.current_work() - 1]
×
UNCOV
994
                .size());
×
995
          }
996
          if (ifp_data_size == settings::ifp_n_generation) {
981,024!
997
            score = p.wgt_last();
981,024✔
998
          }
999
        }
1000
      }
1001
      break;
3,848,482✔
1002

1003
    case N_2N:
789,259,444✔
1004
    case N_3N:
1005
    case N_4N:
1006
    case N_GAMMA:
1007
    case N_P:
1008
    case N_A:
1009
      // This case block only works if cross sections for these reactions have
1010
      // been precalculated. When they are not, we revert to the default case,
1011
      // which looks up cross sections
1012
      if (!simulation::need_depletion_rx)
789,259,444✔
1013
        goto default_case;
426,812,693✔
1014

1015
      if (p.type() != Type::neutron)
362,446,751!
1016
        continue;
×
1017

1018
      int m;
1019
      switch (score_bin) {
1020
        // clang-format off
1021
      case N_GAMMA: m = 0; break;
362,446,751✔
UNCOV
1022
      case N_P:     m = 1; break;
×
UNCOV
1023
      case N_A:     m = 2; break;
×
UNCOV
1024
      case N_2N:    m = 3; break;
×
UNCOV
1025
      case N_3N:    m = 4; break;
×
UNCOV
1026
      case N_4N:    m = 5; break;
×
1027
        // clang-format on
1028
      }
1029
      if (i_nuclide >= 0) {
362,446,751✔
1030
        score = p.neutron_xs(i_nuclide).reaction[m] * atom_density * flux;
358,899,499✔
1031
      } else {
1032
        score = 0.;
3,547,252✔
1033
        if (p.material() != MATERIAL_VOID) {
3,547,252!
1034
          const Material& material {*model::materials[p.material()]};
3,547,252✔
1035
          for (auto i = 0; i < material.nuclide_.size(); ++i) {
7,094,504✔
1036
            auto j_nuclide = material.nuclide_[i];
3,547,252✔
1037
            auto atom_density = material.atom_density(i, p.density_mult());
3,547,252✔
1038
            score += p.neutron_xs(j_nuclide).reaction[m] * atom_density * flux;
3,547,252✔
1039
          }
1040
        }
1041
      }
1042
      break;
362,446,751✔
1043

1044
    case COHERENT:
6,917,251✔
1045
    case INCOHERENT:
1046
    case PHOTOELECTRIC:
1047
    case PAIR_PROD:
1048
      if (p.type() != Type::photon)
6,917,251!
1049
        continue;
6,917,251✔
1050

1051
      if (i_nuclide >= 0) {
×
UNCOV
1052
        const auto& micro = p.photon_xs(i_nuclide);
×
1053
        double xs = (score_bin == COHERENT)        ? micro.coherent
×
1054
                    : (score_bin == INCOHERENT)    ? micro.incoherent
1055
                    : (score_bin == PHOTOELECTRIC) ? micro.photoelectric
1056
                                                   : micro.pair_production;
1057
        score = xs * atom_density * flux;
×
1058
      } else {
UNCOV
1059
        double xs = (score_bin == COHERENT)     ? p.macro_xs().coherent
×
UNCOV
1060
                    : (score_bin == INCOHERENT) ? p.macro_xs().incoherent
×
1061
                    : (score_bin == PHOTOELECTRIC)
UNCOV
1062
                      ? p.macro_xs().photoelectric
×
UNCOV
1063
                      : p.macro_xs().pair_production;
×
UNCOV
1064
        score = xs * flux;
×
1065
      }
UNCOV
1066
      break;
×
1067

1068
    case HEATING:
27,586,178✔
1069
      score = score_particle_heating(
27,586,178✔
1070
        p, tally, flux, HEATING, i_nuclide, atom_density);
1071
      break;
27,586,178✔
1072

1073
    default:
1074
    default_case:
509,677,629✔
1075

1076
      // The default block is really only meant for redundant neutron reactions
1077
      // (e.g. 444, 901)
1078
      if (p.type() != Type::neutron)
509,677,629✔
1079
        continue;
13,324,663✔
1080

1081
      // Any other cross section has to be calculated on-the-fly
1082
      if (score_bin < 2)
496,352,966!
UNCOV
1083
        fatal_error("Invalid score type on tally " + std::to_string(tally.id_));
×
1084
      score = 0.;
496,352,966✔
1085
      if (i_nuclide >= 0) {
496,352,966✔
1086
        score = get_nuclide_xs(p, i_nuclide, score_bin) * atom_density * flux;
399,630,615✔
1087
      } else if (p.material() != MATERIAL_VOID) {
96,722,351✔
1088
        const Material& material {*model::materials[p.material()]};
96,498,083✔
1089
        for (auto i = 0; i < material.nuclide_.size(); ++i) {
427,398,279✔
1090
          auto j_nuclide = material.nuclide_[i];
330,900,196✔
1091
          auto atom_density = material.atom_density(i, p.density_mult());
330,900,196✔
1092
          score +=
330,900,196✔
1093
            get_nuclide_xs(p, j_nuclide, score_bin) * atom_density * flux;
330,900,196✔
1094
        }
1095
      }
1096
    }
14,433,188✔
1097

1098
    // Add derivative information on score for differential tallies.
1099
    if (tally.deriv_ != C_NONE)
2,147,483,647✔
1100
      apply_derivative_to_score(
2,648,470✔
1101
        p, i_tally, i_nuclide, atom_density, score_bin, score);
1102

1103
// Update tally results
1104
#pragma omp atomic
1,946,142,864✔
1105
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
2,147,483,647✔
1106
      score * filter_weight;
2,147,483,647✔
1107
  }
1108
}
2,147,483,647✔
1109

1110
//! Update tally results for continuous-energy tallies with an analog estimator.
1111
//
1112
//! For analog tallies, the flux estimate depends on the score type so the flux
1113
//! argument is really just used for filter weights. The atom_density argument
1114
//! is not used for analog tallies.
1115

1116
void score_general_ce_analog(Particle& p, int i_tally, int start_index,
441,362,422✔
1117
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
1118
  double flux)
1119
{
1120
  Tally& tally {*model::tallies[i_tally]};
441,362,422✔
1121

1122
  // Get the pre-collision energy of the particle.
1123
  auto E = p.E_last();
441,362,422✔
1124

1125
  // Determine how much weight was absorbed due to survival biasing
1126
  double wgt_absorb = settings::survival_biasing
1127
                        ? p.wgt_last() *
441,362,422✔
1128
                            p.neutron_xs(p.event_nuclide()).absorption /
232,419✔
1129
                            p.neutron_xs(p.event_nuclide()).total
232,419✔
1130
                        : 0.0;
441,362,422✔
1131

1132
  using Type = ParticleType;
1133

1134
  for (auto i = 0; i < tally.scores_.size(); ++i) {
1,133,700,605✔
1135
    auto score_bin = tally.scores_[i];
692,338,183✔
1136
    auto score_index = start_index + i;
692,338,183✔
1137
    double score = 0.0;
692,338,183✔
1138

1139
    switch (score_bin) {
692,338,183!
1140
    case SCORE_FLUX:
100,100,216✔
1141
      // All events score to a flux bin. We actually use a collision estimator
1142
      // in place of an analog one since there is no way to count 'events'
1143
      // exactly for the flux
1144
      if (p.type() == Type::neutron || p.type() == Type::photon) {
100,100,216!
1145
        score = flux * p.wgt_last() / p.macro_xs().total;
100,100,216✔
1146
      } else {
UNCOV
1147
        score = 0.;
×
1148
      }
1149
      break;
100,100,216✔
1150

1151
    case SCORE_TOTAL:
87,300,781✔
1152
      // All events will score to the total reaction rate. We can just use
1153
      // use the weight of the particle entering the collision as the score
1154
      score = p.wgt_last() * flux;
87,300,781✔
1155
      break;
87,300,781✔
1156

1157
    case SCORE_INVERSE_VELOCITY:
1,329,185✔
1158
      if (p.type() != Type::neutron)
1,329,185!
1159
        continue;
155,962,356✔
1160

1161
      // All events score to an inverse velocity bin. We actually use a
1162
      // collision estimator in place of an analog one since there is no way
1163
      // to count 'events' exactly for the inverse velocity
1164
      score = flux * p.wgt_last() / (p.macro_xs().total * p.speed());
1,329,185✔
1165
      break;
1,329,185✔
1166

1167
    case SCORE_SCATTER:
220,142,134✔
1168
      if (p.type() != Type::neutron && p.type() != Type::photon)
220,142,134!
UNCOV
1169
        continue;
×
1170

1171
      // Skip any event where the particle didn't scatter
1172
      if (p.event() != TallyEvent::SCATTER)
220,142,134✔
1173
        continue;
7,299,605✔
1174
      // Since only scattering events make it here, again we can use the
1175
      // weight entering the collision as the estimator for the reaction rate
1176
      score = (p.wgt_last() - wgt_absorb) * flux;
212,842,529✔
1177
      break;
212,842,529✔
1178

1179
    case SCORE_NU_SCATTER:
126,503,758✔
1180
      if (p.type() != Type::neutron)
126,503,758!
UNCOV
1181
        continue;
×
1182

1183
      // Only analog estimators are available.
1184
      // Skip any event where the particle didn't scatter
1185
      if (p.event() != TallyEvent::SCATTER)
126,503,758✔
1186
        continue;
4,201,568✔
1187
      // For scattering production, we need to use the pre-collision weight
1188
      // times the yield as the estimate for the number of neutrons exiting a
1189
      // reaction with neutrons in the exit channel
1190
      score = (p.wgt_last() - wgt_absorb) * flux;
122,302,190✔
1191

1192
      // Don't waste time on very common reactions we know have multiplicities
1193
      // of one.
1194
      if (p.event_mt() != ELASTIC && p.event_mt() != N_LEVEL &&
127,842,732!
1195
          !(p.event_mt() >= N_N1 && p.event_mt() <= N_NC)) {
5,540,542!
1196
        // Get yield and apply to score
1197
        auto m =
1198
          data::nuclides[p.event_nuclide()]->reaction_index_[p.event_mt()];
21,161✔
1199
        const auto& rxn {*data::nuclides[p.event_nuclide()]->reactions_[m]};
21,161✔
1200
        score *= (*rxn.products_[0].yield_)(E);
21,161✔
1201
      }
1202
      break;
122,302,190✔
1203

1204
    case SCORE_ABSORPTION:
1,883,574✔
1205
      if (p.type() != Type::neutron && p.type() != Type::photon)
1,883,574!
UNCOV
1206
        continue;
×
1207

1208
      if (settings::survival_biasing) {
1,883,574✔
1209
        // No absorption events actually occur if survival biasing is on --
1210
        // just use weight absorbed in survival biasing
1211
        score = wgt_absorb * flux;
232,419✔
1212
      } else {
1213
        // Skip any event where the particle wasn't absorbed
1214
        if (p.event() == TallyEvent::SCATTER)
1,651,155✔
1215
          continue;
1,585,551✔
1216
        // All fission and absorption events will contribute here, so we
1217
        // can just use the particle's weight entering the collision
1218
        score = p.wgt_last() * flux;
65,604✔
1219
      }
1220
      break;
298,023✔
1221

1222
    case SCORE_FISSION:
6,592,234✔
1223
      if (p.macro_xs().fission == 0)
6,592,234✔
1224
        continue;
5,651,635✔
1225
      if (settings::survival_biasing) {
940,599✔
1226
        // No fission events occur if survival biasing is on -- use collision
1227
        // estimator instead
1228
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
232,419!
1229
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission /
232,419✔
1230
                  p.neutron_xs(p.event_nuclide()).total * flux;
232,419✔
1231
        } else {
UNCOV
1232
          score = 0.;
×
1233
        }
1234
      } else {
1235
        // Skip any non-absorption events
1236
        if (p.event() == TallyEvent::SCATTER)
708,180✔
1237
          continue;
599,456✔
1238
        // All fission events will contribute, so again we can use particle's
1239
        // weight entering the collision as the estimate for the fission
1240
        // reaction rate
1241
        score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission /
108,724✔
1242
                p.neutron_xs(p.event_nuclide()).absorption * flux;
108,724✔
1243
      }
1244
      break;
341,143✔
1245

1246
    case SCORE_NU_FISSION:
114,389,964✔
1247
      if (p.macro_xs().fission == 0)
114,389,964✔
1248
        continue;
38,528,692✔
1249
      if (settings::survival_biasing || p.fission()) {
75,861,272✔
1250
        if (tally.energyout_filter_ != C_NONE) {
4,092,858✔
1251
          // Fission has multiple outgoing neutrons so this helper function
1252
          // is used to handle scoring the multiple filter bins.
1253
          score_fission_eout(p, i_tally, score_index, score_bin);
619,643✔
1254
          continue;
619,643✔
1255
        }
1256
      }
1257
      if (settings::survival_biasing) {
75,241,629✔
1258
        // No fission events occur if survival biasing is on -- use collision
1259
        // estimator instead
1260
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
232,419!
1261
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).nu_fission /
232,419✔
1262
                  p.neutron_xs(p.event_nuclide()).total * flux;
232,419✔
1263
        } else {
UNCOV
1264
          score = 0.;
×
1265
        }
1266
      } else {
1267
        // Skip any non-fission events
1268
        if (!p.fission())
75,009,210✔
1269
          continue;
71,768,414✔
1270
        // If there is no outgoing energy filter, than we only need to score
1271
        // to one bin. For the score to be 'analog', we need to score the
1272
        // number of particles that were banked in the fission bank. Since
1273
        // this was weighted by 1/keff, we multiply by keff to get the proper
1274
        // score.
1275
        score = simulation::keff * p.wgt_bank() * flux;
3,240,796✔
1276
      }
1277
      break;
3,473,215✔
1278

1279
    case SCORE_PROMPT_NU_FISSION:
4,770,260✔
1280
      if (p.macro_xs().fission == 0)
4,770,260✔
1281
        continue;
3,532,870✔
1282
      if (settings::survival_biasing || p.fission()) {
1,237,390!
1283
        if (tally.energyout_filter_ != C_NONE) {
255,255✔
1284
          // Fission has multiple outgoing neutrons so this helper function
1285
          // is used to handle scoring the multiple filter bins.
1286
          score_fission_eout(p, i_tally, score_index, score_bin);
147,378✔
1287
          continue;
147,378✔
1288
        }
1289
      }
1290
      if (settings::survival_biasing) {
1,090,012!
1291
        // No fission events occur if survival biasing is on -- need to
1292
        // calculate fraction of absorptions that would have resulted in
1293
        // prompt-nu-fission
UNCOV
1294
        if (p.neutron_xs(p.event_nuclide()).total > 0) {
×
UNCOV
1295
          score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission *
×
UNCOV
1296
                  data::nuclides[p.event_nuclide()]->nu(
×
UNCOV
1297
                    E, ReactionProduct::EmissionMode::prompt) /
×
UNCOV
1298
                  p.neutron_xs(p.event_nuclide()).total * flux;
×
1299
        } else {
UNCOV
1300
          score = 0.;
×
1301
        }
1302
      } else {
1303
        // Skip any non-fission events
1304
        if (!p.fission())
1,090,012✔
1305
          continue;
982,135✔
1306
        // If there is no outgoing energy filter, than we only need to score
1307
        // to one bin. For the score to be 'analog', we need to score the
1308
        // number of particles that were banked in the fission bank. Since
1309
        // this was weighted by 1/keff, we multiply by keff to get the proper
1310
        // score.
1311
        auto n_delayed = std::accumulate(
107,877✔
1312
          p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
107,877✔
1313
        auto prompt_frac = 1. - n_delayed / static_cast<double>(p.n_bank());
107,877✔
1314
        score = simulation::keff * p.wgt_bank() * prompt_frac * flux;
107,877✔
1315
      }
1316
      break;
107,877✔
1317

1318
    case SCORE_DELAYED_NU_FISSION:
3,612,422✔
1319
      if (p.macro_xs().fission == 0)
3,612,422✔
1320
        continue;
2,354,869✔
1321
      if (settings::survival_biasing || p.fission()) {
1,257,553✔
1322
        if (tally.energyout_filter_ != C_NONE) {
457,842✔
1323
          // Fission has multiple outgoing neutrons so this helper function
1324
          // is used to handle scoring the multiple filter bins.
1325
          score_fission_eout(p, i_tally, score_index, score_bin);
127,490✔
1326
          continue;
127,490✔
1327
        }
1328
      }
1329
      if (settings::survival_biasing) {
1,130,063✔
1330
        // No fission events occur if survival biasing is on -- need to
1331
        // calculate fraction of absorptions that would have resulted in
1332
        // delayed-nu-fission
1333
        if (p.neutron_xs(p.event_nuclide()).total > 0 &&
464,838!
1334
            data::nuclides[p.event_nuclide()]->fissionable_) {
232,419!
1335
          if (tally.delayedgroup_filter_ != C_NONE) {
232,419!
1336
            auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
×
1337
            const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
×
1338
              model::tally_filters[i_dg_filt].get())};
×
1339
            // Tally each delayed group bin individually
UNCOV
1340
            for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
×
1341
              auto dg = filt.groups()[d_bin];
×
1342
              auto yield = data::nuclides[p.event_nuclide()]->nu(
×
1343
                E, ReactionProduct::EmissionMode::delayed, dg);
UNCOV
1344
              score = p.wgt_last() * yield *
×
UNCOV
1345
                      p.neutron_xs(p.event_nuclide()).fission /
×
UNCOV
1346
                      p.neutron_xs(p.event_nuclide()).total * flux;
×
UNCOV
1347
              score_fission_delayed_dg(
×
1348
                i_tally, d_bin, score, score_index, p.filter_matches());
1349
            }
UNCOV
1350
            continue;
×
UNCOV
1351
          } else {
×
1352
            // If the delayed group filter is not present, compute the score
1353
            // by multiplying the absorbed weight by the fraction of the
1354
            // delayed-nu-fission xs to the absorption xs
1355
            score = p.wgt_last() * p.neutron_xs(p.event_nuclide()).fission *
232,419✔
1356
                    data::nuclides[p.event_nuclide()]->nu(
232,419✔
1357
                      E, ReactionProduct::EmissionMode::delayed) /
232,419✔
1358
                    p.neutron_xs(p.event_nuclide()).total * flux;
232,419✔
1359
          }
1360
        }
1361
      } else {
1362
        // Skip any non-fission events
1363
        if (!p.fission())
897,644✔
1364
          continue;
799,711✔
1365
        // If there is no outgoing energy filter, than we only need to score
1366
        // to one bin. For the score to be 'analog', we need to score the
1367
        // number of particles that were banked in the fission bank. Since
1368
        // this was weighted by 1/keff, we multiply by keff to get the proper
1369
        // score. Loop over the neutrons produced from fission and check which
1370
        // ones are delayed. If a delayed neutron is encountered, add its
1371
        // contribution to the fission bank to the score.
1372
        if (tally.delayedgroup_filter_ != C_NONE) {
97,933✔
1373
          auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
63,745✔
1374
          const DelayedGroupFilter& filt {*dynamic_cast<DelayedGroupFilter*>(
63,745!
1375
            model::tally_filters[i_dg_filt].get())};
63,745✔
1376
          // Tally each delayed group bin individually
1377
          for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) {
446,215✔
1378
            auto d = filt.groups()[d_bin];
382,470✔
1379
            score = simulation::keff * p.wgt_bank() / p.n_bank() *
382,470✔
1380
                    p.n_delayed_bank(d - 1) * flux;
382,470✔
1381
            score_fission_delayed_dg(
382,470✔
1382
              i_tally, d_bin, score, score_index, p.filter_matches());
1383
          }
1384
          continue;
63,745✔
1385
        } else {
63,745✔
1386
          // Add the contribution from all delayed groups
1387
          auto n_delayed = std::accumulate(
34,188✔
1388
            p.n_delayed_bank(), p.n_delayed_bank() + MAX_DELAYED_GROUPS, 0);
34,188✔
1389
          score =
34,188✔
1390
            simulation::keff * p.wgt_bank() / p.n_bank() * n_delayed * flux;
34,188✔
1391
        }
1392
      }
1393
      break;
266,607✔
1394

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

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

1484
    case SCORE_KAPPA_FISSION:
1,561,604✔
1485
      if (p.macro_xs().fission == 0.)
1,561,604✔
1486
        continue;
1,017,313✔
1487
      score = 0.;
544,291✔
1488
      // Kappa-fission values are determined from the Q-value listed for the
1489
      // fission cross section.
1490
      if (settings::survival_biasing) {
544,291✔
1491
        // No fission events occur if survival biasing is on -- need to
1492
        // calculate fraction of absorptions that would have resulted in
1493
        // fission scaled by the Q-value
1494
        const auto& nuc {*data::nuclides[p.event_nuclide()]};
232,419✔
1495
        if (p.neutron_xs(p.event_nuclide()).total > 0 && nuc.fissionable_) {
232,419!
1496
          const auto& rxn {*nuc.fission_rx_[0]};
232,419✔
1497
          score = p.wgt_last() * rxn.q_value_ *
232,419✔
1498
                  p.neutron_xs(p.event_nuclide()).fission /
232,419✔
1499
                  p.neutron_xs(p.event_nuclide()).total * flux;
232,419✔
1500
        }
1501
      } else {
1502
        // Skip any non-absorption events
1503
        if (p.event() == TallyEvent::SCATTER)
311,872✔
1504
          continue;
269,368✔
1505
        // All fission events will contribute, so again we can use particle's
1506
        // weight entering the collision as the estimate for the fission
1507
        // reaction rate
1508
        const auto& nuc {*data::nuclides[p.event_nuclide()]};
42,504✔
1509
        if (p.neutron_xs(p.event_nuclide()).absorption > 0 &&
85,008!
1510
            nuc.fissionable_) {
42,504✔
1511
          const auto& rxn {*nuc.fission_rx_[0]};
41,118✔
1512
          score = p.wgt_last() * rxn.q_value_ *
41,118✔
1513
                  p.neutron_xs(p.event_nuclide()).fission /
41,118✔
1514
                  p.neutron_xs(p.event_nuclide()).absorption * flux;
41,118✔
1515
        }
1516
      }
1517
      break;
274,923✔
1518

1519
    case SCORE_EVENTS:
1,329,185✔
1520
// Simply count the number of scoring events
1521
#pragma omp atomic
725,059✔
1522
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
1,329,185✔
1523
      continue;
1,329,185✔
1524

1525
    case ELASTIC:
1,329,185✔
1526
      if (p.type() != Type::neutron)
1,329,185!
UNCOV
1527
        continue;
×
1528

1529
      // Check if event MT matches
1530
      if (p.event_mt() != ELASTIC)
1,329,185✔
1531
        continue;
66,264✔
1532
      score = (p.wgt_last() - wgt_absorb) * flux;
1,262,921✔
1533
      break;
1,262,921✔
1534

1535
    case SCORE_FISS_Q_PROMPT:
2,658,370✔
1536
    case SCORE_FISS_Q_RECOV:
1537
      if (p.macro_xs().fission == 0.)
2,658,370✔
1538
        continue;
2,034,626✔
1539
      score =
623,744✔
1540
        score_fission_q(p, score_bin, tally, flux, i_nuclide, atom_density);
623,744✔
1541
      break;
623,744✔
1542

1543
    case N_2N:
4,070,055✔
1544
    case N_3N:
1545
    case N_4N:
1546
    case N_GAMMA:
1547
    case N_P:
1548
    case N_A:
1549
      // This case block only works if cross sections for these reactions have
1550
      // been precalculated. When they are not, we revert to the default case,
1551
      // which looks up cross sections
1552
      if (!simulation::need_depletion_rx)
4,070,055!
1553
        goto default_case;
4,070,055✔
1554

1555
      if (p.type() != Type::neutron)
×
UNCOV
1556
        continue;
×
1557

1558
      // Check if the event MT matches
1559
      if (p.event_mt() != score_bin)
×
1560
        continue;
×
UNCOV
1561
      score = (p.wgt_last() - wgt_absorb) * flux;
×
1562
      break;
×
1563

UNCOV
1564
    case COHERENT:
×
1565
    case INCOHERENT:
1566
    case PHOTOELECTRIC:
1567
    case PAIR_PROD:
1568
      if (p.type() != Type::photon)
×
UNCOV
1569
        continue;
×
1570

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

1585
    case HEATING:
5,808,374✔
1586
      score = score_particle_heating(
5,808,374✔
1587
        p, tally, flux, HEATING, i_nuclide, atom_density);
1588
      break;
5,808,374✔
1589

1590
    default:
1591
    default_case:
11,697,752✔
1592

1593
      // The default block is really only meant for redundant neutron reactions
1594
      // (e.g. 444, 901)
1595
      if (p.type() != Type::neutron)
11,697,752✔
1596
        continue;
4,859,910✔
1597

1598
      // Any other score is assumed to be a MT number. Thus, we just need
1599
      // to check if it matches the MT number of the event
1600
      if (p.event_mt() != score_bin)
6,837,842✔
1601
        continue;
6,827,931✔
1602
      score = (p.wgt_last() - wgt_absorb) * flux;
9,911✔
1603
    }
1,329,185✔
1604

1605
    // Add derivative information on score for differential tallies.
1606
    if (tally.deriv_ != C_NONE)
536,375,827✔
1607
      apply_derivative_to_score(
138,468✔
1608
        p, i_tally, i_nuclide, atom_density, score_bin, score);
1609

1610
// Update tally results
1611
#pragma omp atomic
305,599,021✔
1612
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
536,375,827✔
1613
      score * filter_weight;
536,375,827✔
1614
  }
1615
}
441,362,422✔
1616

1617
//! Update tally results for multigroup tallies with any estimator.
1618
//
1619
//! For analog tallies, the flux estimate depends on the score type so the flux
1620
//! argument is really just used for filter weights.
1621

1622
void score_general_mg(Particle& p, int i_tally, int start_index,
60,664,923✔
1623
  int filter_index, double filter_weight, int i_nuclide, double atom_density,
1624
  double flux)
1625
{
1626
  auto& tally {*model::tallies[i_tally]};
60,664,923✔
1627

1628
  // Set the direction and group to use with get_xs
1629
  Direction p_u;
60,664,923✔
1630
  int p_g;
1631
  double wgt_absorb = 0.0;
60,664,923✔
1632
  if (tally.estimator_ == TallyEstimator::ANALOG ||
60,664,923✔
1633
      tally.estimator_ == TallyEstimator::COLLISION) {
50,486,689✔
1634
    if (settings::survival_biasing) {
15,011,282!
1635
      // Determine weight that was absorbed
1636
      wgt_absorb = p.wgt_last() * p.macro_xs().absorption / p.macro_xs().total;
×
1637

1638
      // Then we either are alive and had a scatter (and so g changed),
1639
      // or are dead and g did not change
UNCOV
1640
      if (p.alive()) {
×
UNCOV
1641
        p_u = p.u_last();
×
UNCOV
1642
        p_g = p.g_last();
×
1643
      } else {
UNCOV
1644
        p_u = p.u_local();
×
UNCOV
1645
        p_g = p.g();
×
1646
      }
1647
    } else if (p.event() == TallyEvent::SCATTER) {
15,011,282✔
1648

1649
      // Then the energy group has been changed by the scattering routine
1650
      // meaning gin is now in p % last_g
1651
      p_u = p.u_last();
14,328,094✔
1652
      p_g = p.g_last();
14,328,094✔
1653
    } else {
1654

1655
      // No scatter, no change in g.
1656
      p_u = p.u_local();
683,188✔
1657
      p_g = p.g();
683,188✔
1658
    }
1659
  } else {
1660

1661
    // No actual collision so g has not changed.
1662
    p_u = p.u_local();
45,653,641✔
1663
    p_g = p.g();
45,653,641✔
1664
  }
1665

1666
  // For shorthand, assign pointers to the material and nuclide xs set
1667
  auto& nuc_xs = (i_nuclide >= 0) ? data::mg.nuclides_[i_nuclide]
10,183,415✔
1668
                                  : data::mg.macro_xs_[p.material()];
60,664,923✔
1669
  auto& macro_xs = data::mg.macro_xs_[p.material()];
60,664,923✔
1670

1671
  // Find the temperature and angle indices of interest
1672
  int macro_t = p.mg_xs_cache().t;
60,664,923✔
1673
  int macro_a = macro_xs.get_angle_index(p_u);
60,664,923✔
1674
  int nuc_t = 0;
60,664,923✔
1675
  int nuc_a = 0;
60,664,923✔
1676
  if (i_nuclide >= 0) {
60,664,923✔
1677
    nuc_t = nuc_xs.get_temperature_index(p.sqrtkT());
10,183,415✔
1678
    nuc_a = nuc_xs.get_angle_index(p_u);
10,183,415✔
1679
  }
1680

1681
  for (auto i = 0; i < tally.scores_.size(); ++i) {
288,232,967✔
1682
    auto score_bin = tally.scores_[i];
227,568,044✔
1683
    auto score_index = start_index + i;
227,568,044✔
1684

1685
    double score;
1686

1687
    switch (score_bin) {
227,568,044!
1688

1689
    case SCORE_FLUX:
48,064,984✔
1690
      if (tally.estimator_ == TallyEstimator::ANALOG) {
48,064,984✔
1691
        // All events score to a flux bin. We actually use a collision estimator
1692
        // in place of an analog one since there is no way to count 'events'
1693
        // exactly for the flux
1694
        score = flux * p.wgt_last() / p.macro_xs().total;
2,672,593✔
1695
      } else {
1696
        score = flux;
45,392,391✔
1697
      }
1698
      break;
48,064,984✔
1699

1700
    case SCORE_TOTAL:
15,533,782✔
1701
      if (tally.estimator_ == TallyEstimator::ANALOG) {
15,533,782✔
1702
        // All events will score to the total reaction rate. We can just use
1703
        // use the weight of the particle entering the collision as the score
1704
        score = flux * p.wgt_last();
5,345,186✔
1705
        if (i_nuclide >= 0) {
5,345,186✔
1706
          score *= atom_density *
5,345,186✔
1707
                   nuc_xs.get_xs(MgxsType::TOTAL, p_g, nuc_t, nuc_a) /
2,672,593✔
1708
                   macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
2,672,593✔
1709
        }
1710
      } else {
1711
        if (i_nuclide >= 0) {
10,188,596✔
1712
          score = atom_density * flux *
10,188,596✔
1713
                  nuc_xs.get_xs(MgxsType::TOTAL, p_g, nuc_t, nuc_a);
5,094,298✔
1714
        } else {
1715
          score = p.macro_xs().total * flux;
5,094,298✔
1716
        }
1717
      }
1718
      break;
15,533,782✔
1719

1720
    case SCORE_INVERSE_VELOCITY:
15,533,782✔
1721
      if (tally.estimator_ == TallyEstimator::ANALOG ||
15,533,782✔
1722
          tally.estimator_ == TallyEstimator::COLLISION) {
10,188,596✔
1723
        // All events score to an inverse velocity bin. We actually use a
1724
        // collision estimator in place of an analog one since there is no way
1725
        // to count 'events' exactly for the inverse velocity
1726
        score = flux * p.wgt_last();
10,178,234✔
1727
        if (i_nuclide >= 0) {
10,178,234✔
1728
          score *=
5,089,117✔
1729
            nuc_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, nuc_t, nuc_a) /
5,089,117✔
1730
            macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
5,089,117✔
1731
        } else {
1732
          score *=
5,089,117✔
1733
            macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, macro_t, macro_a) /
5,089,117✔
1734
            macro_xs.get_xs(MgxsType::TOTAL, p_g, macro_t, macro_a);
5,089,117✔
1735
        }
1736
      } else {
1737
        if (i_nuclide >= 0) {
5,355,548✔
1738
          score =
2,677,774✔
1739
            flux * nuc_xs.get_xs(MgxsType::INVERSE_VELOCITY, p_g, nuc_t, nuc_a);
2,677,774✔
1740
        } else {
1741
          score = flux * macro_xs.get_xs(
2,677,774✔
1742
                           MgxsType::INVERSE_VELOCITY, p_g, macro_t, macro_a);
1743
        }
1744
      }
1745
      break;
15,533,782✔
1746

1747
    case SCORE_SCATTER:
9,666,096✔
1748
      if (tally.estimator_ == TallyEstimator::ANALOG) {
9,666,096!
1749
        // Skip any event where the particle didn't scatter
1750
        if (p.event() != TallyEvent::SCATTER)
9,666,096✔
1751
          continue;
439,384✔
1752
        // Since only scattering events make it here, again we can use the
1753
        // weight entering the collision as the estimator for the reaction rate
1754
        score = (p.wgt_last() - wgt_absorb) * flux;
9,226,712✔
1755
        if (i_nuclide >= 0) {
9,226,712✔
1756
          score *= atom_density *
9,226,712✔
1757
                   nuc_xs.get_xs(MgxsType::SCATTER_FMU, p.g_last(), &p.g(),
4,613,356✔
1758
                     &p.mu(), nullptr, nuc_t, nuc_a) /
4,613,356✔
1759
                   macro_xs.get_xs(MgxsType::SCATTER_FMU, p.g_last(), &p.g(),
4,613,356✔
1760
                     &p.mu(), nullptr, macro_t, macro_a);
4,613,356✔
1761
        }
1762
      } else {
UNCOV
1763
        if (i_nuclide >= 0) {
×
UNCOV
1764
          score = atom_density * flux *
×
UNCOV
1765
                  nuc_xs.get_xs(MgxsType::SCATTER, p_g, nullptr, &p.mu(),
×
1766
                    nullptr, nuc_t, nuc_a);
1767
        } else {
UNCOV
1768
          score = flux * macro_xs.get_xs(MgxsType::SCATTER, p_g, nullptr,
×
UNCOV
1769
                           &p.mu(), nullptr, macro_t, macro_a);
×
1770
        }
1771
      }
1772
      break;
9,226,712✔
1773

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

1804
    case SCORE_ABSORPTION:
15,533,782✔
1805
      if (tally.estimator_ == TallyEstimator::ANALOG) {
15,533,782✔
1806
        if (settings::survival_biasing) {
5,345,186!
1807
          // No absorption events actually occur if survival biasing is on --
1808
          // just use weight absorbed in survival biasing
UNCOV
1809
          score = wgt_absorb * flux;
×
1810
        } else {
1811
          // Skip any event where the particle wasn't absorbed
1812
          if (p.event() == TallyEvent::SCATTER)
5,345,186✔
1813
            continue;
5,101,382✔
1814
          // All fission and absorption events will contribute here, so we
1815
          // can just use the particle's weight entering the collision
1816
          score = p.wgt_last() * flux;
243,804✔
1817
        }
1818
        if (i_nuclide >= 0) {
243,804✔
1819
          score *= atom_density *
243,804✔
1820
                   nuc_xs.get_xs(MgxsType::ABSORPTION, p_g, nuc_t, nuc_a) /
121,902✔
1821
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
121,902✔
1822
        }
1823
      } else {
1824
        if (i_nuclide >= 0) {
10,188,596✔
1825
          score = atom_density * flux *
10,188,596✔
1826
                  nuc_xs.get_xs(MgxsType::ABSORPTION, p_g, nuc_t, nuc_a);
5,094,298✔
1827
        } else {
1828
          score = p.macro_xs().absorption * flux;
5,094,298✔
1829
        }
1830
      }
1831
      break;
10,432,400✔
1832

1833
    case SCORE_FISSION:
15,533,782✔
1834
      if (tally.estimator_ == TallyEstimator::ANALOG) {
15,533,782✔
1835
        if (settings::survival_biasing) {
5,345,186!
1836
          // No fission events occur if survival biasing is on -- need to
1837
          // calculate fraction of absorptions that would have resulted in
1838
          // fission
UNCOV
1839
          score = wgt_absorb * flux;
×
1840
        } else {
1841
          // Skip any non-absorption events
1842
          if (p.event() == TallyEvent::SCATTER)
5,345,186✔
1843
            continue;
5,101,382✔
1844
          // All fission events will contribute, so again we can use particle's
1845
          // weight entering the collision as the estimate for the fission
1846
          // reaction rate
1847
          score = p.wgt_last() * flux;
243,804✔
1848
        }
1849
        if (i_nuclide >= 0) {
243,804✔
1850
          score *= atom_density *
243,804✔
1851
                   nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
121,902✔
1852
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
121,902✔
1853
        } else {
1854
          score *= macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a) /
121,902✔
1855
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
121,902✔
1856
        }
1857
      } else {
1858
        if (i_nuclide >= 0) {
10,188,596✔
1859
          score = atom_density * flux *
10,188,596✔
1860
                  nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a);
5,094,298✔
1861
        } else {
1862
          score =
5,094,298✔
1863
            flux * macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
5,094,298✔
1864
        }
1865
      }
1866
      break;
10,432,400✔
1867

1868
    case SCORE_NU_FISSION:
20,366,830✔
1869
      if (tally.estimator_ == TallyEstimator::ANALOG) {
20,366,830✔
1870
        if (settings::survival_biasing || p.fission()) {
10,178,234!
1871
          if (tally.energyout_filter_ != C_NONE) {
461,604✔
1872
            // Fission has multiple outgoing neutrons so this helper function
1873
            // is used to handle scoring the multiple filter bins.
1874
            score_fission_eout(p, i_tally, score_index, score_bin);
219,164✔
1875
            continue;
219,164✔
1876
          }
1877
        }
1878
        if (settings::survival_biasing) {
9,959,070!
1879
          // No fission events occur if survival biasing is on -- need to
1880
          // calculate fraction of absorptions that would have resulted in
1881
          // nu-fission
1882
          score = wgt_absorb * flux;
×
1883
          if (i_nuclide >= 0) {
×
UNCOV
1884
            score *=
×
UNCOV
1885
              atom_density *
×
UNCOV
1886
              nuc_xs.get_xs(MgxsType::NU_FISSION, p_g, nuc_t, nuc_a) /
×
UNCOV
1887
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1888
          } else {
UNCOV
1889
            score *=
×
UNCOV
1890
              macro_xs.get_xs(MgxsType::NU_FISSION, p_g, macro_t, macro_a) /
×
UNCOV
1891
              macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
×
1892
          }
1893
        } else {
1894
          // Skip any non-fission events
1895
          if (!p.fission())
9,959,070✔
1896
            continue;
9,716,630✔
1897
          // If there is no outgoing energy filter, than we only need to score
1898
          // to one bin. For the score to be 'analog', we need to score the
1899
          // number of particles that were banked in the fission bank. Since
1900
          // this was weighted by 1/keff, we multiply by keff to get the proper
1901
          // score.
1902
          score = simulation::keff * p.wgt_bank() * flux;
242,440✔
1903
          if (i_nuclide >= 0) {
242,440✔
1904
            score *= atom_density *
242,440✔
1905
                     nuc_xs.get_xs(MgxsType::FISSION, p_g, nuc_t, nuc_a) /
121,220✔
1906
                     macro_xs.get_xs(MgxsType::FISSION, p_g, macro_t, macro_a);
121,220✔
1907
          }
1908
        }
1909
      } else {
1910
        if (i_nuclide >= 0) {
10,188,596✔
1911
          score = atom_density * flux *
10,188,596✔
1912
                  nuc_xs.get_xs(MgxsType::NU_FISSION, p_g, nuc_t, nuc_a);
5,094,298✔
1913
        } else {
1914
          score =
5,094,298✔
1915
            flux * macro_xs.get_xs(MgxsType::NU_FISSION, p_g, macro_t, macro_a);
5,094,298✔
1916
        }
1917
      }
1918
      break;
10,431,036✔
1919

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

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

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

2261
    case SCORE_KAPPA_FISSION:
15,533,782✔
2262
      if (tally.estimator_ == TallyEstimator::ANALOG) {
15,533,782✔
2263
        if (settings::survival_biasing) {
5,345,186!
2264
          // No fission events occur if survival biasing is on -- need to
2265
          // calculate fraction of absorptions that would have resulted in
2266
          // fission scaled by the Q-value
UNCOV
2267
          score = wgt_absorb * flux;
×
2268
        } else {
2269
          // Skip any non-absorption events
2270
          if (p.event() == TallyEvent::SCATTER)
5,345,186✔
2271
            continue;
5,101,382✔
2272
          // All fission events will contribute, so again we can use particle's
2273
          // weight entering the collision as the estimate for the fission
2274
          // reaction rate
2275
          score = p.wgt_last() * flux;
243,804✔
2276
        }
2277
        if (i_nuclide >= 0) {
243,804✔
2278
          score *= atom_density *
243,804✔
2279
                   nuc_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, nuc_t, nuc_a) /
121,902✔
2280
                   macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
121,902✔
2281
        } else {
2282
          score *=
121,902✔
2283
            macro_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, macro_t, macro_a) /
121,902✔
2284
            macro_xs.get_xs(MgxsType::ABSORPTION, p_g, macro_t, macro_a);
121,902✔
2285
        }
2286
      } else {
2287
        if (i_nuclide >= 0) {
10,188,596✔
2288
          score = atom_density * flux *
10,188,596✔
2289
                  nuc_xs.get_xs(MgxsType::KAPPA_FISSION, p_g, nuc_t, nuc_a);
5,094,298✔
2290
        } else {
2291
          score = flux * macro_xs.get_xs(
5,094,298✔
2292
                           MgxsType::KAPPA_FISSION, p_g, macro_t, macro_a);
2293
        }
2294
      }
2295
      break;
10,432,400✔
2296

2297
    case SCORE_EVENTS:
15,533,782✔
2298
// Simply count the number of scoring events
2299
#pragma omp atomic
8,476,555✔
2300
      tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0;
15,533,782✔
2301
      continue;
15,533,782✔
2302

UNCOV
2303
    default:
×
UNCOV
2304
      continue;
×
2305
    }
15,533,782✔
2306

2307
// Update tally results
2308
#pragma omp atomic
93,088,177✔
2309
    tally.results_(filter_index, score_index, TallyResult::VALUE) +=
170,607,316✔
2310
      score * filter_weight;
170,607,316✔
2311
  }
2312
}
60,664,923✔
2313

2314
void score_analog_tally_ce(Particle& p)
112,768,905✔
2315
{
2316
  // Since electrons/positrons are not transported, we assign a flux of zero.
2317
  // Note that the heating score does NOT use the flux and will be non-zero for
2318
  // electrons/positrons.
2319
  double flux =
2320
    (p.type() == ParticleType::neutron || p.type() == ParticleType::photon)
133,643,253✔
2321
      ? 1.0
133,643,253✔
2322
      : 0.0;
112,768,905✔
2323

2324
  for (auto i_tally : model::active_analog_tallies) {
506,598,197✔
2325
    const Tally& tally {*model::tallies[i_tally]};
393,829,292✔
2326

2327
    // Initialize an iterator over valid filter bin combinations.  If there are
2328
    // no valid combinations, use a continue statement to ensure we skip the
2329
    // assume_separate break below.
2330
    auto filter_iter = FilterBinIter(tally, p);
393,829,292✔
2331
    auto end = FilterBinIter(tally, true, &p.filter_matches());
393,829,292✔
2332
    if (filter_iter == end)
393,829,292✔
2333
      continue;
61,514,475✔
2334

2335
    // Loop over filter bins.
2336
    for (; filter_iter != end; ++filter_iter) {
769,692,423✔
2337
      auto filter_index = filter_iter.index_;
437,377,606✔
2338
      auto filter_weight = filter_iter.weight_;
437,377,606✔
2339

2340
      // Loop over nuclide bins.
2341
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
904,176,736✔
2342
        auto i_nuclide = tally.nuclides_[i];
466,799,130✔
2343

2344
        // Tally this event in the present nuclide bin if that bin represents
2345
        // the event nuclide or the total material.  Note that the atomic
2346
        // density argument for score_general is not used for analog tallies.
2347
        if (i_nuclide == p.event_nuclide() || i_nuclide == -1)
466,799,130✔
2348
          score_general_ce_analog(p, i_tally, i * tally.scores_.size(),
441,362,422✔
2349
            filter_index, filter_weight, i_nuclide, -1.0, flux);
2350
      }
2351
    }
2352

2353
    // If the user has specified that we can assume all tallies are spatially
2354
    // separate, this implies that once a tally has been scored to, we needn't
2355
    // check the others. This cuts down on overhead when there are many
2356
    // tallies specified
2357
    if (settings::assume_separate)
332,314,817!
UNCOV
2358
      break;
×
2359
  }
2360

2361
  // Reset all the filter matches for the next tally event.
2362
  for (auto& match : p.filter_matches())
558,586,214✔
2363
    match.bins_present_ = false;
445,817,309✔
2364
}
112,768,905✔
2365

2366
void score_analog_tally_mg(Particle& p)
1,208,262✔
2367
{
2368
  for (auto i_tally : model::active_analog_tallies) {
13,290,882✔
2369
    const Tally& tally {*model::tallies[i_tally]};
12,082,620✔
2370

2371
    // Initialize an iterator over valid filter bin combinations.  If there are
2372
    // no valid combinations, use a continue statement to ensure we skip the
2373
    // assume_separate break below.
2374
    auto filter_iter = FilterBinIter(tally, p);
12,082,620✔
2375
    auto end = FilterBinIter(tally, true, &p.filter_matches());
12,082,620✔
2376
    if (filter_iter == end)
12,082,620✔
2377
      continue;
1,904,386✔
2378

2379
    // Loop over filter bins.
2380
    for (; filter_iter != end; ++filter_iter) {
20,356,468✔
2381
      auto filter_index = filter_iter.index_;
10,178,234✔
2382
      auto filter_weight = filter_iter.weight_;
10,178,234✔
2383

2384
      // Loop over nuclide bins.
2385
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
20,356,468✔
2386
        auto i_nuclide = tally.nuclides_[i];
10,178,234✔
2387

2388
        double atom_density = 0.;
10,178,234✔
2389
        if (i_nuclide >= 0) {
10,178,234✔
2390
          auto j =
2391
            model::materials[p.material()]->mat_nuclide_index_[i_nuclide];
5,089,117✔
2392
          if (j == C_NONE)
5,089,117!
UNCOV
2393
            continue;
×
2394
          atom_density =
2395
            model::materials[p.material()]->atom_density(j, p.density_mult());
5,089,117✔
2396
        }
2397

2398
        score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
10,178,234✔
2399
          filter_weight, i_nuclide, atom_density, 1.0);
2400
      }
2401
    }
2402

2403
    // If the user has specified that we can assume all tallies are spatially
2404
    // separate, this implies that once a tally has been scored to, we needn't
2405
    // check the others. This cuts down on overhead when there are many
2406
    // tallies specified
2407
    if (settings::assume_separate)
10,178,234!
UNCOV
2408
      break;
×
2409
  }
2410

2411
  // Reset all the filter matches for the next tally event.
2412
  for (auto& match : p.filter_matches())
8,457,834✔
2413
    match.bins_present_ = false;
7,249,572✔
2414
}
1,208,262✔
2415

2416
void score_tracklength_tally_general(
1,436,465,054✔
2417
  Particle& p, double flux, const vector<int>& tallies)
2418
{
2419
  // Set 'none' value for log union grid index
2420
  int i_log_union = C_NONE;
1,436,465,054✔
2421

2422
  for (auto i_tally : tallies) {
2,147,483,647✔
2423
    const Tally& tally {*model::tallies[i_tally]};
2,147,483,647✔
2424

2425
    // Initialize an iterator over valid filter bin combinations.  If there are
2426
    // no valid combinations, use a continue statement to ensure we skip the
2427
    // assume_separate break below.
2428
    auto filter_iter = FilterBinIter(tally, p);
2,147,483,647✔
2429
    auto end = FilterBinIter(tally, true, &p.filter_matches());
2,147,483,647✔
2430
    if (filter_iter == end)
2,147,483,647✔
2431
      continue;
1,432,385,225✔
2432

2433
    // Loop over filter bins.
2434
    for (; filter_iter != end; ++filter_iter) {
2,147,483,647✔
2435
      auto filter_index = filter_iter.index_;
1,583,752,849✔
2436
      auto filter_weight = filter_iter.weight_;
1,583,752,849✔
2437

2438
      // Loop over nuclide bins.
2439
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
2,147,483,647✔
2440
        auto i_nuclide = tally.nuclides_[i];
2,147,483,647✔
2441

2442
        double atom_density = 0.;
2,147,483,647✔
2443
        if (i_nuclide >= 0) {
2,147,483,647✔
2444
          if (p.material() != MATERIAL_VOID) {
854,401,609✔
2445
            const auto& mat = model::materials[p.material()];
854,100,979✔
2446
            auto j = mat->mat_nuclide_index_[i_nuclide];
854,100,979✔
2447
            if (j == C_NONE) {
854,100,979✔
2448
              // Determine log union grid index
2449
              if (i_log_union == C_NONE) {
409,792,451✔
2450
                int neutron = static_cast<int>(ParticleType::neutron);
153,897,361✔
2451
                i_log_union = std::log(p.E() / data::energy_min[neutron]) /
153,897,361✔
2452
                              simulation::log_spacing;
2453
              }
2454

2455
              // Update micro xs cache
2456
              if (!tally.multiply_density()) {
409,792,451✔
2457
                p.update_neutron_xs(i_nuclide, i_log_union);
343,467,529✔
2458
                atom_density = 1.0;
343,467,529✔
2459
              }
2460
            } else {
2461
              atom_density = tally.multiply_density()
888,617,056✔
2462
                               ? mat->atom_density(j, p.density_mult())
444,308,528✔
2463
                               : 1.0;
2464
            }
2465
          }
2466
        }
2467

2468
        // TODO: consider replacing this "if" with pointers or templates
2469
        if (settings::run_CE) {
2,147,483,647✔
2470
          score_general_ce_nonanalog(p, i_tally, i * tally.scores_.size(),
2,147,483,647✔
2471
            filter_index, filter_weight, i_nuclide, atom_density, flux);
2472
        } else {
2473
          score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
45,653,641✔
2474
            filter_weight, i_nuclide, atom_density, flux);
2475
        }
2476
      }
2477
    }
2478

2479
    // If the user has specified that we can assume all tallies are spatially
2480
    // separate, this implies that once a tally has been scored to, we needn't
2481
    // check the others. This cuts down on overhead when there are many
2482
    // tallies specified
2483
    if (settings::assume_separate)
1,216,726,390✔
2484
      break;
42,086✔
2485
  }
2486

2487
  // Reset all the filter matches for the next tally event.
2488
  for (auto& match : p.filter_matches())
2,147,483,647✔
2489
    match.bins_present_ = false;
2,147,483,647✔
2490
}
1,436,465,054✔
2491

2492
void score_timed_tracklength_tally(Particle& p, double total_distance)
3,628,317✔
2493
{
2494
  double speed = p.speed();
3,628,317✔
2495
  double total_dt = total_distance / speed;
3,628,317✔
2496

2497
  // save particle last state
2498
  auto time_last = p.time_last();
3,628,317✔
2499
  auto r_last = p.r_last();
3,628,317✔
2500

2501
  // move particle back
2502
  p.move_distance(-total_distance);
3,628,317✔
2503
  p.time() -= total_dt;
3,628,317✔
2504
  p.lifetime() -= total_dt;
3,628,317✔
2505

2506
  double distance_traveled = 0.0;
3,628,317✔
2507
  while (distance_traveled < total_distance) {
11,415,360✔
2508

2509
    double distance = std::min(distance_to_time_boundary(p.time(), speed),
7,787,043✔
2510
      total_distance - distance_traveled);
7,787,043✔
2511
    double dt = distance / speed;
7,787,043✔
2512

2513
    // Save particle last state for tracklength tallies
2514
    p.time_last() = p.time();
7,787,043✔
2515
    p.r_last() = p.r();
7,787,043✔
2516

2517
    // Advance particle in space and time
2518
    p.move_distance(distance);
7,787,043✔
2519
    p.time() += dt;
7,787,043✔
2520
    p.lifetime() += dt;
7,787,043✔
2521

2522
    // Determine the tracklength estimate of the flux
2523
    double flux = p.wgt() * distance;
7,787,043✔
2524

2525
    score_tracklength_tally_general(
7,787,043✔
2526
      p, flux, model::active_timed_tracklength_tallies);
2527
    distance_traveled += distance;
7,787,043✔
2528
  }
2529

2530
  p.time_last() = time_last;
3,628,317✔
2531
  p.r_last() = r_last;
3,628,317✔
2532
}
3,628,317✔
2533

2534
void score_tracklength_tally(Particle& p, double distance)
1,428,678,011✔
2535
{
2536

2537
  // Determine the tracklength estimate of the flux
2538
  double flux = p.wgt() * distance;
1,428,678,011✔
2539

2540
  score_tracklength_tally_general(p, flux, model::active_tracklength_tallies);
1,428,678,011✔
2541
}
1,428,678,011✔
2542

2543
void score_collision_tally(Particle& p)
108,094,101✔
2544
{
2545
  // Determine the collision estimate of the flux
2546
  double flux = 0.0;
108,094,101✔
2547
  if (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) {
108,094,101✔
2548
    flux = p.wgt_last() / p.macro_xs().total;
76,289,887✔
2549
  }
2550

2551
  // Set 'none value for log union grid index
2552
  int i_log_union = C_NONE;
108,094,101✔
2553

2554
  for (auto i_tally : model::active_collision_tallies) {
239,389,933✔
2555
    const Tally& tally {*model::tallies[i_tally]};
131,295,832✔
2556

2557
    // Initialize an iterator over valid filter bin combinations.  If there are
2558
    // no valid combinations, use a continue statement to ensure we skip the
2559
    // assume_separate break below.
2560
    auto filter_iter = FilterBinIter(tally, p);
131,295,832✔
2561
    auto end = FilterBinIter(tally, true, &p.filter_matches());
131,295,832✔
2562
    if (filter_iter == end)
131,295,832✔
2563
      continue;
57,208,397✔
2564

2565
    // Loop over filter bins.
2566
    for (; filter_iter != end; ++filter_iter) {
239,033,893✔
2567
      auto filter_index = filter_iter.index_;
164,946,458✔
2568
      auto filter_weight = filter_iter.weight_;
164,946,458✔
2569

2570
      // Loop over nuclide bins.
2571
      for (auto i = 0; i < tally.nuclides_.size(); ++i) {
336,071,792✔
2572
        auto i_nuclide = tally.nuclides_[i];
171,125,334✔
2573

2574
        double atom_density = 0.;
171,125,334✔
2575
        if (i_nuclide >= 0) {
171,125,334✔
2576
          const auto& mat = model::materials[p.material()];
10,260,965✔
2577
          auto j = mat->mat_nuclide_index_[i_nuclide];
10,260,965✔
2578
          if (j == C_NONE) {
10,260,965✔
2579
            // Determine log union grid index
2580
            if (i_log_union == C_NONE) {
737,275✔
2581
              int neutron = static_cast<int>(ParticleType::neutron);
305,096✔
2582
              i_log_union = std::log(p.E() / data::energy_min[neutron]) /
305,096✔
2583
                            simulation::log_spacing;
2584
            }
2585

2586
            // Update micro xs cache
2587
            if (!tally.multiply_density()) {
737,275!
UNCOV
2588
              p.update_neutron_xs(i_nuclide, i_log_union);
×
UNCOV
2589
              atom_density = 1.0;
×
2590
            }
2591
          } else {
2592
            atom_density = tally.multiply_density()
19,047,380✔
2593
                             ? mat->atom_density(j, p.density_mult())
9,523,690!
2594
                             : 1.0;
2595
          }
2596
        }
2597

2598
        // TODO: consider replacing this "if" with pointers or templates
2599
        if (settings::run_CE) {
171,125,334✔
2600
          score_general_ce_nonanalog(p, i_tally, i * tally.scores_.size(),
166,292,286✔
2601
            filter_index, filter_weight, i_nuclide, atom_density, flux);
2602
        } else {
2603
          score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index,
4,833,048✔
2604
            filter_weight, i_nuclide, atom_density, flux);
2605
        }
2606
      }
2607
    }
2608

2609
    // If the user has specified that we can assume all tallies are spatially
2610
    // separate, this implies that once a tally has been scored to, we needn't
2611
    // check the others. This cuts down on overhead when there are many
2612
    // tallies specified
2613
    if (settings::assume_separate)
74,087,435!
UNCOV
2614
      break;
×
2615
  }
2616

2617
  // Reset all the filter matches for the next tally event.
2618
  for (auto& match : p.filter_matches())
293,754,362✔
2619
    match.bins_present_ = false;
185,660,261✔
2620
}
108,094,101✔
2621

2622
void score_surface_tally(Particle& p, const vector<int>& tallies)
156,081,678✔
2623
{
2624
  double current = p.wgt_last();
156,081,678✔
2625

2626
  for (auto i_tally : tallies) {
320,521,915✔
2627
    auto& tally {*model::tallies[i_tally]};
164,440,237✔
2628

2629
    // Initialize an iterator over valid filter bin combinations.  If there are
2630
    // no valid combinations, use a continue statement to ensure we skip the
2631
    // assume_separate break below.
2632
    auto filter_iter = FilterBinIter(tally, p);
164,440,237✔
2633
    auto end = FilterBinIter(tally, true, &p.filter_matches());
164,440,237✔
2634
    if (filter_iter == end)
164,440,237✔
2635
      continue;
124,844,649✔
2636

2637
    // Loop over filter bins.
2638
    for (; filter_iter != end; ++filter_iter) {
115,608,472✔
2639
      auto filter_index = filter_iter.index_;
76,012,884✔
2640
      auto filter_weight = filter_iter.weight_;
76,012,884✔
2641

2642
      // Loop over scores.
2643
      // There is only one score type for current tallies so there is no need
2644
      // for a further scoring function.
2645
      double score = current * filter_weight;
76,012,884✔
2646
      for (auto score_index = 0; score_index < tally.scores_.size();
152,025,768✔
2647
           ++score_index) {
2648
#pragma omp atomic
43,554,654✔
2649
        tally.results_(filter_index, score_index, TallyResult::VALUE) += score;
76,012,884✔
2650
      }
2651
    }
2652

2653
    // If the user has specified that we can assume all tallies are spatially
2654
    // separate, this implies that once a tally has been scored to, we needn't
2655
    // check the others. This cuts down on overhead when there are many
2656
    // tallies specified
2657
    if (settings::assume_separate)
39,595,588!
UNCOV
2658
      break;
×
2659
  }
2660

2661
  // Reset all the filter matches for the next tally event.
2662
  for (auto& match : p.filter_matches())
804,131,470✔
2663
    match.bins_present_ = false;
648,049,792✔
2664
}
156,081,678✔
2665

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

2676
  // Save original cell/energy information
2677
  int orig_n_coord = p.n_coord();
5,500✔
2678
  int orig_cell = p.coord(0).cell();
5,500✔
2679
  double orig_E_last = p.E_last();
5,500✔
2680

2681
  for (auto i_tally : tallies) {
11,000✔
2682
    auto& tally {*model::tallies[i_tally]};
5,500✔
2683

2684
    // Determine all CellFilter in the tally
2685
    for (const auto& filter : tally.filters()) {
16,500✔
2686
      auto cell_filter =
2687
        dynamic_cast<CellFilter*>(model::tally_filters[filter].get());
11,000!
2688
      if (cell_filter != nullptr) {
11,000✔
2689

2690
        const auto& cells = cell_filter->cells();
5,500✔
2691
        // Loop over all cells in the CellFilter
2692
        for (auto cell_index = 0; cell_index < cells.size(); ++cell_index) {
11,000✔
2693
          int cell_id = cells[cell_index];
5,500✔
2694

2695
          // Temporarily change cell of particle
2696
          p.n_coord() = 1;
5,500✔
2697
          p.coord(0).cell() = cell_id;
5,500✔
2698

2699
          // Determine index of cell in model::pulse_height_cells
2700
          auto it = std::find(model::pulse_height_cells.begin(),
5,500✔
2701
            model::pulse_height_cells.end(), cell_id);
2702
          int index = std::distance(model::pulse_height_cells.begin(), it);
5,500✔
2703

2704
          // Temporarily change energy of particle to pulse-height value
2705
          p.E_last() = p.pht_storage()[index];
5,500✔
2706

2707
          // Initialize an iterator over valid filter bin combinations. If
2708
          // there are no valid combinations, use a continue statement to ensure
2709
          // we skip the assume_separate break below.
2710
          auto filter_iter = FilterBinIter(tally, p);
5,500✔
2711
          auto end = FilterBinIter(tally, true, &p.filter_matches());
5,500✔
2712
          if (filter_iter == end)
5,500!
UNCOV
2713
            continue;
×
2714

2715
          // Loop over filter bins.
2716
          for (; filter_iter != end; ++filter_iter) {
11,000✔
2717
            auto filter_index = filter_iter.index_;
5,500✔
2718
            auto filter_weight = filter_iter.weight_;
5,500✔
2719

2720
            // Loop over scores.
2721
            for (auto score_index = 0; score_index < tally.scores_.size();
11,000✔
2722
                 ++score_index) {
2723
#pragma omp atomic
3,010✔
2724
              tally.results_(filter_index, score_index, TallyResult::VALUE) +=
5,500✔
2725
                filter_weight;
2726
            }
2727
          }
2728

2729
          // Reset all the filter matches for the next tally event.
2730
          for (auto& match : p.filter_matches())
16,500✔
2731
            match.bins_present_ = false;
11,000✔
2732
        }
2733
      }
2734
    }
2735
    // Restore cell/energy
2736
    p.n_coord() = orig_n_coord;
5,500✔
2737
    p.coord(0).cell() = orig_cell;
5,500✔
2738
    p.E_last() = orig_E_last;
5,500✔
2739
  }
2740
}
5,500✔
2741
} // 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