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

openmc-dev / openmc / 21829370254

09 Feb 2026 02:38PM UTC coverage: 81.937% (+0.1%) from 81.817%
21829370254

Pull #3737

github

web-flow
Merge 8740453dd into d0346e94a
Pull Request #3737: Temperature feedback support in the random ray solver.

17362 of 24292 branches covered (71.47%)

Branch coverage included in aggregate %.

333 of 360 new or added lines in 11 files covered. (92.5%)

680 existing lines in 42 files now uncovered.

56324 of 65638 relevant lines covered (85.81%)

76491867.81 hits per line

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

83.66
/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)
1,547,931,710✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
1,547,931,710✔
47
  p.secondary_bank_index() = p.secondary_bank().size();
1,547,931,710✔
48

49
  // Sample reaction for the material the particle is in
50
  switch (p.type().pdg_number()) {
1,547,931,710!
51
  case PDG_NEUTRON:
1,445,078,267✔
52
    sample_neutron_reaction(p);
1,445,078,267✔
53
    break;
1,445,078,267✔
54
  case PDG_PHOTON:
25,099,794✔
55
    sample_photon_reaction(p);
25,099,794✔
56
    break;
25,099,794✔
57
  case PDG_ELECTRON:
77,626,720✔
58
    sample_electron_reaction(p);
77,626,720✔
59
    break;
77,626,720✔
60
  case PDG_POSITRON:
126,929✔
61
    sample_positron_reaction(p);
126,929✔
62
    break;
126,929✔
63
  default:
×
64
    fatal_error("Unsupported particle PDG for collision sampling.");
×
65
  }
66

67
  if (settings::weight_window_checkpoint_collision)
1,547,931,710!
68
    apply_weight_windows(p);
1,547,931,710✔
69

70
  // Kill particle if energy falls below cutoff
71
  int type = p.type().transport_index();
1,547,931,710✔
72
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
1,547,931,710!
73
    p.wgt() = 0.0;
7,542,331✔
74
  }
75

76
  // Display information about collision
77
  if (settings::verbosity >= 10 || p.trace()) {
1,547,931,710!
78
    std::string msg;
96✔
79
    if (p.event() == TallyEvent::KILL) {
96!
80
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
81
    } else if (p.type().is_neutron()) {
96!
82
      msg = fmt::format("    {} with {}. Energy = {} eV.",
276✔
83
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
192✔
84
        p.E());
96✔
85
    } else if (p.type().is_photon()) {
×
86
      msg = fmt::format("    {} with {}. Energy = {} eV.",
×
87
        reaction_name(p.event_mt()),
×
88
        to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
×
89
    } else {
90
      msg = fmt::format("    Disappeared. Energy = {} eV.", p.E());
×
91
    }
92
    write_message(msg, 1);
96✔
93
  }
96✔
94
}
1,547,931,710✔
95

96
void sample_neutron_reaction(Particle& p)
1,445,078,267✔
97
{
98
  // Sample a nuclide within the material
99
  int i_nuclide = sample_nuclide(p);
1,445,078,267✔
100

101
  // Save which nuclide particle had collision with
102
  p.event_nuclide() = i_nuclide;
1,445,078,267✔
103

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

109
  const auto& nuc {data::nuclides[i_nuclide]};
1,445,078,267✔
110

111
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
1,445,078,267✔
112
    auto& rx = sample_fission(i_nuclide, p);
181,997,980✔
113
    if (settings::run_mode == RunMode::EIGENVALUE) {
181,997,980✔
114
      create_fission_sites(p, i_nuclide, rx);
145,728,288✔
115
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
36,269,692✔
116
               settings::create_fission_neutrons) {
117
      create_fission_sites(p, i_nuclide, rx);
711,804✔
118

119
      // Make sure particle population doesn't grow out of control for
120
      // subcritical multiplication problems.
121
      if (p.secondary_bank().size() >= settings::max_secondaries) {
711,804!
122
        fatal_error(
×
123
          "The secondary particle bank appears to be growing without "
124
          "bound. You are likely running a subcritical multiplication problem "
125
          "with k-effective close to or greater than one.");
126
      }
127
    }
128
    p.event_mt() = rx.mt_;
181,997,980✔
129
  }
130

131
  // Create secondary photons
132
  if (settings::photon_transport) {
1,445,078,267✔
133
    sample_secondary_photons(p, i_nuclide);
12,813,888✔
134
  }
135

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

139
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
1,445,078,267✔
140
    absorption(p, i_nuclide);
1,445,073,147✔
141
  }
142
  if (!p.alive())
1,445,078,267✔
143
    return;
33,972,396✔
144

145
  // Sample a scattering reaction and determine the secondary energy of the
146
  // exiting neutron
147
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
1,411,105,871✔
148
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
1,411,105,871!
149
    ncrystal_mat.scatter(p);
231,024✔
150
  } else {
151
    scatter(p, i_nuclide);
1,410,874,847✔
152
  }
153

154
  // Advance URR seed stream 'N' times after energy changes
155
  if (p.E() != p.E_last()) {
1,411,105,871✔
156
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
1,410,641,535✔
157
  }
158

159
  // Play russian roulette if survival biasing is turned on
160
  if (settings::survival_biasing) {
1,411,105,871✔
161
    // if survival normalization is on, use normalized weight cutoff and
162
    // normalized weight survive
163
    if (settings::survival_normalization) {
727,200!
164
      if (p.wgt() < settings::weight_cutoff * p.wgt_born()) {
×
165
        russian_roulette(p, settings::weight_survive * p.wgt_born());
×
166
      }
167
    } else if (p.wgt() < settings::weight_cutoff) {
727,200✔
168
      russian_roulette(p, settings::weight_survive);
82,896✔
169
    }
170
  }
171
}
172

173
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
146,440,092✔
174
{
175
  // If uniform fission source weighting is turned on, we increase or decrease
176
  // the expected number of fission sites produced
177
  double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
146,440,092✔
178

179
  // Determine the expected number of neutrons produced
180
  double nu_t = p.wgt() / simulation::keff * weight *
146,440,092✔
181
                p.neutron_xs(i_nuclide).nu_fission /
146,440,092✔
182
                p.neutron_xs(i_nuclide).total;
146,440,092✔
183

184
  // Sample the number of neutrons produced
185
  int nu = static_cast<int>(nu_t);
146,440,092✔
186
  if (prn(p.current_seed()) <= (nu_t - nu))
146,440,092✔
187
    ++nu;
29,933,013✔
188

189
  // If no neutrons were produced then don't continue
190
  if (nu == 0)
146,440,092✔
191
    return;
110,685,199✔
192

193
  // Initialize the counter of delayed neutrons encountered for each delayed
194
  // group.
195
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
35,754,893✔
196

197
  // Clear out particle's nu fission bank
198
  p.nu_bank().clear();
35,754,893✔
199

200
  p.fission() = true;
35,754,893✔
201

202
  // Determine whether to place fission sites into the shared fission bank
203
  // or the secondary particle bank.
204
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
35,754,893✔
205

206
  // Counter for the number of fission sites successfully stored to the shared
207
  // fission bank or the secondary particle bank
208
  int n_sites_stored;
209

210
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
79,944,302✔
211
    // Initialize fission site object with particle data
212
    SourceSite site;
44,189,409✔
213
    site.r = p.r();
44,189,409✔
214
    site.particle = ParticleType::neutron();
44,189,409✔
215
    site.time = p.time();
44,189,409✔
216
    site.wgt = 1. / weight;
44,189,409✔
217
    site.surf_id = 0;
44,189,409✔
218

219
    // Sample delayed group and angle/energy for fission reaction
220
    sample_fission_neutron(i_nuclide, rx, &site, p);
44,189,409✔
221

222
    // Reject site if it exceeds time cutoff
223
    if (site.delayed_group > 0) {
44,189,409✔
224
      double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
277,653✔
225
      if (site.time > t_cutoff) {
277,653!
226
        continue;
×
227
      }
228
    }
229

230
    // Set parent and progeny IDs
231
    site.parent_id = p.id();
44,189,409✔
232
    site.progeny_id = p.n_progeny()++;
44,189,409✔
233

234
    // Store fission site in bank
235
    if (use_fission_bank) {
44,189,409✔
236
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
43,985,730✔
237
      if (idx == -1) {
43,985,730!
238
        warning(
×
239
          "The shared fission bank is full. Additional fission sites created "
240
          "in this generation will not be banked. Results may be "
241
          "non-deterministic.");
242

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

248
        // Break out of loop as no more sites can be added to fission bank
249
        break;
×
250
      }
251
      // Iterated Fission Probability (IFP) method
252
      if (settings::ifp_on) {
43,985,730✔
253
        ifp(p, idx);
1,967,456✔
254
      }
255
    } else {
256
      p.secondary_bank().push_back(site);
203,679✔
257
      p.n_secondaries()++;
203,679✔
258
    }
259

260
    // Increment the number of neutrons born delayed
261
    if (site.delayed_group > 0) {
44,189,409✔
262
      nu_d[site.delayed_group - 1]++;
277,653✔
263
    }
264

265
    // Write fission particles to nuBank
266
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
44,189,409✔
267
    nu_bank_entry.wgt = site.wgt;
44,189,409✔
268
    nu_bank_entry.E = site.E;
44,189,409✔
269
    nu_bank_entry.delayed_group = site.delayed_group;
44,189,409✔
270
  }
271

272
  // If shared fission bank was full, and no fissions could be added,
273
  // set the particle fission flag to false.
274
  if (n_sites_stored == 0) {
35,754,893!
275
    p.fission() = false;
×
276
    return;
×
277
  }
278

279
  // Set nu to the number of fission sites successfully stored. If the fission
280
  // bank was not found to be full then these values are already equivalent.
281
  nu = n_sites_stored;
35,754,893✔
282

283
  // Store the total weight banked for analog fission tallies
284
  p.n_bank() = nu;
35,754,893✔
285
  p.wgt_bank() = nu / weight;
35,754,893✔
286
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
321,794,037✔
287
    p.n_delayed_bank(d) = nu_d[d];
286,039,144✔
288
  }
289
}
290

291
void sample_photon_reaction(Particle& p)
25,099,794✔
292
{
293
  // Kill photon if below energy cutoff -- an extra check is made here because
294
  // photons with energy below the cutoff may have been produced by neutrons
295
  // reactions or atomic relaxation
296
  int photon = ParticleType::photon().transport_index();
25,099,794✔
297
  if (p.E() < settings::energy_cutoff[photon]) {
25,099,794!
298
    p.E() = 0.0;
×
299
    p.wgt() = 0.0;
×
300
    return;
×
301
  }
302

303
  // Sample element within material
304
  int i_element = sample_element(p);
25,099,794✔
305
  const auto& micro {p.photon_xs(i_element)};
25,099,794✔
306
  const auto& element {*data::elements[i_element]};
25,099,794✔
307

308
  // Calculate photon energy over electron rest mass equivalent
309
  double alpha = p.E() / MASS_ELECTRON_EV;
25,099,794✔
310

311
  // For tallying purposes, this routine might be called directly. In that
312
  // case, we need to sample a reaction via the cutoff variable
313
  double prob = 0.0;
25,099,794✔
314
  double cutoff = prn(p.current_seed()) * micro.total;
25,099,794✔
315

316
  // Coherent (Rayleigh) scattering
317
  prob += micro.coherent;
25,099,794✔
318
  if (prob > cutoff) {
25,099,794✔
319
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
1,381,899✔
320
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
1,381,899✔
321
    p.event() = TallyEvent::SCATTER;
1,381,899✔
322
    p.event_mt() = COHERENT;
1,381,899✔
323
    return;
1,381,899✔
324
  }
325

326
  // Incoherent (Compton) scattering
327
  prob += micro.incoherent;
23,717,895✔
328
  if (prob > cutoff) {
23,717,895✔
329
    double alpha_out;
330
    int i_shell;
331
    element.compton_scatter(
32,382,904✔
332
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
16,191,452✔
333

334
    // Determine binding energy of shell. The binding energy is 0.0 if
335
    // doppler broadening is not used.
336
    double e_b;
337
    if (i_shell == -1) {
16,191,452!
338
      e_b = 0.0;
×
339
    } else {
340
      e_b = element.binding_energy_[i_shell];
16,191,452✔
341
    }
342

343
    // Create Compton electron
344
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
16,191,452✔
345
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
16,191,452✔
346
    int electron = ParticleType::electron().transport_index();
16,191,452✔
347
    if (E_electron >= settings::energy_cutoff[electron]) {
16,191,452✔
348
      double mu_electron = (alpha - alpha_out * p.mu()) /
16,039,283✔
349
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
32,078,566✔
350
                                     2.0 * alpha * alpha_out * p.mu());
16,039,283✔
351
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
16,039,283✔
352
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
16,039,283✔
353
    }
354

355
    // Allow electrons to fill orbital and produce Auger electrons and
356
    // fluorescent photons. Since Compton subshell data does not match atomic
357
    // relaxation data, use the mapping between the data to find the subshell
358
    if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
16,191,452!
359
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
16,191,452✔
360
    }
361

362
    phi += PI;
16,191,452✔
363
    p.E() = alpha_out * MASS_ELECTRON_EV;
16,191,452✔
364
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
16,191,452✔
365
    p.event() = TallyEvent::SCATTER;
16,191,452✔
366
    p.event_mt() = INCOHERENT;
16,191,452✔
367
    return;
16,191,452✔
368
  }
369

370
  // Photoelectric effect
371
  double prob_after = prob + micro.photoelectric;
7,526,443✔
372

373
  if (prob_after > cutoff) {
7,526,443✔
374
    // Get grid index, interpolation factor, and bounding subshell
375
    // cross sections
376
    int i_grid = micro.index_grid;
7,399,514✔
377
    double f = micro.interp_factor;
7,399,514✔
378
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
7,399,514✔
379
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
7,399,514✔
380

381
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
35,324,445!
382
      const auto& shell {element.shells_[i_shell]};
35,324,445✔
383

384
      // Check threshold of reaction
385
      if (xs_lower(i_shell) == 0)
35,324,445✔
386
        continue;
14,933,557✔
387

388
      //  Evaluation subshell photoionization cross section
389
      prob += std::exp(
20,390,888✔
390
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
20,390,888✔
391

392
      if (prob > cutoff) {
20,390,888✔
393
        // Determine binding energy based on whether atomic relaxation data is
394
        // present (if not, use value from Compton profile data)
395
        double binding_energy = element.has_atomic_relaxation_
7,399,514✔
396
                                  ? shell.binding_energy
7,399,514!
397
                                  : element.binding_energy_[i_shell];
×
398

399
        // Determine energy of secondary electron
400
        double E_electron = p.E() - binding_energy;
7,399,514✔
401

402
        // Sample mu using non-relativistic Sauter distribution.
403
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
404
        // model in Serpent 2" by Toni Kaltiaisenaho
405
        double mu;
406
        while (true) {
407
          double r = prn(p.current_seed());
11,099,538✔
408
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
11,099,538✔
409
            double rel_vel =
410
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
7,399,514✔
411
              (E_electron + MASS_ELECTRON_EV);
7,399,514✔
412
            mu =
7,399,514✔
413
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
7,399,514✔
414
            break;
7,399,514✔
415
          }
416
        }
3,700,024✔
417

418
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
7,399,514✔
419
        Direction u;
7,399,514✔
420
        u.x = mu;
7,399,514✔
421
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
7,399,514✔
422
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
7,399,514✔
423

424
        // Create secondary electron
425
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
7,399,514✔
426

427
        // Allow electrons to fill orbital and produce auger electrons
428
        // and fluorescent photons
429
        element.atomic_relaxation(i_shell, p);
7,399,514✔
430
        p.event() = TallyEvent::ABSORB;
7,399,514✔
431
        p.event_mt() = 533 + shell.index_subshell;
7,399,514✔
432
        p.wgt() = 0.0;
7,399,514✔
433
        p.E() = 0.0;
7,399,514✔
434
        return;
7,399,514✔
435
      }
436
    }
437
  }
14,799,028!
438
  prob = prob_after;
126,929✔
439

440
  // Pair production
441
  prob += micro.pair_production;
126,929✔
442
  if (prob > cutoff) {
126,929!
443
    double E_electron, E_positron;
444
    double mu_electron, mu_positron;
445
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
126,929✔
446
      &mu_positron, p.current_seed());
447

448
    // Create secondary electron
449
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
126,929✔
450
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
126,929✔
451

452
    // Create secondary positron
453
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
126,929✔
454
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron());
126,929✔
455
    p.event() = TallyEvent::ABSORB;
126,929✔
456
    p.event_mt() = PAIR_PROD;
126,929✔
457
    p.wgt() = 0.0;
126,929✔
458
    p.E() = 0.0;
126,929✔
459
  }
460
}
461

462
void sample_electron_reaction(Particle& p)
77,626,720✔
463
{
464
  // TODO: create reaction types
465

466
  if (settings::electron_treatment == ElectronTreatment::TTB) {
77,626,720✔
467
    double E_lost;
468
    thick_target_bremsstrahlung(p, &E_lost);
77,185,040✔
469
  }
470

471
  p.E() = 0.0;
77,626,720✔
472
  p.wgt() = 0.0;
77,626,720✔
473
  p.event() = TallyEvent::ABSORB;
77,626,720✔
474
}
77,626,720✔
475

476
void sample_positron_reaction(Particle& p)
126,929✔
477
{
478
  // TODO: create reaction types
479

480
  if (settings::electron_treatment == ElectronTreatment::TTB) {
126,929✔
481
    double E_lost;
482
    thick_target_bremsstrahlung(p, &E_lost);
125,089✔
483
  }
484

485
  // Sample angle isotropically
486
  Direction u = isotropic_direction(p.current_seed());
126,929✔
487

488
  // Create annihilation photon pair traveling in opposite directions
489
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
126,929✔
490
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
126,929✔
491

492
  p.E() = 0.0;
126,929✔
493
  p.wgt() = 0.0;
126,929✔
494
  p.event() = TallyEvent::ABSORB;
126,929✔
495
}
126,929✔
496

497
int sample_nuclide(Particle& p)
1,445,078,267✔
498
{
499
  // Sample cumulative distribution function
500
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
1,445,078,267✔
501

502
  // Get pointers to nuclide/density arrays
503
  const auto& mat {model::materials[p.material()]};
1,445,078,267✔
504
  int n = mat->nuclide_.size();
1,445,078,267✔
505

506
  double prob = 0.0;
1,445,078,267✔
507
  for (int i = 0; i < n; ++i) {
3,061,257,415!
508
    // Get atom density
509
    int i_nuclide = mat->nuclide_[i];
3,061,257,415✔
510
    double atom_density = mat->atom_density(i, p.density_mult());
3,061,257,415✔
511

512
    // Increment probability to compare to cutoff
513
    prob += atom_density * p.neutron_xs(i_nuclide).total;
3,061,257,415✔
514
    if (prob >= cutoff)
3,061,257,415✔
515
      return i_nuclide;
1,445,078,267✔
516
  }
517

518
  // If we reach here, no nuclide was sampled
519
  p.write_restart();
×
520
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
521
}
522

523
int sample_element(Particle& p)
25,099,794✔
524
{
525
  // Sample cumulative distribution function
526
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
25,099,794✔
527

528
  // Get pointers to elements, densities
529
  const auto& mat {model::materials[p.material()]};
25,099,794✔
530

531
  double prob = 0.0;
25,099,794✔
532
  for (int i = 0; i < mat->element_.size(); ++i) {
59,997,832!
533
    // Find atom density
534
    int i_element = mat->element_[i];
59,997,832✔
535
    double atom_density = mat->atom_density(i, p.density_mult());
59,997,832✔
536

537
    // Determine microscopic cross section
538
    double sigma = atom_density * p.photon_xs(i_element).total;
59,997,832✔
539

540
    // Increment probability to compare to cutoff
541
    prob += sigma;
59,997,832✔
542
    if (prob > cutoff) {
59,997,832✔
543
      // Save which nuclide particle had collision with for tally purpose
544
      p.event_nuclide() = mat->nuclide_[i];
25,099,794✔
545

546
      return i_element;
25,099,794✔
547
    }
548
  }
549

550
  // If we made it here, no element was sampled
551
  p.write_restart();
×
552
  fatal_error("Did not sample any element during collision.");
×
553
}
554

555
Reaction& sample_fission(int i_nuclide, Particle& p)
181,997,980✔
556
{
557
  // Get pointer to nuclide
558
  const auto& nuc {data::nuclides[i_nuclide]};
181,997,980✔
559

560
  // If we're in the URR, by default use the first fission reaction. We also
561
  // default to the first reaction if we know that there are no partial fission
562
  // reactions
563
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
181,997,980✔
564
    return *nuc->fission_rx_[0];
181,955,617✔
565
  }
566

567
  // Check to see if we are in a windowed multipole range.  WMP only supports
568
  // the first fission reaction.
569
  if (nuc->multipole_) {
42,363✔
570
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
4,144!
571
      return *nuc->fission_rx_[0];
2,896✔
572
    }
573
  }
574

575
  // Get grid index and interpolation factor and sample fission cdf
576
  const auto& micro = p.neutron_xs(i_nuclide);
39,467✔
577
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
39,467✔
578
  double prob = 0.0;
39,467✔
579

580
  // Loop through each partial fission reaction type
581
  for (auto& rx : nuc->fission_rx_) {
39,542!
582
    // add to cumulative probability
583
    prob += rx->xs(micro);
39,542✔
584

585
    // Create fission bank sites if fission occurs
586
    if (prob > cutoff)
39,542✔
587
      return *rx;
39,467✔
588
  }
589

590
  // If we reached here, no reaction was sampled
591
  throw std::runtime_error {
×
592
    "No fission reaction was sampled for " + nuc->name_};
×
593
}
594

595
void sample_photon_product(
1,945,664✔
596
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
597
{
598
  // Get grid index and interpolation factor and sample photon production cdf
599
  const auto& micro = p.neutron_xs(i_nuclide);
1,945,664✔
600
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,945,664✔
601
  double prob = 0.0;
1,945,664✔
602

603
  // Loop through each reaction type
604
  const auto& nuc {data::nuclides[i_nuclide]};
1,945,664✔
605
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
31,993,936!
606
    // Evaluate neutron cross section
607
    const auto& rx = nuc->reactions_[i];
31,993,936✔
608
    double xs = rx->xs(micro);
31,993,936✔
609

610
    // if cross section is zero for this reaction, skip it
611
    if (xs == 0.0)
31,993,936✔
612
      continue;
10,460,240✔
613

614
    for (int j = 0; j < rx->products_.size(); ++j) {
101,203,632✔
615
      if (rx->products_[j].particle_.is_photon()) {
81,615,600✔
616
        // For fission, artificially increase the photon yield to account
617
        // for delayed photons
618
        double f = 1.0;
63,500,960✔
619
        if (settings::delayed_photon_scaling) {
63,500,960!
620
          if (is_fission(rx->mt_)) {
63,500,960✔
621
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
785,776!
622
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
785,776✔
623
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
785,776✔
624
              f = (energy_prompt + energy_delayed) / (energy_prompt);
785,776✔
625
            }
626
          }
627
        }
628

629
        // add to cumulative probability
630
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
63,500,960✔
631

632
        *i_rx = i;
63,500,960✔
633
        *i_product = j;
63,500,960✔
634
        if (prob > cutoff)
63,500,960✔
635
          return;
1,945,664✔
636
      }
637
    }
638
  }
639
}
640

641
void absorption(Particle& p, int i_nuclide)
1,445,073,147✔
642
{
643
  if (settings::survival_biasing) {
1,445,073,147✔
644
    // Determine weight absorbed in survival biasing
645
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
727,200✔
646
                              p.neutron_xs(i_nuclide).total;
727,200✔
647

648
    // Adjust weight of particle by probability of absorption
649
    p.wgt() -= wgt_absorb;
727,200✔
650

651
    // Score implicit absorption estimate of keff
652
    if (settings::run_mode == RunMode::EIGENVALUE) {
727,200!
653
      p.keff_tally_absorption() += wgt_absorb *
727,200✔
654
                                   p.neutron_xs(i_nuclide).nu_fission /
727,200✔
655
                                   p.neutron_xs(i_nuclide).absorption;
727,200✔
656
    }
657
  } else {
658
    // See if disappearance reaction happens
659
    if (p.neutron_xs(i_nuclide).absorption >
1,444,345,947✔
660
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
1,444,345,947✔
661
      // Score absorption estimate of keff
662
      if (settings::run_mode == RunMode::EIGENVALUE) {
33,972,396✔
663
        p.keff_tally_absorption() += p.wgt() *
51,442,940✔
664
                                     p.neutron_xs(i_nuclide).nu_fission /
25,721,470✔
665
                                     p.neutron_xs(i_nuclide).absorption;
25,721,470✔
666
      }
667

668
      p.wgt() = 0.0;
33,972,396✔
669
      p.event() = TallyEvent::ABSORB;
33,972,396✔
670
      if (!p.fission()) {
33,972,396✔
671
        p.event_mt() = N_DISAPPEAR;
21,243,453✔
672
      }
673
    }
674
  }
675
}
1,445,073,147✔
676

677
void scatter(Particle& p, int i_nuclide)
1,410,874,847✔
678
{
679
  // copy incoming direction
680
  Direction u_old {p.u()};
1,410,874,847✔
681

682
  // Get pointer to nuclide and grid index/interpolation factor
683
  const auto& nuc {data::nuclides[i_nuclide]};
1,410,874,847✔
684
  const auto& micro {p.neutron_xs(i_nuclide)};
1,410,874,847✔
685
  int i_temp = micro.index_temp;
1,410,874,847✔
686

687
  // For tallying purposes, this routine might be called directly. In that
688
  // case, we need to sample a reaction via the cutoff variable
689
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
1,410,874,847✔
690
  bool sampled = false;
1,410,874,847✔
691

692
  // Calculate elastic cross section if it wasn't precalculated
693
  if (micro.elastic == CACHE_INVALID) {
1,410,874,847✔
694
    nuc->calculate_elastic_xs(p);
1,055,585,999✔
695
  }
696

697
  double prob = micro.elastic - micro.thermal;
1,410,874,847✔
698
  if (prob > cutoff) {
1,410,874,847✔
699
    // =======================================================================
700
    // NON-S(A,B) ELASTIC SCATTERING
701

702
    // Determine temperature
703
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
1,198,300,043✔
704

705
    // Perform collision physics for elastic scattering
706
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
1,198,300,043✔
707

708
    p.event_mt() = ELASTIC;
1,198,300,043✔
709
    sampled = true;
1,198,300,043✔
710
  }
711

712
  prob = micro.elastic;
1,410,874,847✔
713
  if (prob > cutoff && !sampled) {
1,410,874,847✔
714
    // =======================================================================
715
    // S(A,B) SCATTERING
716

717
    sab_scatter(i_nuclide, micro.index_sab, p);
185,389,852✔
718

719
    p.event_mt() = ELASTIC;
185,389,852✔
720
    sampled = true;
185,389,852✔
721
  }
722

723
  if (!sampled) {
1,410,874,847✔
724
    // =======================================================================
725
    // INELASTIC SCATTERING
726

727
    int n = nuc->index_inelastic_scatter_.size();
27,184,952✔
728
    int i = 0;
27,184,952✔
729
    for (int j = 0; j < n && prob < cutoff; ++j) {
510,589,552✔
730
      i = nuc->index_inelastic_scatter_[j];
483,404,600✔
731

732
      // add to cumulative probability
733
      prob += nuc->reactions_[i]->xs(micro);
483,404,600✔
734
    }
735

736
    // Perform collision physics for inelastic scattering
737
    const auto& rx {nuc->reactions_[i]};
27,184,952✔
738
    inelastic_scatter(*nuc, *rx, p);
27,184,952✔
739
    p.event_mt() = rx->mt_;
27,184,952✔
740
  }
741

742
  // Set event component
743
  p.event() = TallyEvent::SCATTER;
1,410,874,847✔
744

745
  // Sample new outgoing angle for isotropic-in-lab scattering
746
  const auto& mat {model::materials[p.material()]};
1,410,874,847✔
747
  if (!mat->p0_.empty()) {
1,410,874,847✔
748
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
474,720✔
749
    if (mat->p0_[i_nuc_mat]) {
474,720!
750
      // Sample isotropic-in-lab outgoing direction
751
      p.u() = isotropic_direction(p.current_seed());
474,720✔
752
      p.mu() = u_old.dot(p.u());
474,720✔
753
    }
754
  }
755
}
1,410,874,847✔
756

757
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
1,198,300,043✔
758
{
759
  // get pointer to nuclide
760
  const auto& nuc {data::nuclides[i_nuclide]};
1,198,300,043✔
761

762
  double vel = std::sqrt(p.E());
1,198,300,043✔
763
  double awr = nuc->awr_;
1,198,300,043✔
764

765
  // Neutron velocity in LAB
766
  Direction v_n = vel * p.u();
1,198,300,043✔
767

768
  // Sample velocity of target nucleus
769
  Direction v_t {};
1,198,300,043✔
770
  if (!p.neutron_xs(i_nuclide).use_ptable) {
1,198,300,043✔
771
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
2,290,274,746✔
772
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
1,145,137,373✔
773
  }
774

775
  // Velocity of center-of-mass
776
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
1,198,300,043✔
777

778
  // Transform to CM frame
779
  v_n -= v_cm;
1,198,300,043✔
780

781
  // Find speed of neutron in CM
782
  vel = v_n.norm();
1,198,300,043✔
783

784
  // Sample scattering angle, checking if angle distribution is present (assume
785
  // isotropic otherwise)
786
  double mu_cm;
787
  auto& d = rx.products_[0].distribution_[0];
1,198,300,043✔
788
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
1,198,300,043!
789
  if (!d_->angle().empty()) {
1,198,300,043!
790
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
1,198,300,043✔
791
  } else {
792
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
793
  }
794

795
  // Determine direction cosines in CM
796
  Direction u_cm = v_n / vel;
1,198,300,043✔
797

798
  // Rotate neutron velocity vector to new angle -- note that the speed of the
799
  // neutron in CM does not change in elastic scattering. However, the speed
800
  // will change when we convert back to LAB
801
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
1,198,300,043✔
802

803
  // Transform back to LAB frame
804
  v_n += v_cm;
1,198,300,043✔
805

806
  p.E() = v_n.dot(v_n);
1,198,300,043✔
807
  vel = std::sqrt(p.E());
1,198,300,043✔
808

809
  // compute cosine of scattering angle in LAB frame by taking dot product of
810
  // neutron's pre- and post-collision angle
811
  p.mu() = p.u().dot(v_n) / vel;
1,198,300,043✔
812

813
  // Set energy and direction of particle in LAB frame
814
  p.u() = v_n / vel;
1,198,300,043✔
815

816
  // Because of floating-point roundoff, it may be possible for mu_lab to be
817
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
818
  // -1 or 1
819
  if (std::abs(p.mu()) > 1.0)
1,198,300,043!
820
    p.mu() = std::copysign(1.0, p.mu());
×
821
}
1,198,300,043✔
822

823
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
185,389,852✔
824
{
825
  // Determine temperature index
826
  const auto& micro {p.neutron_xs(i_nuclide)};
185,389,852✔
827
  int i_temp = micro.index_temp_sab;
185,389,852✔
828

829
  // Sample energy and angle
830
  double E_out;
831
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
370,779,704✔
832
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
185,389,852✔
833

834
  // Set energy to outgoing, change direction of particle
835
  p.E() = E_out;
185,389,852✔
836
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
185,389,852✔
837
}
185,389,852✔
838

839
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
1,145,137,373✔
840
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
841
{
842
  // check if nuclide is a resonant scatterer
843
  ResScatMethod sampling_method;
844
  if (nuc.resonant_) {
1,145,137,373✔
845

846
    // sampling method to use
847
    sampling_method = settings::res_scat_method;
122,992✔
848

849
    // upper resonance scattering energy bound (target is at rest above this E)
850
    if (E > settings::res_scat_energy_max) {
122,992✔
851
      return {};
59,280✔
852

853
      // lower resonance scattering energy bound (should be no resonances below)
854
    } else if (E < settings::res_scat_energy_min) {
63,712✔
855
      sampling_method = ResScatMethod::cxs;
36,352✔
856
    }
857

858
    // otherwise, use free gas model
859
  } else {
860
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
1,145,014,381✔
861
      return {};
587,669,820✔
862
    } else {
863
      sampling_method = ResScatMethod::cxs;
557,344,561✔
864
    }
865
  }
866

867
  // use appropriate target velocity sampling method
868
  switch (sampling_method) {
557,408,273!
869
  case ResScatMethod::cxs:
557,380,913✔
870

871
    // sample target velocity with the constant cross section (cxs) approx.
872
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
557,380,913✔
873

874
  case ResScatMethod::dbrc:
27,360✔
875
  case ResScatMethod::rvs: {
876
    double E_red = std::sqrt(nuc.awr_ * E / kT);
27,360✔
877
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
27,360✔
878
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
27,360✔
879

880
    // find lower and upper energy bound indices
881
    // lower index
882
    int i_E_low;
883
    if (E_low < nuc.energy_0K_.front()) {
27,360!
884
      i_E_low = 0;
×
885
    } else if (E_low > nuc.energy_0K_.back()) {
27,360!
886
      i_E_low = nuc.energy_0K_.size() - 2;
×
887
    } else {
888
      i_E_low =
27,360✔
889
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
27,360✔
890
    }
891

892
    // upper index
893
    int i_E_up;
894
    if (E_up < nuc.energy_0K_.front()) {
27,360!
895
      i_E_up = 0;
×
896
    } else if (E_up > nuc.energy_0K_.back()) {
27,360!
897
      i_E_up = nuc.energy_0K_.size() - 2;
×
898
    } else {
899
      i_E_up =
27,360✔
900
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
27,360✔
901
    }
902

903
    if (i_E_up == i_E_low) {
27,360✔
904
      // Handle degenerate case -- if the upper/lower bounds occur for the same
905
      // index, then using cxs is probably a good approximation
906
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
27,360✔
907
    }
908

909
    if (sampling_method == ResScatMethod::dbrc) {
22,592!
910
      // interpolate xs since we're not exactly at the energy indices
911
      double xs_low = nuc.elastic_0K_[i_E_low];
×
912
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
913
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
914
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
915
      double xs_up = nuc.elastic_0K_[i_E_up];
×
916
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
917
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
918
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
919

920
      // get max 0K xs value over range of practical relative energies
921
      double xs_max = *std::max_element(
×
922
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
923
      xs_max = std::max({xs_low, xs_max, xs_up});
×
924

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

938
        // perform Doppler broadening rejection correction (dbrc)
939
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
940
        double R = xs_0K / xs_max;
×
941
        if (prn(seed) < R)
×
942
          return v_target;
×
943
      }
×
944

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

955
      // cdf value at upper bound attainable energy
956
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
22,592✔
957
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
22,592✔
958
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
22,592✔
959

960
      while (true) {
961
        // directly sample Maxwellian
962
        double E_t = -kT * std::log(prn(seed));
248,320✔
963

964
        // sample a relative energy using the xs cdf
965
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
248,320✔
966
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
248,320✔
967
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
248,320✔
968
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
248,320✔
969
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
248,320✔
970
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
248,320✔
971
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
248,320✔
972
                     nuc.energy_0K_[i_E_low + i_E_rel]);
248,320✔
973
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
248,320✔
974

975
        // perform rejection sampling on cosine between
976
        // neutron and target velocities
977
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
248,320✔
978
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
248,320✔
979

980
        if (std::abs(mu) < 1.0) {
248,320✔
981
          // set and accept target velocity
982
          E_t /= nuc.awr_;
22,592✔
983
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
22,592✔
984
        }
985
      }
225,728✔
986
    }
987
  } // case RVS, DBRC
988
  } // switch (sampling_method)
989

990
  UNREACHABLE();
×
991
}
992

993
Direction sample_cxs_target_velocity(
557,385,681✔
994
  double awr, double E, Direction u, double kT, uint64_t* seed)
995
{
996
  double beta_vn = std::sqrt(awr * E / kT);
557,385,681✔
997
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
557,385,681✔
998

999
  double beta_vt_sq;
1000
  double mu;
1001
  while (true) {
1002
    // Sample two random numbers
1003
    double r1 = prn(seed);
650,552,929✔
1004
    double r2 = prn(seed);
650,552,929✔
1005

1006
    if (prn(seed) < alpha) {
650,552,929✔
1007
      // With probability alpha, we sample the distribution p(y) =
1008
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1009
      // Carlo sampler
1010

1011
      beta_vt_sq = -std::log(r1 * r2);
150,409,181✔
1012

1013
    } else {
1014
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1015
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1016
      // Carlo sampler
1017

1018
      double c = std::cos(PI / 2.0 * prn(seed));
500,143,748✔
1019
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
500,143,748✔
1020
    }
1021

1022
    // Determine beta * vt
1023
    double beta_vt = std::sqrt(beta_vt_sq);
650,552,929✔
1024

1025
    // Sample cosine of angle between neutron and target velocity
1026
    mu = uniform_distribution(-1., 1., seed);
650,552,929✔
1027

1028
    // Determine rejection probability
1029
    double accept_prob =
1030
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
650,552,929✔
1031
      (beta_vn + beta_vt);
650,552,929✔
1032

1033
    // Perform rejection sampling on vt and mu
1034
    if (prn(seed) < accept_prob)
650,552,929✔
1035
      break;
557,385,681✔
1036
  }
93,167,248✔
1037

1038
  // Determine speed of target nucleus
1039
  double vt = std::sqrt(beta_vt_sq * kT / awr);
557,385,681✔
1040

1041
  // Determine velocity vector of target nucleus based on neutron's velocity
1042
  // and the sampled angle between them
1043
  return vt * rotate_angle(u, mu, nullptr, seed);
557,385,681✔
1044
}
1045

1046
void sample_fission_neutron(
44,189,409✔
1047
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1048
{
1049
  // Get attributes of particle
1050
  double E_in = p.E();
44,189,409✔
1051
  uint64_t* seed = p.current_seed();
44,189,409✔
1052

1053
  // Determine total nu, delayed nu, and delayed neutron fraction
1054
  const auto& nuc {data::nuclides[i_nuclide]};
44,189,409✔
1055
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
44,189,409✔
1056
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
44,189,409✔
1057
  double beta = nu_d / nu_t;
44,189,409✔
1058

1059
  if (prn(seed) < beta) {
44,189,409✔
1060
    // ====================================================================
1061
    // DELAYED NEUTRON SAMPLED
1062

1063
    // sampled delayed precursor group
1064
    double xi = prn(seed) * nu_d;
277,653✔
1065
    double prob = 0.0;
277,653✔
1066
    int group;
1067
    for (group = 1; group < nuc->n_precursor_; ++group) {
1,033,944✔
1068
      // determine delayed neutron precursor yield for group j
1069
      double yield = (*rx.products_[group].yield_)(E_in);
1,014,312✔
1070

1071
      // Check if this group is sampled
1072
      prob += yield;
1,014,312✔
1073
      if (xi < prob)
1,014,312✔
1074
        break;
258,021✔
1075
    }
1076

1077
    // if the sum of the probabilities is slightly less than one and the
1078
    // random number is greater, j will be greater than nuc %
1079
    // n_precursor -- check for this condition
1080
    group = std::min(group, nuc->n_precursor_);
277,653✔
1081

1082
    // set the delayed group for the particle born from fission
1083
    site->delayed_group = group;
277,653✔
1084

1085
    // Sample time of emission based on decay constant of precursor
1086
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
277,653✔
1087
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
277,653✔
1088

1089
  } else {
1090
    // ====================================================================
1091
    // PROMPT NEUTRON SAMPLED
1092

1093
    // set the delayed group for the particle born from fission to 0
1094
    site->delayed_group = 0;
43,911,756✔
1095
  }
1096

1097
  // sample from prompt neutron energy distribution
1098
  int n_sample = 0;
44,189,409✔
1099
  double mu;
1100
  while (true) {
1101
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
44,189,409✔
1102

1103
    // resample if energy is greater than maximum neutron energy
1104
    int neutron = ParticleType::neutron().transport_index();
44,189,409✔
1105
    if (site->E < data::energy_max[neutron])
44,189,409!
1106
      break;
44,189,409✔
1107

1108
    // check for large number of resamples
1109
    ++n_sample;
×
1110
    if (n_sample == MAX_SAMPLE) {
×
1111
      // particle_write_restart(p)
1112
      fatal_error("Resampled energy distribution maximum number of times "
×
1113
                  "for nuclide " +
×
1114
                  nuc->name_);
×
1115
    }
1116
  }
×
1117

1118
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1119
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
44,189,409✔
1120
}
44,189,409✔
1121

1122
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
27,184,952✔
1123
{
1124
  // copy energy of neutron
1125
  double E_in = p.E();
27,184,952✔
1126

1127
  // sample outgoing energy and scattering cosine
1128
  double E;
1129
  double mu;
1130
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
27,184,952✔
1131

1132
  // if scattering system is in center-of-mass, transfer cosine of scattering
1133
  // angle and outgoing energy from CM to LAB
1134
  if (rx.scatter_in_cm_) {
27,184,952✔
1135
    double E_cm = E;
27,106,415✔
1136

1137
    // determine outgoing energy in lab
1138
    double A = nuc.awr_;
27,106,415✔
1139
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
27,106,415✔
1140
                 ((A + 1.0) * (A + 1.0));
27,106,415✔
1141

1142
    // determine outgoing angle in lab
1143
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
27,106,415✔
1144
  }
1145

1146
  // Because of floating-point roundoff, it may be possible for mu to be
1147
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1148
  // or 1
1149
  if (std::abs(mu) > 1.0)
27,184,952!
1150
    mu = std::copysign(1.0, mu);
×
1151

1152
  // Set outgoing energy and scattering angle
1153
  p.E() = E;
27,184,952✔
1154
  p.mu() = mu;
27,184,952✔
1155

1156
  // change direction of particle
1157
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
27,184,952✔
1158

1159
  // evaluate yield
1160
  double yield = (*rx.products_[0].yield_)(E_in);
27,184,952✔
1161
  if (std::floor(yield) == yield && yield > 0) {
27,184,952!
1162
    // If yield is integral, create exactly that many secondary particles
1163
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
27,330,569✔
1164
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
145,671✔
1165
    }
1166
  } else {
27,184,898✔
1167
    // Otherwise, change weight of particle based on yield
UNCOV
1168
    p.wgt() *= yield;
54✔
1169
  }
1170
}
27,184,952✔
1171

1172
void sample_secondary_photons(Particle& p, int i_nuclide)
12,813,888✔
1173
{
1174
  // Sample the number of photons produced
1175
  double y_t =
1176
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
12,813,888✔
1177
  double photon_wgt = p.wgt();
12,813,888✔
1178
  int y = 1;
12,813,888✔
1179

1180
  if (settings::use_decay_photons) {
12,813,888✔
1181
    // For decay photons, sample a single photon and modify the weight
1182
    if (y_t <= 0.0)
104,736✔
1183
      return;
25,136✔
1184
    photon_wgt *= y_t;
79,600✔
1185
  } else {
1186
    // For prompt photons, sample an integral number of photons with weight
1187
    // equal to the neutron's weight
1188
    y = static_cast<int>(y_t);
12,709,152✔
1189
    if (prn(p.current_seed()) <= y_t - y)
12,709,152✔
1190
      ++y;
836,160✔
1191
  }
1192

1193
  // Sample each secondary photon
1194
  for (int i = 0; i < y; ++i) {
14,734,416✔
1195
    // Sample the reaction and product
1196
    int i_rx;
1197
    int i_product;
1198
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,945,664✔
1199

1200
    // Sample the outgoing energy and angle
1201
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,945,664✔
1202
    double E;
1203
    double mu;
1204
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,945,664✔
1205

1206
    // Sample the new direction
1207
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,945,664✔
1208

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

1219
    // Create the secondary photon
1220
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
1,945,664✔
1221

1222
    // Tag secondary particle with parent nuclide
1223
    if (created_photon && settings::use_decay_photons) {
1,945,664✔
1224
      p.secondary_bank().back().parent_nuclide =
76,864✔
1225
        rx->products_[i_product].parent_nuclide_;
76,864✔
1226
    }
1227
  }
1228
}
1229

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