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

openmc-dev / openmc / 25631610076

10 May 2026 02:44PM UTC coverage: 81.026% (-0.4%) from 81.388%
25631610076

Pull #3757

github

web-flow
Merge debc5a921 into d56cda254
Pull Request #3757: Testing point detectors

17769 of 25812 branches covered (68.84%)

Branch coverage included in aggregate %.

51 of 373 new or added lines in 25 files covered. (13.67%)

3 existing lines in 2 files now uncovered.

58805 of 68694 relevant lines covered (85.6%)

40257430.53 hits per line

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

84.41
/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)
925,053,289✔
46
{
47
  // Add to collision counter for particle
48
  ++(p.n_collision());
925,053,289✔
49
  p.secondary_bank_index() = p.secondary_bank().size();
925,053,289!
50

51
  // Sample reaction for the material the particle is in
52
  switch (p.type().pdg_number()) {
925,053,289!
53
  case PDG_NEUTRON:
870,768,979✔
54
    sample_neutron_reaction(p);
870,768,979✔
55
    break;
870,768,979✔
56
  case PDG_PHOTON:
13,421,230✔
57
    sample_photon_reaction(p);
13,421,230✔
58
    break;
13,421,230✔
59
  case PDG_ELECTRON:
40,800,006✔
60
    sample_electron_reaction(p);
40,800,006✔
61
    break;
40,800,006✔
62
  case PDG_POSITRON:
63,074✔
63
    sample_positron_reaction(p);
63,074✔
64
    break;
63,074✔
65
  default:
×
66
    fatal_error("Unsupported particle PDG for collision sampling.");
×
67
  }
68

69
  if (settings::weight_windows_on) {
925,053,289✔
70
    auto [ww_found, ww] = search_weight_window(p);
61,098,822✔
71
    if (!ww_found && p.type() == ParticleType::neutron()) {
61,098,822✔
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);
52,379✔
75
    } else if (settings::weight_window_checkpoint_collision) {
61,046,443!
76
      // if collision checkpointing is on, apply weight window
77
      apply_weight_window(p, ww);
61,046,443✔
78
    }
79
  }
80

81
  // Kill particle if energy falls below cutoff
82
  int type = p.type().transport_index();
925,053,289!
83
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
925,053,289!
84
    p.wgt() = 0.0;
4,016,863✔
85
  }
86

87
  // Display information about collision
88
  if (settings::verbosity >= 10 || p.trace()) {
925,053,289!
89
    std::string msg;
48!
90
    if (p.event() == TallyEvent::KILL) {
48!
91
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
92
    } else if (p.type().is_neutron()) {
48!
93
      msg = fmt::format("    {} with {}. Energy = {} eV.",
96✔
94
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
96✔
95
        p.E());
48✔
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);
48✔
104
  }
48✔
105
}
925,053,289✔
106

107
void sample_neutron_reaction(Particle& p)
870,768,979✔
108
{
109
  // Sample a nuclide within the material
110
  int i_nuclide = sample_nuclide(p);
870,768,979✔
111

112
  // Save which nuclide particle had collision with
113
  p.event_nuclide() = i_nuclide;
870,768,979✔
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]};
870,768,979✔
121

122
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
870,768,979✔
123
    auto& rx = sample_fission(i_nuclide, p);
123,126,131✔
124
    if (settings::run_mode == RunMode::EIGENVALUE) {
123,126,131✔
125
      create_fission_sites(p, i_nuclide, rx);
104,881,351✔
126
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
18,244,780✔
127
               settings::create_fission_neutrons) {
128
      create_fission_sites(p, i_nuclide, rx);
465,836✔
129

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

142
  // Create secondary photons
143
  if (settings::photon_transport) {
870,768,979✔
144
    sample_secondary_photons(p, i_nuclide);
6,096,032✔
145
  }
146

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

150
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
870,768,979✔
151
    absorption(p, i_nuclide);
870,686,341✔
152
  }
153
  if (!p.alive())
870,768,979✔
154
    return;
155

156
  // Sample a scattering reaction and determine the secondary energy of the
157
  // exiting neutron
158
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
848,948,829✔
159
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
848,948,829!
160
    ncrystal_mat.scatter(p);
115,512✔
161
  } else {
162
    scatter(p, i_nuclide);
848,833,317✔
163
  }
164

165
  // Advance URR seed stream 'N' times after energy changes
166
  if (p.E() != p.E_last()) {
848,948,829✔
167
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
848,716,661✔
168
  }
169

170
  // Play russian roulette if there are no weight windows
171
  if (!settings::weight_windows_on)
848,948,829✔
172
    apply_russian_roulette(p);
802,645,514✔
173
}
174

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

181
  // Determine the expected number of neutrons produced
182
  double nu_t = p.wgt() / simulation::keff * weight *
105,347,187✔
183
                p.neutron_xs(i_nuclide).nu_fission /
105,347,187✔
184
                p.neutron_xs(i_nuclide).total;
105,347,187✔
185

186
  // Sample the number of neutrons produced
187
  int nu = static_cast<int>(nu_t);
105,347,187✔
188
  if (prn(p.current_seed()) <= (nu_t - nu))
105,347,187✔
189
    ++nu;
17,573,494✔
190

191
  // If no neutrons were produced then don't continue
192
  if (nu == 0)
105,347,187✔
193
    return;
83,512,819✔
194

195
  // Initialize the counter of delayed neutrons encountered for each delayed
196
  // group.
197
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
21,834,368✔
198

199
  // Clear out particle's nu fission bank
200
  p.nu_bank().clear();
21,834,368✔
201

202
  p.fission() = true;
21,834,368✔
203

204
  // Determine whether to place fission sites into the shared fission bank
205
  // or the secondary particle bank.
206
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
21,834,368✔
207

208
  // Counter for the number of fission sites successfully stored to the shared
209
  // fission bank or the secondary particle bank
210
  int n_sites_stored;
21,834,368✔
211

212
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
50,291,281✔
213
    // Initialize fission site object with particle data
214
    SourceSite site;
28,456,913✔
215
    site.r = p.r();
28,456,913✔
216
    site.particle = ParticleType::neutron();
28,456,913✔
217
    site.time = p.time();
28,456,913✔
218
    site.wgt = 1. / weight;
28,456,913✔
219
    site.surf_id = 0;
28,456,913✔
220

221
    // Sample delayed group and angle/energy for fission reaction
222
    sample_fission_neutron(i_nuclide, rx, &site, p);
28,456,913✔
223

224
    // Reject site if it exceeds time cutoff
225
    if (site.delayed_group > 0) {
28,456,913✔
226
      double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
187,507!
227
      if (site.time > t_cutoff) {
187,507!
228
        continue;
×
229
      }
230
    }
231

232
    // Set parent and progeny IDs
233
    site.parent_id = p.id();
28,456,913✔
234
    site.progeny_id = p.n_progeny()++;
28,456,913✔
235

236
    // Store fission site in bank
237
    if (use_fission_bank) {
28,456,913✔
238
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
28,253,490✔
239
      if (idx == -1) {
28,253,490!
240
        warning(
×
241
          "The shared fission bank is full. Additional fission sites created "
242
          "in this generation will not be banked. Results may be "
243
          "non-deterministic.");
244

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

250
        // Break out of loop as no more sites can be added to fission bank
251
        break;
×
252
      }
253
      // Iterated Fission Probability (IFP) method
254
      if (settings::ifp_on) {
28,253,490✔
255
        ifp(p, idx);
983,728✔
256
      }
257
    } else {
258
      p.secondary_bank().push_back(site);
203,423✔
259
      p.n_secondaries()++;
203,423✔
260
    }
261

262
    // Increment the number of neutrons born delayed
263
    if (site.delayed_group > 0) {
28,456,913✔
264
      nu_d[site.delayed_group - 1]++;
187,507✔
265
    }
266

267
    // Write fission particles to nuBank
268
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
28,456,913✔
269
    nu_bank_entry.wgt = site.wgt;
28,456,913✔
270
    nu_bank_entry.E = site.E;
28,456,913✔
271
    nu_bank_entry.delayed_group = site.delayed_group;
28,456,913✔
272
  }
273

274
  // If shared fission bank was full, and no fissions could be added,
275
  // set the particle fission flag to false.
276
  if (n_sites_stored == 0) {
21,834,368!
277
    p.fission() = false;
×
278
    return;
×
279
  }
280

281
  // Set nu to the number of fission sites successfully stored. If the fission
282
  // bank was not found to be full then these values are already equivalent.
283
  nu = n_sites_stored;
21,834,368✔
284

285
  // Store the total weight banked for analog fission tallies
286
  p.n_bank() = nu;
21,834,368✔
287
  p.wgt_bank() = nu / weight;
21,834,368✔
288
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
196,509,312✔
289
    p.n_delayed_bank(d) = nu_d[d];
174,674,944✔
290
  }
291
}
292

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

305
  // Sample element within material
306
  int i_element = sample_element(p);
13,420,646✔
307
  const auto& micro {p.photon_xs(i_element)};
13,420,646✔
308
  const auto& element {*data::elements[i_element]};
13,420,646✔
309

310
  // Calculate photon energy over electron rest mass equivalent
311
  double alpha = p.E() / MASS_ELECTRON_EV;
13,420,646✔
312

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

318
  // Coherent (Rayleigh) scattering
319
  prob += micro.coherent;
13,420,646✔
320
  if (prob > cutoff) {
13,420,646✔
321
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
734,396✔
322
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
734,396✔
323
    p.event() = TallyEvent::SCATTER;
734,396✔
324
    p.event_mt() = COHERENT;
734,396✔
325
    return;
734,396✔
326
  }
327

328
  // Incoherent (Compton) scattering
329
  prob += micro.incoherent;
12,686,250✔
330
  if (prob > cutoff) {
12,686,250✔
331
    double alpha_out;
8,677,915✔
332
    int i_shell;
8,677,915✔
333
    element.compton_scatter(
8,677,915✔
334
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
8,677,915✔
335

336
    // Determine binding energy of shell. The binding energy is 0.0 if
337
    // doppler broadening is not used.
338
    double e_b;
8,677,915✔
339
    if (i_shell == -1) {
8,677,915!
340
      e_b = 0.0;
341
    } else {
342
      e_b = element.binding_energy_[i_shell];
8,677,915✔
343
    }
344

345
    // Create Compton electron
346
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
8,677,915✔
347
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
8,677,915✔
348
    int electron = ParticleType::electron().transport_index();
8,677,915✔
349
    if (E_electron >= settings::energy_cutoff[electron]) {
8,677,915✔
350
      double mu_electron = (alpha - alpha_out * p.mu()) /
8,597,659✔
351
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
8,597,659✔
352
                                     2.0 * alpha * alpha_out * p.mu());
8,597,659✔
353
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
8,597,659✔
354
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
8,597,659✔
355
    }
356

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

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

373
  // Photoelectric effect
374
  double prob_after = prob + micro.photoelectric;
4,008,335✔
375

376
  if (prob_after > cutoff) {
4,008,335✔
377
    // Get grid index, interpolation factor, and bounding subshell
378
    // cross sections
379
    int i_grid = micro.index_grid;
3,945,261✔
380
    double f = micro.interp_factor;
3,945,261✔
381
    tensor::View<const double> xs_lower = element.cross_sections_.slice(i_grid);
3,945,261✔
382
    tensor::View<const double> xs_upper =
3,945,261✔
383
      element.cross_sections_.slice(i_grid + 1);
3,945,261✔
384

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

388
      // Check threshold of reaction
389
      if (xs_lower(i_shell) == 0)
18,094,885✔
390
        continue;
7,528,321✔
391

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

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

403
        // Determine energy of secondary electron
404
        double E_electron = p.E() - binding_energy;
3,945,261✔
405

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

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

428
        // Create secondary electron
429
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
3,945,261✔
430

431
        // Allow electrons to fill orbital and produce auger electrons
432
        // and fluorescent photons
433
        if (settings::atomic_relaxation) {
3,945,261✔
434
          element.atomic_relaxation(i_shell, p);
3,865,261✔
435
        }
436
        p.event() = TallyEvent::ABSORB;
3,945,261✔
437
        p.event_mt() = 533 + shell.index_subshell;
3,945,261✔
438
        p.wgt() = 0.0;
3,945,261✔
439
        p.E() = 0.0;
3,945,261✔
440
        return;
3,945,261✔
441
      }
442
    }
443
  }
7,890,522✔
444
  prob = prob_after;
63,074✔
445

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

454
    // Create secondary electron
455
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
63,074✔
456
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
63,074✔
457

458
    // Create secondary positron
459
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
63,074✔
460
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron());
63,074✔
461
    p.event() = TallyEvent::ABSORB;
63,074✔
462
    p.event_mt() = PAIR_PROD;
63,074✔
463
    p.wgt() = 0.0;
63,074✔
464
    p.E() = 0.0;
63,074✔
465
  }
466
}
467

468
void sample_electron_reaction(Particle& p)
40,800,006✔
469
{
470
  // TODO: create reaction types
471

472
  if (settings::electron_treatment == ElectronTreatment::TTB) {
40,800,006✔
473
    double E_lost;
38,922,158✔
474
    thick_target_bremsstrahlung(p, &E_lost);
38,922,158✔
475
  }
476

477
  p.E() = 0.0;
40,800,006✔
478
  p.wgt() = 0.0;
40,800,006✔
479
  p.event() = TallyEvent::ABSORB;
40,800,006✔
480
}
40,800,006✔
481

482
void sample_positron_reaction(Particle& p)
63,074✔
483
{
484
  // TODO: create reaction types
485

486
  if (settings::electron_treatment == ElectronTreatment::TTB) {
63,074✔
487
    double E_lost;
62,890✔
488
    thick_target_bremsstrahlung(p, &E_lost);
62,890✔
489
  }
490

491
  // Sample angle isotropically
492
  Direction u = isotropic_direction(p.current_seed());
63,074✔
493

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

498
  p.E() = 0.0;
63,074✔
499
  p.wgt() = 0.0;
63,074✔
500
  p.event() = TallyEvent::ABSORB;
63,074✔
501
}
63,074✔
502

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

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

512
  double prob = 0.0;
870,768,979✔
513
  for (int i = 0; i < n; ++i) {
1,827,079,660!
514
    // Get atom density
515
    int i_nuclide = mat->nuclide_[i];
1,827,079,660✔
516
    double atom_density = mat->atom_density(i, p.density_mult());
1,827,079,660✔
517

518
    // Increment probability to compare to cutoff
519
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,827,079,660✔
520
    if (prob >= cutoff)
1,827,079,660✔
521
      return i_nuclide;
870,768,979✔
522
  }
523

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

529
int sample_element(Particle& p)
13,420,646✔
530
{
531
  // Sample cumulative distribution function
532
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
13,420,646✔
533

534
  // Get pointers to elements, densities
535
  const auto& mat {model::materials[p.material()]};
13,420,646✔
536

537
  double prob = 0.0;
13,420,646✔
538
  for (int i = 0; i < mat->element_.size(); ++i) {
31,026,787!
539
    // Find atom density
540
    int i_element = mat->element_[i];
31,026,787✔
541
    double atom_density = mat->atom_density(i, p.density_mult());
31,026,787✔
542

543
    // Determine microscopic cross section
544
    double sigma = atom_density * p.photon_xs(i_element).total;
31,026,787✔
545

546
    // Increment probability to compare to cutoff
547
    prob += sigma;
31,026,787✔
548
    if (prob > cutoff) {
31,026,787✔
549
      // Save which nuclide particle had collision with for tally purpose
550
      p.event_nuclide() = mat->nuclide_[i];
13,420,646✔
551

552
      return i_element;
13,420,646✔
553
    }
554
  }
555

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

561
Reaction& sample_fission(int i_nuclide, Particle& p)
123,126,131✔
562
{
563
  // Get pointer to nuclide
564
  const auto& nuc {data::nuclides[i_nuclide]};
123,126,131✔
565

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

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

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

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

591
    // Create fission bank sites if fission occurs
592
    if (prob > cutoff)
41,791✔
593
      return *rx;
41,733✔
594
  }
595

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

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

609
  // Loop through each reaction type
610
  const auto& nuc {data::nuclides[i_nuclide]};
964,912✔
611
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
15,807,632!
612
    // Evaluate neutron cross section
613
    const auto& rx = nuc->reactions_[i];
15,807,632✔
614
    double xs = rx->xs(micro);
15,807,632✔
615

616
    // if cross section is zero for this reaction, skip it
617
    if (xs == 0.0)
15,807,632✔
618
      continue;
4,906,480✔
619

620
    for (int j = 0; j < rx->products_.size(); ++j) {
50,432,240✔
621
      if (rx->products_[j].particle_.is_photon()) {
40,496,000✔
622
        // For fission, artificially increase the photon yield to account
623
        // for delayed photons
624
        double f = 1.0;
31,344,792✔
625
        if (settings::delayed_photon_scaling) {
31,344,792!
626
          if (is_fission(rx->mt_)) {
31,344,792✔
627
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
392,888!
628
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
392,888✔
629
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
392,888✔
630
              f = (energy_prompt + energy_delayed) / (energy_prompt);
392,888✔
631
            }
632
          }
633
        }
634

635
        // add to cumulative probability
636
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
31,344,792✔
637

638
        *i_rx = i;
31,344,792✔
639
        *i_product = j;
31,344,792✔
640
        if (prob > cutoff)
31,344,792✔
641
          return;
642
      }
643
    }
644
  }
645
}
646

647
void absorption(Particle& p, int i_nuclide)
870,686,341✔
648
{
649
  if (settings::survival_biasing) {
870,686,341✔
650
    // Determine weight absorbed in survival biasing
651
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
4,178,160✔
652
                              p.neutron_xs(i_nuclide).total;
4,178,160✔
653

654
    // Adjust weight of particle by probability of absorption
655
    p.wgt() -= wgt_absorb;
4,178,160✔
656

657
    // Score implicit absorption estimate of keff
658
    if (settings::run_mode == RunMode::EIGENVALUE) {
4,178,160✔
659
      p.keff_tally_absorption() += wgt_absorb *
363,600✔
660
                                   p.neutron_xs(i_nuclide).nu_fission /
363,600✔
661
                                   p.neutron_xs(i_nuclide).absorption;
363,600✔
662
    }
663
  } else {
664
    // See if disappearance reaction happens
665
    if (p.neutron_xs(i_nuclide).absorption >
866,508,181✔
666
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
866,508,181✔
667
      // Score absorption estimate of keff
668
      if (settings::run_mode == RunMode::EIGENVALUE) {
21,819,454✔
669
        p.keff_tally_absorption() += p.wgt() *
17,659,058✔
670
                                     p.neutron_xs(i_nuclide).nu_fission /
17,659,058✔
671
                                     p.neutron_xs(i_nuclide).absorption;
17,659,058✔
672
      }
673

674
      p.wgt() = 0.0;
21,819,454✔
675
      p.event() = TallyEvent::ABSORB;
21,819,454✔
676
      if (!p.fission()) {
21,819,454✔
677
        p.event_mt() = N_DISAPPEAR;
12,815,509✔
678
      }
679
    }
680
  }
681
}
870,686,341✔
682

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

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

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

698
  // Calculate elastic cross section if it wasn't precalculated
699
  if (micro.elastic == CACHE_INVALID) {
848,833,317✔
700
    nuc->calculate_elastic_xs(p);
664,929,048✔
701
  }
702

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

708
    // Determine temperature
709
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
738,791,057✔
710

711
    // Perform collision physics for elastic scattering
712
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
738,791,057✔
713

714
    p.event_mt() = ELASTIC;
738,791,057✔
715
    sampled = true;
738,791,057✔
716
  }
717

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

723
    sab_scatter(i_nuclide, micro.index_sab, p);
92,714,744✔
724

725
    p.event_mt() = ELASTIC;
92,714,744✔
726
    sampled = true;
92,714,744✔
727
  }
728

729
  if (!sampled) {
848,833,317✔
730
    // =======================================================================
731
    // INELASTIC SCATTERING
732

733
    int n = nuc->index_inelastic_scatter_.size();
17,327,516✔
734
    int i = 0;
17,327,516✔
735
    for (int j = 0; j < n && prob < cutoff; ++j) {
326,533,424✔
736
      i = nuc->index_inelastic_scatter_[j];
309,205,908✔
737

738
      // add to cumulative probability
739
      prob += nuc->reactions_[i]->xs(micro);
309,205,908✔
740
    }
741

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

748
  // Set event component
749
  p.event() = TallyEvent::SCATTER;
848,833,317✔
750

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

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

768
  double vel = std::sqrt(p.E());
738,791,057✔
769
  double awr = nuc->awr_;
738,791,057✔
770

771
  // Neutron velocity in LAB
772
  Direction v_n = vel * p.u();
738,791,057✔
773

774
  // Sample velocity of target nucleus
775
  Direction v_t {};
738,791,057✔
776
  if (!p.neutron_xs(i_nuclide).use_ptable) {
738,791,057✔
777
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
706,072,382✔
778
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
706,072,382✔
779
  }
780

781
  // Velocity of center-of-mass
782
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
738,791,057✔
783

784
  // Transform to CM frame
785
  v_n -= v_cm;
738,791,057✔
786

787
  // Find speed of neutron in CM
788
  vel = v_n.norm();
738,791,057✔
789

790
  if (!model::active_point_tallies.empty()) {
738,791,057!
NEW
791
    score_point_tally_elastic(p, i_nuclide, rx, 0, v_t);
×
792
  }
793

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

805
  // Determine direction cosines in CM
806
  Direction u_cm = v_n / vel;
738,791,057✔
807

808
  // Rotate neutron velocity vector to new angle -- note that the speed of the
809
  // neutron in CM does not change in elastic scattering. However, the speed
810
  // will change when we convert back to LAB
811
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
738,791,057✔
812

813
  // Transform back to LAB frame
814
  v_n += v_cm;
738,791,057✔
815

816
  p.E() = v_n.dot(v_n);
738,791,057✔
817
  vel = std::sqrt(p.E());
738,791,057✔
818

819
  // compute cosine of scattering angle in LAB frame by taking dot product of
820
  // neutron's pre- and post-collision angle
821
  p.mu() = p.u().dot(v_n) / vel;
738,791,057✔
822

823
  // Set energy and direction of particle in LAB frame
824
  p.u() = v_n / vel;
738,791,057!
825

826
  // Because of floating-point roundoff, it may be possible for mu_lab to be
827
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
828
  // -1 or 1
829
  if (std::abs(p.mu()) > 1.0)
738,791,057!
830
    p.mu() = std::copysign(1.0, p.mu());
×
831
}
738,791,057✔
832

833
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
92,714,744✔
834
{
835
  // Determine temperature index
836
  const auto& micro {p.neutron_xs(i_nuclide)};
92,714,744!
837
  int i_temp = micro.index_temp_sab;
92,714,744✔
838

839
  // Sample energy and angle
840
  double E_out;
92,714,744✔
841
  auto& sab = data::thermal_scatt[i_sab]->data_[i_temp];
92,714,744!
842

843
  if (!model::active_point_tallies.empty()) {
92,714,744!
NEW
844
    score_point_tally_sab(p, i_nuclide, sab, micro);
×
845
  }
846

847
  sab.sample(micro, p.E(), &E_out, &p.mu(), p.current_seed());
92,714,744✔
848

849
  // Set energy to outgoing, change direction of particle
850
  p.E() = E_out;
92,714,744✔
851
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
92,714,744✔
852
}
92,714,744✔
853

854
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
706,072,382✔
855
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
856
{
857
  // check if nuclide is a resonant scatterer
858
  ResScatMethod sampling_method;
706,072,382✔
859
  if (nuc.resonant_) {
706,072,382✔
860

861
    // sampling method to use
862
    sampling_method = settings::res_scat_method;
61,496✔
863

864
    // upper resonance scattering energy bound (target is at rest above this E)
865
    if (E > settings::res_scat_energy_max) {
61,496✔
866
      return {};
29,640✔
867

868
      // lower resonance scattering energy bound (should be no resonances below)
869
    } else if (E < settings::res_scat_energy_min) {
31,856✔
870
      sampling_method = ResScatMethod::cxs;
871
    }
872

873
    // otherwise, use free gas model
874
  } else {
875
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
706,010,886✔
876
      return {};
335,897,413✔
877
    } else {
878
      sampling_method = ResScatMethod::cxs;
879
    }
880
  }
881

882
  // use appropriate target velocity sampling method
883
  switch (sampling_method) {
13,680!
884
  case ResScatMethod::cxs:
370,131,649✔
885

886
    // sample target velocity with the constant cross section (cxs) approx.
887
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
370,131,649✔
888

889
  case ResScatMethod::dbrc:
13,680✔
890
  case ResScatMethod::rvs: {
13,680✔
891
    double E_red = std::sqrt(nuc.awr_ * E / kT);
13,680✔
892
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
27,360!
893
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
13,680✔
894

895
    // find lower and upper energy bound indices
896
    // lower index
897
    int i_E_low;
13,680✔
898
    if (E_low < nuc.energy_0K_.front()) {
13,680!
899
      i_E_low = 0;
900
    } else if (E_low > nuc.energy_0K_.back()) {
13,680!
901
      i_E_low = nuc.energy_0K_.size() - 2;
×
902
    } else {
903
      i_E_low =
13,680✔
904
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
13,680✔
905
    }
906

907
    // upper index
908
    int i_E_up;
13,680✔
909
    if (E_up < nuc.energy_0K_.front()) {
13,680!
910
      i_E_up = 0;
911
    } else if (E_up > nuc.energy_0K_.back()) {
13,680!
912
      i_E_up = nuc.energy_0K_.size() - 2;
×
913
    } else {
914
      i_E_up =
13,680✔
915
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
13,680✔
916
    }
917

918
    if (i_E_up == i_E_low) {
13,680✔
919
      // Handle degenerate case -- if the upper/lower bounds occur for the same
920
      // index, then using cxs is probably a good approximation
921
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
13,680✔
922
    }
923

924
    if (sampling_method == ResScatMethod::dbrc) {
11,296!
925
      // interpolate xs since we're not exactly at the energy indices
926
      double xs_low = nuc.elastic_0K_[i_E_low];
×
927
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
928
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
929
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
930
      double xs_up = nuc.elastic_0K_[i_E_up];
×
931
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
932
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
933
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
934

935
      // get max 0K xs value over range of practical relative energies
936
      double xs_max = *std::max_element(
×
937
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
938
      xs_max = std::max({xs_low, xs_max, xs_up});
×
939

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

953
        // perform Doppler broadening rejection correction (dbrc)
954
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
955
        double R = xs_0K / xs_max;
×
956
        if (prn(seed) < R)
×
957
          return v_target;
×
958
      }
959

960
    } else if (sampling_method == ResScatMethod::rvs) {
11,296✔
961
      // interpolate xs CDF since we're not exactly at the energy indices
962
      // cdf value at lower bound attainable energy
963
      double cdf_low = 0.0;
11,296✔
964
      if (E_low > nuc.energy_0K_.front()) {
11,296!
965
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
11,296✔
966
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
11,296✔
967
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
11,296✔
968
      }
969

970
      // cdf value at upper bound attainable energy
971
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
11,296✔
972
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
11,296✔
973
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
11,296✔
974

975
      while (true) {
237,024✔
976
        // directly sample Maxwellian
977
        double E_t = -kT * std::log(prn(seed));
124,160✔
978

979
        // sample a relative energy using the xs cdf
980
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
124,160✔
981
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
124,160✔
982
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
124,160✔
983
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
124,160✔
984
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
124,160✔
985
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
124,160✔
986
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
124,160✔
987
                     nuc.energy_0K_[i_E_low + i_E_rel]);
124,160✔
988
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
124,160✔
989

990
        // perform rejection sampling on cosine between
991
        // neutron and target velocities
992
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
124,160✔
993
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
124,160✔
994

995
        if (std::abs(mu) < 1.0) {
124,160✔
996
          // set and accept target velocity
997
          E_t /= nuc.awr_;
11,296✔
998
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
11,296✔
999
        }
1000
      }
112,864✔
1001
    }
1002
  } // case RVS, DBRC
1003
  } // switch (sampling_method)
1004

1005
  UNREACHABLE();
×
1006
}
1007

1008
Direction sample_cxs_target_velocity(
370,134,033✔
1009
  double awr, double E, Direction u, double kT, uint64_t* seed)
1010
{
1011
  double beta_vn = std::sqrt(awr * E / kT);
370,134,033✔
1012
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
370,134,033✔
1013

1014
  double beta_vt_sq;
428,569,238✔
1015
  double mu;
428,569,238✔
1016
  while (true) {
428,569,238✔
1017
    // Sample two random numbers
1018
    double r1 = prn(seed);
428,569,238✔
1019
    double r2 = prn(seed);
428,569,238✔
1020

1021
    if (prn(seed) < alpha) {
428,569,238✔
1022
      // With probability alpha, we sample the distribution p(y) =
1023
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1024
      // Carlo sampler
1025

1026
      beta_vt_sq = -std::log(r1 * r2);
93,044,407✔
1027

1028
    } else {
1029
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1030
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1031
      // Carlo sampler
1032

1033
      double c = std::cos(PI / 2.0 * prn(seed));
335,524,831✔
1034
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
335,524,831✔
1035
    }
1036

1037
    // Determine beta * vt
1038
    double beta_vt = std::sqrt(beta_vt_sq);
428,569,238✔
1039

1040
    // Sample cosine of angle between neutron and target velocity
1041
    mu = uniform_distribution(-1., 1., seed);
428,569,238✔
1042

1043
    // Determine rejection probability
1044
    double accept_prob =
428,569,238✔
1045
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
428,569,238✔
1046
      (beta_vn + beta_vt);
428,569,238✔
1047

1048
    // Perform rejection sampling on vt and mu
1049
    if (prn(seed) < accept_prob)
428,569,238✔
1050
      break;
1051
  }
1052

1053
  // Determine speed of target nucleus
1054
  double vt = std::sqrt(beta_vt_sq * kT / awr);
370,134,033✔
1055

1056
  // Determine velocity vector of target nucleus based on neutron's velocity
1057
  // and the sampled angle between them
1058
  return vt * rotate_angle(u, mu, nullptr, seed);
370,134,033✔
1059
}
1060

1061
void sample_fission_neutron(
28,456,913✔
1062
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1063
{
1064
  // Get attributes of particle
1065
  double E_in = p.E();
28,456,913✔
1066
  uint64_t* seed = p.current_seed();
28,456,913✔
1067

1068
  // Determine total nu, delayed nu, and delayed neutron fraction
1069
  const auto& nuc {data::nuclides[i_nuclide]};
28,456,913✔
1070
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
28,456,913✔
1071
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
28,456,913✔
1072
  double beta = nu_d / nu_t;
28,456,913✔
1073

1074
  if (prn(seed) < beta) {
28,456,913✔
1075
    // ====================================================================
1076
    // DELAYED NEUTRON SAMPLED
1077

1078
    // sampled delayed precursor group
1079
    double xi = prn(seed) * nu_d;
187,507✔
1080
    double prob = 0.0;
187,507✔
1081
    int group;
187,507✔
1082
    for (group = 1; group < nuc->n_precursor_; ++group) {
699,279✔
1083
      // determine delayed neutron precursor yield for group j
1084
      double yield = (*rx.products_[group].yield_)(E_in);
685,671✔
1085

1086
      // Check if this group is sampled
1087
      prob += yield;
685,671✔
1088
      if (xi < prob)
685,671✔
1089
        break;
1090
    }
1091

1092
    // if the sum of the probabilities is slightly less than one and the
1093
    // random number is greater, j will be greater than nuc %
1094
    // n_precursor -- check for this condition
1095
    group = std::min(group, nuc->n_precursor_);
187,507!
1096

1097
    // set the delayed group for the particle born from fission
1098
    site->delayed_group = group;
187,507✔
1099

1100
    // Sample time of emission based on decay constant of precursor
1101
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
187,507✔
1102
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
187,507✔
1103

1104
  } else {
1105
    // ====================================================================
1106
    // PROMPT NEUTRON SAMPLED
1107

1108
    // set the delayed group for the particle born from fission to 0
1109
    site->delayed_group = 0;
28,269,406✔
1110
  }
1111

1112
  if (!model::active_point_tallies.empty()) {
28,456,913!
NEW
1113
    score_point_tally_fission(p, i_nuclide, rx, site->delayed_group);
×
1114
  }
1115

1116
  // sample from prompt neutron energy distribution
1117
  int n_sample = 0;
1118
  double mu;
28,456,916✔
1119
  while (true) {
28,456,916✔
1120
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
28,456,916✔
1121

1122
    // resample if energy is greater than maximum neutron energy
1123
    int neutron = ParticleType::neutron().transport_index();
28,456,916✔
1124
    if (site->E < data::energy_max[neutron])
28,456,916✔
1125
      break;
1126

1127
    // check for large number of resamples
1128
    ++n_sample;
3✔
1129
    if (n_sample == MAX_SAMPLE) {
3!
1130
      // particle_write_restart(p)
1131
      fatal_error("Resampled energy distribution maximum number of times "
×
1132
                  "for nuclide " +
×
1133
                  nuc->name_);
×
1134
    }
1135
  }
1136

1137
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1138
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
28,456,913✔
1139
}
28,456,913✔
1140

1141
void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p)
17,327,516✔
1142
{
1143
  // Get pointer to nuclide
1144
  const auto& nuc {data::nuclides[i_nuclide]};
17,327,516✔
1145

1146
  // copy energy of neutron
1147
  double E_in = p.E();
17,327,516✔
1148

1149
  // sample outgoing energy and scattering cosine
1150
  double E;
17,327,516✔
1151
  double mu;
17,327,516✔
1152
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
17,327,516✔
1153

1154
  double yield = (*rx.products_[0].yield_)(E_in);
17,327,516✔
1155

1156
  if (!model::active_point_tallies.empty()) {
17,327,516!
NEW
1157
    score_point_tally_inelastic(p, i_nuclide, rx, 0, yield);
×
1158
  }
1159

1160
  // if scattering system is in center-of-mass, transfer cosine of scattering
1161
  // angle and outgoing energy from CM to LAB
1162
  if (rx.scatter_in_cm_) {
17,327,516✔
1163
    double E_cm = E;
17,287,238✔
1164

1165
    // determine outgoing energy in lab
1166
    double A = nuc->awr_;
17,287,238✔
1167
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
17,287,238✔
1168
                 ((A + 1.0) * (A + 1.0));
17,287,238✔
1169

1170
    // determine outgoing angle in lab
1171
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
17,287,238✔
1172
  }
1173

1174
  // Because of floating-point roundoff, it may be possible for mu to be
1175
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1176
  // or 1
1177
  if (std::abs(mu) > 1.0)
17,327,516!
1178
    mu = std::copysign(1.0, mu);
×
1179

1180
  // Set outgoing energy and scattering angle
1181
  p.E() = E;
17,327,516✔
1182
  p.mu() = mu;
17,327,516✔
1183

1184
  // change direction of particle
1185
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
17,327,516!
1186

1187
  if (std::floor(yield) == yield && yield > 0) {
17,327,516!
1188
    // If yield is integral, create exactly that many secondary particles
1189
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
17,426,722✔
1190
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
99,260✔
1191
    }
1192
  } else {
1193
    // Otherwise, change weight of particle based on yield
1194
    p.wgt() *= yield;
54✔
1195
  }
1196
}
17,327,516✔
1197

1198
void sample_secondary_photons(Particle& p, int i_nuclide)
6,096,032✔
1199
{
1200
  // Sample the number of photons produced
1201
  double y_t =
6,096,032✔
1202
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
6,096,032✔
1203
  double photon_wgt = p.wgt();
6,096,032✔
1204
  int y = 1;
6,096,032✔
1205

1206
  if (settings::use_decay_photons) {
6,096,032✔
1207
    // For decay photons, sample a single photon and modify the weight
1208
    if (y_t <= 0.0)
52,368✔
1209
      return;
1210
    photon_wgt *= y_t;
39,800✔
1211
  } else {
1212
    // For prompt photons, sample an integral number of photons with weight
1213
    // equal to the neutron's weight
1214
    y = static_cast<int>(y_t);
6,043,664✔
1215
    if (prn(p.current_seed()) <= y_t - y)
6,043,664✔
1216
      ++y;
407,896✔
1217
  }
1218

1219
  // Sample each secondary photon
1220
  for (int i = 0; i < y; ++i) {
7,048,376✔
1221
    // Sample the reaction and product
1222
    int i_rx;
964,912✔
1223
    int i_product;
964,912✔
1224
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
964,912✔
1225

1226
    // Sample the outgoing energy and angle
1227
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
964,912✔
1228
    double E;
964,912✔
1229
    double mu;
964,912✔
1230
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
964,912✔
1231

1232
    // Sample the new direction
1233
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
964,912✔
1234

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

1245
    // Create the secondary photon
1246
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
964,912✔
1247

1248
    // Tag secondary particle with parent nuclide
1249
    if (created_photon && settings::use_decay_photons) {
964,912✔
1250
      p.secondary_bank().back().parent_nuclide =
38,432✔
1251
        rx->products_[i_product].parent_nuclide_;
38,432✔
1252
    }
1253
  }
1254
}
1255

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