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

openmc-dev / openmc / 22596289416

02 Mar 2026 09:22PM UTC coverage: 80.951% (-0.6%) from 81.508%
22596289416

Pull #3757

github

web-flow
Merge d60b97b21 into 823b4c96c
Pull Request #3757: Testing point detectors

17543 of 25514 branches covered (68.76%)

Branch coverage included in aggregate %.

117 of 510 new or added lines in 26 files covered. (22.94%)

22 existing lines in 3 files now uncovered.

57759 of 67508 relevant lines covered (85.56%)

44598697.25 hits per line

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

83.76
/src/physics.cpp
1
#include "openmc/physics.h"
2

3
#include "openmc/bank.h"
4
#include "openmc/bremsstrahlung.h"
5
#include "openmc/chain.h"
6
#include "openmc/constants.h"
7
#include "openmc/distribution_multi.h"
8
#include "openmc/eigenvalue.h"
9
#include "openmc/endf.h"
10
#include "openmc/error.h"
11
#include "openmc/ifp.h"
12
#include "openmc/material.h"
13
#include "openmc/math_functions.h"
14
#include "openmc/message_passing.h"
15
#include "openmc/ncrystal_interface.h"
16
#include "openmc/nuclide.h"
17
#include "openmc/photon.h"
18
#include "openmc/physics_common.h"
19
#include "openmc/random_dist.h"
20
#include "openmc/random_lcg.h"
21
#include "openmc/reaction.h"
22
#include "openmc/search.h"
23
#include "openmc/secondary_uncorrelated.h"
24
#include "openmc/settings.h"
25
#include "openmc/simulation.h"
26
#include "openmc/string_utils.h"
27
#include "openmc/tallies/tally.h"
28
#include "openmc/tallies/tally_scoring.h"
29
#include "openmc/thermal.h"
30
#include "openmc/weight_windows.h"
31

32
#include <fmt/core.h>
33

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

38
namespace openmc {
39

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

44
void collision(Particle& p)
1,070,992,750✔
45
{
46
  // Add to collision counter for particle
47
  ++(p.n_collision());
1,070,992,750✔
48
  p.secondary_bank_index() = p.secondary_bank().size();
1,070,992,750!
49

50
  // Sample reaction for the material the particle is in
51
  switch (p.type().pdg_number()) {
1,070,992,750!
52
  case PDG_NEUTRON:
999,577,530✔
53
    sample_neutron_reaction(p);
999,577,530✔
54
    break;
999,577,530✔
55
  case PDG_PHOTON:
17,498,207✔
56
    sample_photon_reaction(p);
17,498,207✔
57
    break;
17,498,207✔
58
  case PDG_ELECTRON:
53,829,276✔
59
    sample_electron_reaction(p);
53,829,276✔
60
    break;
53,829,276✔
61
  case PDG_POSITRON:
87,737✔
62
    sample_positron_reaction(p);
87,737✔
63
    break;
87,737✔
64
  default:
×
65
    fatal_error("Unsupported particle PDG for collision sampling.");
×
66
  }
67

68
  if (settings::weight_windows_on) {
1,070,992,750✔
69
    auto [ww_found, ww] = search_weight_window(p);
83,952,026✔
70
    if (!ww_found && p.type() == ParticleType::neutron()) {
83,952,026✔
71
      // if the weight window is not valid, apply russian roulette for neutrons
72
      // (regardless of weight window collision checkpoint setting)
73
      apply_russian_roulette(p);
66,995✔
74
    } else if (settings::weight_window_checkpoint_collision) {
83,885,031!
75
      // if collision checkpointing is on, apply weight window
76
      apply_weight_window(p, ww);
83,885,031✔
77
    }
78
  }
79

80
  // Kill particle if energy falls below cutoff
81
  int type = p.type().transport_index();
1,070,992,750!
82
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
1,070,992,750!
83
    p.wgt() = 0.0;
5,226,008✔
84
  }
85

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

106
void sample_neutron_reaction(Particle& p)
999,577,530✔
107
{
108
  // Sample a nuclide within the material
109
  int i_nuclide = sample_nuclide(p);
999,577,530✔
110

111
  // Save which nuclide particle had collision with
112
  p.event_nuclide() = i_nuclide;
999,577,530✔
113

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

119
  const auto& nuc {data::nuclides[i_nuclide]};
999,577,530✔
120

121
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
999,577,530✔
122
    auto& rx = sample_fission(i_nuclide, p);
125,530,924✔
123
    if (settings::run_mode == RunMode::EIGENVALUE) {
125,530,924✔
124
      create_fission_sites(p, i_nuclide, rx);
100,526,802✔
125
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
25,004,122✔
126
               settings::create_fission_neutrons) {
127
      create_fission_sites(p, i_nuclide, rx);
558,074✔
128

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

141
  // Create secondary photons
142
  if (settings::photon_transport) {
999,577,530✔
143
    sample_secondary_photons(p, i_nuclide);
8,382,044✔
144
  }
145

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

149
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
999,577,530✔
150
    absorption(p, i_nuclide);
999,574,010✔
151
  }
152
  if (!p.alive())
999,577,530✔
153
    return;
154

155
  // Sample a scattering reaction and determine the secondary energy of the
156
  // exiting neutron
157
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
976,148,297✔
158
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
976,148,297!
159
    ncrystal_mat.scatter(p);
158,829✔
160
  } else {
161
    scatter(p, i_nuclide);
975,989,468✔
162
  }
163

164
  // Advance URR seed stream 'N' times after energy changes
165
  if (p.E() != p.E_last()) {
976,148,297✔
166
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
975,829,066✔
167
  }
168

169
  // Play russian roulette if there are no weight windows
170
  if (!settings::weight_windows_on)
976,148,297✔
171
    apply_russian_roulette(p);
912,547,315✔
172
}
173

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

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

185
  // Sample the number of neutrons produced
186
  int nu = static_cast<int>(nu_t);
101,084,876✔
187
  if (prn(p.current_seed()) <= (nu_t - nu))
101,084,876✔
188
    ++nu;
20,684,891✔
189

190
  // If no neutrons were produced then don't continue
191
  if (nu == 0)
101,084,876✔
192
    return;
76,385,369✔
193

194
  // Initialize the counter of delayed neutrons encountered for each delayed
195
  // group.
196
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
24,699,507✔
197

198
  // Clear out particle's nu fission bank
199
  p.nu_bank().clear();
24,699,507✔
200

201
  p.fission() = true;
24,699,507✔
202

203
  // Determine whether to place fission sites into the shared fission bank
204
  // or the secondary particle bank.
205
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
24,699,507✔
206

207
  // Counter for the number of fission sites successfully stored to the shared
208
  // fission bank or the secondary particle bank
209
  int n_sites_stored;
24,699,507✔
210

211
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
55,213,463✔
212
    // Initialize fission site object with particle data
213
    SourceSite site;
30,513,956✔
214
    site.r = p.r();
30,513,956✔
215
    site.particle = ParticleType::neutron();
30,513,956✔
216
    site.time = p.time();
30,513,956✔
217
    site.wgt = 1. / weight;
30,513,956✔
218
    site.surf_id = 0;
30,513,956✔
219

220
    // Sample delayed group and angle/energy for fission reaction
221
    sample_fission_neutron(i_nuclide, rx, &site, p);
30,513,956✔
222

223
    // Reject site if it exceeds time cutoff
224
    if (site.delayed_group > 0) {
30,513,956✔
225
      double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
191,523!
226
      if (site.time > t_cutoff) {
191,523!
227
        continue;
×
228
      }
229
    }
230

231
    // Set parent and progeny IDs
232
    site.parent_id = p.id();
30,513,956✔
233
    site.progeny_id = p.n_progeny()++;
30,513,956✔
234

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

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

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

261
    // Increment the number of neutrons born delayed
262
    if (site.delayed_group > 0) {
30,513,956✔
263
      nu_d[site.delayed_group - 1]++;
191,523✔
264
    }
265

266
    // Write fission particles to nuBank
267
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
30,513,956✔
268
    nu_bank_entry.wgt = site.wgt;
30,513,956✔
269
    nu_bank_entry.E = site.E;
30,513,956✔
270
    nu_bank_entry.delayed_group = site.delayed_group;
30,513,956✔
271
  }
272

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

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

284
  // Store the total weight banked for analog fission tallies
285
  p.n_bank() = nu;
24,699,507✔
286
  p.wgt_bank() = nu / weight;
24,699,507✔
287
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
222,295,563✔
288
    p.n_delayed_bank(d) = nu_d[d];
197,596,056✔
289
  }
290
}
291

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

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

309
  // Calculate photon energy over electron rest mass equivalent
310
  double alpha = p.E() / MASS_ELECTRON_EV;
17,498,207✔
311

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

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

327
  // Incoherent (Compton) scattering
328
  prob += micro.incoherent;
16,532,890✔
329
  if (prob > cutoff) {
16,532,890✔
330
    double alpha_out;
11,317,805✔
331
    int i_shell;
11,317,805✔
332
    element.compton_scatter(
11,317,805✔
333
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
11,317,805✔
334

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

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

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

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

371
  // Photoelectric effect
372
  double prob_after = prob + micro.photoelectric;
5,215,085✔
373

374
  if (prob_after > cutoff) {
5,215,085✔
375
    // Get grid index, interpolation factor, and bounding subshell
376
    // cross sections
377
    int i_grid = micro.index_grid;
5,127,348✔
378
    double f = micro.interp_factor;
5,127,348✔
379
    tensor::View<const double> xs_lower = element.cross_sections_.slice(i_grid);
5,127,348✔
380
    tensor::View<const double> xs_upper =
5,127,348✔
381
      element.cross_sections_.slice(i_grid + 1);
5,127,348✔
382

383
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
24,329,040!
384
      const auto& shell {element.shells_[i_shell]};
24,329,040✔
385

386
      // Check threshold of reaction
387
      if (xs_lower(i_shell) == 0)
24,329,040✔
388
        continue;
10,266,535✔
389

390
      //  Evaluation subshell photoionization cross section
391
      prob += std::exp(
14,062,505✔
392
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
14,062,505✔
393

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

401
        // Determine energy of secondary electron
402
        double E_electron = p.E() - binding_energy;
5,127,348✔
403

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

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

426
        // Create secondary electron
427
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
5,127,348✔
428

429
        // Allow electrons to fill orbital and produce auger electrons
430
        // and fluorescent photons
431
        element.atomic_relaxation(i_shell, p);
5,127,348✔
432
        p.event() = TallyEvent::ABSORB;
5,127,348✔
433
        p.event_mt() = 533 + shell.index_subshell;
5,127,348✔
434
        p.wgt() = 0.0;
5,127,348✔
435
        p.E() = 0.0;
5,127,348✔
436
        return;
5,127,348✔
437
      }
438
    }
439
  }
10,254,696✔
440
  prob = prob_after;
87,737✔
441

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

450
    // Create secondary electron
451
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
87,737✔
452
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron());
87,737✔
453

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

464
void sample_electron_reaction(Particle& p)
53,829,276✔
465
{
466
  // TODO: create reaction types
467

468
  if (settings::electron_treatment == ElectronTreatment::TTB) {
53,829,276✔
469
    double E_lost;
53,525,621✔
470
    thick_target_bremsstrahlung(p, &E_lost);
53,525,621✔
471
  }
472

473
  p.E() = 0.0;
53,829,276✔
474
  p.wgt() = 0.0;
53,829,276✔
475
  p.event() = TallyEvent::ABSORB;
53,829,276✔
476
}
53,829,276✔
477

478
void sample_positron_reaction(Particle& p)
87,737✔
479
{
480
  // TODO: create reaction types
481

482
  if (settings::electron_treatment == ElectronTreatment::TTB) {
87,737✔
483
    double E_lost;
86,472✔
484
    thick_target_bremsstrahlung(p, &E_lost);
86,472✔
485
  }
486

487
  // Sample angle isotropically
488
  Direction u = isotropic_direction(p.current_seed());
87,737✔
489

490
  // Create annihilation photon pair traveling in opposite directions
491
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
87,737✔
492
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
87,737✔
493

494
  p.E() = 0.0;
87,737✔
495
  p.wgt() = 0.0;
87,737✔
496
  p.event() = TallyEvent::ABSORB;
87,737✔
497
}
87,737✔
498

499
int sample_nuclide(Particle& p)
999,577,530✔
500
{
501
  // Sample cumulative distribution function
502
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
999,577,530✔
503

504
  // Get pointers to nuclide/density arrays
505
  const auto& mat {model::materials[p.material()]};
999,577,530✔
506
  int n = mat->nuclide_.size();
999,577,530✔
507

508
  double prob = 0.0;
999,577,530✔
509
  for (int i = 0; i < n; ++i) {
2,123,846,658!
510
    // Get atom density
511
    int i_nuclide = mat->nuclide_[i];
2,123,846,658✔
512
    double atom_density = mat->atom_density(i, p.density_mult());
2,123,846,658✔
513

514
    // Increment probability to compare to cutoff
515
    prob += atom_density * p.neutron_xs(i_nuclide).total;
2,123,846,658✔
516
    if (prob >= cutoff)
2,123,846,658✔
517
      return i_nuclide;
999,577,530✔
518
  }
519

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

525
int sample_element(Particle& p)
17,498,207✔
526
{
527
  // Sample cumulative distribution function
528
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
17,498,207✔
529

530
  // Get pointers to elements, densities
531
  const auto& mat {model::materials[p.material()]};
17,498,207✔
532

533
  double prob = 0.0;
17,498,207✔
534
  for (int i = 0; i < mat->element_.size(); ++i) {
41,419,207!
535
    // Find atom density
536
    int i_element = mat->element_[i];
41,419,207✔
537
    double atom_density = mat->atom_density(i, p.density_mult());
41,419,207✔
538

539
    // Determine microscopic cross section
540
    double sigma = atom_density * p.photon_xs(i_element).total;
41,419,207✔
541

542
    // Increment probability to compare to cutoff
543
    prob += sigma;
41,419,207✔
544
    if (prob > cutoff) {
41,419,207✔
545
      // Save which nuclide particle had collision with for tally purpose
546
      p.event_nuclide() = mat->nuclide_[i];
17,498,207✔
547

548
      return i_element;
17,498,207✔
549
    }
550
  }
551

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

557
Reaction& sample_fission(int i_nuclide, Particle& p)
125,530,924✔
558
{
559
  // Get pointer to nuclide
560
  const auto& nuc {data::nuclides[i_nuclide]};
125,530,924✔
561

562
  // If we're in the URR, by default use the first fission reaction. We also
563
  // default to the first reaction if we know that there are no partial fission
564
  // reactions
565
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
125,530,924✔
566
    return *nuc->fission_rx_[0];
125,500,291✔
567
  }
568

569
  // Check to see if we are in a windowed multipole range.  WMP only supports
570
  // the first fission reaction.
571
  if (nuc->multipole_) {
30,633✔
572
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
2,849!
573
      return *nuc->fission_rx_[0];
1,991✔
574
    }
575
  }
576

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

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

587
    // Create fission bank sites if fission occurs
588
    if (prob > cutoff)
28,702✔
589
      return *rx;
28,642✔
590
  }
591

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

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

605
  // Loop through each reaction type
606
  const auto& nuc {data::nuclides[i_nuclide]};
1,326,754✔
607
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
21,735,494!
608
    // Evaluate neutron cross section
609
    const auto& rx = nuc->reactions_[i];
21,735,494✔
610
    double xs = rx->xs(micro);
21,735,494✔
611

612
    // if cross section is zero for this reaction, skip it
613
    if (xs == 0.0)
21,735,494✔
614
      continue;
6,746,410✔
615

616
    for (int j = 0; j < rx->products_.size(); ++j) {
69,344,330✔
617
      if (rx->products_[j].particle_.is_photon()) {
55,682,000✔
618
        // For fission, artificially increase the photon yield to account
619
        // for delayed photons
620
        double f = 1.0;
43,099,089✔
621
        if (settings::delayed_photon_scaling) {
43,099,089!
622
          if (is_fission(rx->mt_)) {
43,099,089✔
623
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
540,221!
624
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
540,221✔
625
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
540,221✔
626
              f = (energy_prompt + energy_delayed) / (energy_prompt);
540,221✔
627
            }
628
          }
629
        }
630

631
        // add to cumulative probability
632
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
43,099,089✔
633

634
        *i_rx = i;
43,099,089✔
635
        *i_product = j;
43,099,089✔
636
        if (prob > cutoff)
43,099,089✔
637
          return;
638
      }
639
    }
640
  }
641
}
642

643
void absorption(Particle& p, int i_nuclide)
999,574,010✔
644
{
645
  if (settings::survival_biasing) {
999,574,010✔
646
    // Determine weight absorbed in survival biasing
647
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
5,744,970✔
648
                              p.neutron_xs(i_nuclide).total;
5,744,970✔
649

650
    // Adjust weight of particle by probability of absorption
651
    p.wgt() -= wgt_absorb;
5,744,970✔
652

653
    // Score implicit absorption estimate of keff
654
    if (settings::run_mode == RunMode::EIGENVALUE) {
5,744,970✔
655
      p.keff_tally_absorption() += wgt_absorb *
499,950✔
656
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950✔
657
                                   p.neutron_xs(i_nuclide).absorption;
499,950✔
658
    }
659
  } else {
660
    // See if disappearance reaction happens
661
    if (p.neutron_xs(i_nuclide).absorption >
993,829,040✔
662
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
993,829,040✔
663
      // Score absorption estimate of keff
664
      if (settings::run_mode == RunMode::EIGENVALUE) {
23,428,276✔
665
        p.keff_tally_absorption() += p.wgt() *
17,742,565✔
666
                                     p.neutron_xs(i_nuclide).nu_fission /
17,742,565✔
667
                                     p.neutron_xs(i_nuclide).absorption;
17,742,565✔
668
      }
669

670
      p.wgt() = 0.0;
23,428,276✔
671
      p.event() = TallyEvent::ABSORB;
23,428,276✔
672
      if (!p.fission()) {
23,428,276✔
673
        p.event_mt() = N_DISAPPEAR;
14,646,408✔
674
      }
675
    }
676
  }
677
}
999,574,010✔
678

679
void scatter(Particle& p, int i_nuclide)
975,989,468✔
680
{
681
  // copy incoming direction
682
  Direction u_old {p.u()};
975,989,468✔
683

684
  // Get pointer to nuclide and grid index/interpolation factor
685
  const auto& nuc {data::nuclides[i_nuclide]};
975,989,468✔
686
  const auto& micro {p.neutron_xs(i_nuclide)};
975,989,468✔
687
  int i_temp = micro.index_temp;
975,989,468✔
688

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

694
  // Calculate elastic cross section if it wasn't precalculated
695
  if (micro.elastic == CACHE_INVALID) {
975,989,468✔
696
    nuc->calculate_elastic_xs(p);
728,908,647✔
697
  }
698

699
  double prob = micro.elastic - micro.thermal;
975,989,468✔
700
  if (prob > cutoff) {
975,989,468✔
701
    // =======================================================================
702
    // NON-S(A,B) ELASTIC SCATTERING
703

704
    // Determine temperature
705
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
829,783,049✔
706

707
    // Perform collision physics for elastic scattering
708
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
829,783,049✔
709

710
    p.event_mt() = ELASTIC;
829,783,049✔
711
    sampled = true;
829,783,049✔
712
  }
713

714
  prob = micro.elastic;
975,989,468✔
715
  if (prob > cutoff && !sampled) {
975,989,468✔
716
    // =======================================================================
717
    // S(A,B) SCATTERING
718

719
    sab_scatter(i_nuclide, micro.index_sab, p);
127,478,075✔
720

721
    p.event_mt() = ELASTIC;
127,478,075✔
722
    sampled = true;
127,478,075✔
723
  }
724

725
  if (!sampled) {
975,989,468✔
726
    // =======================================================================
727
    // INELASTIC SCATTERING
728

729
    int n = nuc->index_inelastic_scatter_.size();
18,728,344✔
730
    int i = 0;
18,728,344✔
731
    for (int j = 0; j < n && prob < cutoff; ++j) {
352,544,903✔
732
      i = nuc->index_inelastic_scatter_[j];
333,816,559✔
733

734
      // add to cumulative probability
735
      prob += nuc->reactions_[i]->xs(micro);
333,816,559✔
736
    }
737

738
    // Perform collision physics for inelastic scattering
739
    const auto& rx {nuc->reactions_[i]};
18,728,344✔
740
    inelastic_scatter(i_nuclide, *rx, p);
18,728,344✔
741
    p.event_mt() = rx->mt_;
18,728,344✔
742
  }
743

744
  // Set event component
745
  p.event() = TallyEvent::SCATTER;
975,989,468✔
746

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

759
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
829,783,049✔
760
{
761
  // get pointer to nuclide
762
  const auto& nuc {data::nuclides[i_nuclide]};
829,783,049✔
763

764
  double vel = std::sqrt(p.E());
829,783,049✔
765
  double awr = nuc->awr_;
829,783,049✔
766

767
  // Neutron velocity in LAB
768
  Direction v_n = vel * p.u();
829,783,049✔
769

770
  // Sample velocity of target nucleus
771
  Direction v_t {};
829,783,049✔
772
  if (!p.neutron_xs(i_nuclide).use_ptable) {
829,783,049✔
773
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
790,439,028✔
774
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
790,439,028✔
775
  }
776

777
  // Velocity of center-of-mass
778
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
829,783,049✔
779

780
  // Transform to CM frame
781
  v_n -= v_cm;
829,783,049✔
782

783
  // Find speed of neutron in CM
784
  vel = v_n.norm();
829,783,049✔
785

786
  if (!model::active_point_tallies.empty()) {
829,783,049!
NEW
787
    score_point_tally(p, i_nuclide, rx, 0, &v_t);
×
788
  }
789

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

801
  // Determine direction cosines in CM
802
  Direction u_cm = v_n / vel;
829,783,049✔
803

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

809
  // Transform back to LAB frame
810
  v_n += v_cm;
829,783,049✔
811

812
  p.E() = v_n.dot(v_n);
829,783,049✔
813
  vel = std::sqrt(p.E());
829,783,049✔
814

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

819
  // Set energy and direction of particle in LAB frame
820
  p.u() = v_n / vel;
829,783,049!
821

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

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

835
  // Sample energy and angle
836
  double E_out;
127,478,075✔
837
  auto& sab = data::thermal_scatt[i_sab]->data_[i_temp];
127,478,075✔
838
  sab.sample(micro, p.E(), &E_out, &p.mu(), p.current_seed());
127,478,075✔
839

840
  if (!model::active_point_tallies.empty()) {
127,478,075!
NEW
841
    score_point_tally(p, i_nuclide, sab, micro);
×
842
  }
843

844
  // Set energy to outgoing, change direction of particle
845
  p.E() = E_out;
127,478,075✔
846
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
127,478,075✔
847
}
127,478,075✔
848

849
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
790,439,028✔
850
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
851
{
852
  // check if nuclide is a resonant scatterer
853
  ResScatMethod sampling_method;
790,439,028✔
854
  if (nuc.resonant_) {
790,439,028✔
855

856
    // sampling method to use
857
    sampling_method = settings::res_scat_method;
84,557✔
858

859
    // upper resonance scattering energy bound (target is at rest above this E)
860
    if (E > settings::res_scat_energy_max) {
84,557✔
861
      return {};
40,755✔
862

863
      // lower resonance scattering energy bound (should be no resonances below)
864
    } else if (E < settings::res_scat_energy_min) {
43,802✔
865
      sampling_method = ResScatMethod::cxs;
866
    }
867

868
    // otherwise, use free gas model
869
  } else {
870
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
790,354,471✔
871
      return {};
406,187,448✔
872
    } else {
873
      sampling_method = ResScatMethod::cxs;
874
    }
875
  }
876

877
  // use appropriate target velocity sampling method
878
  switch (sampling_method) {
18,810!
879
  case ResScatMethod::cxs:
384,192,015✔
880

881
    // sample target velocity with the constant cross section (cxs) approx.
882
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
384,192,015✔
883

884
  case ResScatMethod::dbrc:
18,810✔
885
  case ResScatMethod::rvs: {
18,810✔
886
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810✔
887
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
37,620!
888
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810✔
889

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

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

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

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

930
      // get max 0K xs value over range of practical relative energies
931
      double xs_max = *std::max_element(
×
932
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
933
      xs_max = std::max({xs_low, xs_max, xs_up});
×
934

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

948
        // perform Doppler broadening rejection correction (dbrc)
949
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
950
        double R = xs_0K / xs_max;
×
951
        if (prn(seed) < R)
×
952
          return v_target;
×
953
      }
954

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

965
      // cdf value at upper bound attainable energy
966
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,532✔
967
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,532✔
968
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,532✔
969

970
      while (true) {
325,908✔
971
        // directly sample Maxwellian
972
        double E_t = -kT * std::log(prn(seed));
170,720✔
973

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

985
        // perform rejection sampling on cosine between
986
        // neutron and target velocities
987
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
170,720✔
988
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
170,720✔
989

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

1000
  UNREACHABLE();
×
1001
}
1002

1003
Direction sample_cxs_target_velocity(
384,195,293✔
1004
  double awr, double E, Direction u, double kT, uint64_t* seed)
1005
{
1006
  double beta_vn = std::sqrt(awr * E / kT);
384,195,293✔
1007
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
384,195,293✔
1008

1009
  double beta_vt_sq;
448,177,685✔
1010
  double mu;
448,177,685✔
1011
  while (true) {
448,177,685✔
1012
    // Sample two random numbers
1013
    double r1 = prn(seed);
448,177,685✔
1014
    double r2 = prn(seed);
448,177,685✔
1015

1016
    if (prn(seed) < alpha) {
448,177,685✔
1017
      // With probability alpha, we sample the distribution p(y) =
1018
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1019
      // Carlo sampler
1020

1021
      beta_vt_sq = -std::log(r1 * r2);
103,295,574✔
1022

1023
    } else {
1024
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1025
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1026
      // Carlo sampler
1027

1028
      double c = std::cos(PI / 2.0 * prn(seed));
344,882,111✔
1029
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
344,882,111✔
1030
    }
1031

1032
    // Determine beta * vt
1033
    double beta_vt = std::sqrt(beta_vt_sq);
448,177,685✔
1034

1035
    // Sample cosine of angle between neutron and target velocity
1036
    mu = uniform_distribution(-1., 1., seed);
448,177,685✔
1037

1038
    // Determine rejection probability
1039
    double accept_prob =
448,177,685✔
1040
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
448,177,685✔
1041
      (beta_vn + beta_vt);
448,177,685✔
1042

1043
    // Perform rejection sampling on vt and mu
1044
    if (prn(seed) < accept_prob)
448,177,685✔
1045
      break;
1046
  }
1047

1048
  // Determine speed of target nucleus
1049
  double vt = std::sqrt(beta_vt_sq * kT / awr);
384,195,293✔
1050

1051
  // Determine velocity vector of target nucleus based on neutron's velocity
1052
  // and the sampled angle between them
1053
  return vt * rotate_angle(u, mu, nullptr, seed);
384,195,293✔
1054
}
1055

1056
void sample_fission_neutron(
30,513,956✔
1057
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1058
{
1059
  // Get attributes of particle
1060
  double E_in = p.E();
30,513,956✔
1061
  uint64_t* seed = p.current_seed();
30,513,956✔
1062

1063
  // Determine total nu, delayed nu, and delayed neutron fraction
1064
  const auto& nuc {data::nuclides[i_nuclide]};
30,513,956✔
1065
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
30,513,956✔
1066
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
30,513,956✔
1067
  double beta = nu_d / nu_t;
30,513,956✔
1068

1069
  if (prn(seed) < beta) {
30,513,956✔
1070
    // ====================================================================
1071
    // DELAYED NEUTRON SAMPLED
1072

1073
    // sampled delayed precursor group
1074
    double xi = prn(seed) * nu_d;
191,523✔
1075
    double prob = 0.0;
191,523✔
1076
    int group;
191,523✔
1077
    for (group = 1; group < nuc->n_precursor_; ++group) {
713,294✔
1078
      // determine delayed neutron precursor yield for group j
1079
      double yield = (*rx.products_[group].yield_)(E_in);
699,748✔
1080

1081
      // Check if this group is sampled
1082
      prob += yield;
699,748✔
1083
      if (xi < prob)
699,748✔
1084
        break;
1085
    }
1086

1087
    // if the sum of the probabilities is slightly less than one and the
1088
    // random number is greater, j will be greater than nuc %
1089
    // n_precursor -- check for this condition
1090
    group = std::min(group, nuc->n_precursor_);
191,523!
1091

1092
    // set the delayed group for the particle born from fission
1093
    site->delayed_group = group;
191,523✔
1094

1095
    // Sample time of emission based on decay constant of precursor
1096
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
191,523✔
1097
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
191,523✔
1098

1099
  } else {
1100
    // ====================================================================
1101
    // PROMPT NEUTRON SAMPLED
1102

1103
    // set the delayed group for the particle born from fission to 0
1104
    site->delayed_group = 0;
30,322,433✔
1105
  }
1106

1107
  if (!model::active_point_tallies.empty()) {
30,513,956!
NEW
1108
    score_point_tally(p, i_nuclide, rx, site->delayed_group, nullptr);
×
1109
  }
1110

1111
  // sample from prompt neutron energy distribution
1112
  int n_sample = 0;
1113
  double mu;
30,513,956✔
1114
  while (true) {
30,513,956✔
1115
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
30,513,956✔
1116

1117
    // resample if energy is greater than maximum neutron energy
1118
    int neutron = ParticleType::neutron().transport_index();
30,513,956✔
1119
    if (site->E < data::energy_max[neutron])
30,513,956!
1120
      break;
1121

1122
    // check for large number of resamples
1123
    ++n_sample;
×
1124
    if (n_sample == MAX_SAMPLE) {
×
1125
      // particle_write_restart(p)
1126
      fatal_error("Resampled energy distribution maximum number of times "
×
1127
                  "for nuclide " +
×
1128
                  nuc->name_);
×
1129
    }
1130
  }
1131

1132
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1133
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
30,513,956✔
1134
}
30,513,956✔
1135

1136
void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p)
18,728,344✔
1137
{
1138
  // Get pointer to nuclide
1139
  const auto& nuc {data::nuclides[i_nuclide]};
18,728,344✔
1140

1141
  // copy energy of neutron
1142
  double E_in = p.E();
18,728,344✔
1143

1144
  // sample outgoing energy and scattering cosine
1145
  double E;
18,728,344✔
1146
  double mu;
18,728,344✔
1147
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
18,728,344✔
1148

1149
  if (!model::active_point_tallies.empty()) {
18,728,344!
NEW
1150
    score_point_tally(p, i_nuclide, rx, 0, nullptr);
×
1151
  }
1152

1153
  // if scattering system is in center-of-mass, transfer cosine of scattering
1154
  // angle and outgoing energy from CM to LAB
1155
  if (rx.scatter_in_cm_) {
18,728,344✔
1156
    double E_cm = E;
18,673,040✔
1157

1158
    // determine outgoing energy in lab
1159
    double A = nuc->awr_;
18,673,040✔
1160
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
18,673,040✔
1161
                 ((A + 1.0) * (A + 1.0));
18,673,040✔
1162

1163
    // determine outgoing angle in lab
1164
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
18,673,040✔
1165
  }
1166

1167
  // Because of floating-point roundoff, it may be possible for mu to be
1168
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1169
  // or 1
1170
  if (std::abs(mu) > 1.0)
18,728,344!
1171
    mu = std::copysign(1.0, mu);
×
1172

1173
  // Set outgoing energy and scattering angle
1174
  p.E() = E;
18,728,344✔
1175
  p.mu() = mu;
18,728,344✔
1176

1177
  // change direction of particle
1178
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
18,728,344✔
1179

1180
  // evaluate yield
1181
  double yield = (*rx.products_[0].yield_)(E_in);
18,728,344✔
1182
  if (std::floor(yield) == yield && yield > 0) {
18,728,344!
1183
    // If yield is integral, create exactly that many secondary particles
1184
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
18,837,364✔
1185
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
109,074✔
1186
    }
1187
  } else {
1188
    // Otherwise, change weight of particle based on yield
1189
    p.wgt() *= yield;
54✔
1190
  }
1191
}
18,728,344✔
1192

1193
void sample_secondary_photons(Particle& p, int i_nuclide)
8,382,044✔
1194
{
1195
  // Sample the number of photons produced
1196
  double y_t =
8,382,044✔
1197
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,382,044✔
1198
  double photon_wgt = p.wgt();
8,382,044✔
1199
  int y = 1;
8,382,044✔
1200

1201
  if (settings::use_decay_photons) {
8,382,044✔
1202
    // For decay photons, sample a single photon and modify the weight
1203
    if (y_t <= 0.0)
72,006✔
1204
      return;
1205
    photon_wgt *= y_t;
54,725✔
1206
  } else {
1207
    // For prompt photons, sample an integral number of photons with weight
1208
    // equal to the neutron's weight
1209
    y = static_cast<int>(y_t);
8,310,038✔
1210
    if (prn(p.current_seed()) <= y_t - y)
8,310,038✔
1211
      ++y;
560,857✔
1212
  }
1213

1214
  // Sample each secondary photon
1215
  for (int i = 0; i < y; ++i) {
9,691,517✔
1216
    // Sample the reaction and product
1217
    int i_rx;
1,326,754✔
1218
    int i_product;
1,326,754✔
1219
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,326,754✔
1220

1221
    // Sample the outgoing energy and angle
1222
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,326,754✔
1223
    double E;
1,326,754✔
1224
    double mu;
1,326,754✔
1225
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,326,754✔
1226

1227
    // Sample the new direction
1228
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,326,754✔
1229

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

1240
    // Create the secondary photon
1241
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
1,326,754✔
1242

1243
    // Tag secondary particle with parent nuclide
1244
    if (created_photon && settings::use_decay_photons) {
1,326,754✔
1245
      p.secondary_bank().back().parent_nuclide =
52,844✔
1246
        rx->products_[i_product].parent_nuclide_;
52,844✔
1247
    }
1248
  }
1249
}
1250

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