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

openmc-dev / openmc / 21458205742

28 Jan 2026 10:40PM UTC coverage: 81.974% (-0.02%) from 81.994%
21458205742

Pull #3751

github

web-flow
Merge a14249281 into 008d58460
Pull Request #3751: Resolve conflict with weight windows and global russian roulette

17239 of 24006 branches covered (71.81%)

Branch coverage included in aggregate %.

87 of 100 new or added lines in 5 files covered. (87.0%)

94 existing lines in 2 files now uncovered.

55702 of 64975 relevant lines covered (85.73%)

44017310.75 hits per line

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

82.57
/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)
942,326,598✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
942,326,598✔
47

48
  // Sample reaction for the material the particle is in
49
  switch (p.type()) {
942,326,598!
50
  case ParticleType::neutron:
870,872,150✔
51
    sample_neutron_reaction(p);
870,872,150✔
52
    break;
870,872,150✔
53
  case ParticleType::photon:
17,468,722✔
54
    sample_photon_reaction(p);
17,468,722✔
55
    break;
17,468,722✔
56
  case ParticleType::electron:
53,895,811✔
57
    sample_electron_reaction(p);
53,895,811✔
58
    break;
53,895,811✔
59
  case ParticleType::positron:
89,915✔
60
    sample_positron_reaction(p);
89,915✔
61
    break;
89,915✔
62
  }
63

64
  if (settings::weight_windows_on) {
942,326,598✔
65
    if (settings::weight_window_checkpoint_collision) {
78,450,811!
66
      // If collision checkpoint is enabled, apply weight window
67
      // if valid and apply global russian roulette if not.
68
      auto ww = search_weight_window(p);
78,450,811✔
69
      if (ww.is_valid()) {
78,450,811✔
70
        apply_weight_window(p, ww);
53,754,177✔
71
      } else if (p.type() == ParticleType::neutron) {
24,696,634✔
72
        apply_russian_roulette(p);
11,115,052✔
73
      }
NEW
74
    } else if (p.type() == ParticleType::neutron) {
×
75
      // If collision checkpoint is disabled and weight windows are enabled,
76
      // apply russian roulette if the particle is a neutron and it is outside
77
      // the weight window domain
NEW
78
      auto ww = search_weight_window(p);
×
NEW
79
      if (!ww.is_valid())
×
NEW
80
        apply_russian_roulette(p);
×
81
    }
82
  }
83

84
  // Kill particle if energy falls below cutoff
85
  int type = static_cast<int>(p.type());
942,326,598✔
86
  if (p.E() < settings::energy_cutoff[type]) {
942,326,598✔
87
    p.wgt() = 0.0;
5,227,352✔
88
  }
89

90
  // Display information about collision
91
  if (settings::verbosity >= 10 || p.trace()) {
942,326,598!
92
    std::string msg;
66✔
93
    if (p.event() == TallyEvent::KILL) {
66!
94
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
95
    } else if (p.type() == ParticleType::neutron) {
66!
96
      msg = fmt::format("    {} with {}. Energy = {} eV.",
186✔
97
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
132✔
98
        p.E());
66✔
99
    } else if (p.type() == ParticleType::photon) {
×
100
      msg = fmt::format("    {} with {}. Energy = {} eV.",
×
101
        reaction_name(p.event_mt()),
×
102
        to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
×
103
    } else {
104
      msg = fmt::format("    Disappeared. Energy = {} eV.", p.E());
×
105
    }
106
    write_message(msg, 1);
66✔
107
  }
66✔
108
}
942,326,598✔
109

110
void sample_neutron_reaction(Particle& p)
870,872,150✔
111
{
112
  // Sample a nuclide within the material
113
  int i_nuclide = sample_nuclide(p);
870,872,150✔
114

115
  // Save which nuclide particle had collision with
116
  p.event_nuclide() = i_nuclide;
870,872,150✔
117

118
  // Create fission bank sites. Note that while a fission reaction is sampled,
119
  // it never actually "happens", i.e. the weight of the particle does not
120
  // change when sampling fission sites. The following block handles all
121
  // absorption (including fission)
122

123
  const auto& nuc {data::nuclides[i_nuclide]};
870,872,150✔
124

125
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
870,872,150✔
126
    auto& rx = sample_fission(i_nuclide, p);
114,589,874✔
127
    if (settings::run_mode == RunMode::EIGENVALUE) {
114,589,874✔
128
      create_fission_sites(p, i_nuclide, rx);
98,474,918✔
129
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
16,114,956✔
130
               settings::create_fission_neutrons) {
131
      create_fission_sites(p, i_nuclide, rx);
558,074✔
132

133
      // Make sure particle population doesn't grow out of control for
134
      // subcritical multiplication problems.
135
      if (p.secondary_bank().size() >= settings::max_secondaries) {
558,074!
136
        fatal_error(
×
137
          "The secondary particle bank appears to be growing without "
138
          "bound. You are likely running a subcritical multiplication problem "
139
          "with k-effective close to or greater than one.");
140
      }
141
    }
142
    p.event_mt() = rx.mt_;
114,589,874✔
143
  }
144

145
  // Create secondary photons
146
  if (settings::photon_transport) {
870,872,150✔
147
    sample_secondary_photons(p, i_nuclide);
8,809,548✔
148
  }
149

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

153
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
870,872,150✔
154
    absorption(p, i_nuclide);
870,868,630✔
155
  }
156
  if (!p.alive())
870,872,150✔
157
    return;
21,814,076✔
158

159
  // Sample a scattering reaction and determine the secondary energy of the
160
  // exiting neutron
161
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
849,058,074✔
162
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
849,058,074!
163
    ncrystal_mat.scatter(p);
158,829✔
164
  } else {
165
    scatter(p, i_nuclide);
848,899,245✔
166
  }
167

168
  // Advance URR seed stream 'N' times after energy changes
169
  if (p.E() != p.E_last()) {
849,058,074✔
170
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
848,738,843✔
171
  }
172

173
  // Play russian roulette if there are no weight windows
174
  if (!settings::weight_windows_on)
849,058,074✔
175
    apply_russian_roulette(p);
790,267,466✔
176
}
177

178
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
99,032,992✔
179
{
180
  // If uniform fission source weighting is turned on, we increase or decrease
181
  // the expected number of fission sites produced
182
  double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
99,032,992✔
183

184
  // Determine the expected number of neutrons produced
185
  double nu_t = p.wgt() / simulation::keff * weight *
99,032,992✔
186
                p.neutron_xs(i_nuclide).nu_fission /
99,032,992✔
187
                p.neutron_xs(i_nuclide).total;
99,032,992✔
188

189
  // Sample the number of neutrons produced
190
  int nu = static_cast<int>(nu_t);
99,032,992✔
191
  if (prn(p.current_seed()) <= (nu_t - nu))
99,032,992✔
192
    ++nu;
20,417,276✔
193

194
  // If no neutrons were produced then don't continue
195
  if (nu == 0)
99,032,992✔
196
    return;
74,683,810✔
197

198
  // Initialize the counter of delayed neutrons encountered for each delayed
199
  // group.
200
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
24,349,182✔
201

202
  // Clear out particle's nu fission bank
203
  p.nu_bank().clear();
24,349,182✔
204

205
  p.fission() = true;
24,349,182✔
206

207
  // Determine whether to place fission sites into the shared fission bank
208
  // or the secondary particle bank.
209
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
24,349,182✔
210

211
  // Counter for the number of fission sites successfully stored to the shared
212
  // fission bank or the secondary particle bank
213
  int n_sites_stored;
214

215
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
54,349,949✔
216
    // Initialize fission site object with particle data
217
    SourceSite site;
30,000,767✔
218
    site.r = p.r();
30,000,767✔
219
    site.particle = ParticleType::neutron;
30,000,767✔
220
    site.time = p.time();
30,000,767✔
221
    site.wgt = 1. / weight;
30,000,767✔
222
    site.surf_id = 0;
30,000,767✔
223

224
    // Sample delayed group and angle/energy for fission reaction
225
    sample_fission_neutron(i_nuclide, rx, &site, p);
30,000,767✔
226

227
    // Reject site if it exceeds time cutoff
228
    if (site.delayed_group > 0) {
30,000,767✔
229
      double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
187,898✔
230
      if (site.time > t_cutoff) {
187,898!
231
        continue;
×
232
      }
233
    }
234

235
    // Set parent and progeny IDs
236
    site.parent_id = p.id();
30,000,767✔
237
    site.progeny_id = p.n_progeny()++;
30,000,767✔
238

239
    // Store fission site in bank
240
    if (use_fission_bank) {
30,000,767✔
241
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
29,797,248✔
242
      if (idx == -1) {
29,797,248!
243
        warning(
×
244
          "The shared fission bank is full. Additional fission sites created "
245
          "in this generation will not be banked. Results may be "
246
          "non-deterministic.");
247

248
        // Decrement number of particle progeny as storage was unsuccessful.
249
        // This step is needed so that the sum of all progeny is equal to the
250
        // size of the shared fission bank.
251
        p.n_progeny()--;
×
252

253
        // Break out of loop as no more sites can be added to fission bank
254
        break;
×
255
      }
256
      // Iterated Fission Probability (IFP) method
257
      if (settings::ifp_on) {
29,797,248✔
258
        ifp(p, idx);
1,352,626✔
259
      }
260
    } else {
261
      p.secondary_bank().push_back(site);
203,519✔
262
    }
263

264
    // Increment the number of neutrons born delayed
265
    if (site.delayed_group > 0) {
30,000,767✔
266
      nu_d[site.delayed_group - 1]++;
187,898✔
267
    }
268

269
    // Write fission particles to nuBank
270
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
30,000,767✔
271
    nu_bank_entry.wgt = site.wgt;
30,000,767✔
272
    nu_bank_entry.E = site.E;
30,000,767✔
273
    nu_bank_entry.delayed_group = site.delayed_group;
30,000,767✔
274
  }
275

276
  // If shared fission bank was full, and no fissions could be added,
277
  // set the particle fission flag to false.
278
  if (n_sites_stored == 0) {
24,349,182!
279
    p.fission() = false;
×
280
    return;
×
281
  }
282

283
  // Set nu to the number of fission sites successfully stored. If the fission
284
  // bank was not found to be full then these values are already equivalent.
285
  nu = n_sites_stored;
24,349,182✔
286

287
  // Store the total weight banked for analog fission tallies
288
  p.n_bank() = nu;
24,349,182✔
289
  p.wgt_bank() = nu / weight;
24,349,182✔
290
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
219,142,638✔
291
    p.n_delayed_bank(d) = nu_d[d];
194,793,456✔
292
  }
293
}
294

295
void sample_photon_reaction(Particle& p)
17,468,722✔
296
{
297
  // Kill photon if below energy cutoff -- an extra check is made here because
298
  // photons with energy below the cutoff may have been produced by neutrons
299
  // reactions or atomic relaxation
300
  int photon = static_cast<int>(ParticleType::photon);
17,468,722✔
301
  if (p.E() < settings::energy_cutoff[photon]) {
17,468,722!
302
    p.E() = 0.0;
×
303
    p.wgt() = 0.0;
×
304
    return;
×
305
  }
306

307
  // Sample element within material
308
  int i_element = sample_element(p);
17,468,722✔
309
  const auto& micro {p.photon_xs(i_element)};
17,468,722✔
310
  const auto& element {*data::elements[i_element]};
17,468,722✔
311

312
  // Calculate photon energy over electron rest mass equivalent
313
  double alpha = p.E() / MASS_ELECTRON_EV;
17,468,722✔
314

315
  // For tallying purposes, this routine might be called directly. In that
316
  // case, we need to sample a reaction via the cutoff variable
317
  double prob = 0.0;
17,468,722✔
318
  double cutoff = prn(p.current_seed()) * micro.total;
17,468,722✔
319

320
  // Coherent (Rayleigh) scattering
321
  prob += micro.coherent;
17,468,722✔
322
  if (prob > cutoff) {
17,468,722✔
323
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
958,129✔
324
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
958,129✔
325
    p.event() = TallyEvent::SCATTER;
958,129✔
326
    p.event_mt() = COHERENT;
958,129✔
327
    return;
958,129✔
328
  }
329

330
  // Incoherent (Compton) scattering
331
  prob += micro.incoherent;
16,510,593✔
332
  if (prob > cutoff) {
16,510,593✔
333
    double alpha_out;
334
    int i_shell;
335
    element.compton_scatter(
22,588,328✔
336
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
11,294,164✔
337

338
    // Determine binding energy of shell. The binding energy is 0.0 if
339
    // doppler broadening is not used.
340
    double e_b;
341
    if (i_shell == -1) {
11,294,164!
342
      e_b = 0.0;
×
343
    } else {
344
      e_b = element.binding_energy_[i_shell];
11,294,164✔
345
    }
346

347
    // Create Compton electron
348
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
11,294,164✔
349
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
11,294,164✔
350
    int electron = static_cast<int>(ParticleType::electron);
11,294,164✔
351
    if (E_electron >= settings::energy_cutoff[electron]) {
11,294,164✔
352
      double mu_electron = (alpha - alpha_out * p.mu()) /
11,188,882✔
353
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
22,377,764✔
354
                                     2.0 * alpha * alpha_out * p.mu());
11,188,882✔
355
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
11,188,882✔
356
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
11,188,882✔
357
    }
358

359
    // Allow electrons to fill orbital and produce Auger electrons and
360
    // fluorescent photons. Since Compton subshell data does not match atomic
361
    // relaxation data, use the mapping between the data to find the subshell
362
    if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
11,294,164!
363
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
11,294,164✔
364
    }
365

366
    phi += PI;
11,294,164✔
367
    p.E() = alpha_out * MASS_ELECTRON_EV;
11,294,164✔
368
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
11,294,164✔
369
    p.event() = TallyEvent::SCATTER;
11,294,164✔
370
    p.event_mt() = INCOHERENT;
11,294,164✔
371
    return;
11,294,164✔
372
  }
373

374
  // Photoelectric effect
375
  double prob_after = prob + micro.photoelectric;
5,216,429✔
376

377
  if (prob_after > cutoff) {
5,216,429✔
378
    // Get grid index, interpolation factor, and bounding subshell
379
    // cross sections
380
    int i_grid = micro.index_grid;
5,126,514✔
381
    double f = micro.interp_factor;
5,126,514✔
382
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
5,126,514✔
383
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
5,126,514✔
384

385
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
24,356,968!
386
      const auto& shell {element.shells_[i_shell]};
24,356,968✔
387

388
      // Check threshold of reaction
389
      if (xs_lower(i_shell) == 0)
24,356,968✔
390
        continue;
10,279,620✔
391

392
      //  Evaluation subshell photoionization cross section
393
      prob += std::exp(
14,077,348✔
394
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
14,077,348✔
395

396
      if (prob > cutoff) {
14,077,348✔
397
        // Determine binding energy based on whether atomic relaxation data is
398
        // present (if not, use value from Compton profile data)
399
        double binding_energy = element.has_atomic_relaxation_
5,126,514✔
400
                                  ? shell.binding_energy
5,126,514!
401
                                  : element.binding_energy_[i_shell];
×
402

403
        // Determine energy of secondary electron
404
        double E_electron = p.E() - binding_energy;
5,126,514✔
405

406
        // Sample mu using non-relativistic Sauter distribution.
407
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
408
        // model in Serpent 2" by Toni Kaltiaisenaho
409
        double mu;
410
        while (true) {
411
          double r = prn(p.current_seed());
7,689,422✔
412
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
7,689,422✔
413
            double rel_vel =
414
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
5,126,514✔
415
              (E_electron + MASS_ELECTRON_EV);
5,126,514✔
416
            mu =
5,126,514✔
417
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
5,126,514✔
418
            break;
5,126,514✔
419
          }
420
        }
2,562,908✔
421

422
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
5,126,514✔
423
        Direction u;
5,126,514✔
424
        u.x = mu;
5,126,514✔
425
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
5,126,514✔
426
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
5,126,514✔
427

428
        // Create secondary electron
429
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
5,126,514✔
430

431
        // Allow electrons to fill orbital and produce auger electrons
432
        // and fluorescent photons
433
        element.atomic_relaxation(i_shell, p);
5,126,514✔
434
        p.event() = TallyEvent::ABSORB;
5,126,514✔
435
        p.event_mt() = 533 + shell.index_subshell;
5,126,514✔
436
        p.wgt() = 0.0;
5,126,514✔
437
        p.E() = 0.0;
5,126,514✔
438
        return;
5,126,514✔
439
      }
440
    }
441
  }
10,253,028!
442
  prob = prob_after;
89,915✔
443

444
  // Pair production
445
  prob += micro.pair_production;
89,915✔
446
  if (prob > cutoff) {
89,915!
447
    double E_electron, E_positron;
448
    double mu_electron, mu_positron;
449
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
89,915✔
450
      &mu_positron, p.current_seed());
451

452
    // Create secondary electron
453
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
89,915✔
454
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
89,915✔
455

456
    // Create secondary positron
457
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
89,915✔
458
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
89,915✔
459

460
    p.event() = TallyEvent::ABSORB;
89,915✔
461
    p.event_mt() = PAIR_PROD;
89,915✔
462
    p.wgt() = 0.0;
89,915✔
463
    p.E() = 0.0;
89,915✔
464
  }
465
}
466

467
void sample_electron_reaction(Particle& p)
53,895,811✔
468
{
469
  // TODO: create reaction types
470

471
  if (settings::electron_treatment == ElectronTreatment::TTB) {
53,895,811✔
472
    double E_lost;
473
    thick_target_bremsstrahlung(p, &E_lost);
53,067,709✔
474
  }
475

476
  p.E() = 0.0;
53,895,811✔
477
  p.wgt() = 0.0;
53,895,811✔
478
  p.event() = TallyEvent::ABSORB;
53,895,811✔
479
}
53,895,811✔
480

481
void sample_positron_reaction(Particle& p)
89,915✔
482
{
483
  // TODO: create reaction types
484

485
  if (settings::electron_treatment == ElectronTreatment::TTB) {
89,915✔
486
    double E_lost;
487
    thick_target_bremsstrahlung(p, &E_lost);
85,999✔
488
  }
489

490
  // Sample angle isotropically
491
  Direction u = isotropic_direction(p.current_seed());
89,915✔
492

493
  // Create annihilation photon pair traveling in opposite directions
494
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
89,915✔
495
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
89,915✔
496

497
  p.E() = 0.0;
89,915✔
498
  p.wgt() = 0.0;
89,915✔
499
  p.event() = TallyEvent::ABSORB;
89,915✔
500
}
89,915✔
501

502
int sample_nuclide(Particle& p)
870,872,150✔
503
{
504
  // Sample cumulative distribution function
505
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
870,872,150✔
506

507
  // Get pointers to nuclide/density arrays
508
  const auto& mat {model::materials[p.material()]};
870,872,150✔
509
  int n = mat->nuclide_.size();
870,872,150✔
510

511
  double prob = 0.0;
870,872,150✔
512
  for (int i = 0; i < n; ++i) {
1,857,920,702!
513
    // Get atom density
514
    int i_nuclide = mat->nuclide_[i];
1,857,920,702✔
515
    double atom_density = mat->atom_density(i, p.density_mult());
1,857,920,702✔
516

517
    // Increment probability to compare to cutoff
518
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,857,920,702✔
519
    if (prob >= cutoff)
1,857,920,702✔
520
      return i_nuclide;
870,872,150✔
521
  }
522

523
  // If we reach here, no nuclide was sampled
524
  p.write_restart();
×
525
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
526
}
527

528
int sample_element(Particle& p)
17,468,722✔
529
{
530
  // Sample cumulative distribution function
531
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
17,468,722✔
532

533
  // Get pointers to elements, densities
534
  const auto& mat {model::materials[p.material()]};
17,468,722✔
535

536
  double prob = 0.0;
17,468,722✔
537
  for (int i = 0; i < mat->element_.size(); ++i) {
41,566,668!
538
    // Find atom density
539
    int i_element = mat->element_[i];
41,566,668✔
540
    double atom_density = mat->atom_density(i, p.density_mult());
41,566,668✔
541

542
    // Determine microscopic cross section
543
    double sigma = atom_density * p.photon_xs(i_element).total;
41,566,668✔
544

545
    // Increment probability to compare to cutoff
546
    prob += sigma;
41,566,668✔
547
    if (prob > cutoff) {
41,566,668✔
548
      // Save which nuclide particle had collision with for tally purpose
549
      p.event_nuclide() = mat->nuclide_[i];
17,468,722✔
550

551
      return i_element;
17,468,722✔
552
    }
553
  }
554

555
  // If we made it here, no element was sampled
556
  p.write_restart();
×
557
  fatal_error("Did not sample any element during collision.");
×
558
}
559

560
Reaction& sample_fission(int i_nuclide, Particle& p)
114,589,874✔
561
{
562
  // Get pointer to nuclide
563
  const auto& nuc {data::nuclides[i_nuclide]};
114,589,874✔
564

565
  // If we're in the URR, by default use the first fission reaction. We also
566
  // default to the first reaction if we know that there are no partial fission
567
  // reactions
568
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
114,589,874✔
569
    return *nuc->fission_rx_[0];
114,562,110✔
570
  }
571

572
  // Check to see if we are in a windowed multipole range.  WMP only supports
573
  // the first fission reaction.
574
  if (nuc->multipole_) {
27,764!
575
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
576
      return *nuc->fission_rx_[0];
×
577
    }
578
  }
579

580
  // Get grid index and interpolation factor and sample fission cdf
581
  const auto& micro = p.neutron_xs(i_nuclide);
27,764✔
582
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
27,764✔
583
  double prob = 0.0;
27,764✔
584

585
  // Loop through each partial fission reaction type
586
  for (auto& rx : nuc->fission_rx_) {
27,824!
587
    // add to cumulative probability
588
    prob += rx->xs(micro);
27,824✔
589

590
    // Create fission bank sites if fission occurs
591
    if (prob > cutoff)
27,824✔
592
      return *rx;
27,764✔
593
  }
594

595
  // If we reached here, no reaction was sampled
596
  throw std::runtime_error {
×
597
    "No fission reaction was sampled for " + nuc->name_};
×
598
}
599

600
void sample_photon_product(
1,337,644✔
601
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
602
{
603
  // Get grid index and interpolation factor and sample photon production cdf
604
  const auto& micro = p.neutron_xs(i_nuclide);
1,337,644✔
605
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,337,644✔
606
  double prob = 0.0;
1,337,644✔
607

608
  // Loop through each reaction type
609
  const auto& nuc {data::nuclides[i_nuclide]};
1,337,644✔
610
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
21,995,831!
611
    // Evaluate neutron cross section
612
    const auto& rx = nuc->reactions_[i];
21,995,831✔
613
    double xs = rx->xs(micro);
21,995,831✔
614

615
    // if cross section is zero for this reaction, skip it
616
    if (xs == 0.0)
21,995,831✔
617
      continue;
7,191,415✔
618

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

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

637
        *i_rx = i;
43,656,910✔
638
        *i_product = j;
43,656,910✔
639
        if (prob > cutoff)
43,656,910✔
640
          return;
1,337,644✔
641
      }
642
    }
643
  }
644
}
645

646
void absorption(Particle& p, int i_nuclide)
870,868,630✔
647
{
648
  if (settings::survival_biasing) {
870,868,630✔
649
    // Determine weight absorbed in survival biasing
650
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
499,950✔
651
                              p.neutron_xs(i_nuclide).total;
499,950✔
652

653
    // Adjust weight of particle by probability of absorption
654
    p.wgt() -= wgt_absorb;
499,950✔
655

656
    // Score implicit absorption estimate of keff
657
    if (settings::run_mode == RunMode::EIGENVALUE) {
499,950!
658
      p.keff_tally_absorption() += wgt_absorb *
499,950✔
659
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950✔
660
                                   p.neutron_xs(i_nuclide).absorption;
499,950✔
661
    }
662
  } else {
663
    // See if disappearance reaction happens
664
    if (p.neutron_xs(i_nuclide).absorption >
870,368,680✔
665
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
870,368,680✔
666
      // Score absorption estimate of keff
667
      if (settings::run_mode == RunMode::EIGENVALUE) {
21,814,076✔
668
        p.keff_tally_absorption() += p.wgt() *
34,485,720✔
669
                                     p.neutron_xs(i_nuclide).nu_fission /
17,242,860✔
670
                                     p.neutron_xs(i_nuclide).absorption;
17,242,860✔
671
      }
672

673
      p.wgt() = 0.0;
21,814,076✔
674
      p.event() = TallyEvent::ABSORB;
21,814,076✔
675
      if (!p.fission()) {
21,814,076✔
676
        p.event_mt() = N_DISAPPEAR;
13,276,861✔
677
      }
678
    }
679
  }
680
}
870,868,630✔
681

682
void scatter(Particle& p, int i_nuclide)
848,899,245✔
683
{
684
  // copy incoming direction
685
  Direction u_old {p.u()};
848,899,245✔
686

687
  // Get pointer to nuclide and grid index/interpolation factor
688
  const auto& nuc {data::nuclides[i_nuclide]};
848,899,245✔
689
  const auto& micro {p.neutron_xs(i_nuclide)};
848,899,245✔
690
  int i_temp = micro.index_temp;
848,899,245✔
691

692
  // For tallying purposes, this routine might be called directly. In that
693
  // case, we need to sample a reaction via the cutoff variable
694
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
848,899,245✔
695
  bool sampled = false;
848,899,245✔
696

697
  // Calculate elastic cross section if it wasn't precalculated
698
  if (micro.elastic == CACHE_INVALID) {
848,899,245✔
699
    nuc->calculate_elastic_xs(p);
699,907,170✔
700
  }
701

702
  double prob = micro.elastic - micro.thermal;
848,899,245✔
703
  if (prob > cutoff) {
848,899,245✔
704
    // =======================================================================
705
    // NON-S(A,B) ELASTIC SCATTERING
706

707
    // Determine temperature
708
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
726,126,109✔
709

710
    // Perform collision physics for elastic scattering
711
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
726,126,109✔
712

713
    p.event_mt() = ELASTIC;
726,126,109✔
714
    sampled = true;
726,126,109✔
715
  }
716

717
  prob = micro.elastic;
848,899,245✔
718
  if (prob > cutoff && !sampled) {
848,899,245✔
719
    // =======================================================================
720
    // S(A,B) SCATTERING
721

722
    sab_scatter(i_nuclide, micro.index_sab, p);
105,302,963✔
723

724
    p.event_mt() = ELASTIC;
105,302,963✔
725
    sampled = true;
105,302,963✔
726
  }
727

728
  if (!sampled) {
848,899,245✔
729
    // =======================================================================
730
    // INELASTIC SCATTERING
731

732
    int n = nuc->index_inelastic_scatter_.size();
17,470,173✔
733
    int i = 0;
17,470,173✔
734
    for (int j = 0; j < n && prob < cutoff; ++j) {
335,349,649✔
735
      i = nuc->index_inelastic_scatter_[j];
317,879,476✔
736

737
      // add to cumulative probability
738
      prob += nuc->reactions_[i]->xs(micro);
317,879,476✔
739
    }
740

741
    // Perform collision physics for inelastic scattering
742
    const auto& rx {nuc->reactions_[i]};
17,470,173✔
743
    inelastic_scatter(*nuc, *rx, p);
17,470,173✔
744
    p.event_mt() = rx->mt_;
17,470,173✔
745
  }
746

747
  // Set event component
748
  p.event() = TallyEvent::SCATTER;
848,899,245✔
749

750
  // Sample new outgoing angle for isotropic-in-lab scattering
751
  const auto& mat {model::materials[p.material()]};
848,899,245✔
752
  if (!mat->p0_.empty()) {
848,899,245✔
753
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
326,370✔
754
    if (mat->p0_[i_nuc_mat]) {
326,370!
755
      // Sample isotropic-in-lab outgoing direction
756
      p.u() = isotropic_direction(p.current_seed());
326,370✔
757
      p.mu() = u_old.dot(p.u());
326,370✔
758
    }
759
  }
760
}
848,899,245✔
761

762
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
726,126,109✔
763
{
764
  // get pointer to nuclide
765
  const auto& nuc {data::nuclides[i_nuclide]};
726,126,109✔
766

767
  double vel = std::sqrt(p.E());
726,126,109✔
768
  double awr = nuc->awr_;
726,126,109✔
769

770
  // Neutron velocity in LAB
771
  Direction v_n = vel * p.u();
726,126,109✔
772

773
  // Sample velocity of target nucleus
774
  Direction v_t {};
726,126,109✔
775
  if (!p.neutron_xs(i_nuclide).use_ptable) {
726,126,109✔
776
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,396,657,320✔
777
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
698,328,660✔
778
  }
779

780
  // Velocity of center-of-mass
781
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
726,126,109✔
782

783
  // Transform to CM frame
784
  v_n -= v_cm;
726,126,109✔
785

786
  // Find speed of neutron in CM
787
  vel = v_n.norm();
726,126,109✔
788

789
  // Sample scattering angle, checking if angle distribution is present (assume
790
  // isotropic otherwise)
791
  double mu_cm;
792
  auto& d = rx.products_[0].distribution_[0];
726,126,109✔
793
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
726,126,109!
794
  if (!d_->angle().empty()) {
726,126,109!
795
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
726,126,109✔
796
  } else {
797
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
798
  }
799

800
  // Determine direction cosines in CM
801
  Direction u_cm = v_n / vel;
726,126,109✔
802

803
  // Rotate neutron velocity vector to new angle -- note that the speed of the
804
  // neutron in CM does not change in elastic scattering. However, the speed
805
  // will change when we convert back to LAB
806
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
726,126,109✔
807

808
  // Transform back to LAB frame
809
  v_n += v_cm;
726,126,109✔
810

811
  p.E() = v_n.dot(v_n);
726,126,109✔
812
  vel = std::sqrt(p.E());
726,126,109✔
813

814
  // compute cosine of scattering angle in LAB frame by taking dot product of
815
  // neutron's pre- and post-collision angle
816
  p.mu() = p.u().dot(v_n) / vel;
726,126,109✔
817

818
  // Set energy and direction of particle in LAB frame
819
  p.u() = v_n / vel;
726,126,109✔
820

821
  // Because of floating-point roundoff, it may be possible for mu_lab to be
822
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
823
  // -1 or 1
824
  if (std::abs(p.mu()) > 1.0)
726,126,109!
825
    p.mu() = std::copysign(1.0, p.mu());
×
826
}
726,126,109✔
827

828
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
105,302,963✔
829
{
830
  // Determine temperature index
831
  const auto& micro {p.neutron_xs(i_nuclide)};
105,302,963✔
832
  int i_temp = micro.index_temp_sab;
105,302,963✔
833

834
  // Sample energy and angle
835
  double E_out;
836
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
210,605,926✔
837
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
105,302,963✔
838

839
  // Set energy to outgoing, change direction of particle
840
  p.E() = E_out;
105,302,963✔
841
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
105,302,963✔
842
}
105,302,963✔
843

844
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
698,328,660✔
845
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
846
{
847
  // check if nuclide is a resonant scatterer
848
  ResScatMethod sampling_method;
849
  if (nuc.resonant_) {
698,328,660✔
850

851
    // sampling method to use
852
    sampling_method = settings::res_scat_method;
84,557✔
853

854
    // upper resonance scattering energy bound (target is at rest above this E)
855
    if (E > settings::res_scat_energy_max) {
84,557✔
856
      return {};
40,755✔
857

858
      // lower resonance scattering energy bound (should be no resonances below)
859
    } else if (E < settings::res_scat_energy_min) {
43,802✔
860
      sampling_method = ResScatMethod::cxs;
24,992✔
861
    }
862

863
    // otherwise, use free gas model
864
  } else {
865
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
698,244,103✔
866
      return {};
335,856,158✔
867
    } else {
868
      sampling_method = ResScatMethod::cxs;
362,387,945✔
869
    }
870
  }
871

872
  // use appropriate target velocity sampling method
873
  switch (sampling_method) {
362,431,747!
874
  case ResScatMethod::cxs:
362,412,937✔
875

876
    // sample target velocity with the constant cross section (cxs) approx.
877
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
362,412,937✔
878

879
  case ResScatMethod::dbrc:
18,810✔
880
  case ResScatMethod::rvs: {
881
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810✔
882
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
18,810✔
883
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810✔
884

885
    // find lower and upper energy bound indices
886
    // lower index
887
    int i_E_low;
888
    if (E_low < nuc.energy_0K_.front()) {
18,810!
889
      i_E_low = 0;
×
890
    } else if (E_low > nuc.energy_0K_.back()) {
18,810!
891
      i_E_low = nuc.energy_0K_.size() - 2;
×
892
    } else {
893
      i_E_low =
18,810✔
894
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
18,810✔
895
    }
896

897
    // upper index
898
    int i_E_up;
899
    if (E_up < nuc.energy_0K_.front()) {
18,810!
900
      i_E_up = 0;
×
901
    } else if (E_up > nuc.energy_0K_.back()) {
18,810!
902
      i_E_up = nuc.energy_0K_.size() - 2;
×
903
    } else {
904
      i_E_up =
18,810✔
905
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
18,810✔
906
    }
907

908
    if (i_E_up == i_E_low) {
18,810✔
909
      // Handle degenerate case -- if the upper/lower bounds occur for the same
910
      // index, then using cxs is probably a good approximation
911
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
18,810✔
912
    }
913

914
    if (sampling_method == ResScatMethod::dbrc) {
15,532!
915
      // interpolate xs since we're not exactly at the energy indices
916
      double xs_low = nuc.elastic_0K_[i_E_low];
×
917
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
918
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
919
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
920
      double xs_up = nuc.elastic_0K_[i_E_up];
×
921
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
922
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
923
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
924

925
      // get max 0K xs value over range of practical relative energies
926
      double xs_max = *std::max_element(
×
927
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
928
      xs_max = std::max({xs_low, xs_max, xs_up});
×
929

930
      while (true) {
931
        double E_rel;
932
        Direction v_target;
×
933
        while (true) {
934
          // sample target velocity with the constant cross section (cxs)
935
          // approx.
936
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
937
          Direction v_rel = v_neut - v_target;
×
938
          E_rel = v_rel.dot(v_rel);
×
939
          if (E_rel < E_up)
×
940
            break;
×
941
        }
×
942

943
        // perform Doppler broadening rejection correction (dbrc)
944
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
945
        double R = xs_0K / xs_max;
×
946
        if (prn(seed) < R)
×
947
          return v_target;
×
948
      }
×
949

950
    } else if (sampling_method == ResScatMethod::rvs) {
15,532!
951
      // interpolate xs CDF since we're not exactly at the energy indices
952
      // cdf value at lower bound attainable energy
953
      double cdf_low = 0.0;
15,532✔
954
      if (E_low > nuc.energy_0K_.front()) {
15,532!
955
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
15,532✔
956
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
15,532✔
957
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
15,532✔
958
      }
959

960
      // cdf value at upper bound attainable energy
961
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,532✔
962
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,532✔
963
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,532✔
964

965
      while (true) {
966
        // directly sample Maxwellian
967
        double E_t = -kT * std::log(prn(seed));
170,720✔
968

969
        // sample a relative energy using the xs cdf
970
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
170,720✔
971
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
170,720✔
972
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
170,720✔
973
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
170,720✔
974
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
170,720✔
975
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
170,720✔
976
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
170,720✔
977
                     nuc.energy_0K_[i_E_low + i_E_rel]);
170,720✔
978
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
170,720✔
979

980
        // perform rejection sampling on cosine between
981
        // neutron and target velocities
982
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
170,720✔
983
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
170,720✔
984

985
        if (std::abs(mu) < 1.0) {
170,720✔
986
          // set and accept target velocity
987
          E_t /= nuc.awr_;
15,532✔
988
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,532✔
989
        }
990
      }
155,188✔
991
    }
992
  } // case RVS, DBRC
993
  } // switch (sampling_method)
994

995
  UNREACHABLE();
×
996
}
997

998
Direction sample_cxs_target_velocity(
362,416,215✔
999
  double awr, double E, Direction u, double kT, uint64_t* seed)
1000
{
1001
  double beta_vn = std::sqrt(awr * E / kT);
362,416,215✔
1002
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
362,416,215✔
1003

1004
  double beta_vt_sq;
1005
  double mu;
1006
  while (true) {
1007
    // Sample two random numbers
1008
    double r1 = prn(seed);
425,673,417✔
1009
    double r2 = prn(seed);
425,673,417✔
1010

1011
    if (prn(seed) < alpha) {
425,673,417✔
1012
      // With probability alpha, we sample the distribution p(y) =
1013
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1014
      // Carlo sampler
1015

1016
      beta_vt_sq = -std::log(r1 * r2);
102,440,686✔
1017

1018
    } else {
1019
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1020
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1021
      // Carlo sampler
1022

1023
      double c = std::cos(PI / 2.0 * prn(seed));
323,232,731✔
1024
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
323,232,731✔
1025
    }
1026

1027
    // Determine beta * vt
1028
    double beta_vt = std::sqrt(beta_vt_sq);
425,673,417✔
1029

1030
    // Sample cosine of angle between neutron and target velocity
1031
    mu = uniform_distribution(-1., 1., seed);
425,673,417✔
1032

1033
    // Determine rejection probability
1034
    double accept_prob =
1035
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
425,673,417✔
1036
      (beta_vn + beta_vt);
425,673,417✔
1037

1038
    // Perform rejection sampling on vt and mu
1039
    if (prn(seed) < accept_prob)
425,673,417✔
1040
      break;
362,416,215✔
1041
  }
63,257,202✔
1042

1043
  // Determine speed of target nucleus
1044
  double vt = std::sqrt(beta_vt_sq * kT / awr);
362,416,215✔
1045

1046
  // Determine velocity vector of target nucleus based on neutron's velocity
1047
  // and the sampled angle between them
1048
  return vt * rotate_angle(u, mu, nullptr, seed);
362,416,215✔
1049
}
1050

1051
void sample_fission_neutron(
30,000,767✔
1052
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1053
{
1054
  // Get attributes of particle
1055
  double E_in = p.E();
30,000,767✔
1056
  uint64_t* seed = p.current_seed();
30,000,767✔
1057

1058
  // Determine total nu, delayed nu, and delayed neutron fraction
1059
  const auto& nuc {data::nuclides[i_nuclide]};
30,000,767✔
1060
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
30,000,767✔
1061
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
30,000,767✔
1062
  double beta = nu_d / nu_t;
30,000,767✔
1063

1064
  if (prn(seed) < beta) {
30,000,767✔
1065
    // ====================================================================
1066
    // DELAYED NEUTRON SAMPLED
1067

1068
    // sampled delayed precursor group
1069
    double xi = prn(seed) * nu_d;
187,898✔
1070
    double prob = 0.0;
187,898✔
1071
    int group;
1072
    for (group = 1; group < nuc->n_precursor_; ++group) {
699,245✔
1073
      // determine delayed neutron precursor yield for group j
1074
      double yield = (*rx.products_[group].yield_)(E_in);
686,044✔
1075

1076
      // Check if this group is sampled
1077
      prob += yield;
686,044✔
1078
      if (xi < prob)
686,044✔
1079
        break;
174,697✔
1080
    }
1081

1082
    // if the sum of the probabilities is slightly less than one and the
1083
    // random number is greater, j will be greater than nuc %
1084
    // n_precursor -- check for this condition
1085
    group = std::min(group, nuc->n_precursor_);
187,898✔
1086

1087
    // set the delayed group for the particle born from fission
1088
    site->delayed_group = group;
187,898✔
1089

1090
    // Sample time of emission based on decay constant of precursor
1091
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
187,898✔
1092
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
187,898✔
1093

1094
  } else {
1095
    // ====================================================================
1096
    // PROMPT NEUTRON SAMPLED
1097

1098
    // set the delayed group for the particle born from fission to 0
1099
    site->delayed_group = 0;
29,812,869✔
1100
  }
1101

1102
  // sample from prompt neutron energy distribution
1103
  int n_sample = 0;
30,000,767✔
1104
  double mu;
1105
  while (true) {
1106
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
30,000,767✔
1107

1108
    // resample if energy is greater than maximum neutron energy
1109
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
30,000,767✔
1110
    if (site->E < data::energy_max[neutron])
30,000,767!
1111
      break;
30,000,767✔
1112

1113
    // check for large number of resamples
1114
    ++n_sample;
×
1115
    if (n_sample == MAX_SAMPLE) {
×
1116
      // particle_write_restart(p)
1117
      fatal_error("Resampled energy distribution maximum number of times "
×
1118
                  "for nuclide " +
×
1119
                  nuc->name_);
×
1120
    }
1121
  }
×
1122

1123
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1124
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
30,000,767✔
1125
}
30,000,767✔
1126

1127
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
17,470,173✔
1128
{
1129
  // copy energy of neutron
1130
  double E_in = p.E();
17,470,173✔
1131

1132
  // sample outgoing energy and scattering cosine
1133
  double E;
1134
  double mu;
1135
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
17,470,173✔
1136

1137
  // if scattering system is in center-of-mass, transfer cosine of scattering
1138
  // angle and outgoing energy from CM to LAB
1139
  if (rx.scatter_in_cm_) {
17,470,173✔
1140
    double E_cm = E;
17,417,543✔
1141

1142
    // determine outgoing energy in lab
1143
    double A = nuc.awr_;
17,417,543✔
1144
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
17,417,543✔
1145
                 ((A + 1.0) * (A + 1.0));
17,417,543✔
1146

1147
    // determine outgoing angle in lab
1148
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
17,417,543✔
1149
  }
1150

1151
  // Because of floating-point roundoff, it may be possible for mu to be
1152
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1153
  // or 1
1154
  if (std::abs(mu) > 1.0)
17,470,173!
1155
    mu = std::copysign(1.0, mu);
×
1156

1157
  // Set outgoing energy and scattering angle
1158
  p.E() = E;
17,470,173✔
1159
  p.mu() = mu;
17,470,173✔
1160

1161
  // change direction of particle
1162
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
17,470,173✔
1163

1164
  // evaluate yield
1165
  double yield = (*rx.products_[0].yield_)(E_in);
17,470,173✔
1166
  if (std::floor(yield) == yield && yield > 0) {
17,470,173!
1167
    // If yield is integral, create exactly that many secondary particles
1168
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
17,572,243✔
1169
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
102,124✔
1170
    }
1171
  } else {
17,470,119✔
1172
    // Otherwise, change weight of particle based on yield
1173
    p.wgt() *= yield;
54✔
1174
  }
1175
}
17,470,173✔
1176

1177
void sample_secondary_photons(Particle& p, int i_nuclide)
8,809,548✔
1178
{
1179
  // Sample the number of photons produced
1180
  double y_t =
1181
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,809,548✔
1182
  double photon_wgt = p.wgt();
8,809,548✔
1183
  int y = 1;
8,809,548✔
1184

1185
  if (settings::use_decay_photons) {
8,809,548✔
1186
    // For decay photons, sample a single photon and modify the weight
1187
    if (y_t <= 0.0)
72,006✔
1188
      return;
17,281✔
1189
    photon_wgt *= y_t;
54,725✔
1190
  } else {
1191
    // For prompt photons, sample an integral number of photons with weight
1192
    // equal to the neutron's weight
1193
    y = static_cast<int>(y_t);
8,737,542✔
1194
    if (prn(p.current_seed()) <= y_t - y)
8,737,542✔
1195
      ++y;
574,860✔
1196
  }
1197

1198
  // Sample each secondary photon
1199
  for (int i = 0; i < y; ++i) {
10,129,911✔
1200
    // Sample the reaction and product
1201
    int i_rx;
1202
    int i_product;
1203
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,337,644✔
1204

1205
    // Sample the outgoing energy and angle
1206
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,337,644✔
1207
    double E;
1208
    double mu;
1209
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,337,644✔
1210

1211
    // Sample the new direction
1212
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,337,644✔
1213

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

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

1227
    // Tag secondary particle with parent nuclide
1228
    if (created_photon && settings::use_decay_photons) {
1,337,644✔
1229
      p.secondary_bank().back().parent_nuclide =
52,844✔
1230
        rx->products_[i_product].parent_nuclide_;
52,844✔
1231
    }
1232
  }
1233
}
1234

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