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

openmc-dev / openmc / 23285088002

19 Mar 2026 07:51AM UTC coverage: 80.71% (-0.7%) from 81.447%
23285088002

Pull #3886

github

web-flow
Merge 9cdb2d588 into 3ce6cbfdd
Pull Request #3886: Implement python tally types

16322 of 23494 branches covered (69.47%)

Branch coverage included in aggregate %.

215 of 264 new or added lines in 11 files covered. (81.44%)

713 existing lines in 49 files now uncovered.

56449 of 66670 relevant lines covered (84.67%)

14035992.85 hits per line

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

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

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

31
#include <fmt/core.h>
32

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

37
namespace openmc {
38

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

43
void collision(Particle& p)
194,093,895✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
194,093,895✔
47
  p.secondary_bank_index() = p.secondary_bank().size();
194,093,895!
48

49
  // Sample reaction for the material the particle is in
50
  switch (p.type().pdg_number()) {
194,093,895!
51
  case PDG_NEUTRON:
181,014,952✔
52
    sample_neutron_reaction(p);
181,014,952✔
53
    break;
181,014,952✔
54
  case PDG_PHOTON:
3,229,042✔
55
    sample_photon_reaction(p);
3,229,042✔
56
    break;
3,229,042✔
57
  case PDG_ELECTRON:
9,833,949✔
58
    sample_electron_reaction(p);
9,833,949✔
59
    break;
9,833,949✔
60
  case PDG_POSITRON:
15,952✔
61
    sample_positron_reaction(p);
15,952✔
62
    break;
15,952✔
63
  default:
×
64
    fatal_error("Unsupported particle PDG for collision sampling.");
×
65
  }
66

67
  if (settings::weight_windows_on) {
194,093,895✔
68
    auto [ww_found, ww] = search_weight_window(p);
15,230,516✔
69
    if (!ww_found && p.type() == ParticleType::neutron()) {
15,230,516✔
70
      // if the weight window is not valid, apply russian roulette for neutrons
71
      // (regardless of weight window collision checkpoint setting)
72
      apply_russian_roulette(p);
9,744✔
73
    } else if (settings::weight_window_checkpoint_collision) {
15,220,772!
74
      // if collision checkpointing is on, apply weight window
75
      apply_weight_window(p, ww);
15,220,772✔
76
    }
77
  }
78

79
  // Kill particle if energy falls below cutoff
80
  int type = p.type().transport_index();
194,093,895!
81
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
194,093,895!
82
    p.wgt() = 0.0;
970,575✔
83
  }
84

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

105
void sample_neutron_reaction(Particle& p)
181,014,952✔
106
{
107
  // Sample a nuclide within the material
108
  int i_nuclide = sample_nuclide(p);
181,014,952✔
109

110
  // Save which nuclide particle had collision with
111
  p.event_nuclide() = i_nuclide;
181,014,952✔
112

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

118
  const auto& nuc {data::nuclides[i_nuclide]};
181,014,952✔
119

120
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
181,014,952✔
121
    auto& rx = sample_fission(i_nuclide, p);
22,832,252✔
122
    if (settings::run_mode == RunMode::EIGENVALUE) {
22,832,252✔
123
      create_fission_sites(p, i_nuclide, rx);
18,326,024✔
124
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
4,506,228✔
125
               settings::create_fission_neutrons) {
126
      create_fission_sites(p, i_nuclide, rx);
61,492✔
127

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

140
  // Create secondary photons
141
  if (settings::photon_transport) {
181,014,952✔
142
    sample_secondary_photons(p, i_nuclide);
1,524,008✔
143
  }
144

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

148
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
181,014,952✔
149
    absorption(p, i_nuclide);
181,014,312✔
150
  }
151
  if (!p.alive())
181,014,952✔
152
    return;
153

154
  // Sample a scattering reaction and determine the secondary energy of the
155
  // exiting neutron
156
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
176,764,099✔
157
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
176,764,099!
158
    ncrystal_mat.scatter(p);
28,878✔
159
  } else {
160
    scatter(p, i_nuclide);
176,735,221✔
161
  }
162

163
  // Advance URR seed stream 'N' times after energy changes
164
  if (p.E() != p.E_last()) {
176,764,099✔
165
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
176,706,057✔
166
  }
167

168
  // Play russian roulette if there are no weight windows
169
  if (!settings::weight_windows_on)
176,764,099✔
170
    apply_russian_roulette(p);
165,235,617✔
171
}
172

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

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

184
  // Sample the number of neutrons produced
185
  int nu = static_cast<int>(nu_t);
18,387,516✔
186
  if (prn(p.current_seed()) <= (nu_t - nu))
18,387,516✔
187
    ++nu;
3,760,988✔
188

189
  // If no neutrons were produced then don't continue
190
  if (nu == 0)
18,387,516✔
191
    return;
13,902,681✔
192

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

197
  // Clear out particle's nu fission bank
198
  p.nu_bank().clear();
4,484,835✔
199

200
  p.fission() = true;
4,484,835✔
201

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

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

210
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
10,017,815✔
211
    // Initialize fission site object with particle data
212
    SourceSite site;
5,532,980✔
213
    site.r = p.r();
5,532,980✔
214
    site.particle = ParticleType::neutron();
5,532,980✔
215
    site.time = p.time();
5,532,980✔
216
    site.wgt = 1. / weight;
5,532,980✔
217
    site.surf_id = 0;
5,532,980✔
218

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

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

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

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

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

248
        // Break out of loop as no more sites can be added to fission bank
249
        break;
×
250
      }
251
      // Iterated Fission Probability (IFP) method
252
      if (settings::ifp_on) {
5,532,916✔
253
        ifp(p, idx);
245,932✔
254
      }
255
    } else {
256
      p.secondary_bank().push_back(site);
64✔
257
      p.n_secondaries()++;
64✔
258
    }
259

260
    // Increment the number of neutrons born delayed
261
    if (site.delayed_group > 0) {
5,532,980✔
262
      nu_d[site.delayed_group - 1]++;
34,796✔
263
    }
264

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

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

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

283
  // Store the total weight banked for analog fission tallies
284
  p.n_bank() = nu;
4,484,835✔
285
  p.wgt_bank() = nu / weight;
4,484,835✔
286
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
40,363,515✔
287
    p.n_delayed_bank(d) = nu_d[d];
35,878,680✔
288
  }
289
}
290

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

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

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

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

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

326
  // Incoherent (Compton) scattering
327
  prob += micro.incoherent;
3,051,072✔
328
  if (prob > cutoff) {
3,051,072✔
329
    double alpha_out;
2,082,483✔
330
    int i_shell;
2,082,483✔
331
    element.compton_scatter(
2,082,483✔
332
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
2,082,483✔
333

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

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

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

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

371
  // Photoelectric effect
372
  double prob_after = prob + micro.photoelectric;
968,589✔
373

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

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

386
      // Check threshold of reaction
387
      if (xs_lower(i_shell) == 0)
4,453,070✔
388
        continue;
1,866,543✔
389

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

394
      if (prob > cutoff) {
2,586,527✔
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_
952,637✔
398
                                  ? shell.binding_energy
952,637!
399
                                  : element.binding_energy_[i_shell];
×
400

401
        // Determine energy of secondary electron
402
        double E_electron = p.E() - binding_energy;
952,637✔
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;
1,429,073✔
408
        while (true) {
1,429,073✔
409
          double r = prn(p.current_seed());
1,429,073✔
410
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
1,429,073✔
411
            double rel_vel =
952,637✔
412
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
952,637✔
413
              (E_electron + MASS_ELECTRON_EV);
952,637✔
414
            mu =
952,637✔
415
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
952,637✔
416
            break;
952,637✔
417
          }
418
        }
419

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

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

429
        // Allow electrons to fill orbital and produce auger electrons
430
        // and fluorescent photons
431
        if (settings::atomic_relaxation) {
952,637✔
432
          element.atomic_relaxation(i_shell, p);
932,637✔
433
        }
434
        p.event() = TallyEvent::ABSORB;
952,637✔
435
        p.event_mt() = 533 + shell.index_subshell;
952,637✔
436
        p.wgt() = 0.0;
952,637✔
437
        p.E() = 0.0;
952,637✔
438
        return;
952,637✔
439
      }
440
    }
441
  }
1,905,274✔
442
  prob = prob_after;
15,952✔
443

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

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

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

466
void sample_electron_reaction(Particle& p)
9,833,949✔
467
{
468
  // TODO: create reaction types
469

470
  if (settings::electron_treatment == ElectronTreatment::TTB) {
9,833,949✔
471
    double E_lost;
9,734,969✔
472
    thick_target_bremsstrahlung(p, &E_lost);
9,734,969✔
473
  }
474

475
  p.E() = 0.0;
9,833,949✔
476
  p.wgt() = 0.0;
9,833,949✔
477
  p.event() = TallyEvent::ABSORB;
9,833,949✔
478
}
9,833,949✔
479

480
void sample_positron_reaction(Particle& p)
15,952✔
481
{
482
  // TODO: create reaction types
483

484
  if (settings::electron_treatment == ElectronTreatment::TTB) {
15,952✔
485
    double E_lost;
15,722✔
486
    thick_target_bremsstrahlung(p, &E_lost);
15,722✔
487
  }
488

489
  // Sample angle isotropically
490
  Direction u = isotropic_direction(p.current_seed());
15,952✔
491

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

496
  p.E() = 0.0;
15,952✔
497
  p.wgt() = 0.0;
15,952✔
498
  p.event() = TallyEvent::ABSORB;
15,952✔
499
}
15,952✔
500

501
int sample_nuclide(Particle& p)
181,014,952✔
502
{
503
  // Sample cumulative distribution function
504
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
181,014,952✔
505

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

510
  double prob = 0.0;
181,014,952✔
511
  for (int i = 0; i < n; ++i) {
385,512,958!
512
    // Get atom density
513
    int i_nuclide = mat->nuclide_[i];
385,512,958✔
514
    double atom_density = mat->atom_density(i, p.density_mult());
385,512,958✔
515

516
    // Increment probability to compare to cutoff
517
    prob += atom_density * p.neutron_xs(i_nuclide).total;
385,512,958✔
518
    if (prob >= cutoff)
385,512,958✔
519
      return i_nuclide;
181,014,952✔
520
  }
521

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

527
int sample_element(Particle& p)
3,229,042✔
528
{
529
  // Sample cumulative distribution function
530
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
3,229,042✔
531

532
  // Get pointers to elements, densities
533
  const auto& mat {model::materials[p.material()]};
3,229,042✔
534

535
  double prob = 0.0;
3,229,042✔
536
  for (int i = 0; i < mat->element_.size(); ++i) {
7,579,402!
537
    // Find atom density
538
    int i_element = mat->element_[i];
7,579,402✔
539
    double atom_density = mat->atom_density(i, p.density_mult());
7,579,402✔
540

541
    // Determine microscopic cross section
542
    double sigma = atom_density * p.photon_xs(i_element).total;
7,579,402✔
543

544
    // Increment probability to compare to cutoff
545
    prob += sigma;
7,579,402✔
546
    if (prob > cutoff) {
7,579,402✔
547
      // Save which nuclide particle had collision with for tally purpose
548
      p.event_nuclide() = mat->nuclide_[i];
3,229,042✔
549

550
      return i_element;
3,229,042✔
551
    }
552
  }
553

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

559
Reaction& sample_fission(int i_nuclide, Particle& p)
22,832,252✔
560
{
561
  // Get pointer to nuclide
562
  const auto& nuc {data::nuclides[i_nuclide]};
22,832,252✔
563

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

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

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

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

589
    // Create fission bank sites if fission occurs
590
    if (prob > cutoff)
4,360✔
591
      return *rx;
4,354✔
592
  }
593

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

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

607
  // Loop through each reaction type
608
  const auto& nuc {data::nuclides[i_nuclide]};
241,228✔
609
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
3,951,908!
610
    // Evaluate neutron cross section
611
    const auto& rx = nuc->reactions_[i];
3,951,908✔
612
    double xs = rx->xs(micro);
3,951,908✔
613

614
    // if cross section is zero for this reaction, skip it
615
    if (xs == 0.0)
3,951,908✔
616
      continue;
1,226,620✔
617

618
    for (int j = 0; j < rx->products_.size(); ++j) {
12,608,060✔
619
      if (rx->products_[j].particle_.is_photon()) {
10,124,000✔
620
        // For fission, artificially increase the photon yield to account
621
        // for delayed photons
622
        double f = 1.0;
7,836,198✔
623
        if (settings::delayed_photon_scaling) {
7,836,198!
624
          if (is_fission(rx->mt_)) {
7,836,198✔
625
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
98,222!
626
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
98,222✔
627
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
98,222✔
628
              f = (energy_prompt + energy_delayed) / (energy_prompt);
98,222✔
629
            }
630
          }
631
        }
632

633
        // add to cumulative probability
634
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
7,836,198✔
635

636
        *i_rx = i;
7,836,198✔
637
        *i_product = j;
7,836,198✔
638
        if (prob > cutoff)
7,836,198✔
639
          return;
640
      }
641
    }
642
  }
643
}
644

645
void absorption(Particle& p, int i_nuclide)
181,014,312✔
646
{
647
  if (settings::survival_biasing) {
181,014,312✔
648
    // Determine weight absorbed in survival biasing
649
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
1,044,540✔
650
                              p.neutron_xs(i_nuclide).total;
1,044,540✔
651

652
    // Adjust weight of particle by probability of absorption
653
    p.wgt() -= wgt_absorb;
1,044,540✔
654

655
    // Score implicit absorption estimate of keff
656
    if (settings::run_mode == RunMode::EIGENVALUE) {
1,044,540✔
657
      p.keff_tally_absorption() += wgt_absorb *
90,900✔
658
                                   p.neutron_xs(i_nuclide).nu_fission /
90,900✔
659
                                   p.neutron_xs(i_nuclide).absorption;
90,900✔
660
    }
661
  } else {
662
    // See if disappearance reaction happens
663
    if (p.neutron_xs(i_nuclide).absorption >
179,969,772✔
664
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
179,969,772✔
665
      // Score absorption estimate of keff
666
      if (settings::run_mode == RunMode::EIGENVALUE) {
4,250,679✔
667
        p.keff_tally_absorption() += p.wgt() *
3,235,100✔
668
                                     p.neutron_xs(i_nuclide).nu_fission /
3,235,100✔
669
                                     p.neutron_xs(i_nuclide).absorption;
3,235,100✔
670
      }
671

672
      p.wgt() = 0.0;
4,250,679✔
673
      p.event() = TallyEvent::ABSORB;
4,250,679✔
674
      if (!p.fission()) {
4,250,679✔
675
        p.event_mt() = N_DISAPPEAR;
2,658,712✔
676
      }
677
    }
678
  }
679
}
181,014,312✔
680

681
void scatter(Particle& p, int i_nuclide)
176,735,221✔
682
{
683
  // copy incoming direction
684
  Direction u_old {p.u()};
176,735,221✔
685

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

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

696
  // Calculate elastic cross section if it wasn't precalculated
697
  if (micro.elastic == CACHE_INVALID) {
176,735,221✔
698
    nuc->calculate_elastic_xs(p);
131,812,675✔
699
  }
700

701
  double prob = micro.elastic - micro.thermal;
176,735,221✔
702
  if (prob > cutoff) {
176,735,221✔
703
    // =======================================================================
704
    // NON-S(A,B) ELASTIC SCATTERING
705

706
    // Determine temperature
707
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
150,146,204✔
708

709
    // Perform collision physics for elastic scattering
710
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
150,146,204✔
711

712
    p.event_mt() = ELASTIC;
150,146,204✔
713
    sampled = true;
150,146,204✔
714
  }
715

716
  prob = micro.elastic;
176,735,221✔
717
  if (prob > cutoff && !sampled) {
176,735,221✔
718
    // =======================================================================
719
    // S(A,B) SCATTERING
720

721
    sab_scatter(i_nuclide, micro.index_sab, p);
23,189,108✔
722

723
    p.event_mt() = ELASTIC;
23,189,108✔
724
    sampled = true;
23,189,108✔
725
  }
726

727
  if (!sampled) {
176,735,221✔
728
    // =======================================================================
729
    // INELASTIC SCATTERING
730

731
    int n = nuc->index_inelastic_scatter_.size();
3,399,909✔
732
    int i = 0;
3,399,909✔
733
    for (int j = 0; j < n && prob < cutoff; ++j) {
64,077,270✔
734
      i = nuc->index_inelastic_scatter_[j];
60,677,361✔
735

736
      // add to cumulative probability
737
      prob += nuc->reactions_[i]->xs(micro);
60,677,361✔
738
    }
739

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

746
  // Set event component
747
  p.event() = TallyEvent::SCATTER;
176,735,221✔
748

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

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

766
  double vel = std::sqrt(p.E());
150,146,204✔
767
  double awr = nuc->awr_;
150,146,204✔
768

769
  // Neutron velocity in LAB
770
  Direction v_n = vel * p.u();
150,146,204✔
771

772
  // Sample velocity of target nucleus
773
  Direction v_t {};
150,146,204✔
774
  if (!p.neutron_xs(i_nuclide).use_ptable) {
150,146,204✔
775
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
143,005,352✔
776
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
143,005,352✔
777
  }
778

779
  // Velocity of center-of-mass
780
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
150,146,204✔
781

782
  // Transform to CM frame
783
  v_n -= v_cm;
150,146,204✔
784

785
  // Find speed of neutron in CM
786
  vel = v_n.norm();
150,146,204✔
787

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

799
  // Determine direction cosines in CM
800
  Direction u_cm = v_n / vel;
150,146,204✔
801

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

807
  // Transform back to LAB frame
808
  v_n += v_cm;
150,146,204✔
809

810
  p.E() = v_n.dot(v_n);
150,146,204✔
811
  vel = std::sqrt(p.E());
150,146,204✔
812

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

817
  // Set energy and direction of particle in LAB frame
818
  p.u() = v_n / vel;
150,146,204!
819

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

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

833
  // Sample energy and angle
834
  double E_out;
23,189,108✔
835
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
23,189,108✔
836
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
23,189,108✔
837

838
  // Set energy to outgoing, change direction of particle
839
  p.E() = E_out;
23,189,108✔
840
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
23,189,108✔
841
}
23,189,108✔
842

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

850
    // sampling method to use
851
    sampling_method = settings::res_scat_method;
15,374✔
852

853
    // upper resonance scattering energy bound (target is at rest above this E)
854
    if (E > settings::res_scat_energy_max) {
15,374✔
855
      return {};
7,410✔
856

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

862
    // otherwise, use free gas model
863
  } else {
864
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
142,989,978✔
865
      return {};
73,826,882✔
866
    } else {
867
      sampling_method = ResScatMethod::cxs;
868
    }
869
  }
870

871
  // use appropriate target velocity sampling method
872
  switch (sampling_method) {
3,420!
873
  case ResScatMethod::cxs:
69,167,640✔
874

875
    // sample target velocity with the constant cross section (cxs) approx.
876
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
69,167,640✔
877

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

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

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

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

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

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

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

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

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

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

964
      while (true) {
59,256✔
965
        // directly sample Maxwellian
966
        double E_t = -kT * std::log(prn(seed));
31,040✔
967

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

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

984
        if (std::abs(mu) < 1.0) {
31,040✔
985
          // set and accept target velocity
986
          E_t /= nuc.awr_;
2,824✔
987
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
2,824✔
988
        }
989
      }
28,216✔
990
    }
991
  } // case RVS, DBRC
992
  } // switch (sampling_method)
993

994
  UNREACHABLE();
×
995
}
996

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

1003
  double beta_vt_sq;
80,620,854✔
1004
  double mu;
80,620,854✔
1005
  while (true) {
80,620,854✔
1006
    // Sample two random numbers
1007
    double r1 = prn(seed);
80,620,854✔
1008
    double r2 = prn(seed);
80,620,854✔
1009

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

1015
      beta_vt_sq = -std::log(r1 * r2);
18,484,706✔
1016

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

1022
      double c = std::cos(PI / 2.0 * prn(seed));
62,136,148✔
1023
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
62,136,148✔
1024
    }
1025

1026
    // Determine beta * vt
1027
    double beta_vt = std::sqrt(beta_vt_sq);
80,620,854✔
1028

1029
    // Sample cosine of angle between neutron and target velocity
1030
    mu = uniform_distribution(-1., 1., seed);
80,620,854✔
1031

1032
    // Determine rejection probability
1033
    double accept_prob =
80,620,854✔
1034
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
80,620,854✔
1035
      (beta_vn + beta_vt);
80,620,854✔
1036

1037
    // Perform rejection sampling on vt and mu
1038
    if (prn(seed) < accept_prob)
80,620,854✔
1039
      break;
1040
  }
1041

1042
  // Determine speed of target nucleus
1043
  double vt = std::sqrt(beta_vt_sq * kT / awr);
69,168,236✔
1044

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

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

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

1063
  if (prn(seed) < beta) {
5,532,980✔
1064
    // ====================================================================
1065
    // DELAYED NEUTRON SAMPLED
1066

1067
    // sampled delayed precursor group
1068
    double xi = prn(seed) * nu_d;
34,796✔
1069
    double prob = 0.0;
34,796✔
1070
    int group;
34,796✔
1071
    for (group = 1; group < nuc->n_precursor_; ++group) {
129,546✔
1072
      // determine delayed neutron precursor yield for group j
1073
      double yield = (*rx.products_[group].yield_)(E_in);
127,096✔
1074

1075
      // Check if this group is sampled
1076
      prob += yield;
127,096✔
1077
      if (xi < prob)
127,096✔
1078
        break;
1079
    }
1080

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

1086
    // set the delayed group for the particle born from fission
1087
    site->delayed_group = group;
34,796✔
1088

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

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

1097
    // set the delayed group for the particle born from fission to 0
1098
    site->delayed_group = 0;
5,498,184✔
1099
  }
1100

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

1107
    // resample if energy is greater than maximum neutron energy
1108
    int neutron = ParticleType::neutron().transport_index();
5,532,980✔
1109
    if (site->E < data::energy_max[neutron])
5,532,980!
1110
      break;
1111

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

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

1126
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
3,399,909✔
1127
{
1128
  // copy energy of neutron
1129
  double E_in = p.E();
3,399,909✔
1130

1131
  // sample outgoing energy and scattering cosine
1132
  double E;
3,399,909✔
1133
  double mu;
3,399,909✔
1134
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
3,399,909✔
1135

1136
  // if scattering system is in center-of-mass, transfer cosine of scattering
1137
  // angle and outgoing energy from CM to LAB
1138
  if (rx.scatter_in_cm_) {
3,399,909✔
1139
    double E_cm = E;
3,389,891✔
1140

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

1146
    // determine outgoing angle in lab
1147
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
3,389,891✔
1148
  }
1149

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

1156
  // Set outgoing energy and scattering angle
1157
  p.E() = E;
3,399,909✔
1158
  p.mu() = mu;
3,399,909✔
1159

1160
  // change direction of particle
1161
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
3,399,909✔
1162

1163
  // evaluate yield
1164
  double yield = (*rx.products_[0].yield_)(E_in);
3,399,909✔
1165
  if (std::floor(yield) == yield && yield > 0) {
3,399,909!
1166
    // If yield is integral, create exactly that many secondary particles
1167
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
3,416,781✔
1168
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
16,872✔
1169
    }
1170
  } else {
1171
    // Otherwise, change weight of particle based on yield
UNCOV
1172
    p.wgt() *= yield;
×
1173
  }
1174
}
3,399,909✔
1175

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

1184
  if (settings::use_decay_photons) {
1,524,008✔
1185
    // For decay photons, sample a single photon and modify the weight
1186
    if (y_t <= 0.0)
13,092✔
1187
      return;
1188
    photon_wgt *= y_t;
9,950✔
1189
  } else {
1190
    // For prompt photons, sample an integral number of photons with weight
1191
    // equal to the neutron's weight
1192
    y = static_cast<int>(y_t);
1,510,916✔
1193
    if (prn(p.current_seed()) <= y_t - y)
1,510,916✔
1194
      ++y;
101,974✔
1195
  }
1196

1197
  // Sample each secondary photon
1198
  for (int i = 0; i < y; ++i) {
1,762,094✔
1199
    // Sample the reaction and product
1200
    int i_rx;
241,228✔
1201
    int i_product;
241,228✔
1202
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
241,228✔
1203

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

1210
    // Sample the new direction
1211
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
241,228✔
1212

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

1223
    // Create the secondary photon
1224
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
241,228✔
1225

1226
    // Tag secondary particle with parent nuclide
1227
    if (created_photon && settings::use_decay_photons) {
241,228✔
1228
      p.secondary_bank().back().parent_nuclide =
9,608✔
1229
        rx->products_[i_product].parent_nuclide_;
9,608✔
1230
    }
1231
  }
1232
}
1233

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