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

openmc-dev / openmc / 18299973882

07 Oct 2025 02:14AM UTC coverage: 81.92% (-3.3%) from 85.194%
18299973882

push

github

web-flow
Switch to using coveralls github action for reporting (#3594)

16586 of 23090 branches covered (71.83%)

Branch coverage included in aggregate %.

53703 of 62712 relevant lines covered (85.63%)

43428488.21 hits per line

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

82.94
/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)
774,053,766✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
774,053,766✔
47

48
  // Sample reaction for the material the particle is in
49
  switch (p.type()) {
774,053,766!
50
  case ParticleType::neutron:
705,909,145✔
51
    sample_neutron_reaction(p);
705,909,145✔
52
    break;
705,909,145✔
53
  case ParticleType::photon:
16,212,941✔
54
    sample_photon_reaction(p);
16,212,941✔
55
    break;
16,212,941✔
56
  case ParticleType::electron:
51,845,989✔
57
    sample_electron_reaction(p);
51,845,989✔
58
    break;
51,845,989✔
59
  case ParticleType::positron:
85,691✔
60
    sample_positron_reaction(p);
85,691✔
61
    break;
85,691✔
62
  }
63

64
  if (settings::weight_window_checkpoint_collision)
774,053,766!
65
    apply_weight_windows(p);
774,053,766✔
66

67
  // Kill particle if energy falls below cutoff
68
  int type = static_cast<int>(p.type());
774,053,766✔
69
  if (p.E() < settings::energy_cutoff[type]) {
774,053,766✔
70
    p.wgt() = 0.0;
5,007,735✔
71
  }
72

73
  // Display information about collision
74
  if (settings::verbosity >= 10 || p.trace()) {
774,053,766!
75
    std::string msg;
66✔
76
    if (p.event() == TallyEvent::KILL) {
66!
77
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
78
    } else if (p.type() == ParticleType::neutron) {
66!
79
      msg = fmt::format("    {} with {}. Energy = {} eV.",
186✔
80
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
132✔
81
        p.E());
66✔
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);
66✔
90
  }
66✔
91
}
774,053,766✔
92

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

98
  // Save which nuclide particle had collision with
99
  p.event_nuclide() = i_nuclide;
705,909,145✔
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]};
705,909,145✔
107

108
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
705,909,145✔
109
    auto& rx = sample_fission(i_nuclide, p);
96,168,510✔
110
    if (settings::run_mode == RunMode::EIGENVALUE) {
96,168,510✔
111
      create_fission_sites(p, i_nuclide, rx);
91,081,571✔
112
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
5,086,939✔
113
               settings::create_fission_neutrons) {
114
      create_fission_sites(p, i_nuclide, rx);
5,073,035✔
115

116
      // Make sure particle population doesn't grow out of control for
117
      // subcritical multiplication problems.
118
      if (p.secondary_bank().size() >= settings::max_secondaries) {
5,073,035!
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) {
705,909,145✔
129
    sample_secondary_photons(p, i_nuclide);
8,809,548✔
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) {
705,909,145✔
136
    absorption(p, i_nuclide);
705,905,615✔
137
  }
138
  if (!p.alive())
705,909,145✔
139
    return;
18,253,311✔
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();
687,655,834✔
144
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
687,655,834!
145
    ncrystal_mat.scatter(p);
158,829✔
146
  } else {
147
    scatter(p, i_nuclide);
687,497,005✔
148
  }
149

150
  // Advance URR seed stream 'N' times after energy changes
151
  if (p.E() != p.E_last()) {
687,655,834✔
152
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
687,336,603✔
153
  }
154

155
  // Play russian roulette if survival biasing is turned on
156
  if (settings::survival_biasing) {
687,655,834✔
157
    // if survival normalization is on, use normalized weight cutoff and
158
    // normalized weight survive
159
    if (settings::survival_normalization) {
499,950!
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) {
499,950✔
164
      russian_roulette(p, settings::weight_survive);
56,991✔
165
    }
166
  }
167
}
168

169
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
96,154,606✔
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;
96,154,606✔
174

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

180
  // Sample the number of neutrons produced
181
  int nu = static_cast<int>(nu_t);
96,154,606✔
182
  if (prn(p.current_seed()) <= (nu_t - nu))
96,154,606✔
183
    ++nu;
15,873,373✔
184

185
  // If no neutrons were produced then don't continue
186
  if (nu == 0)
96,154,606✔
187
    return;
76,375,013✔
188

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

193
  // Clear out particle's nu fission bank
194
  p.nu_bank().clear();
19,779,593✔
195

196
  p.fission() = true;
19,779,593✔
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);
19,779,593✔
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++) {
44,918,062✔
207
    // Initialize fission site object with particle data
208
    SourceSite site;
25,138,469✔
209
    site.r = p.r();
25,138,469✔
210
    site.particle = ParticleType::neutron;
25,138,469✔
211
    site.time = p.time();
25,138,469✔
212
    site.wgt = 1. / weight;
25,138,469✔
213
    site.surf_id = 0;
25,138,469✔
214

215
    // Sample delayed group and angle/energy for fission reaction
216
    sample_fission_neutron(i_nuclide, rx, &site, p);
25,138,469✔
217

218
    // Reject site if it exceeds time cutoff
219
    if (site.delayed_group > 0) {
25,138,469✔
220
      double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
157,611✔
221
      if (site.time > t_cutoff) {
157,611!
222
        continue;
×
223
      }
224
    }
225

226
    // Set parent and progeny IDs
227
    site.parent_id = p.id();
25,138,469✔
228
    site.progeny_id = p.n_progeny()++;
25,138,469✔
229

230
    // Store fission site in bank
231
    if (use_fission_bank) {
25,138,469✔
232
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
24,799,738✔
233
      if (idx == -1) {
24,799,738!
234
        warning(
×
235
          "The shared fission bank is full. Additional fission sites created "
236
          "in this generation will not be banked. Results may be "
237
          "non-deterministic.");
238

239
        // Decrement number of particle progeny as storage was unsuccessful.
240
        // This step is needed so that the sum of all progeny is equal to the
241
        // size of the shared fission bank.
242
        p.n_progeny()--;
×
243

244
        // Break out of loop as no more sites can be added to fission bank
245
        break;
×
246
      }
247
      // Iterated Fission Probability (IFP) method
248
      if (settings::ifp_on) {
24,799,738✔
249
        ifp(p, idx);
1,352,626✔
250
      }
251
    } else {
252
      p.secondary_bank().push_back(site);
338,731✔
253
    }
254

255
    // Increment the number of neutrons born delayed
256
    if (site.delayed_group > 0) {
25,138,469✔
257
      nu_d[site.delayed_group - 1]++;
157,611✔
258
    }
259

260
    // Write fission particles to nuBank
261
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
25,138,469✔
262
    nu_bank_entry.wgt = site.wgt;
25,138,469✔
263
    nu_bank_entry.E = site.E;
25,138,469✔
264
    nu_bank_entry.delayed_group = site.delayed_group;
25,138,469✔
265
  }
266

267
  // If shared fission bank was full, and no fissions could be added,
268
  // set the particle fission flag to false.
269
  if (n_sites_stored == 0) {
19,779,593!
270
    p.fission() = false;
×
271
    return;
×
272
  }
273

274
  // Set nu to the number of fission sites successfully stored. If the fission
275
  // bank was not found to be full then these values are already equivalent.
276
  nu = n_sites_stored;
19,779,593✔
277

278
  // Store the total weight banked for analog fission tallies
279
  p.n_bank() = nu;
19,779,593✔
280
  p.wgt_bank() = nu / weight;
19,779,593✔
281
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
178,016,337✔
282
    p.n_delayed_bank(d) = nu_d[d];
158,236,744✔
283
  }
284
}
285

286
void sample_photon_reaction(Particle& p)
16,212,941✔
287
{
288
  // Kill photon if below energy cutoff -- an extra check is made here because
289
  // photons with energy below the cutoff may have been produced by neutrons
290
  // reactions or atomic relaxation
291
  int photon = static_cast<int>(ParticleType::photon);
16,212,941✔
292
  if (p.E() < settings::energy_cutoff[photon]) {
16,212,941!
293
    p.E() = 0.0;
×
294
    p.wgt() = 0.0;
×
295
    return;
×
296
  }
297

298
  // Sample element within material
299
  int i_element = sample_element(p);
16,212,941✔
300
  const auto& micro {p.photon_xs(i_element)};
16,212,941✔
301
  const auto& element {*data::elements[i_element]};
16,212,941✔
302

303
  // Calculate photon energy over electron rest mass equivalent
304
  double alpha = p.E() / MASS_ELECTRON_EV;
16,212,941✔
305

306
  // For tallying purposes, this routine might be called directly. In that
307
  // case, we need to sample a reaction via the cutoff variable
308
  double prob = 0.0;
16,212,941✔
309
  double cutoff = prn(p.current_seed()) * micro.total;
16,212,941✔
310

311
  // Coherent (Rayleigh) scattering
312
  prob += micro.coherent;
16,212,941✔
313
  if (prob > cutoff) {
16,212,941✔
314
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
894,765✔
315
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
894,765✔
316
    p.event() = TallyEvent::SCATTER;
894,765✔
317
    p.event_mt() = COHERENT;
894,765✔
318
    return;
894,765✔
319
  }
320

321
  // Incoherent (Compton) scattering
322
  prob += micro.incoherent;
15,318,176✔
323
  if (prob > cutoff) {
15,318,176✔
324
    double alpha_out;
325
    int i_shell;
326
    element.compton_scatter(
20,642,728✔
327
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
10,321,364✔
328

329
    // Determine binding energy of shell. The binding energy is 0.0 if
330
    // doppler broadening is not used.
331
    double e_b;
332
    if (i_shell == -1) {
10,321,364!
333
      e_b = 0.0;
×
334
    } else {
335
      e_b = element.binding_energy_[i_shell];
10,321,364✔
336
    }
337

338
    // Create Compton electron
339
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
10,321,364✔
340
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
10,321,364✔
341
    int electron = static_cast<int>(ParticleType::electron);
10,321,364✔
342
    if (E_electron >= settings::energy_cutoff[electron]) {
10,321,364✔
343
      double mu_electron = (alpha - alpha_out * p.mu()) /
10,224,105✔
344
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
20,448,210✔
345
                                     2.0 * alpha * alpha_out * p.mu());
10,224,105✔
346
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
10,224,105✔
347
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
10,224,105✔
348
    }
349

350
    // Allow electrons to fill orbital and produce Auger electrons and
351
    // fluorescent photons. Since Compton subshell data does not match atomic
352
    // relaxation data, use the mapping between the data to find the subshell
353
    if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
10,321,364!
354
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
10,321,364✔
355
    }
356

357
    phi += PI;
10,321,364✔
358
    p.E() = alpha_out * MASS_ELECTRON_EV;
10,321,364✔
359
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
10,321,364✔
360
    p.event() = TallyEvent::SCATTER;
10,321,364✔
361
    p.event_mt() = INCOHERENT;
10,321,364✔
362
    return;
10,321,364✔
363
  }
364

365
  // Photoelectric effect
366
  double prob_after = prob + micro.photoelectric;
4,996,812✔
367

368
  if (prob_after > cutoff) {
4,996,812✔
369
    // Get grid index, interpolation factor, and bounding subshell
370
    // cross sections
371
    int i_grid = micro.index_grid;
4,911,121✔
372
    double f = micro.interp_factor;
4,911,121✔
373
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
4,911,121✔
374
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
4,911,121✔
375

376
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
24,085,191!
377
      const auto& shell {element.shells_[i_shell]};
24,085,191✔
378

379
      // Check threshold of reaction
380
      if (xs_lower(i_shell) == 0)
24,085,191✔
381
        continue;
10,260,632✔
382

383
      //  Evaluation subshell photoionization cross section
384
      prob += std::exp(
13,824,559✔
385
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
13,824,559✔
386

387
      if (prob > cutoff) {
13,824,559✔
388
        // Determine binding energy based on whether atomic relaxation data is
389
        // present (if not, use value from Compton profile data)
390
        double binding_energy = element.has_atomic_relaxation_
4,911,121✔
391
                                  ? shell.binding_energy
4,911,121!
392
                                  : element.binding_energy_[i_shell];
×
393

394
        // Determine energy of secondary electron
395
        double E_electron = p.E() - binding_energy;
4,911,121✔
396

397
        // Sample mu using non-relativistic Sauter distribution.
398
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
399
        // model in Serpent 2" by Toni Kaltiaisenaho
400
        double mu;
401
        while (true) {
402
          double r = prn(p.current_seed());
7,366,140✔
403
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
7,366,140✔
404
            double rel_vel =
405
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
4,911,121✔
406
              (E_electron + MASS_ELECTRON_EV);
4,911,121✔
407
            mu =
4,911,121✔
408
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
4,911,121✔
409
            break;
4,911,121✔
410
          }
411
        }
2,455,019✔
412

413
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
4,911,121✔
414
        Direction u;
4,911,121✔
415
        u.x = mu;
4,911,121✔
416
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
4,911,121✔
417
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
4,911,121✔
418

419
        // Create secondary electron
420
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
4,911,121✔
421

422
        // Allow electrons to fill orbital and produce auger electrons
423
        // and fluorescent photons
424
        element.atomic_relaxation(i_shell, p);
4,911,121✔
425
        p.event() = TallyEvent::ABSORB;
4,911,121✔
426
        p.event_mt() = 533 + shell.index_subshell;
4,911,121✔
427
        p.wgt() = 0.0;
4,911,121✔
428
        p.E() = 0.0;
4,911,121✔
429
        return;
4,911,121✔
430
      }
431
    }
432
  }
9,822,242!
433
  prob = prob_after;
85,691✔
434

435
  // Pair production
436
  prob += micro.pair_production;
85,691✔
437
  if (prob > cutoff) {
85,691!
438
    double E_electron, E_positron;
439
    double mu_electron, mu_positron;
440
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
85,691✔
441
      &mu_positron, p.current_seed());
442

443
    // Create secondary electron
444
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
85,691✔
445
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
85,691✔
446

447
    // Create secondary positron
448
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
85,691✔
449
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
85,691✔
450

451
    p.event() = TallyEvent::ABSORB;
85,691✔
452
    p.event_mt() = PAIR_PROD;
85,691✔
453
    p.wgt() = 0.0;
85,691✔
454
    p.E() = 0.0;
85,691✔
455
  }
456
}
457

458
void sample_electron_reaction(Particle& p)
51,845,989✔
459
{
460
  // TODO: create reaction types
461

462
  if (settings::electron_treatment == ElectronTreatment::TTB) {
51,845,989✔
463
    double E_lost;
464
    thick_target_bremsstrahlung(p, &E_lost);
51,843,327✔
465
  }
466

467
  p.E() = 0.0;
51,845,989✔
468
  p.wgt() = 0.0;
51,845,989✔
469
  p.event() = TallyEvent::ABSORB;
51,845,989✔
470
}
51,845,989✔
471

472
void sample_positron_reaction(Particle& p)
85,691✔
473
{
474
  // TODO: create reaction types
475

476
  if (settings::electron_treatment == ElectronTreatment::TTB) {
85,691✔
477
    double E_lost;
478
    thick_target_bremsstrahlung(p, &E_lost);
85,680✔
479
  }
480

481
  // Sample angle isotropically
482
  Direction u = isotropic_direction(p.current_seed());
85,691✔
483

484
  // Create annihilation photon pair traveling in opposite directions
485
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
85,691✔
486
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
85,691✔
487

488
  p.E() = 0.0;
85,691✔
489
  p.wgt() = 0.0;
85,691✔
490
  p.event() = TallyEvent::ABSORB;
85,691✔
491
}
85,691✔
492

493
int sample_nuclide(Particle& p)
705,909,145✔
494
{
495
  // Sample cumulative distribution function
496
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
705,909,145✔
497

498
  // Get pointers to nuclide/density arrays
499
  const auto& mat {model::materials[p.material()]};
705,909,145✔
500
  int n = mat->nuclide_.size();
705,909,145✔
501

502
  double prob = 0.0;
705,909,145✔
503
  for (int i = 0; i < n; ++i) {
1,561,516,838!
504
    // Get atom density
505
    int i_nuclide = mat->nuclide_[i];
1,561,516,838✔
506
    double atom_density = mat->atom_density(i, p.density_mult());
1,561,516,838✔
507

508
    // Increment probability to compare to cutoff
509
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,561,516,838✔
510
    if (prob >= cutoff)
1,561,516,838✔
511
      return i_nuclide;
705,909,145✔
512
  }
513

514
  // If we reach here, no nuclide was sampled
515
  p.write_restart();
×
516
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
517
}
518

519
int sample_element(Particle& p)
16,212,941✔
520
{
521
  // Sample cumulative distribution function
522
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
16,212,941✔
523

524
  // Get pointers to elements, densities
525
  const auto& mat {model::materials[p.material()]};
16,212,941✔
526

527
  double prob = 0.0;
16,212,941✔
528
  for (int i = 0; i < mat->element_.size(); ++i) {
39,361,774!
529
    // Find atom density
530
    int i_element = mat->element_[i];
39,361,774✔
531
    double atom_density = mat->atom_density(i, p.density_mult());
39,361,774✔
532

533
    // Determine microscopic cross section
534
    double sigma = atom_density * p.photon_xs(i_element).total;
39,361,774✔
535

536
    // Increment probability to compare to cutoff
537
    prob += sigma;
39,361,774✔
538
    if (prob > cutoff) {
39,361,774✔
539
      // Save which nuclide particle had collision with for tally purpose
540
      p.event_nuclide() = mat->nuclide_[i];
16,212,941✔
541

542
      return i_element;
16,212,941✔
543
    }
544
  }
545

546
  // If we made it here, no element was sampled
547
  p.write_restart();
×
548
  fatal_error("Did not sample any element during collision.");
×
549
}
550

551
Reaction& sample_fission(int i_nuclide, Particle& p)
96,168,510✔
552
{
553
  // Get pointer to nuclide
554
  const auto& nuc {data::nuclides[i_nuclide]};
96,168,510✔
555

556
  // If we're in the URR, by default use the first fission reaction. We also
557
  // default to the first reaction if we know that there are no partial fission
558
  // reactions
559
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
96,168,510✔
560
    return *nuc->fission_rx_[0];
96,142,668✔
561
  }
562

563
  // Check to see if we are in a windowed multipole range.  WMP only supports
564
  // the first fission reaction.
565
  if (nuc->multipole_) {
25,842!
566
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
567
      return *nuc->fission_rx_[0];
×
568
    }
569
  }
570

571
  // Get grid index and interpolation factor and sample fission cdf
572
  const auto& micro = p.neutron_xs(i_nuclide);
25,842✔
573
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
25,842✔
574
  double prob = 0.0;
25,842✔
575

576
  // Loop through each partial fission reaction type
577
  for (auto& rx : nuc->fission_rx_) {
25,893!
578
    // add to cumulative probability
579
    prob += rx->xs(micro);
25,893✔
580

581
    // Create fission bank sites if fission occurs
582
    if (prob > cutoff)
25,893✔
583
      return *rx;
25,842✔
584
  }
585

586
  // If we reached here, no reaction was sampled
587
  throw std::runtime_error {
×
588
    "No fission reaction was sampled for " + nuc->name_};
×
589
}
590

591
void sample_photon_product(
1,337,644✔
592
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
593
{
594
  // Get grid index and interpolation factor and sample photon production cdf
595
  const auto& micro = p.neutron_xs(i_nuclide);
1,337,644✔
596
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,337,644✔
597
  double prob = 0.0;
1,337,644✔
598

599
  // Loop through each reaction type
600
  const auto& nuc {data::nuclides[i_nuclide]};
1,337,644✔
601
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
21,995,831!
602
    // Evaluate neutron cross section
603
    const auto& rx = nuc->reactions_[i];
21,995,831✔
604
    double xs = rx->xs(micro);
21,995,831✔
605

606
    // if cross section is zero for this reaction, skip it
607
    if (xs == 0.0)
21,995,831✔
608
      continue;
7,191,415✔
609

610
    for (int j = 0; j < rx->products_.size(); ++j) {
69,577,497✔
611
      if (rx->products_[j].particle_ == ParticleType::photon) {
56,110,725✔
612
        // For fission, artificially increase the photon yield to account
613
        // for delayed photons
614
        double f = 1.0;
43,656,910✔
615
        if (settings::delayed_photon_scaling) {
43,656,910!
616
          if (is_fission(rx->mt_)) {
43,656,910✔
617
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
540,221!
618
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
540,221✔
619
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
540,221✔
620
              f = (energy_prompt + energy_delayed) / (energy_prompt);
540,221✔
621
            }
622
          }
623
        }
624

625
        // add to cumulative probability
626
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
43,656,910✔
627

628
        *i_rx = i;
43,656,910✔
629
        *i_product = j;
43,656,910✔
630
        if (prob > cutoff)
43,656,910✔
631
          return;
1,337,644✔
632
      }
633
    }
634
  }
635
}
636

637
void absorption(Particle& p, int i_nuclide)
705,905,615✔
638
{
639
  if (settings::survival_biasing) {
705,905,615✔
640
    // Determine weight absorbed in survival biasing
641
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
499,950✔
642
                              p.neutron_xs(i_nuclide).total;
499,950✔
643

644
    // Adjust weight of particle by probability of absorption
645
    p.wgt() -= wgt_absorb;
499,950✔
646

647
    // Score implicit absorption estimate of keff
648
    if (settings::run_mode == RunMode::EIGENVALUE) {
499,950!
649
      p.keff_tally_absorption() += wgt_absorb *
499,950✔
650
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950✔
651
                                   p.neutron_xs(i_nuclide).absorption;
499,950✔
652
    }
653
  } else {
654
    // See if disappearance reaction happens
655
    if (p.neutron_xs(i_nuclide).absorption >
705,405,665✔
656
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
705,405,665✔
657
      // Score absorption estimate of keff
658
      if (settings::run_mode == RunMode::EIGENVALUE) {
18,253,311✔
659
        p.keff_tally_absorption() += p.wgt() *
31,033,864✔
660
                                     p.neutron_xs(i_nuclide).nu_fission /
15,516,932✔
661
                                     p.neutron_xs(i_nuclide).absorption;
15,516,932✔
662
      }
663

664
      p.wgt() = 0.0;
18,253,311✔
665
      p.event() = TallyEvent::ABSORB;
18,253,311✔
666
      p.event_mt() = N_DISAPPEAR;
18,253,311✔
667
    }
668
  }
669
}
705,905,615✔
670

671
void scatter(Particle& p, int i_nuclide)
687,497,005✔
672
{
673
  // copy incoming direction
674
  Direction u_old {p.u()};
687,497,005✔
675

676
  // Get pointer to nuclide and grid index/interpolation factor
677
  const auto& nuc {data::nuclides[i_nuclide]};
687,497,005✔
678
  const auto& micro {p.neutron_xs(i_nuclide)};
687,497,005✔
679
  int i_temp = micro.index_temp;
687,497,005✔
680

681
  // For tallying purposes, this routine might be called directly. In that
682
  // case, we need to sample a reaction via the cutoff variable
683
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
687,497,005✔
684
  bool sampled = false;
687,497,005✔
685

686
  // Calculate elastic cross section if it wasn't precalculated
687
  if (micro.elastic == CACHE_INVALID) {
687,497,005✔
688
    nuc->calculate_elastic_xs(p);
576,655,417✔
689
  }
690

691
  double prob = micro.elastic - micro.thermal;
687,497,005✔
692
  if (prob > cutoff) {
687,497,005✔
693
    // =======================================================================
694
    // NON-S(A,B) ELASTIC SCATTERING
695

696
    // Determine temperature
697
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
596,043,810✔
698

699
    // Perform collision physics for elastic scattering
700
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
596,043,810✔
701

702
    p.event_mt() = ELASTIC;
596,043,810✔
703
    sampled = true;
596,043,810✔
704
  }
705

706
  prob = micro.elastic;
687,497,005✔
707
  if (prob > cutoff && !sampled) {
687,497,005✔
708
    // =======================================================================
709
    // S(A,B) SCATTERING
710

711
    sab_scatter(i_nuclide, micro.index_sab, p);
75,567,737✔
712

713
    p.event_mt() = ELASTIC;
75,567,737✔
714
    sampled = true;
75,567,737✔
715
  }
716

717
  if (!sampled) {
687,497,005✔
718
    // =======================================================================
719
    // INELASTIC SCATTERING
720

721
    int n = nuc->index_inelastic_scatter_.size();
15,885,458✔
722
    int i = 0;
15,885,458✔
723
    for (int j = 0; j < n && prob < cutoff; ++j) {
268,909,405✔
724
      i = nuc->index_inelastic_scatter_[j];
253,023,947✔
725

726
      // add to cumulative probability
727
      prob += nuc->reactions_[i]->xs(micro);
253,023,947✔
728
    }
729

730
    // Perform collision physics for inelastic scattering
731
    const auto& rx {nuc->reactions_[i]};
15,885,458✔
732
    inelastic_scatter(*nuc, *rx, p);
15,885,458✔
733
    p.event_mt() = rx->mt_;
15,885,458✔
734
  }
735

736
  // Set event component
737
  p.event() = TallyEvent::SCATTER;
687,497,005✔
738

739
  // Sample new outgoing angle for isotropic-in-lab scattering
740
  const auto& mat {model::materials[p.material()]};
687,497,005✔
741
  if (!mat->p0_.empty()) {
687,497,005✔
742
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
326,370✔
743
    if (mat->p0_[i_nuc_mat]) {
326,370!
744
      // Sample isotropic-in-lab outgoing direction
745
      p.u() = isotropic_direction(p.current_seed());
326,370✔
746
      p.mu() = u_old.dot(p.u());
326,370✔
747
    }
748
  }
749
}
687,497,005✔
750

751
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
596,043,810✔
752
{
753
  // get pointer to nuclide
754
  const auto& nuc {data::nuclides[i_nuclide]};
596,043,810✔
755

756
  double vel = std::sqrt(p.E());
596,043,810✔
757
  double awr = nuc->awr_;
596,043,810✔
758

759
  // Neutron velocity in LAB
760
  Direction v_n = vel * p.u();
596,043,810✔
761

762
  // Sample velocity of target nucleus
763
  Direction v_t {};
596,043,810✔
764
  if (!p.neutron_xs(i_nuclide).use_ptable) {
596,043,810✔
765
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,153,339,500✔
766
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
576,669,750✔
767
  }
768

769
  // Velocity of center-of-mass
770
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
596,043,810✔
771

772
  // Transform to CM frame
773
  v_n -= v_cm;
596,043,810✔
774

775
  // Find speed of neutron in CM
776
  vel = v_n.norm();
596,043,810✔
777

778
  // Sample scattering angle, checking if angle distribution is present (assume
779
  // isotropic otherwise)
780
  double mu_cm;
781
  auto& d = rx.products_[0].distribution_[0];
596,043,810✔
782
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
596,043,810!
783
  if (!d_->angle().empty()) {
596,043,810!
784
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
596,043,810✔
785
  } else {
786
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
787
  }
788

789
  // Determine direction cosines in CM
790
  Direction u_cm = v_n / vel;
596,043,810✔
791

792
  // Rotate neutron velocity vector to new angle -- note that the speed of the
793
  // neutron in CM does not change in elastic scattering. However, the speed
794
  // will change when we convert back to LAB
795
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
596,043,810✔
796

797
  // Transform back to LAB frame
798
  v_n += v_cm;
596,043,810✔
799

800
  p.E() = v_n.dot(v_n);
596,043,810✔
801
  vel = std::sqrt(p.E());
596,043,810✔
802

803
  // compute cosine of scattering angle in LAB frame by taking dot product of
804
  // neutron's pre- and post-collision angle
805
  p.mu() = p.u().dot(v_n) / vel;
596,043,810✔
806

807
  // Set energy and direction of particle in LAB frame
808
  p.u() = v_n / vel;
596,043,810✔
809

810
  // Because of floating-point roundoff, it may be possible for mu_lab to be
811
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
812
  // -1 or 1
813
  if (std::abs(p.mu()) > 1.0)
596,043,810!
814
    p.mu() = std::copysign(1.0, p.mu());
×
815
}
596,043,810✔
816

817
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
75,567,737✔
818
{
819
  // Determine temperature index
820
  const auto& micro {p.neutron_xs(i_nuclide)};
75,567,737✔
821
  int i_temp = micro.index_temp_sab;
75,567,737✔
822

823
  // Sample energy and angle
824
  double E_out;
825
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
151,135,474✔
826
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
75,567,737✔
827

828
  // Set energy to outgoing, change direction of particle
829
  p.E() = E_out;
75,567,737✔
830
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
75,567,737✔
831
}
75,567,737✔
832

833
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
576,669,750✔
834
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
835
{
836
  // check if nuclide is a resonant scatterer
837
  ResScatMethod sampling_method;
838
  if (nuc.resonant_) {
576,669,750✔
839

840
    // sampling method to use
841
    sampling_method = settings::res_scat_method;
84,557✔
842

843
    // upper resonance scattering energy bound (target is at rest above this E)
844
    if (E > settings::res_scat_energy_max) {
84,557✔
845
      return {};
40,755✔
846

847
      // lower resonance scattering energy bound (should be no resonances below)
848
    } else if (E < settings::res_scat_energy_min) {
43,802✔
849
      sampling_method = ResScatMethod::cxs;
24,992✔
850
    }
851

852
    // otherwise, use free gas model
853
  } else {
854
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
576,585,193✔
855
      return {};
235,210,661✔
856
    } else {
857
      sampling_method = ResScatMethod::cxs;
341,374,532✔
858
    }
859
  }
860

861
  // use appropriate target velocity sampling method
862
  switch (sampling_method) {
341,418,334!
863
  case ResScatMethod::cxs:
341,399,524✔
864

865
    // sample target velocity with the constant cross section (cxs) approx.
866
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
341,399,524✔
867

868
  case ResScatMethod::dbrc:
18,810✔
869
  case ResScatMethod::rvs: {
870
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810✔
871
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
18,810✔
872
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810✔
873

874
    // find lower and upper energy bound indices
875
    // lower index
876
    int i_E_low;
877
    if (E_low < nuc.energy_0K_.front()) {
18,810!
878
      i_E_low = 0;
×
879
    } else if (E_low > nuc.energy_0K_.back()) {
18,810!
880
      i_E_low = nuc.energy_0K_.size() - 2;
×
881
    } else {
882
      i_E_low =
18,810✔
883
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
18,810✔
884
    }
885

886
    // upper index
887
    int i_E_up;
888
    if (E_up < nuc.energy_0K_.front()) {
18,810!
889
      i_E_up = 0;
×
890
    } else if (E_up > nuc.energy_0K_.back()) {
18,810!
891
      i_E_up = nuc.energy_0K_.size() - 2;
×
892
    } else {
893
      i_E_up =
18,810✔
894
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
18,810✔
895
    }
896

897
    if (i_E_up == i_E_low) {
18,810✔
898
      // Handle degenerate case -- if the upper/lower bounds occur for the same
899
      // index, then using cxs is probably a good approximation
900
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
18,810✔
901
    }
902

903
    if (sampling_method == ResScatMethod::dbrc) {
15,532!
904
      // interpolate xs since we're not exactly at the energy indices
905
      double xs_low = nuc.elastic_0K_[i_E_low];
×
906
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
907
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
908
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
909
      double xs_up = nuc.elastic_0K_[i_E_up];
×
910
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
911
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
912
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
913

914
      // get max 0K xs value over range of practical relative energies
915
      double xs_max = *std::max_element(
×
916
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
917
      xs_max = std::max({xs_low, xs_max, xs_up});
×
918

919
      while (true) {
920
        double E_rel;
921
        Direction v_target;
×
922
        while (true) {
923
          // sample target velocity with the constant cross section (cxs)
924
          // approx.
925
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
926
          Direction v_rel = v_neut - v_target;
×
927
          E_rel = v_rel.dot(v_rel);
×
928
          if (E_rel < E_up)
×
929
            break;
×
930
        }
×
931

932
        // perform Doppler broadening rejection correction (dbrc)
933
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
934
        double R = xs_0K / xs_max;
×
935
        if (prn(seed) < R)
×
936
          return v_target;
×
937
      }
×
938

939
    } else if (sampling_method == ResScatMethod::rvs) {
15,532!
940
      // interpolate xs CDF since we're not exactly at the energy indices
941
      // cdf value at lower bound attainable energy
942
      double cdf_low = 0.0;
15,532✔
943
      if (E_low > nuc.energy_0K_.front()) {
15,532!
944
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
15,532✔
945
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
15,532✔
946
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
15,532✔
947
      }
948

949
      // cdf value at upper bound attainable energy
950
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,532✔
951
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,532✔
952
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,532✔
953

954
      while (true) {
955
        // directly sample Maxwellian
956
        double E_t = -kT * std::log(prn(seed));
170,720✔
957

958
        // sample a relative energy using the xs cdf
959
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
170,720✔
960
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
170,720✔
961
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
170,720✔
962
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
170,720✔
963
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
170,720✔
964
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
170,720✔
965
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
170,720✔
966
                     nuc.energy_0K_[i_E_low + i_E_rel]);
170,720✔
967
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
170,720✔
968

969
        // perform rejection sampling on cosine between
970
        // neutron and target velocities
971
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
170,720✔
972
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
170,720✔
973

974
        if (std::abs(mu) < 1.0) {
170,720✔
975
          // set and accept target velocity
976
          E_t /= nuc.awr_;
15,532✔
977
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,532✔
978
        }
979
      }
155,188✔
980
    }
981
  } // case RVS, DBRC
982
  } // switch (sampling_method)
983

984
  UNREACHABLE();
×
985
}
986

987
Direction sample_cxs_target_velocity(
341,402,802✔
988
  double awr, double E, Direction u, double kT, uint64_t* seed)
989
{
990
  double beta_vn = std::sqrt(awr * E / kT);
341,402,802✔
991
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
341,402,802✔
992

993
  double beta_vt_sq;
994
  double mu;
995
  while (true) {
996
    // Sample two random numbers
997
    double r1 = prn(seed);
404,387,380✔
998
    double r2 = prn(seed);
404,387,380✔
999

1000
    if (prn(seed) < alpha) {
404,387,380✔
1001
      // With probability alpha, we sample the distribution p(y) =
1002
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1003
      // Carlo sampler
1004

1005
      beta_vt_sq = -std::log(r1 * r2);
102,338,110✔
1006

1007
    } else {
1008
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1009
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1010
      // Carlo sampler
1011

1012
      double c = std::cos(PI / 2.0 * prn(seed));
302,049,270✔
1013
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
302,049,270✔
1014
    }
1015

1016
    // Determine beta * vt
1017
    double beta_vt = std::sqrt(beta_vt_sq);
404,387,380✔
1018

1019
    // Sample cosine of angle between neutron and target velocity
1020
    mu = uniform_distribution(-1., 1., seed);
404,387,380✔
1021

1022
    // Determine rejection probability
1023
    double accept_prob =
1024
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
404,387,380✔
1025
      (beta_vn + beta_vt);
404,387,380✔
1026

1027
    // Perform rejection sampling on vt and mu
1028
    if (prn(seed) < accept_prob)
404,387,380✔
1029
      break;
341,402,802✔
1030
  }
62,984,578✔
1031

1032
  // Determine speed of target nucleus
1033
  double vt = std::sqrt(beta_vt_sq * kT / awr);
341,402,802✔
1034

1035
  // Determine velocity vector of target nucleus based on neutron's velocity
1036
  // and the sampled angle between them
1037
  return vt * rotate_angle(u, mu, nullptr, seed);
341,402,802✔
1038
}
1039

1040
void sample_fission_neutron(
25,138,469✔
1041
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1042
{
1043
  // Get attributes of particle
1044
  double E_in = p.E();
25,138,469✔
1045
  uint64_t* seed = p.current_seed();
25,138,469✔
1046

1047
  // Determine total nu, delayed nu, and delayed neutron fraction
1048
  const auto& nuc {data::nuclides[i_nuclide]};
25,138,469✔
1049
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
25,138,469✔
1050
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
25,138,469✔
1051
  double beta = nu_d / nu_t;
25,138,469✔
1052

1053
  if (prn(seed) < beta) {
25,138,469✔
1054
    // ====================================================================
1055
    // DELAYED NEUTRON SAMPLED
1056

1057
    // sampled delayed precursor group
1058
    double xi = prn(seed) * nu_d;
157,611✔
1059
    double prob = 0.0;
157,611✔
1060
    int group;
1061
    for (group = 1; group < nuc->n_precursor_; ++group) {
588,983✔
1062
      // determine delayed neutron precursor yield for group j
1063
      double yield = (*rx.products_[group].yield_)(E_in);
577,988✔
1064

1065
      // Check if this group is sampled
1066
      prob += yield;
577,988✔
1067
      if (xi < prob)
577,988✔
1068
        break;
146,616✔
1069
    }
1070

1071
    // if the sum of the probabilities is slightly less than one and the
1072
    // random number is greater, j will be greater than nuc %
1073
    // n_precursor -- check for this condition
1074
    group = std::min(group, nuc->n_precursor_);
157,611✔
1075

1076
    // set the delayed group for the particle born from fission
1077
    site->delayed_group = group;
157,611✔
1078

1079
    // Sample time of emission based on decay constant of precursor
1080
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
157,611✔
1081
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
157,611✔
1082

1083
  } else {
1084
    // ====================================================================
1085
    // PROMPT NEUTRON SAMPLED
1086

1087
    // set the delayed group for the particle born from fission to 0
1088
    site->delayed_group = 0;
24,980,858✔
1089
  }
1090

1091
  // sample from prompt neutron energy distribution
1092
  int n_sample = 0;
25,138,469✔
1093
  double mu;
1094
  while (true) {
1095
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
25,138,469✔
1096

1097
    // resample if energy is greater than maximum neutron energy
1098
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
25,138,469✔
1099
    if (site->E < data::energy_max[neutron])
25,138,469!
1100
      break;
25,138,469✔
1101

1102
    // check for large number of resamples
1103
    ++n_sample;
×
1104
    if (n_sample == MAX_SAMPLE) {
×
1105
      // particle_write_restart(p)
1106
      fatal_error("Resampled energy distribution maximum number of times "
×
1107
                  "for nuclide " +
×
1108
                  nuc->name_);
×
1109
    }
1110
  }
×
1111

1112
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1113
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
25,138,469✔
1114
}
25,138,469✔
1115

1116
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
15,885,458✔
1117
{
1118
  // copy energy of neutron
1119
  double E_in = p.E();
15,885,458✔
1120

1121
  // sample outgoing energy and scattering cosine
1122
  double E;
1123
  double mu;
1124
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
15,885,458✔
1125

1126
  // if scattering system is in center-of-mass, transfer cosine of scattering
1127
  // angle and outgoing energy from CM to LAB
1128
  if (rx.scatter_in_cm_) {
15,885,458✔
1129
    double E_cm = E;
15,835,541✔
1130

1131
    // determine outgoing energy in lab
1132
    double A = nuc.awr_;
15,835,541✔
1133
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
15,835,541✔
1134
                 ((A + 1.0) * (A + 1.0));
15,835,541✔
1135

1136
    // determine outgoing angle in lab
1137
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
15,835,541✔
1138
  }
1139

1140
  // Because of floating-point roundoff, it may be possible for mu to be
1141
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1142
  // or 1
1143
  if (std::abs(mu) > 1.0)
15,885,458!
1144
    mu = std::copysign(1.0, mu);
×
1145

1146
  // Set outgoing energy and scattering angle
1147
  p.E() = E;
15,885,458✔
1148
  p.mu() = mu;
15,885,458✔
1149

1150
  // change direction of particle
1151
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
15,885,458✔
1152

1153
  // evaluate yield
1154
  double yield = (*rx.products_[0].yield_)(E_in);
15,885,458✔
1155
  if (std::floor(yield) == yield && yield > 0) {
15,885,458!
1156
    // If yield is integral, create exactly that many secondary particles
1157
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
15,957,677✔
1158
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
72,295✔
1159
    }
1160
  } else {
15,885,382✔
1161
    // Otherwise, change weight of particle based on yield
1162
    p.wgt() *= yield;
76✔
1163
  }
1164
}
15,885,458✔
1165

1166
void sample_secondary_photons(Particle& p, int i_nuclide)
8,809,548✔
1167
{
1168
  // Sample the number of photons produced
1169
  double y_t =
1170
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,809,548✔
1171
  double photon_wgt = p.wgt();
8,809,548✔
1172
  int y = 1;
8,809,548✔
1173

1174
  if (settings::use_decay_photons) {
8,809,548✔
1175
    // For decay photons, sample a single photon and modify the weight
1176
    if (y_t <= 0.0)
72,006✔
1177
      return;
17,281✔
1178
    photon_wgt *= y_t;
54,725✔
1179
  } else {
1180
    // For prompt photons, sample an integral number of photons with weight
1181
    // equal to the neutron's weight
1182
    y = static_cast<int>(y_t);
8,737,542✔
1183
    if (prn(p.current_seed()) <= y_t - y)
8,737,542✔
1184
      ++y;
574,860✔
1185
  }
1186

1187
  // Sample each secondary photon
1188
  for (int i = 0; i < y; ++i) {
10,129,911✔
1189
    // Sample the reaction and product
1190
    int i_rx;
1191
    int i_product;
1192
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,337,644✔
1193

1194
    // Sample the outgoing energy and angle
1195
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,337,644✔
1196
    double E;
1197
    double mu;
1198
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,337,644✔
1199

1200
    // Sample the new direction
1201
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,337,644✔
1202

1203
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1204
    // secondary photons from non-fission reactions to properly balance energy
1205
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1206
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1207
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1208
    double wgt = photon_wgt;
1,337,644✔
1209
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
1,337,644✔
1210
      wgt *= simulation::keff;
348,128✔
1211
    }
1212

1213
    // Create the secondary photon
1214
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,337,644✔
1215

1216
    // Tag secondary particle with parent nuclide
1217
    if (created_photon && settings::use_decay_photons) {
1,337,644✔
1218
      p.secondary_bank().back().parent_nuclide =
52,844✔
1219
        rx->products_[i_product].parent_nuclide_;
52,844✔
1220
    }
1221
  }
1222
}
1223

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

© 2025 Coveralls, Inc