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

openmc-dev / openmc / 26776718665

01 Jun 2026 07:22PM UTC coverage: 80.79% (-0.6%) from 81.356%
26776718665

Pull #3757

github

web-flow
Merge 623c6fa99 into 111eb7706
Pull Request #3757: Implementation of point detectors

18026 of 26305 branches covered (68.53%)

Branch coverage included in aggregate %.

52 of 384 new or added lines in 25 files covered. (13.54%)

201 existing lines in 5 files now uncovered.

59242 of 69336 relevant lines covered (85.44%)

48198237.58 hits per line

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

83.67
/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/next_event_scoring.h"
28
#include "openmc/tallies/tally.h"
29
#include "openmc/tallies/tally_scoring.h"
30
#include "openmc/thermal.h"
31
#include "openmc/weight_windows.h"
32

33
#include <fmt/core.h>
34

35
#include "openmc/tensor.h"
36
#include <algorithm> // for max, min, max_element
37
#include <cmath>     // for sqrt, exp, log, abs, copysign
38

39
namespace openmc {
40

41
//==============================================================================
42
// Non-member functions
43
//==============================================================================
44

45
void collision(Particle& p)
1,460,653,359✔
46
{
47
  // Add to collision counter for particle
48
  ++(p.n_collision());
1,460,653,359✔
49
  p.secondary_bank_index() = p.local_secondary_bank().size();
1,460,653,359!
50

51
  // Sample reaction for the material the particle is in
52
  switch (p.type().pdg_number()) {
1,460,653,359!
53
  case PDG_NEUTRON:
1,336,662,426✔
54
    sample_neutron_reaction(p);
1,336,662,426✔
55
    break;
1,336,662,426✔
56
  case PDG_PHOTON:
35,757,886✔
57
    sample_photon_reaction(p);
35,757,886✔
58
    break;
35,757,886✔
59
  case PDG_ELECTRON:
88,054,168✔
60
    sample_electron_reaction(p);
88,054,168✔
61
    break;
88,054,168✔
62
  case PDG_POSITRON:
178,879✔
63
    sample_positron_reaction(p);
178,879✔
64
    break;
178,879✔
65
  default:
×
66
    fatal_error("Unsupported particle PDG for collision sampling.");
×
67
  }
68

69
  if (settings::weight_windows_on) {
1,460,653,359✔
70
    auto [ww_found, ww] = search_weight_window(p);
271,114,629✔
71
    if (!ww_found && p.type() == ParticleType::neutron()) {
271,114,629✔
72
      // if the weight window is not valid, apply russian roulette for neutrons
73
      // (regardless of weight window collision checkpoint setting)
74
      apply_russian_roulette(p);
66,749✔
75
    } else if (settings::weight_window_checkpoint_collision) {
271,047,880!
76
      // if collision checkpointing is on, apply weight window
77
      apply_weight_window(p, ww);
271,047,880✔
78
    }
79
  }
80

81
  // Kill particle if energy falls below cutoff
82
  int type = p.type().transport_index();
1,460,653,359!
83
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
1,460,653,359!
84
    p.wgt() = 0.0;
8,039,085✔
85
  }
86

87
  // Display information about collision
88
  if (settings::verbosity >= 10 || p.trace()) {
1,460,653,359!
89
    std::string msg;
121!
90
    if (p.event() == TallyEvent::KILL) {
121!
91
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
92
    } else if (p.type().is_neutron()) {
121!
93
      msg = fmt::format("    {} with {}. Energy = {} eV.",
242✔
94
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
242✔
95
        p.E());
121✔
96
    } else if (p.type().is_photon()) {
×
97
      msg = fmt::format("    {} with {}. Energy = {} eV.",
×
98
        reaction_name(p.event_mt()),
×
99
        to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
×
100
    } else {
101
      msg = fmt::format("    Disappeared. Energy = {} eV.", p.E());
×
102
    }
103
    write_message(msg, 1);
121✔
104
  }
121✔
105
}
1,460,653,359✔
106

107
void sample_neutron_reaction(Particle& p)
1,336,662,426✔
108
{
109
  // Sample a nuclide within the material
110
  int i_nuclide = sample_nuclide(p);
1,336,662,426✔
111

112
  // Save which nuclide particle had collision with
113
  p.event_nuclide() = i_nuclide;
1,336,662,426✔
114

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

120
  const auto& nuc {data::nuclides[i_nuclide]};
1,336,662,426✔
121

122
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
1,336,662,426✔
123
    auto& rx = sample_fission(i_nuclide, p);
169,046,141✔
124
    if (settings::run_mode == RunMode::EIGENVALUE) {
169,046,141✔
125
      create_fission_sites(p, i_nuclide, rx);
144,017,588✔
126
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
25,028,553✔
127
               settings::create_fission_neutrons) {
128
      create_fission_sites(p, i_nuclide, rx);
582,505✔
129

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

143
  // Create secondary photons
144
  if (settings::photon_transport) {
1,336,662,426✔
145
    sample_secondary_photons(p, i_nuclide);
66,633,380✔
146
  }
147

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

151
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
1,336,662,426✔
152
    absorption(p, i_nuclide);
1,336,549,076✔
153
  }
154
  if (!p.alive())
1,336,662,426✔
155
    return;
156

157
  // Sample a scattering reaction and determine the secondary energy of the
158
  // exiting neutron
159
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
1,304,559,021✔
160
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
1,304,559,021!
161
    if (!model::active_point_tallies.empty())
158,829!
NEW
162
      fatal_error("Next-Event estimator does not support ncrystal materials");
×
163
    ncrystal_mat.scatter(p);
158,829✔
164
  } else {
165
    scatter(p, i_nuclide);
1,304,400,192✔
166
  }
167

168
  // Advance URR seed stream 'N' times after energy changes
169
  if (p.E() != p.E_last()) {
1,304,559,021✔
170
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
1,304,239,790✔
171
  }
172

173
  // Play russian roulette if there are no weight windows
174
  if (!settings::weight_windows_on)
1,304,559,021✔
175
    apply_russian_roulette(p);
1,104,044,308✔
176
}
177

178
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
144,600,093✔
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;
144,600,093✔
183

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

189
  // Sample the number of neutrons produced
190
  int nu = static_cast<int>(nu_t);
144,600,093✔
191
  if (prn(p.current_seed()) <= (nu_t - nu))
144,600,093✔
192
    ++nu;
24,111,616✔
193

194
  // If no neutrons were produced then don't continue
195
  if (nu == 0)
144,600,093✔
196
    return;
114,641,998✔
197

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

202
  // Clear out particle's nu fission bank
203
  p.nu_bank().clear();
29,958,095✔
204

205
  p.fission() = true;
29,958,095✔
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);
29,958,095✔
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;
29,958,095✔
214

215
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
68,988,784✔
216
    // Initialize fission site object with particle data
217
    SourceSite site;
39,030,689✔
218
    site.r = p.r();
39,030,689✔
219
    site.particle = ParticleType::neutron();
39,030,689✔
220
    site.time = p.time();
39,030,689✔
221
    site.wgt = 1. / weight;
39,030,689✔
222
    site.surf_id = 0;
39,030,689✔
223

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

227
    // Reject site if it exceeds time cutoff
228
    if (site.delayed_group > 0) {
39,030,689✔
229
      double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
257,031!
230
      if (site.time > t_cutoff) {
257,031!
231
        continue;
×
232
      }
233
    }
234

235
    // Set parent and progeny IDs
236
    site.parent_id = p.current_work();
39,030,689✔
237
    site.progeny_id = p.n_progeny()++;
39,030,689✔
238

239
    // Store fission site in bank
240
    if (use_fission_bank) {
39,030,689✔
241
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
38,816,456✔
242
      if (idx == -1) {
38,816,456!
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) {
38,816,456✔
258
        ifp(p, idx);
1,352,626✔
259
      }
260
    } else {
261
      site.wgt_born = p.wgt_born();
214,233✔
262
      site.wgt_ww_born = p.wgt_ww_born();
214,233✔
263
      site.n_split = p.n_split();
214,233✔
264
      p.local_secondary_bank().push_back(site);
214,233✔
265
      p.n_secondaries()++;
214,233✔
266
    }
267

268
    // Increment the number of neutrons born delayed
269
    if (site.delayed_group > 0) {
39,030,689✔
270
      nu_d[site.delayed_group - 1]++;
257,031✔
271
    }
272

273
    // Write fission particles to nuBank
274
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
39,030,689✔
275
    nu_bank_entry.wgt = site.wgt;
39,030,689✔
276
    nu_bank_entry.E = site.E;
39,030,689✔
277
    nu_bank_entry.delayed_group = site.delayed_group;
39,030,689✔
278
  }
279

280
  // If shared fission bank was full, and no fissions could be added,
281
  // set the particle fission flag to false.
282
  if (n_sites_stored == 0) {
29,958,095!
283
    p.fission() = false;
×
284
    return;
×
285
  }
286

287
  // Set nu to the number of fission sites successfully stored. If the fission
288
  // bank was not found to be full then these values are already equivalent.
289
  nu = n_sites_stored;
29,958,095✔
290

291
  // Store the total weight banked for analog fission tallies
292
  p.n_bank() = nu;
29,958,095✔
293
  p.wgt_bank() = nu / weight;
29,958,095✔
294
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
269,622,855✔
295
    p.n_delayed_bank(d) = nu_d[d];
239,664,760✔
296
  }
297
}
298

299
void sample_photon_reaction(Particle& p)
35,757,886✔
300
{
301
  // Kill photon if below energy cutoff -- an extra check is made here because
302
  // photons with energy below the cutoff may have been produced by neutrons
303
  // reactions or atomic relaxation
304
  int photon = ParticleType::photon().transport_index();
35,757,886✔
305
  if (p.E() < settings::energy_cutoff[photon]) {
35,757,886✔
306
    p.E() = 0.0;
803✔
307
    p.wgt() = 0.0;
803✔
308
    return;
803✔
309
  }
310

311
  // Sample element within material
312
  int i_element = sample_element(p);
35,757,083✔
313
  const auto& micro {p.photon_xs(i_element)};
35,757,083✔
314
  const auto& element {*data::elements[i_element]};
35,757,083✔
315

316
  // Calculate photon energy over electron rest mass equivalent
317
  double alpha = p.E() / MASS_ELECTRON_EV;
35,757,083✔
318

319
  // For tallying purposes, this routine might be called directly. In that
320
  // case, we need to sample a reaction via the cutoff variable
321
  double prob = 0.0;
35,757,083✔
322
  double cutoff = prn(p.current_seed()) * micro.total;
35,757,083✔
323

324
  // Coherent (Rayleigh) scattering
325
  prob += micro.coherent;
35,757,083✔
326
  if (prob > cutoff) {
35,757,083✔
327
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
1,771,609✔
328
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
1,771,609✔
329
    p.event() = TallyEvent::SCATTER;
1,771,609✔
330
    p.event_mt() = COHERENT;
1,771,609✔
331
    return;
1,771,609✔
332
  }
333

334
  // Incoherent (Compton) scattering
335
  prob += micro.incoherent;
33,985,474✔
336
  if (prob > cutoff) {
33,985,474✔
337
    double alpha_out;
25,958,115✔
338
    int i_shell;
25,958,115✔
339
    element.compton_scatter(
25,958,115✔
340
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
25,958,115✔
341

342
    // Determine binding energy of shell. The binding energy is 0.0 if
343
    // doppler broadening is not used.
344
    double e_b;
25,958,115✔
345
    if (i_shell == -1) {
25,958,115!
346
      e_b = 0.0;
347
    } else {
348
      e_b = element.binding_energy_[i_shell];
25,958,115✔
349
    }
350

351
    // Create Compton electron
352
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
25,958,115✔
353
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
25,958,115✔
354
    int electron = ParticleType::electron().transport_index();
25,958,115✔
355
    if (E_electron >= settings::energy_cutoff[electron]) {
25,958,115✔
356
      double mu_electron = (alpha - alpha_out * p.mu()) /
25,757,899✔
357
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
25,757,899✔
358
                                     2.0 * alpha * alpha_out * p.mu());
25,757,899✔
359
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
25,757,899✔
360
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
25,757,899✔
361
    }
362

363
    // Allow electrons to fill orbital and produce Auger electrons and
364
    // fluorescent photons. Since Compton subshell data does not match atomic
365
    // relaxation data, use the mapping between the data to find the subshell
366
    if (settings::atomic_relaxation && i_shell >= 0 &&
25,958,115!
367
        element.subshell_map_[i_shell] >= 0) {
25,826,412!
368
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
25,826,412✔
369
    }
370

371
    phi += PI;
25,958,115✔
372
    p.E() = alpha_out * MASS_ELECTRON_EV;
25,958,115✔
373
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
25,958,115✔
374
    p.event() = TallyEvent::SCATTER;
25,958,115✔
375
    p.event_mt() = INCOHERENT;
25,958,115✔
376
    return;
25,958,115✔
377
  }
378

379
  // Photoelectric effect
380
  double prob_after = prob + micro.photoelectric;
8,027,359✔
381

382
  if (prob_after > cutoff) {
8,027,359✔
383
    // Get grid index, interpolation factor, and bounding subshell
384
    // cross sections
385
    int i_grid = micro.index_grid;
7,848,480✔
386
    double f = micro.interp_factor;
7,848,480✔
387
    tensor::View<const double> xs_lower = element.cross_sections_.slice(i_grid);
7,848,480✔
388
    tensor::View<const double> xs_upper =
7,848,480✔
389
      element.cross_sections_.slice(i_grid + 1);
7,848,480✔
390

391
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
27,669,597!
392
      const auto& shell {element.shells_[i_shell]};
27,669,597✔
393

394
      // Check threshold of reaction
395
      if (xs_lower(i_shell) == 0)
27,669,597✔
396
        continue;
10,403,154✔
397

398
      //  Evaluation subshell photoionization cross section
399
      prob += std::exp(
17,266,443✔
400
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
17,266,443✔
401

402
      if (prob > cutoff) {
17,266,443✔
403
        // Determine binding energy based on whether atomic relaxation data is
404
        // present (if not, use value from Compton profile data)
405
        double binding_energy = element.has_atomic_relaxation_
7,848,480✔
406
                                  ? shell.binding_energy
7,848,480!
407
                                  : element.binding_energy_[i_shell];
×
408

409
        // Determine energy of secondary electron
410
        double E_electron = p.E() - binding_energy;
7,848,480✔
411

412
        // Sample mu using non-relativistic Sauter distribution.
413
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
414
        // model in Serpent 2" by Toni Kaltiaisenaho
415
        double mu;
11,762,967✔
416
        while (true) {
11,762,967✔
417
          double r = prn(p.current_seed());
11,762,967✔
418
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
11,762,967✔
419
            double rel_vel =
7,848,480✔
420
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
7,848,480✔
421
              (E_electron + MASS_ELECTRON_EV);
7,848,480✔
422
            mu =
7,848,480✔
423
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
7,848,480✔
424
            break;
7,848,480✔
425
          }
426
        }
427

428
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
7,848,480✔
429
        Direction u;
7,848,480✔
430
        u.x = mu;
7,848,480✔
431
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
7,848,480✔
432
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
7,848,480✔
433

434
        // Create secondary electron
435
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
7,848,480✔
436

437
        // Allow electrons to fill orbital and produce auger electrons
438
        // and fluorescent photons
439
        if (settings::atomic_relaxation) {
7,848,480✔
440
          element.atomic_relaxation(i_shell, p);
7,738,480✔
441
        }
442
        p.event() = TallyEvent::ABSORB;
7,848,480✔
443
        p.event_mt() = 533 + shell.index_subshell;
7,848,480✔
444
        p.wgt() = 0.0;
7,848,480✔
445
        p.E() = 0.0;
7,848,480✔
446
        return;
7,848,480✔
447
      }
448
    }
449
  }
15,696,960✔
450
  prob = prob_after;
178,879✔
451

452
  // Pair production
453
  prob += micro.pair_production;
178,879✔
454
  if (prob > cutoff) {
178,879!
455
    double E_electron, E_positron;
178,879✔
456
    double mu_electron, mu_positron;
178,879✔
457
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
178,879✔
458
      &mu_positron, p.current_seed());
459

460
    // Create secondary electron
461
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
178,879✔
462
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
178,879✔
463

464
    // Create secondary positron
465
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
178,879✔
466
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron());
178,879✔
467
    p.event() = TallyEvent::ABSORB;
178,879✔
468
    p.event_mt() = PAIR_PROD;
178,879✔
469
    p.wgt() = 0.0;
178,879✔
470
    p.E() = 0.0;
178,879✔
471
  }
472
}
473

474
void sample_electron_reaction(Particle& p)
88,054,168✔
475
{
476
  // TODO: create reaction types
477

478
  if (settings::electron_treatment == ElectronTreatment::TTB) {
88,054,168✔
479
    double E_lost;
85,472,127✔
480
    thick_target_bremsstrahlung(p, &E_lost);
85,472,127✔
481
  }
482

483
  p.E() = 0.0;
88,054,168✔
484
  p.wgt() = 0.0;
88,054,168✔
485
  p.event() = TallyEvent::ABSORB;
88,054,168✔
486
}
88,054,168✔
487

488
void sample_positron_reaction(Particle& p)
178,879✔
489
{
490
  // TODO: create reaction types
491

492
  if (settings::electron_treatment == ElectronTreatment::TTB) {
178,879✔
493
    double E_lost;
178,626✔
494
    thick_target_bremsstrahlung(p, &E_lost);
178,626✔
495
  }
496

497
  // Sample angle isotropically
498
  Direction u = isotropic_direction(p.current_seed());
178,879✔
499

500
  // Create annihilation photon pair traveling in opposite directions
501
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
178,879✔
502
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
178,879✔
503

504
  p.E() = 0.0;
178,879✔
505
  p.wgt() = 0.0;
178,879✔
506
  p.event() = TallyEvent::ABSORB;
178,879✔
507
}
178,879✔
508

509
int sample_nuclide(Particle& p)
1,336,662,426✔
510
{
511
  // Sample cumulative distribution function
512
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
1,336,662,426✔
513

514
  // Get pointers to nuclide/density arrays
515
  const auto& mat {model::materials[p.material()]};
1,336,662,426✔
516
  int n = mat->nuclide_.size();
1,336,662,426✔
517

518
  double prob = 0.0;
1,336,662,426✔
519
  for (int i = 0; i < n; ++i) {
2,147,483,647!
520
    // Get atom density
521
    int i_nuclide = mat->nuclide_[i];
2,147,483,647✔
522
    double atom_density = mat->atom_density(i, p.density_mult());
2,147,483,647✔
523

524
    // Increment probability to compare to cutoff
525
    prob += atom_density * p.neutron_xs(i_nuclide).total;
2,147,483,647✔
526
    if (prob >= cutoff)
2,147,483,647✔
527
      return i_nuclide;
1,336,662,426✔
528
  }
529

530
  // If we reach here, no nuclide was sampled
531
  p.write_restart();
×
532
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
533
}
534

535
int sample_element(Particle& p)
35,757,083✔
536
{
537
  // Sample cumulative distribution function
538
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
35,757,083✔
539

540
  // Get pointers to elements, densities
541
  const auto& mat {model::materials[p.material()]};
35,757,083✔
542

543
  double prob = 0.0;
35,757,083✔
544
  for (int i = 0; i < mat->element_.size(); ++i) {
134,187,224!
545
    // Find atom density
546
    int i_element = mat->element_[i];
134,187,224✔
547
    double atom_density = mat->atom_density(i, p.density_mult());
134,187,224✔
548

549
    // Determine microscopic cross section
550
    double sigma = atom_density * p.photon_xs(i_element).total;
134,187,224✔
551

552
    // Increment probability to compare to cutoff
553
    prob += sigma;
134,187,224✔
554
    if (prob > cutoff) {
134,187,224✔
555
      // Save which nuclide particle had collision with for tally purpose
556
      p.event_nuclide() = mat->nuclide_[i];
35,757,083✔
557

558
      return i_element;
35,757,083✔
559
    }
560
  }
561

562
  // If we made it here, no element was sampled
563
  p.write_restart();
×
564
  fatal_error("Did not sample any element during collision.");
×
565
}
566

567
Reaction& sample_fission(int i_nuclide, Particle& p)
169,046,141✔
568
{
569
  // Get pointer to nuclide
570
  const auto& nuc {data::nuclides[i_nuclide]};
169,046,141✔
571

572
  // If we're in the URR, by default use the first fission reaction. We also
573
  // default to the first reaction if we know that there are no partial fission
574
  // reactions
575
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
169,046,141✔
576
    return *nuc->fission_rx_[0];
168,988,498✔
577
  }
578

579
  // Check to see if we are in a windowed multipole range.  WMP only supports
580
  // the first fission reaction.
581
  if (nuc->multipole_) {
57,643✔
582
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
2,849!
583
      return *nuc->fission_rx_[0];
1,991✔
584
    }
585
  }
586

587
  // Get grid index and interpolation factor and sample fission cdf
588
  const auto& micro = p.neutron_xs(i_nuclide);
55,652✔
589
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
55,652✔
590
  double prob = 0.0;
55,652✔
591

592
  // Loop through each partial fission reaction type
593
  for (auto& rx : nuc->fission_rx_) {
55,717!
594
    // add to cumulative probability
595
    prob += rx->xs(micro);
55,717✔
596

597
    // Create fission bank sites if fission occurs
598
    if (prob > cutoff)
55,717✔
599
      return *rx;
55,652✔
600
  }
601

602
  // If we reached here, no reaction was sampled
603
  throw std::runtime_error {
×
604
    "No fission reaction was sampled for " + nuc->name_};
×
605
}
606

607
void sample_photon_product(
2,668,622✔
608
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
609
{
610
  // Get grid index and interpolation factor and sample photon production cdf
611
  const auto& micro = p.neutron_xs(i_nuclide);
2,668,622✔
612
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
2,668,622✔
613
  double prob = 0.0;
2,668,622✔
614

615
  // Loop through each reaction type
616
  const auto& nuc {data::nuclides[i_nuclide]};
2,668,622✔
617
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
48,818,264!
618
    // Evaluate neutron cross section
619
    const auto& rx = nuc->reactions_[i];
48,818,264✔
620
    double xs = rx->xs(micro);
48,818,264✔
621

622
    // if cross section is zero for this reaction, skip it
623
    if (xs == 0.0)
48,818,264✔
624
      continue;
30,019,165✔
625

626
    for (int j = 0; j < rx->products_.size(); ++j) {
134,143,944✔
627
      if (rx->products_[j].particle_.is_photon()) {
118,013,467✔
628
        // For fission, artificially increase the photon yield to account
629
        // for delayed photons
630
        double f = 1.0;
103,488,209✔
631
        if (settings::delayed_photon_scaling) {
103,488,209!
632
          if (is_fission(rx->mt_)) {
103,488,209✔
633
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
540,221!
634
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
540,221✔
635
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
540,221✔
636
              f = (energy_prompt + energy_delayed) / (energy_prompt);
540,221✔
637
            }
638
          }
639
        }
640

641
        // add to cumulative probability
642
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
103,488,209✔
643

644
        *i_rx = i;
103,488,209✔
645
        *i_product = j;
103,488,209✔
646
        if (prob > cutoff)
103,488,209✔
647
          return;
648
      }
649
    }
650
  }
651
}
652

653
void absorption(Particle& p, int i_nuclide)
1,336,549,076✔
654
{
655
  if (settings::survival_biasing) {
1,336,549,076✔
656
    // Determine weight absorbed in survival biasing
657
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
9,679,604✔
658
                              p.neutron_xs(i_nuclide).total;
9,679,604✔
659

660
    // Adjust weight of particle by probability of absorption
661
    p.wgt() -= wgt_absorb;
9,679,604✔
662

663
    // Score implicit absorption estimate of keff
664
    if (settings::run_mode == RunMode::EIGENVALUE) {
9,679,604✔
665
      p.keff_tally_absorption() += wgt_absorb *
499,950✔
666
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950✔
667
                                   p.neutron_xs(i_nuclide).absorption;
499,950✔
668
    }
669
  } else {
670
    // See if disappearance reaction happens
671
    if (p.neutron_xs(i_nuclide).absorption >
1,326,869,472✔
672
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
1,326,869,472✔
673
      // Score absorption estimate of keff
674
      if (settings::run_mode == RunMode::EIGENVALUE) {
32,101,975✔
675
        p.keff_tally_absorption() += p.wgt() *
24,260,417✔
676
                                     p.neutron_xs(i_nuclide).nu_fission /
24,260,417✔
677
                                     p.neutron_xs(i_nuclide).absorption;
24,260,417✔
678
      }
679

680
      p.wgt() = 0.0;
32,101,975✔
681
      p.event() = TallyEvent::ABSORB;
32,101,975✔
682
      if (!p.fission()) {
32,101,975✔
683
        p.event_mt() = N_DISAPPEAR;
19,750,357✔
684
      }
685
    }
686
  }
687
}
1,336,549,076✔
688

689
void scatter(Particle& p, int i_nuclide)
1,304,400,192✔
690
{
691
  // copy incoming direction
692
  Direction u_old {p.u()};
1,304,400,192✔
693

694
  // Get pointer to nuclide and grid index/interpolation factor
695
  const auto& nuc {data::nuclides[i_nuclide]};
1,304,400,192✔
696
  const auto& micro {p.neutron_xs(i_nuclide)};
1,304,400,192✔
697
  int i_temp = micro.index_temp;
1,304,400,192✔
698

699
  // For tallying purposes, this routine might be called directly. In that
700
  // case, we need to sample a reaction via the cutoff variable
701
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
1,304,400,192✔
702
  bool sampled = false;
1,304,400,192✔
703

704
  // Calculate elastic cross section if it wasn't precalculated
705
  if (micro.elastic == CACHE_INVALID) {
1,304,400,192✔
706
    nuc->calculate_elastic_xs(p);
1,049,035,209✔
707
  }
708

709
  double prob = micro.elastic - micro.thermal;
1,304,400,192✔
710
  if (prob > cutoff) {
1,304,400,192✔
711
    // =======================================================================
712
    // NON-S(A,B) ELASTIC SCATTERING
713

714
    // Determine temperature
715
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
1,152,717,138✔
716

717
    // Perform collision physics for elastic scattering
718
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
1,152,717,138✔
719

720
    p.event_mt() = ELASTIC;
1,152,717,138✔
721
    sampled = true;
1,152,717,138✔
722
  }
723

724
  prob = micro.elastic;
1,304,400,192✔
725
  if (prob > cutoff && !sampled) {
1,304,400,192✔
726
    // =======================================================================
727
    // S(A,B) SCATTERING
728

729
    sab_scatter(i_nuclide, micro.index_sab, p);
127,575,678✔
730

731
    p.event_mt() = ELASTIC;
127,575,678✔
732
    sampled = true;
127,575,678✔
733
  }
734

735
  if (!sampled) {
1,304,400,192✔
736
    // =======================================================================
737
    // INELASTIC SCATTERING
738

739
    int n = nuc->index_inelastic_scatter_.size();
24,107,376✔
740
    int i = 0;
24,107,376✔
741
    for (int j = 0; j < n && prob < cutoff; ++j) {
450,971,234✔
742
      i = nuc->index_inelastic_scatter_[j];
426,863,858✔
743

744
      // add to cumulative probability
745
      prob += nuc->reactions_[i]->xs(micro);
426,863,858✔
746
    }
747

748
    // Perform collision physics for inelastic scattering
749
    const auto& rx {nuc->reactions_[i]};
24,107,376✔
750
    inelastic_scatter(i_nuclide, *rx, p);
24,107,376✔
751
    p.event_mt() = rx->mt_;
24,107,376✔
752
  }
753

754
  // Set event component
755
  p.event() = TallyEvent::SCATTER;
1,304,400,192✔
756

757
  // Sample new outgoing angle for isotropic-in-lab scattering
758
  const auto& mat {model::materials[p.material()]};
1,304,400,192!
759
  if (!mat->p0_.empty()) {
1,304,400,192!
760
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
326,370✔
761
    if (mat->p0_[i_nuc_mat]) {
326,370!
762
      // Sample isotropic-in-lab outgoing direction
763
      p.u() = isotropic_direction(p.current_seed());
326,370✔
764
      p.mu() = u_old.dot(p.u());
326,370✔
765
    }
766
  }
767
}
1,304,400,192✔
768

769
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
1,152,717,138✔
770
{
771
  // get pointer to nuclide
772
  const auto& nuc {data::nuclides[i_nuclide]};
1,152,717,138✔
773

774
  double vel = std::sqrt(p.E());
1,152,717,138✔
775
  double awr = nuc->awr_;
1,152,717,138✔
776

777
  // Neutron velocity in LAB
778
  Direction v_n = vel * p.u();
1,152,717,138✔
779

780
  // Sample velocity of target nucleus
781
  Direction v_t {};
1,152,717,138✔
782
  if (!p.neutron_xs(i_nuclide).use_ptable) {
1,152,717,138✔
783
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,105,485,960✔
784
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
1,105,485,960✔
785
  }
786

787
  // Velocity of center-of-mass
788
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
1,152,717,138✔
789

790
  // Transform to CM frame
791
  v_n -= v_cm;
1,152,717,138✔
792

793
  // Find speed of neutron in CM
794
  vel = v_n.norm();
1,152,717,138✔
795

796
  if (!model::active_point_tallies.empty()) {
1,152,717,138!
NEW
797
    score_point_tally_elastic(p, i_nuclide, rx, 0, v_t);
×
798
  }
799

800
  // Sample scattering angle, checking if angle distribution is present (assume
801
  // isotropic otherwise)
802
  double mu_cm;
1,152,717,138✔
803
  auto& d = rx.products_[0].distribution_[0];
1,152,717,138!
804
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
1,152,717,138!
805
  if (!d_->angle().empty()) {
1,152,717,138!
806
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
1,152,717,138✔
807
  } else {
808
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
809
  }
810

811
  // Determine direction cosines in CM
812
  Direction u_cm = v_n / vel;
1,152,717,138✔
813

814
  // Rotate neutron velocity vector to new angle -- note that the speed of the
815
  // neutron in CM does not change in elastic scattering. However, the speed
816
  // will change when we convert back to LAB
817
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
1,152,717,138✔
818

819
  // Transform back to LAB frame
820
  v_n += v_cm;
1,152,717,138✔
821

822
  p.E() = v_n.dot(v_n);
1,152,717,138✔
823
  vel = std::sqrt(p.E());
1,152,717,138✔
824

825
  // compute cosine of scattering angle in LAB frame by taking dot product of
826
  // neutron's pre- and post-collision angle
827
  p.mu() = p.u().dot(v_n) / vel;
1,152,717,138✔
828

829
  // Set energy and direction of particle in LAB frame
830
  p.u() = v_n / vel;
1,152,717,138!
831

832
  // Because of floating-point roundoff, it may be possible for mu_lab to be
833
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
834
  // -1 or 1
835
  if (std::abs(p.mu()) > 1.0)
1,152,717,138!
836
    p.mu() = std::copysign(1.0, p.mu());
×
837
}
1,152,717,138✔
838

839
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
127,575,678✔
840
{
841
  // Determine temperature index
842
  const auto& micro {p.neutron_xs(i_nuclide)};
127,575,678!
843
  int i_temp = micro.index_temp_sab;
127,575,678✔
844

845
  // Sample energy and angle
846
  double E_out;
127,575,678✔
847
  auto& sab = data::thermal_scatt[i_sab]->data_[i_temp];
127,575,678!
848

849
  if (!model::active_point_tallies.empty()) {
127,575,678!
NEW
850
    score_point_tally_sab(p, i_nuclide, sab, micro);
×
851
  }
852

853
  sab.sample(micro, p.E(), &E_out, &p.mu(), p.current_seed());
127,575,678✔
854

855
  // Set energy to outgoing, change direction of particle
856
  p.E() = E_out;
127,575,678✔
857
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
127,575,678✔
858
}
127,575,678✔
859

860
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
1,105,485,960✔
861
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
862
{
863
  // check if nuclide is a resonant scatterer
864
  ResScatMethod sampling_method;
1,105,485,960✔
865
  if (nuc.resonant_) {
1,105,485,960✔
866

867
    // sampling method to use
868
    sampling_method = settings::res_scat_method;
84,557✔
869

870
    // upper resonance scattering energy bound (target is at rest above this E)
871
    if (E > settings::res_scat_energy_max) {
84,557✔
872
      return {};
40,755✔
873

874
      // lower resonance scattering energy bound (should be no resonances below)
875
    } else if (E < settings::res_scat_energy_min) {
43,802✔
876
      sampling_method = ResScatMethod::cxs;
877
    }
878

879
    // otherwise, use free gas model
880
  } else {
881
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
1,105,401,403✔
882
      return {};
475,924,345✔
883
    } else {
884
      sampling_method = ResScatMethod::cxs;
885
    }
886
  }
887

888
  // use appropriate target velocity sampling method
889
  switch (sampling_method) {
18,810!
890
  case ResScatMethod::cxs:
629,502,050✔
891

892
    // sample target velocity with the constant cross section (cxs) approx.
893
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
629,502,050✔
894

895
  case ResScatMethod::dbrc:
18,810✔
896
  case ResScatMethod::rvs: {
18,810✔
897
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810✔
898
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
37,620!
899
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810✔
900

901
    // find lower and upper energy bound indices
902
    // lower index
903
    int i_E_low;
18,810✔
904
    if (E_low < nuc.energy_0K_.front()) {
18,810!
905
      i_E_low = 0;
906
    } else if (E_low > nuc.energy_0K_.back()) {
18,810!
907
      i_E_low = nuc.energy_0K_.size() - 2;
×
908
    } else {
909
      i_E_low =
18,810✔
910
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
18,810✔
911
    }
912

913
    // upper index
914
    int i_E_up;
18,810✔
915
    if (E_up < nuc.energy_0K_.front()) {
18,810!
916
      i_E_up = 0;
917
    } else if (E_up > nuc.energy_0K_.back()) {
18,810!
918
      i_E_up = nuc.energy_0K_.size() - 2;
×
919
    } else {
920
      i_E_up =
18,810✔
921
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
18,810✔
922
    }
923

924
    if (i_E_up == i_E_low) {
18,810✔
925
      // Handle degenerate case -- if the upper/lower bounds occur for the same
926
      // index, then using cxs is probably a good approximation
927
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
18,810✔
928
    }
929

930
    if (sampling_method == ResScatMethod::dbrc) {
15,532!
931
      // interpolate xs since we're not exactly at the energy indices
932
      double xs_low = nuc.elastic_0K_[i_E_low];
×
933
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
934
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
935
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
936
      double xs_up = nuc.elastic_0K_[i_E_up];
×
937
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
938
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
939
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
940

941
      // get max 0K xs value over range of practical relative energies
942
      double xs_max = *std::max_element(
×
943
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
944
      xs_max = std::max({xs_low, xs_max, xs_up});
×
945

946
      while (true) {
×
947
        double E_rel;
×
948
        Direction v_target;
×
949
        while (true) {
×
950
          // sample target velocity with the constant cross section (cxs)
951
          // approx.
952
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
953
          Direction v_rel = v_neut - v_target;
×
954
          E_rel = v_rel.dot(v_rel);
×
955
          if (E_rel < E_up)
×
956
            break;
957
        }
958

959
        // perform Doppler broadening rejection correction (dbrc)
960
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
961
        double R = xs_0K / xs_max;
×
962
        if (prn(seed) < R)
×
963
          return v_target;
×
964
      }
965

966
    } else if (sampling_method == ResScatMethod::rvs) {
15,532✔
967
      // interpolate xs CDF since we're not exactly at the energy indices
968
      // cdf value at lower bound attainable energy
969
      double cdf_low = 0.0;
15,532✔
970
      if (E_low > nuc.energy_0K_.front()) {
15,532!
971
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
15,532✔
972
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
15,532✔
973
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
15,532✔
974
      }
975

976
      // cdf value at upper bound attainable energy
977
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,532✔
978
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,532✔
979
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,532✔
980

981
      while (true) {
325,908✔
982
        // directly sample Maxwellian
983
        double E_t = -kT * std::log(prn(seed));
170,720✔
984

985
        // sample a relative energy using the xs cdf
986
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
170,720✔
987
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
170,720✔
988
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
170,720✔
989
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
170,720✔
990
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
170,720✔
991
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
170,720✔
992
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
170,720✔
993
                     nuc.energy_0K_[i_E_low + i_E_rel]);
170,720✔
994
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
170,720✔
995

996
        // perform rejection sampling on cosine between
997
        // neutron and target velocities
998
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
170,720✔
999
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
170,720✔
1000

1001
        if (std::abs(mu) < 1.0) {
170,720✔
1002
          // set and accept target velocity
1003
          E_t /= nuc.awr_;
15,532✔
1004
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,532✔
1005
        }
1006
      }
155,188✔
1007
    }
1008
  } // case RVS, DBRC
1009
  } // switch (sampling_method)
1010

1011
  UNREACHABLE();
×
1012
}
1013

1014
Direction sample_cxs_target_velocity(
629,505,328✔
1015
  double awr, double E, Direction u, double kT, uint64_t* seed)
1016
{
1017
  double beta_vn = std::sqrt(awr * E / kT);
629,505,328✔
1018
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
629,505,328✔
1019

1020
  double beta_vt_sq;
749,258,709✔
1021
  double mu;
749,258,709✔
1022
  while (true) {
749,258,709✔
1023
    // Sample two random numbers
1024
    double r1 = prn(seed);
749,258,709✔
1025
    double r2 = prn(seed);
749,258,709✔
1026

1027
    if (prn(seed) < alpha) {
749,258,709✔
1028
      // With probability alpha, we sample the distribution p(y) =
1029
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1030
      // Carlo sampler
1031

1032
      beta_vt_sq = -std::log(r1 * r2);
191,791,068✔
1033

1034
    } else {
1035
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1036
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1037
      // Carlo sampler
1038

1039
      double c = std::cos(PI / 2.0 * prn(seed));
557,467,641✔
1040
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
557,467,641✔
1041
    }
1042

1043
    // Determine beta * vt
1044
    double beta_vt = std::sqrt(beta_vt_sq);
749,258,709✔
1045

1046
    // Sample cosine of angle between neutron and target velocity
1047
    mu = uniform_distribution(-1., 1., seed);
749,258,709✔
1048

1049
    // Determine rejection probability
1050
    double accept_prob =
749,258,709✔
1051
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
749,258,709✔
1052
      (beta_vn + beta_vt);
749,258,709✔
1053

1054
    // Perform rejection sampling on vt and mu
1055
    if (prn(seed) < accept_prob)
749,258,709✔
1056
      break;
1057
  }
1058

1059
  // Determine speed of target nucleus
1060
  double vt = std::sqrt(beta_vt_sq * kT / awr);
629,505,328✔
1061

1062
  // Determine velocity vector of target nucleus based on neutron's velocity
1063
  // and the sampled angle between them
1064
  return vt * rotate_angle(u, mu, nullptr, seed);
629,505,328✔
1065
}
1066

1067
void sample_fission_neutron(
39,030,689✔
1068
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1069
{
1070
  // Get attributes of particle
1071
  double E_in = p.E();
39,030,689✔
1072
  uint64_t* seed = p.current_seed();
39,030,689✔
1073

1074
  // Determine total nu, delayed nu, and delayed neutron fraction
1075
  const auto& nuc {data::nuclides[i_nuclide]};
39,030,689✔
1076
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
39,030,689✔
1077
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
39,030,689✔
1078
  double beta = nu_d / nu_t;
39,030,689✔
1079

1080
  if (prn(seed) < beta) {
39,030,689✔
1081
    // ====================================================================
1082
    // DELAYED NEUTRON SAMPLED
1083

1084
    // sampled delayed precursor group
1085
    double xi = prn(seed) * nu_d;
257,031✔
1086
    double prob = 0.0;
257,031✔
1087
    int group;
257,031✔
1088
    for (group = 1; group < nuc->n_precursor_; ++group) {
958,290✔
1089
      // determine delayed neutron precursor yield for group j
1090
      double yield = (*rx.products_[group].yield_)(E_in);
939,728✔
1091

1092
      // Check if this group is sampled
1093
      prob += yield;
939,728✔
1094
      if (xi < prob)
939,728✔
1095
        break;
1096
    }
1097

1098
    // if the sum of the probabilities is slightly less than one and the
1099
    // random number is greater, j will be greater than nuc %
1100
    // n_precursor -- check for this condition
1101
    group = std::min(group, nuc->n_precursor_);
257,031!
1102

1103
    // set the delayed group for the particle born from fission
1104
    site->delayed_group = group;
257,031✔
1105

1106
    // Sample time of emission based on decay constant of precursor
1107
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
257,031✔
1108
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
257,031✔
1109

1110
  } else {
1111
    // ====================================================================
1112
    // PROMPT NEUTRON SAMPLED
1113

1114
    // set the delayed group for the particle born from fission to 0
1115
    site->delayed_group = 0;
38,773,658✔
1116
  }
1117

1118
  if (!model::active_point_tallies.empty()) {
39,030,689!
NEW
1119
    score_point_tally_fission(p, i_nuclide, rx, site->delayed_group);
×
1120
  }
1121

1122
  // sample from prompt neutron energy distribution
1123
  int n_sample = 0;
1124
  double mu;
39,030,689✔
1125
  while (true) {
39,030,689✔
1126
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
39,030,689✔
1127

1128
    // resample if energy is greater than maximum neutron energy
1129
    int neutron = ParticleType::neutron().transport_index();
39,030,689✔
1130
    if (site->E < data::energy_max[neutron])
39,030,689!
1131
      break;
1132

1133
    // check for large number of resamples
UNCOV
1134
    ++n_sample;
×
UNCOV
1135
    if (n_sample == MAX_SAMPLE) {
×
1136
      // particle_write_restart(p)
1137
      fatal_error("Resampled energy distribution maximum number of times "
×
1138
                  "for nuclide " +
×
1139
                  nuc->name_);
×
1140
    }
1141
  }
1142

1143
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1144
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
39,030,689✔
1145
}
39,030,689✔
1146

1147
void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p)
24,107,376✔
1148
{
1149
  // Get pointer to nuclide
1150
  const auto& nuc {data::nuclides[i_nuclide]};
24,107,376✔
1151

1152
  // copy energy of neutron
1153
  double E_in = p.E();
24,107,376✔
1154

1155
  // sample outgoing energy and scattering cosine
1156
  double E;
24,107,376✔
1157
  double mu;
24,107,376✔
1158
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
24,107,376✔
1159

1160
  double yield = (*rx.products_[0].yield_)(E_in);
24,107,376✔
1161

1162
  if (!model::active_point_tallies.empty()) {
24,107,376!
NEW
1163
    score_point_tally_inelastic(p, i_nuclide, rx, 0, yield);
×
1164
  }
1165

1166
  // if scattering system is in center-of-mass, transfer cosine of scattering
1167
  // angle and outgoing energy from CM to LAB
1168
  if (rx.scatter_in_cm_) {
24,107,376✔
1169
    double E_cm = E;
24,047,485✔
1170

1171
    // determine outgoing energy in lab
1172
    double A = nuc->awr_;
24,047,485✔
1173
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
24,047,485✔
1174
                 ((A + 1.0) * (A + 1.0));
24,047,485✔
1175

1176
    // determine outgoing angle in lab
1177
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
24,047,485✔
1178
  }
1179

1180
  // Because of floating-point roundoff, it may be possible for mu to be
1181
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1182
  // or 1
1183
  if (std::abs(mu) > 1.0)
24,107,376!
1184
    mu = std::copysign(1.0, mu);
×
1185

1186
  // Set outgoing energy and scattering angle
1187
  p.E() = E;
24,107,376✔
1188
  p.mu() = mu;
24,107,376✔
1189

1190
  // change direction of particle
1191
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
24,107,376!
1192

1193
  if (std::floor(yield) == yield && yield > 0) {
24,107,376!
1194
    // If yield is integral, create exactly that many secondary particles
1195
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
24,240,676✔
1196
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
133,354✔
1197
    }
1198
  } else {
1199
    // Otherwise, change weight of particle based on yield
1200
    p.wgt() *= yield;
54✔
1201
  }
1202
}
24,107,376✔
1203

1204
void sample_secondary_photons(Particle& p, int i_nuclide)
66,633,380✔
1205
{
1206
  // Sample the number of photons produced
1207
  double y_t =
66,633,380✔
1208
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
66,633,380✔
1209
  double photon_wgt = p.wgt();
66,633,380✔
1210
  int y = 1;
66,633,380✔
1211

1212
  if (settings::use_decay_photons) {
66,633,380✔
1213
    // For decay photons, sample a single photon and modify the weight
1214
    if (y_t <= 0.0)
72,006✔
1215
      return;
1216
    photon_wgt *= y_t;
54,725✔
1217
  } else {
1218
    // For prompt photons, sample an integral number of photons with weight
1219
    // equal to the neutron's weight
1220
    y = static_cast<int>(y_t);
66,561,374✔
1221
    if (prn(p.current_seed()) <= y_t - y)
66,561,374✔
1222
      ++y;
1,869,989✔
1223
  }
1224

1225
  // Sample each secondary photon
1226
  for (int i = 0; i < y; ++i) {
69,284,721✔
1227
    // Sample the reaction and product
1228
    int i_rx;
2,668,622✔
1229
    int i_product;
2,668,622✔
1230
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
2,668,622✔
1231

1232
    // Sample the outgoing energy and angle
1233
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
2,668,622✔
1234
    double E;
2,668,622✔
1235
    double mu;
2,668,622✔
1236
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
2,668,622✔
1237

1238
    // Sample the new direction
1239
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
2,668,622✔
1240

1241
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1242
    // secondary photons from non-fission reactions to properly balance energy
1243
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1244
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1245
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1246
    double wgt = photon_wgt;
2,668,622✔
1247
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
2,668,622✔
1248
      wgt *= simulation::keff;
348,128✔
1249
    }
1250

1251
    // Create the secondary photon
1252
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
2,668,622✔
1253

1254
    // Pre-add photon energy to pht_storage so pht_secondary_particles()
1255
    // subtraction results in net zero
1256
    if (created_photon && !model::active_pulse_height_tallies.empty()) {
2,668,622✔
1257
      auto it = std::find(model::pulse_height_cells.begin(),
528✔
1258
        model::pulse_height_cells.end(), p.lowest_coord().cell());
528!
1259
      if (it != model::pulse_height_cells.end()) {
528!
1260
        int index = std::distance(model::pulse_height_cells.begin(), it);
528✔
1261
        p.pht_storage()[index] += E;
528✔
1262
      }
1263
    }
1264

1265
    // Tag secondary particle with parent nuclide
1266
    if (created_photon && settings::use_decay_photons) {
2,668,622✔
1267
      p.local_secondary_bank().back().parent_nuclide =
52,844✔
1268
        rx->products_[i_product].parent_nuclide_;
52,844✔
1269
    }
1270
  }
1271
}
1272

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