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

openmc-dev / openmc / 15648508499

14 Jun 2025 04:41AM UTC coverage: 85.162% (+0.04%) from 85.126%
15648508499

Pull #3346

github

web-flow
Merge 95507c5a3 into b11eb0265
Pull Request #3346: Allow specifying number of equiprobable angles for thermal scattering data generation

52373 of 61498 relevant lines covered (85.16%)

36636961.06 hits per line

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

88.97
/src/physics.cpp
1
#include "openmc/physics.h"
2

3
#include "openmc/bank.h"
4
#include "openmc/bremsstrahlung.h"
5
#include "openmc/chain.h"
6
#include "openmc/constants.h"
7
#include "openmc/distribution_multi.h"
8
#include "openmc/eigenvalue.h"
9
#include "openmc/endf.h"
10
#include "openmc/error.h"
11
#include "openmc/ifp.h"
12
#include "openmc/material.h"
13
#include "openmc/math_functions.h"
14
#include "openmc/message_passing.h"
15
#include "openmc/ncrystal_interface.h"
16
#include "openmc/nuclide.h"
17
#include "openmc/photon.h"
18
#include "openmc/physics_common.h"
19
#include "openmc/random_dist.h"
20
#include "openmc/random_lcg.h"
21
#include "openmc/reaction.h"
22
#include "openmc/search.h"
23
#include "openmc/secondary_uncorrelated.h"
24
#include "openmc/settings.h"
25
#include "openmc/simulation.h"
26
#include "openmc/string_utils.h"
27
#include "openmc/tallies/tally.h"
28
#include "openmc/thermal.h"
29
#include "openmc/weight_windows.h"
30

31
#include <fmt/core.h>
32

33
#include <algorithm> // for max, min, max_element
34
#include <cmath>     // for sqrt, exp, log, abs, copysign
35
#include <xtensor/xview.hpp>
36

37
namespace openmc {
38

39
//==============================================================================
40
// Non-member functions
41
//==============================================================================
42

43
void collision(Particle& p)
713,570,518✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
713,570,518✔
47

48
  // Sample reaction for the material the particle is in
49
  switch (p.type()) {
713,570,518✔
50
  case ParticleType::neutron:
651,310,161✔
51
    sample_neutron_reaction(p);
651,310,161✔
52
    break;
651,310,161✔
53
  case ParticleType::photon:
13,743,485✔
54
    sample_photon_reaction(p);
13,743,485✔
55
    break;
13,743,485✔
56
  case ParticleType::electron:
48,430,907✔
57
    sample_electron_reaction(p);
48,430,907✔
58
    break;
48,430,907✔
59
  case ParticleType::positron:
85,965✔
60
    sample_positron_reaction(p);
85,965✔
61
    break;
85,965✔
62
  }
63

64
  if (settings::weight_window_checkpoint_collision)
713,570,518✔
65
    apply_weight_windows(p);
713,570,518✔
66

67
  // Kill particle if energy falls below cutoff
68
  int type = static_cast<int>(p.type());
713,570,518✔
69
  if (p.E() < settings::energy_cutoff[type]) {
713,570,518✔
70
    p.wgt() = 0.0;
4,725,576✔
71
  }
72

73
  // Display information about collision
74
  if (settings::verbosity >= 10 || p.trace()) {
713,570,518✔
75
    std::string msg;
77✔
76
    if (p.event() == TallyEvent::KILL) {
77✔
77
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
78
    } else if (p.type() == ParticleType::neutron) {
77✔
79
      msg = fmt::format("    {} with {}. Energy = {} eV.",
217✔
80
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
154✔
81
        p.E());
77✔
82
    } else if (p.type() == ParticleType::photon) {
×
83
      msg = fmt::format("    {} with {}. Energy = {} eV.",
×
84
        reaction_name(p.event_mt()),
×
85
        to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
×
86
    } else {
87
      msg = fmt::format("    Disappeared. Energy = {} eV.", p.E());
×
88
    }
89
    write_message(msg, 1);
77✔
90
  }
77✔
91
}
713,570,518✔
92

93
void sample_neutron_reaction(Particle& p)
651,310,161✔
94
{
95
  // Sample a nuclide within the material
96
  int i_nuclide = sample_nuclide(p);
651,310,161✔
97

98
  // Save which nuclide particle had collision with
99
  p.event_nuclide() = i_nuclide;
651,310,161✔
100

101
  // Create fission bank sites. Note that while a fission reaction is sampled,
102
  // it never actually "happens", i.e. the weight of the particle does not
103
  // change when sampling fission sites. The following block handles all
104
  // absorption (including fission)
105

106
  const auto& nuc {data::nuclides[i_nuclide]};
651,310,161✔
107

108
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
651,310,161✔
109
    auto& rx = sample_fission(i_nuclide, p);
83,458,241✔
110
    if (settings::run_mode == RunMode::EIGENVALUE) {
83,458,241✔
111
      create_fission_sites(p, i_nuclide, rx);
78,374,240✔
112
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
5,084,001✔
113
               settings::create_fission_neutrons) {
114
      create_fission_sites(p, i_nuclide, rx);
5,070,097✔
115

116
      // Make sure particle population doesn't grow out of control for
117
      // subcritical multiplication problems.
118
      if (p.secondary_bank().size() >= 10000) {
5,070,097✔
119
        fatal_error(
×
120
          "The secondary particle bank appears to be growing without "
121
          "bound. You are likely running a subcritical multiplication problem "
122
          "with k-effective close to or greater than one.");
123
      }
124
    }
125
  }
126

127
  // Create secondary photons
128
  if (settings::photon_transport) {
651,310,161✔
129
    sample_secondary_photons(p, i_nuclide);
8,919,086✔
130
  }
131

132
  // If survival biasing is being used, the following subroutine adjusts the
133
  // weight of the particle. Otherwise, it checks to see if absorption occurs
134

135
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
651,310,161✔
136
    absorption(p, i_nuclide);
651,306,334✔
137
  }
138
  if (!p.alive())
651,310,161✔
139
    return;
16,038,444✔
140

141
  // Sample a scattering reaction and determine the secondary energy of the
142
  // exiting neutron
143
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
635,271,717✔
144
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
635,271,717✔
145
    ncrystal_mat.scatter(p);
158,829✔
146
  } else {
147
    scatter(p, i_nuclide);
635,112,888✔
148
  }
149

150
  // Advance URR seed stream 'N' times after energy changes
151
  if (p.E() != p.E_last()) {
635,271,717✔
152
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
634,948,548✔
153
  }
154

155
  // Play russian roulette if survival biasing is turned on
156
  if (settings::survival_biasing) {
635,271,717✔
157
    // if survival normalization is on, use normalized weight cutoff and
158
    // normalized weight survive
159
    if (settings::survival_normalization) {
511,401✔
160
      if (p.wgt() < settings::weight_cutoff * p.wgt_born()) {
×
161
        russian_roulette(p, settings::weight_survive * p.wgt_born());
×
162
      }
163
    } else if (p.wgt() < settings::weight_cutoff) {
511,401✔
164
      russian_roulette(p, settings::weight_survive);
58,652✔
165
    }
166
  }
167
}
168

169
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
83,444,337✔
170
{
171
  // If uniform fission source weighting is turned on, we increase or decrease
172
  // the expected number of fission sites produced
173
  double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
83,444,337✔
174

175
  // Determine the expected number of neutrons produced
176
  double nu_t = p.wgt() / simulation::keff * weight *
83,444,337✔
177
                p.neutron_xs(i_nuclide).nu_fission /
83,444,337✔
178
                p.neutron_xs(i_nuclide).total;
83,444,337✔
179

180
  // Sample the number of neutrons produced
181
  int nu = static_cast<int>(nu_t);
83,444,337✔
182
  if (prn(p.current_seed()) <= (nu_t - nu))
83,444,337✔
183
    ++nu;
13,601,885✔
184

185
  // If no neutrons were produced then don't continue
186
  if (nu == 0)
83,444,337✔
187
    return;
65,925,926✔
188

189
  // Initialize the counter of delayed neutrons encountered for each delayed
190
  // group.
191
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
17,518,411✔
192

193
  // Clear out particle's nu fission bank
194
  p.nu_bank().clear();
17,518,411✔
195

196
  p.fission() = true;
17,518,411✔
197

198
  // Determine whether to place fission sites into the shared fission bank
199
  // or the secondary particle bank.
200
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
17,518,411✔
201

202
  // Counter for the number of fission sites successfully stored to the shared
203
  // fission bank or the secondary particle bank
204
  int n_sites_stored;
205

206
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
40,120,721✔
207
    // Initialize fission site object with particle data
208
    SourceSite site;
22,602,310✔
209
    site.r = p.r();
22,602,310✔
210
    site.particle = ParticleType::neutron;
22,602,310✔
211
    site.time = p.time();
22,602,310✔
212
    site.wgt = 1. / weight;
22,602,310✔
213
    site.parent_id = p.id();
22,602,310✔
214
    site.progeny_id = p.n_progeny()++;
22,602,310✔
215
    site.surf_id = 0;
22,602,310✔
216

217
    // Sample delayed group and angle/energy for fission reaction
218
    sample_fission_neutron(i_nuclide, rx, &site, p);
22,602,310✔
219

220
    // Store fission site in bank
221
    if (use_fission_bank) {
22,602,310✔
222
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
22,263,555✔
223
      if (idx == -1) {
22,263,555✔
224
        warning(
×
225
          "The shared fission bank is full. Additional fission sites created "
226
          "in this generation will not be banked. Results may be "
227
          "non-deterministic.");
228

229
        // Decrement number of particle progeny as storage was unsuccessful.
230
        // This step is needed so that the sum of all progeny is equal to the
231
        // size of the shared fission bank.
232
        p.n_progeny()--;
×
233

234
        // Break out of loop as no more sites can be added to fission bank
235
        break;
×
236
      }
237
      // Iterated Fission Probability (IFP) method
238
      if (settings::ifp_on) {
22,263,555✔
239
        ifp(p, site, idx);
221,925✔
240
      }
241
    } else {
242
      p.secondary_bank().push_back(site);
338,755✔
243
    }
244

245
    // Set the delayed group on the particle as well
246
    p.delayed_group() = site.delayed_group;
22,602,310✔
247

248
    // Increment the number of neutrons born delayed
249
    if (p.delayed_group() > 0) {
22,602,310✔
250
      nu_d[p.delayed_group() - 1]++;
137,742✔
251
    }
252

253
    // Write fission particles to nuBank
254
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
22,602,310✔
255
    nu_bank_entry.wgt = site.wgt;
22,602,310✔
256
    nu_bank_entry.E = site.E;
22,602,310✔
257
    nu_bank_entry.delayed_group = site.delayed_group;
22,602,310✔
258
  }
259

260
  // If shared fission bank was full, and no fissions could be added,
261
  // set the particle fission flag to false.
262
  if (n_sites_stored == 0) {
17,518,411✔
263
    p.fission() = false;
×
264
    return;
×
265
  }
266

267
  // Set nu to the number of fission sites successfully stored. If the fission
268
  // bank was not found to be full then these values are already equivalent.
269
  nu = n_sites_stored;
17,518,411✔
270

271
  // Store the total weight banked for analog fission tallies
272
  p.n_bank() = nu;
17,518,411✔
273
  p.wgt_bank() = nu / weight;
17,518,411✔
274
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
157,665,699✔
275
    p.n_delayed_bank(d) = nu_d[d];
140,147,288✔
276
  }
277
}
278

279
void sample_photon_reaction(Particle& p)
13,743,485✔
280
{
281
  // Kill photon if below energy cutoff -- an extra check is made here because
282
  // photons with energy below the cutoff may have been produced by neutrons
283
  // reactions or atomic relaxation
284
  int photon = static_cast<int>(ParticleType::photon);
13,743,485✔
285
  if (p.E() < settings::energy_cutoff[photon]) {
13,743,485✔
286
    p.E() = 0.0;
×
287
    p.wgt() = 0.0;
×
288
    return;
×
289
  }
290

291
  // Sample element within material
292
  int i_element = sample_element(p);
13,743,485✔
293
  const auto& micro {p.photon_xs(i_element)};
13,743,485✔
294
  const auto& element {*data::elements[i_element]};
13,743,485✔
295

296
  // Calculate photon energy over electron rest mass equivalent
297
  double alpha = p.E() / MASS_ELECTRON_EV;
13,743,485✔
298

299
  // For tallying purposes, this routine might be called directly. In that
300
  // case, we need to sample a reaction via the cutoff variable
301
  double prob = 0.0;
13,743,485✔
302
  double cutoff = prn(p.current_seed()) * micro.total;
13,743,485✔
303

304
  // Coherent (Rayleigh) scattering
305
  prob += micro.coherent;
13,743,485✔
306
  if (prob > cutoff) {
13,743,485✔
307
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
734,318✔
308
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
734,318✔
309
    p.event() = TallyEvent::SCATTER;
734,318✔
310
    p.event_mt() = COHERENT;
734,318✔
311
    return;
734,318✔
312
  }
313

314
  // Incoherent (Compton) scattering
315
  prob += micro.incoherent;
13,009,167✔
316
  if (prob > cutoff) {
13,009,167✔
317
    double alpha_out;
318
    int i_shell;
319
    element.compton_scatter(
16,589,028✔
320
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
8,294,514✔
321

322
    // Determine binding energy of shell. The binding energy is 0.0 if
323
    // doppler broadening is not used.
324
    double e_b;
325
    if (i_shell == -1) {
8,294,514✔
326
      e_b = 0.0;
×
327
    } else {
328
      e_b = element.binding_energy_[i_shell];
8,294,514✔
329
    }
330

331
    // Create Compton electron
332
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
8,294,514✔
333
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
8,294,514✔
334
    int electron = static_cast<int>(ParticleType::electron);
8,294,514✔
335
    if (E_electron >= settings::energy_cutoff[electron]) {
8,294,514✔
336
      double mu_electron = (alpha - alpha_out * p.mu()) /
8,217,599✔
337
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
16,435,198✔
338
                                     2.0 * alpha * alpha_out * p.mu());
8,217,599✔
339
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
8,217,599✔
340
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
8,217,599✔
341
    }
342

343
    // Allow electrons to fill orbital and produce Auger electrons and
344
    // fluorescent photons. Since Compton subshell data does not match atomic
345
    // relaxation data, use the mapping between the data to find the subshell
346
    if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
8,294,514✔
347
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
8,294,514✔
348
    }
349

350
    phi += PI;
8,294,514✔
351
    p.E() = alpha_out * MASS_ELECTRON_EV;
8,294,514✔
352
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
8,294,514✔
353
    p.event() = TallyEvent::SCATTER;
8,294,514✔
354
    p.event_mt() = INCOHERENT;
8,294,514✔
355
    return;
8,294,514✔
356
  }
357

358
  // Photoelectric effect
359
  double prob_after = prob + micro.photoelectric;
4,714,653✔
360

361
  if (prob_after > cutoff) {
4,714,653✔
362
    // Get grid index, interpolation factor, and bounding subshell
363
    // cross sections
364
    int i_grid = micro.index_grid;
4,628,688✔
365
    double f = micro.interp_factor;
4,628,688✔
366
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
4,628,688✔
367
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
4,628,688✔
368

369
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
23,513,997✔
370
      const auto& shell {element.shells_[i_shell]};
23,513,997✔
371

372
      // Check threshold of reaction
373
      if (xs_lower(i_shell) == 0)
23,513,997✔
374
        continue;
10,124,873✔
375

376
      //  Evaluation subshell photoionization cross section
377
      prob += std::exp(
13,389,124✔
378
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
13,389,124✔
379

380
      if (prob > cutoff) {
13,389,124✔
381
        // Determine binding energy based on whether atomic relaxation data is
382
        // present (if not, use value from Compton profile data)
383
        double binding_energy = element.has_atomic_relaxation_
4,628,688✔
384
                                  ? shell.binding_energy
4,628,688✔
385
                                  : element.binding_energy_[i_shell];
×
386

387
        // Determine energy of secondary electron
388
        double E_electron = p.E() - binding_energy;
4,628,688✔
389

390
        // Sample mu using non-relativistic Sauter distribution.
391
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
392
        // model in Serpent 2" by Toni Kaltiaisenaho
393
        double mu;
394
        while (true) {
395
          double r = prn(p.current_seed());
6,947,365✔
396
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
6,947,365✔
397
            double rel_vel =
398
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
4,628,688✔
399
              (E_electron + MASS_ELECTRON_EV);
4,628,688✔
400
            mu =
4,628,688✔
401
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
4,628,688✔
402
            break;
4,628,688✔
403
          }
404
        }
2,318,677✔
405

406
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
4,628,688✔
407
        Direction u;
4,628,688✔
408
        u.x = mu;
4,628,688✔
409
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
4,628,688✔
410
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
4,628,688✔
411

412
        // Create secondary electron
413
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
4,628,688✔
414

415
        // Allow electrons to fill orbital and produce auger electrons
416
        // and fluorescent photons
417
        element.atomic_relaxation(i_shell, p);
4,628,688✔
418
        p.event() = TallyEvent::ABSORB;
4,628,688✔
419
        p.event_mt() = 533 + shell.index_subshell;
4,628,688✔
420
        p.wgt() = 0.0;
4,628,688✔
421
        p.E() = 0.0;
4,628,688✔
422
        return;
4,628,688✔
423
      }
424
    }
425
  }
9,257,376✔
426
  prob = prob_after;
85,965✔
427

428
  // Pair production
429
  prob += micro.pair_production;
85,965✔
430
  if (prob > cutoff) {
85,965✔
431
    double E_electron, E_positron;
432
    double mu_electron, mu_positron;
433
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
85,965✔
434
      &mu_positron, p.current_seed());
435

436
    // Create secondary electron
437
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
85,965✔
438
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
85,965✔
439

440
    // Create secondary positron
441
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
85,965✔
442
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
85,965✔
443

444
    p.event() = TallyEvent::ABSORB;
85,965✔
445
    p.event_mt() = PAIR_PROD;
85,965✔
446
    p.wgt() = 0.0;
85,965✔
447
    p.E() = 0.0;
85,965✔
448
  }
449
}
450

451
void sample_electron_reaction(Particle& p)
48,430,907✔
452
{
453
  // TODO: create reaction types
454

455
  if (settings::electron_treatment == ElectronTreatment::TTB) {
48,430,907✔
456
    double E_lost;
457
    thick_target_bremsstrahlung(p, &E_lost);
48,428,245✔
458
  }
459

460
  p.E() = 0.0;
48,430,907✔
461
  p.wgt() = 0.0;
48,430,907✔
462
  p.event() = TallyEvent::ABSORB;
48,430,907✔
463
}
48,430,907✔
464

465
void sample_positron_reaction(Particle& p)
85,965✔
466
{
467
  // TODO: create reaction types
468

469
  if (settings::electron_treatment == ElectronTreatment::TTB) {
85,965✔
470
    double E_lost;
471
    thick_target_bremsstrahlung(p, &E_lost);
85,954✔
472
  }
473

474
  // Sample angle isotropically
475
  Direction u = isotropic_direction(p.current_seed());
85,965✔
476

477
  // Create annihilation photon pair traveling in opposite directions
478
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
85,965✔
479
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
85,965✔
480

481
  p.E() = 0.0;
85,965✔
482
  p.wgt() = 0.0;
85,965✔
483
  p.event() = TallyEvent::ABSORB;
85,965✔
484
}
85,965✔
485

486
int sample_nuclide(Particle& p)
651,310,161✔
487
{
488
  // Sample cumulative distribution function
489
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
651,310,161✔
490

491
  // Get pointers to nuclide/density arrays
492
  const auto& mat {model::materials[p.material()]};
651,310,161✔
493
  int n = mat->nuclide_.size();
651,310,161✔
494

495
  double prob = 0.0;
651,310,161✔
496
  for (int i = 0; i < n; ++i) {
1,455,303,346✔
497
    // Get atom density
498
    int i_nuclide = mat->nuclide_[i];
1,455,303,346✔
499
    double atom_density = mat->atom_density_[i];
1,455,303,346✔
500

501
    // Increment probability to compare to cutoff
502
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,455,303,346✔
503
    if (prob >= cutoff)
1,455,303,346✔
504
      return i_nuclide;
651,310,161✔
505
  }
506

507
  // If we reach here, no nuclide was sampled
508
  p.write_restart();
×
509
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
510
}
511

512
int sample_element(Particle& p)
13,743,485✔
513
{
514
  // Sample cumulative distribution function
515
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
13,743,485✔
516

517
  // Get pointers to elements, densities
518
  const auto& mat {model::materials[p.material()]};
13,743,485✔
519

520
  double prob = 0.0;
13,743,485✔
521
  for (int i = 0; i < mat->element_.size(); ++i) {
34,671,370✔
522
    // Find atom density
523
    int i_element = mat->element_[i];
34,671,370✔
524
    double atom_density = mat->atom_density_[i];
34,671,370✔
525

526
    // Determine microscopic cross section
527
    double sigma = atom_density * p.photon_xs(i_element).total;
34,671,370✔
528

529
    // Increment probability to compare to cutoff
530
    prob += sigma;
34,671,370✔
531
    if (prob > cutoff) {
34,671,370✔
532
      // Save which nuclide particle had collision with for tally purpose
533
      p.event_nuclide() = mat->nuclide_[i];
13,743,485✔
534

535
      return i_element;
13,743,485✔
536
    }
537
  }
538

539
  // If we made it here, no element was sampled
540
  p.write_restart();
×
541
  fatal_error("Did not sample any element during collision.");
×
542
}
543

544
Reaction& sample_fission(int i_nuclide, Particle& p)
83,458,241✔
545
{
546
  // Get pointer to nuclide
547
  const auto& nuc {data::nuclides[i_nuclide]};
83,458,241✔
548

549
  // If we're in the URR, by default use the first fission reaction. We also
550
  // default to the first reaction if we know that there are no partial fission
551
  // reactions
552
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
83,458,241✔
553
    return *nuc->fission_rx_[0];
83,433,179✔
554
  }
555

556
  // Check to see if we are in a windowed multipole range.  WMP only supports
557
  // the first fission reaction.
558
  if (nuc->multipole_) {
25,062✔
559
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
560
      return *nuc->fission_rx_[0];
×
561
    }
562
  }
563

564
  // Get grid index and interpolation factor and sample fission cdf
565
  const auto& micro = p.neutron_xs(i_nuclide);
25,062✔
566
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
25,062✔
567
  double prob = 0.0;
25,062✔
568

569
  // Loop through each partial fission reaction type
570
  for (auto& rx : nuc->fission_rx_) {
25,076✔
571
    // add to cumulative probability
572
    prob += rx->xs(micro);
25,076✔
573

574
    // Create fission bank sites if fission occurs
575
    if (prob > cutoff)
25,076✔
576
      return *rx;
25,062✔
577
  }
578

579
  // If we reached here, no reaction was sampled
580
  throw std::runtime_error {
×
581
    "No fission reaction was sampled for " + nuc->name_};
×
582
}
583

584
void sample_photon_product(
1,327,073✔
585
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
586
{
587
  // Get grid index and interpolation factor and sample photon production cdf
588
  const auto& micro = p.neutron_xs(i_nuclide);
1,327,073✔
589
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,327,073✔
590
  double prob = 0.0;
1,327,073✔
591

592
  // Loop through each reaction type
593
  const auto& nuc {data::nuclides[i_nuclide]};
1,327,073✔
594
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
21,897,095✔
595
    // Evaluate neutron cross section
596
    const auto& rx = nuc->reactions_[i];
21,897,095✔
597
    double xs = rx->xs(micro);
21,897,095✔
598

599
    // if cross section is zero for this reaction, skip it
600
    if (xs == 0.0)
21,897,095✔
601
      continue;
7,157,326✔
602

603
    for (int j = 0; j < rx->products_.size(); ++j) {
69,049,596✔
604
      if (rx->products_[j].particle_ == ParticleType::photon) {
55,636,900✔
605
        // For fission, artificially increase the photon yield to account
606
        // for delayed photons
607
        double f = 1.0;
43,308,595✔
608
        if (settings::delayed_photon_scaling) {
43,308,595✔
609
          if (is_fission(rx->mt_)) {
43,308,595✔
610
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
525,778✔
611
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
525,778✔
612
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
525,778✔
613
              f = (energy_prompt + energy_delayed) / (energy_prompt);
525,778✔
614
            }
615
          }
616
        }
617

618
        // add to cumulative probability
619
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
43,308,595✔
620

621
        *i_rx = i;
43,308,595✔
622
        *i_product = j;
43,308,595✔
623
        if (prob > cutoff)
43,308,595✔
624
          return;
1,327,073✔
625
      }
626
    }
627
  }
628
}
629

630
void absorption(Particle& p, int i_nuclide)
651,306,334✔
631
{
632
  if (settings::survival_biasing) {
651,306,334✔
633
    // Determine weight absorbed in survival biasing
634
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
511,401✔
635
                              p.neutron_xs(i_nuclide).total;
511,401✔
636

637
    // Adjust weight of particle by probability of absorption
638
    p.wgt() -= wgt_absorb;
511,401✔
639

640
    // Score implicit absorption estimate of keff
641
    if (settings::run_mode == RunMode::EIGENVALUE) {
511,401✔
642
      p.keff_tally_absorption() += wgt_absorb *
511,401✔
643
                                   p.neutron_xs(i_nuclide).nu_fission /
511,401✔
644
                                   p.neutron_xs(i_nuclide).absorption;
511,401✔
645
    }
646
  } else {
647
    // See if disappearance reaction happens
648
    if (p.neutron_xs(i_nuclide).absorption >
650,794,933✔
649
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
650,794,933✔
650
      // Score absorption estimate of keff
651
      if (settings::run_mode == RunMode::EIGENVALUE) {
16,038,444✔
652
        p.keff_tally_absorption() += p.wgt() *
27,098,546✔
653
                                     p.neutron_xs(i_nuclide).nu_fission /
13,549,273✔
654
                                     p.neutron_xs(i_nuclide).absorption;
13,549,273✔
655
      }
656

657
      p.wgt() = 0.0;
16,038,444✔
658
      p.event() = TallyEvent::ABSORB;
16,038,444✔
659
      p.event_mt() = N_DISAPPEAR;
16,038,444✔
660
    }
661
  }
662
}
651,306,334✔
663

664
void scatter(Particle& p, int i_nuclide)
635,112,888✔
665
{
666
  // copy incoming direction
667
  Direction u_old {p.u()};
635,112,888✔
668

669
  // Get pointer to nuclide and grid index/interpolation factor
670
  const auto& nuc {data::nuclides[i_nuclide]};
635,112,888✔
671
  const auto& micro {p.neutron_xs(i_nuclide)};
635,112,888✔
672
  int i_temp = micro.index_temp;
635,112,888✔
673

674
  // For tallying purposes, this routine might be called directly. In that
675
  // case, we need to sample a reaction via the cutoff variable
676
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
635,112,888✔
677
  bool sampled = false;
635,112,888✔
678

679
  // Calculate elastic cross section if it wasn't precalculated
680
  if (micro.elastic == CACHE_INVALID) {
635,112,888✔
681
    nuc->calculate_elastic_xs(p);
531,257,924✔
682
  }
683

684
  double prob = micro.elastic - micro.thermal;
635,112,888✔
685
  if (prob > cutoff) {
635,112,888✔
686
    // =======================================================================
687
    // NON-S(A,B) ELASTIC SCATTERING
688

689
    // Determine temperature
690
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
552,463,282✔
691

692
    // Perform collision physics for elastic scattering
693
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
552,463,282✔
694

695
    p.event_mt() = ELASTIC;
552,463,282✔
696
    sampled = true;
552,463,282✔
697
  }
698

699
  prob = micro.elastic;
635,112,888✔
700
  if (prob > cutoff && !sampled) {
635,112,888✔
701
    // =======================================================================
702
    // S(A,B) SCATTERING
703

704
    sab_scatter(i_nuclide, micro.index_sab, p);
70,733,105✔
705

706
    p.event_mt() = ELASTIC;
70,733,105✔
707
    sampled = true;
70,733,105✔
708
  }
709

710
  if (!sampled) {
635,112,888✔
711
    // =======================================================================
712
    // INELASTIC SCATTERING
713

714
    int n = nuc->index_inelastic_scatter_.size();
11,916,501✔
715
    int i = 0;
11,916,501✔
716
    for (int j = 0; j < n && prob < cutoff; ++j) {
211,160,630✔
717
      i = nuc->index_inelastic_scatter_[j];
199,244,129✔
718

719
      // add to cumulative probability
720
      prob += nuc->reactions_[i]->xs(micro);
199,244,129✔
721
    }
722

723
    // Perform collision physics for inelastic scattering
724
    const auto& rx {nuc->reactions_[i]};
11,916,501✔
725
    inelastic_scatter(*nuc, *rx, p);
11,916,501✔
726
    p.event_mt() = rx->mt_;
11,916,501✔
727
  }
728

729
  // Set event component
730
  p.event() = TallyEvent::SCATTER;
635,112,888✔
731

732
  // Sample new outgoing angle for isotropic-in-lab scattering
733
  const auto& mat {model::materials[p.material()]};
635,112,888✔
734
  if (!mat->p0_.empty()) {
635,112,888✔
735
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
311,531✔
736
    if (mat->p0_[i_nuc_mat]) {
311,531✔
737
      // Sample isotropic-in-lab outgoing direction
738
      p.u() = isotropic_direction(p.current_seed());
311,531✔
739
      p.mu() = u_old.dot(p.u());
311,531✔
740
    }
741
  }
742
}
635,112,888✔
743

744
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
552,463,282✔
745
{
746
  // get pointer to nuclide
747
  const auto& nuc {data::nuclides[i_nuclide]};
552,463,282✔
748

749
  double vel = std::sqrt(p.E());
552,463,282✔
750
  double awr = nuc->awr_;
552,463,282✔
751

752
  // Neutron velocity in LAB
753
  Direction v_n = vel * p.u();
552,463,282✔
754

755
  // Sample velocity of target nucleus
756
  Direction v_t {};
552,463,282✔
757
  if (!p.neutron_xs(i_nuclide).use_ptable) {
552,463,282✔
758
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,070,364,776✔
759
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
535,182,388✔
760
  }
761

762
  // Velocity of center-of-mass
763
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
552,463,282✔
764

765
  // Transform to CM frame
766
  v_n -= v_cm;
552,463,282✔
767

768
  // Find speed of neutron in CM
769
  vel = v_n.norm();
552,463,282✔
770

771
  // Sample scattering angle, checking if angle distribution is present (assume
772
  // isotropic otherwise)
773
  double mu_cm;
774
  auto& d = rx.products_[0].distribution_[0];
552,463,282✔
775
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
552,463,282✔
776
  if (!d_->angle().empty()) {
552,463,282✔
777
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
552,463,282✔
778
  } else {
779
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
780
  }
781

782
  // Determine direction cosines in CM
783
  Direction u_cm = v_n / vel;
552,463,282✔
784

785
  // Rotate neutron velocity vector to new angle -- note that the speed of the
786
  // neutron in CM does not change in elastic scattering. However, the speed
787
  // will change when we convert back to LAB
788
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
552,463,282✔
789

790
  // Transform back to LAB frame
791
  v_n += v_cm;
552,463,282✔
792

793
  p.E() = v_n.dot(v_n);
552,463,282✔
794
  vel = std::sqrt(p.E());
552,463,282✔
795

796
  // compute cosine of scattering angle in LAB frame by taking dot product of
797
  // neutron's pre- and post-collision angle
798
  p.mu() = p.u().dot(v_n) / vel;
552,463,282✔
799

800
  // Set energy and direction of particle in LAB frame
801
  p.u() = v_n / vel;
552,463,282✔
802

803
  // Because of floating-point roundoff, it may be possible for mu_lab to be
804
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
805
  // -1 or 1
806
  if (std::abs(p.mu()) > 1.0)
552,463,282✔
807
    p.mu() = std::copysign(1.0, p.mu());
×
808
}
552,463,282✔
809

810
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
70,733,105✔
811
{
812
  // Determine temperature index
813
  const auto& micro {p.neutron_xs(i_nuclide)};
70,733,105✔
814
  int i_temp = micro.index_temp_sab;
70,733,105✔
815

816
  // Sample energy and angle
817
  double E_out;
818
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
141,466,210✔
819
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
70,733,105✔
820

821
  // Set energy to outgoing, change direction of particle
822
  p.E() = E_out;
70,733,105✔
823
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
70,733,105✔
824
}
70,733,105✔
825

826
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
535,182,388✔
827
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
828
{
829
  // check if nuclide is a resonant scatterer
830
  ResScatMethod sampling_method;
831
  if (nuc.resonant_) {
535,182,388✔
832

833
    // sampling method to use
834
    sampling_method = settings::res_scat_method;
83,050✔
835

836
    // upper resonance scattering energy bound (target is at rest above this E)
837
    if (E > settings::res_scat_energy_max) {
83,050✔
838
      return {};
40,513✔
839

840
      // lower resonance scattering energy bound (should be no resonances below)
841
    } else if (E < settings::res_scat_energy_min) {
42,537✔
842
      sampling_method = ResScatMethod::cxs;
23,881✔
843
    }
844

845
    // otherwise, use free gas model
846
  } else {
847
    if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) {
535,099,338✔
848
      return {};
211,539,982✔
849
    } else {
850
      sampling_method = ResScatMethod::cxs;
323,559,356✔
851
    }
852
  }
853

854
  // use appropriate target velocity sampling method
855
  switch (sampling_method) {
323,601,893✔
856
  case ResScatMethod::cxs:
323,583,237✔
857

858
    // sample target velocity with the constant cross section (cxs) approx.
859
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
323,583,237✔
860

861
  case ResScatMethod::dbrc:
18,656✔
862
  case ResScatMethod::rvs: {
863
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,656✔
864
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
18,656✔
865
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,656✔
866

867
    // find lower and upper energy bound indices
868
    // lower index
869
    int i_E_low;
870
    if (E_low < nuc.energy_0K_.front()) {
18,656✔
871
      i_E_low = 0;
×
872
    } else if (E_low > nuc.energy_0K_.back()) {
18,656✔
873
      i_E_low = nuc.energy_0K_.size() - 2;
×
874
    } else {
875
      i_E_low =
18,656✔
876
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
18,656✔
877
    }
878

879
    // upper index
880
    int i_E_up;
881
    if (E_up < nuc.energy_0K_.front()) {
18,656✔
882
      i_E_up = 0;
×
883
    } else if (E_up > nuc.energy_0K_.back()) {
18,656✔
884
      i_E_up = nuc.energy_0K_.size() - 2;
×
885
    } else {
886
      i_E_up =
18,656✔
887
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
18,656✔
888
    }
889

890
    if (i_E_up == i_E_low) {
18,656✔
891
      // Handle degenerate case -- if the upper/lower bounds occur for the same
892
      // index, then using cxs is probably a good approximation
893
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
18,656✔
894
    }
895

896
    if (sampling_method == ResScatMethod::dbrc) {
15,455✔
897
      // interpolate xs since we're not exactly at the energy indices
898
      double xs_low = nuc.elastic_0K_[i_E_low];
×
899
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
900
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
901
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
902
      double xs_up = nuc.elastic_0K_[i_E_up];
×
903
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
904
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
905
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
906

907
      // get max 0K xs value over range of practical relative energies
908
      double xs_max = *std::max_element(
×
909
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
910
      xs_max = std::max({xs_low, xs_max, xs_up});
×
911

912
      while (true) {
913
        double E_rel;
914
        Direction v_target;
×
915
        while (true) {
916
          // sample target velocity with the constant cross section (cxs)
917
          // approx.
918
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
919
          Direction v_rel = v_neut - v_target;
×
920
          E_rel = v_rel.dot(v_rel);
×
921
          if (E_rel < E_up)
×
922
            break;
×
923
        }
924

925
        // perform Doppler broadening rejection correction (dbrc)
926
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
927
        double R = xs_0K / xs_max;
×
928
        if (prn(seed) < R)
×
929
          return v_target;
×
930
      }
931

932
    } else if (sampling_method == ResScatMethod::rvs) {
15,455✔
933
      // interpolate xs CDF since we're not exactly at the energy indices
934
      // cdf value at lower bound attainable energy
935
      double cdf_low = 0.0;
15,455✔
936
      if (E_low > nuc.energy_0K_.front()) {
15,455✔
937
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
15,455✔
938
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
15,455✔
939
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
15,455✔
940
      }
941

942
      // cdf value at upper bound attainable energy
943
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,455✔
944
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,455✔
945
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,455✔
946

947
      while (true) {
948
        // directly sample Maxwellian
949
        double E_t = -kT * std::log(prn(seed));
162,547✔
950

951
        // sample a relative energy using the xs cdf
952
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
162,547✔
953
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
162,547✔
954
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
162,547✔
955
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
162,547✔
956
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
162,547✔
957
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
162,547✔
958
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
162,547✔
959
                     nuc.energy_0K_[i_E_low + i_E_rel]);
162,547✔
960
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
162,547✔
961

962
        // perform rejection sampling on cosine between
963
        // neutron and target velocities
964
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
162,547✔
965
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
162,547✔
966

967
        if (std::abs(mu) < 1.0) {
162,547✔
968
          // set and accept target velocity
969
          E_t /= nuc.awr_;
15,455✔
970
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,455✔
971
        }
972
      }
147,092✔
973
    }
974
  } // case RVS, DBRC
975
  } // switch (sampling_method)
976

977
  UNREACHABLE();
×
978
}
979

980
Direction sample_cxs_target_velocity(
323,586,438✔
981
  double awr, double E, Direction u, double kT, uint64_t* seed)
982
{
983
  double beta_vn = std::sqrt(awr * E / kT);
323,586,438✔
984
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
323,586,438✔
985

986
  double beta_vt_sq;
987
  double mu;
988
  while (true) {
989
    // Sample two random numbers
990
    double r1 = prn(seed);
384,004,853✔
991
    double r2 = prn(seed);
384,004,853✔
992

993
    if (prn(seed) < alpha) {
384,004,853✔
994
      // With probability alpha, we sample the distribution p(y) =
995
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
996
      // Carlo sampler
997

998
      beta_vt_sq = -std::log(r1 * r2);
98,227,038✔
999

1000
    } else {
1001
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1002
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1003
      // Carlo sampler
1004

1005
      double c = std::cos(PI / 2.0 * prn(seed));
285,777,815✔
1006
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
285,777,815✔
1007
    }
1008

1009
    // Determine beta * vt
1010
    double beta_vt = std::sqrt(beta_vt_sq);
384,004,853✔
1011

1012
    // Sample cosine of angle between neutron and target velocity
1013
    mu = uniform_distribution(-1., 1., seed);
384,004,853✔
1014

1015
    // Determine rejection probability
1016
    double accept_prob =
1017
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
384,004,853✔
1018
      (beta_vn + beta_vt);
384,004,853✔
1019

1020
    // Perform rejection sampling on vt and mu
1021
    if (prn(seed) < accept_prob)
384,004,853✔
1022
      break;
323,586,438✔
1023
  }
60,418,415✔
1024

1025
  // Determine speed of target nucleus
1026
  double vt = std::sqrt(beta_vt_sq * kT / awr);
323,586,438✔
1027

1028
  // Determine velocity vector of target nucleus based on neutron's velocity
1029
  // and the sampled angle between them
1030
  return vt * rotate_angle(u, mu, nullptr, seed);
323,586,438✔
1031
}
1032

1033
void sample_fission_neutron(
22,602,310✔
1034
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1035
{
1036
  // Get attributes of particle
1037
  double E_in = p.E();
22,602,310✔
1038
  uint64_t* seed = p.current_seed();
22,602,310✔
1039

1040
  // Determine total nu, delayed nu, and delayed neutron fraction
1041
  const auto& nuc {data::nuclides[i_nuclide]};
22,602,310✔
1042
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
22,602,310✔
1043
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
22,602,310✔
1044
  double beta = nu_d / nu_t;
22,602,310✔
1045

1046
  if (prn(seed) < beta) {
22,602,310✔
1047
    // ====================================================================
1048
    // DELAYED NEUTRON SAMPLED
1049

1050
    // sampled delayed precursor group
1051
    double xi = prn(seed) * nu_d;
137,742✔
1052
    double prob = 0.0;
137,742✔
1053
    int group;
1054
    for (group = 1; group < nuc->n_precursor_; ++group) {
518,028✔
1055
      // determine delayed neutron precursor yield for group j
1056
      double yield = (*rx.products_[group].yield_)(E_in);
507,015✔
1057

1058
      // Check if this group is sampled
1059
      prob += yield;
507,015✔
1060
      if (xi < prob)
507,015✔
1061
        break;
126,729✔
1062
    }
1063

1064
    // if the sum of the probabilities is slightly less than one and the
1065
    // random number is greater, j will be greater than nuc %
1066
    // n_precursor -- check for this condition
1067
    group = std::min(group, nuc->n_precursor_);
137,742✔
1068

1069
    // set the delayed group for the particle born from fission
1070
    site->delayed_group = group;
137,742✔
1071

1072
  } else {
1073
    // ====================================================================
1074
    // PROMPT NEUTRON SAMPLED
1075

1076
    // set the delayed group for the particle born from fission to 0
1077
    site->delayed_group = 0;
22,464,568✔
1078
  }
1079

1080
  // sample from prompt neutron energy distribution
1081
  int n_sample = 0;
22,602,310✔
1082
  double mu;
1083
  while (true) {
1084
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
22,602,310✔
1085

1086
    // resample if energy is greater than maximum neutron energy
1087
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
22,602,310✔
1088
    if (site->E < data::energy_max[neutron])
22,602,310✔
1089
      break;
22,602,310✔
1090

1091
    // check for large number of resamples
1092
    ++n_sample;
×
1093
    if (n_sample == MAX_SAMPLE) {
×
1094
      // particle_write_restart(p)
1095
      fatal_error("Resampled energy distribution maximum number of times "
×
1096
                  "for nuclide " +
×
1097
                  nuc->name_);
×
1098
    }
1099
  }
1100

1101
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1102
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
22,602,310✔
1103
}
22,602,310✔
1104

1105
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
11,916,501✔
1106
{
1107
  // copy energy of neutron
1108
  double E_in = p.E();
11,916,501✔
1109

1110
  // sample outgoing energy and scattering cosine
1111
  double E;
1112
  double mu;
1113
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
11,916,501✔
1114

1115
  // if scattering system is in center-of-mass, transfer cosine of scattering
1116
  // angle and outgoing energy from CM to LAB
1117
  if (rx.scatter_in_cm_) {
11,916,501✔
1118
    double E_cm = E;
11,871,263✔
1119

1120
    // determine outgoing energy in lab
1121
    double A = nuc.awr_;
11,871,263✔
1122
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
11,871,263✔
1123
                 ((A + 1.0) * (A + 1.0));
11,871,263✔
1124

1125
    // determine outgoing angle in lab
1126
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
11,871,263✔
1127
  }
1128

1129
  // Because of floating-point roundoff, it may be possible for mu to be
1130
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1131
  // or 1
1132
  if (std::abs(mu) > 1.0)
11,916,501✔
1133
    mu = std::copysign(1.0, mu);
×
1134

1135
  // Set outgoing energy and scattering angle
1136
  p.E() = E;
11,916,501✔
1137
  p.mu() = mu;
11,916,501✔
1138

1139
  // change direction of particle
1140
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
11,916,501✔
1141

1142
  // evaluate yield
1143
  double yield = (*rx.products_[0].yield_)(E_in);
11,916,501✔
1144
  if (std::floor(yield) == yield && yield > 0) {
11,916,501✔
1145
    // If yield is integral, create exactly that many secondary particles
1146
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
11,980,983✔
1147
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
64,580✔
1148
    }
1149
  } else {
11,916,403✔
1150
    // Otherwise, change weight of particle based on yield
1151
    p.wgt() *= yield;
98✔
1152
  }
1153
}
11,916,501✔
1154

1155
void sample_secondary_photons(Particle& p, int i_nuclide)
8,919,086✔
1156
{
1157
  // Sample the number of photons produced
1158
  double y_t =
1159
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,919,086✔
1160
  double photon_wgt = p.wgt();
8,919,086✔
1161
  int y = 1;
8,919,086✔
1162

1163
  if (settings::use_decay_photons) {
8,919,086✔
1164
    // For decay photons, sample a single photon and modify the weight
1165
    if (y_t <= 0.0)
72,006✔
1166
      return;
17,281✔
1167
    photon_wgt *= y_t;
54,725✔
1168
  } else {
1169
    // For prompt photons, sample an integral number of photons with weight
1170
    // equal to the neutron's weight
1171
    y = static_cast<int>(y_t);
8,847,080✔
1172
    if (prn(p.current_seed()) <= y_t - y)
8,847,080✔
1173
      ++y;
573,155✔
1174
  }
1175

1176
  // Sample each secondary photon
1177
  for (int i = 0; i < y; ++i) {
10,228,878✔
1178
    // Sample the reaction and product
1179
    int i_rx;
1180
    int i_product;
1181
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,327,073✔
1182

1183
    // Sample the outgoing energy and angle
1184
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,327,073✔
1185
    double E;
1186
    double mu;
1187
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,327,073✔
1188

1189
    // Sample the new direction
1190
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,327,073✔
1191

1192
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1193
    // secondary photons from non-fission reactions to properly balance energy
1194
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1195
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1196
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1197
    double wgt = photon_wgt;
1,327,073✔
1198
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
1,327,073✔
1199
      wgt *= simulation::keff;
348,920✔
1200
    }
1201

1202
    // Create the secondary photon
1203
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,327,073✔
1204

1205
    // Tag secondary particle with parent nuclide
1206
    if (created_photon && settings::use_decay_photons) {
1,327,073✔
1207
      p.secondary_bank().back().parent_nuclide =
52,844✔
1208
        rx->products_[i_product].parent_nuclide_;
52,844✔
1209
    }
1210
  }
1211
}
1212

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