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

openmc-dev / openmc / 33216693309

28 Aug 2026 10:24PM UTC coverage: 81.398% (+0.07%) from 81.333%
33216693309

Pull #3944

github

web-flow
Merge 6f608b242 into 15cd5eb34
Pull Request #3944: DNP drift (regular mesh only)

19101 of 27661 branches covered (69.05%)

Branch coverage included in aggregate %.

764 of 873 new or added lines in 22 files covered. (87.51%)

38 existing lines in 2 files now uncovered.

61044 of 70800 relevant lines covered (86.22%)

50669890.45 hits per line

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

83.03
/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/dnp_drift.h"
9
#include "openmc/eigenvalue.h"
10
#include "openmc/endf.h"
11
#include "openmc/error.h"
12
#include "openmc/ifp.h"
13
#include "openmc/material.h"
14
#include "openmc/math_functions.h"
15
#include "openmc/message_passing.h"
16
#include "openmc/ncrystal_interface.h"
17
#include "openmc/nuclide.h"
18
#include "openmc/photon.h"
19
#include "openmc/physics_common.h"
20
#include "openmc/random_dist.h"
21
#include "openmc/random_lcg.h"
22
#include "openmc/reaction.h"
23
#include "openmc/search.h"
24
#include "openmc/secondary_uncorrelated.h"
25
#include "openmc/settings.h"
26
#include "openmc/simulation.h"
27
#include "openmc/string_utils.h"
28
#include "openmc/tallies/tally.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,625,674,757 ✔
45
{
46
  // Add to collision counter for particle
47
  ++(p.n_collision());
1,625,674,757 ✔
48
  p.secondary_bank_index() = p.local_secondary_bank().size();
1,625,674,757 !
49

50
  // Sample reaction for the material the particle is in
51
  switch (p.type().pdg_number()) {
1,625,674,757 !
52
  case PDG_NEUTRON:
1,591,224,614 ✔
53
    sample_neutron_reaction(p);
1,591,224,614 ✔
54
    break;
1,591,224,614 ✔
55
  case PDG_PHOTON:
34,340,143 ✔
56
    sample_photon_reaction(p);
34,340,143 ✔
57
    break;
34,340,143 ✔
58
  case PDG_ELECTRON:
110,000 ✔
59
    sample_electron_reaction(p);
110,000 ✔
60
    break;
110,000 ✔
61
  case PDG_POSITRON:
×
62
    sample_positron_reaction(p);
×
63
    break;
×
64
  default:
×
65
    fatal_error("Unsupported particle PDG for collision sampling.");
×
66
  }
67

68
  if (settings::weight_windows_on) {
1,625,674,757 ✔
69
    auto [ww_found, ww] = search_weight_window(p);
417,639,735 ✔
70
    if (!ww_found && p.type() == ParticleType::neutron()) {
417,639,735 ✔
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,274 ✔
74
    } else if (settings::weight_window_checkpoint_collision) {
417,573,461 !
75
      // if collision checkpointing is on, apply weight window
76
      apply_weight_window(p, ww);
417,573,461 ✔
77
    }
78
  }
79

80
  // Kill particle if energy falls below cutoff
81
  int type = p.type().transport_index();
1,625,674,757 !
82
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
1,625,674,757 !
83
    p.wgt() = 0.0;
8,101,203 ✔
84
  }
85

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

106
void sample_neutron_reaction(Particle& p)
1,591,224,614 ✔
107
{
108
  // Sample a nuclide within the material
109
  int i_nuclide = sample_nuclide(p);
1,591,224,614 ✔
110

111
  // Save which nuclide particle had collision with
112
  p.event_nuclide() = i_nuclide;
1,591,224,614 ✔
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]};
1,591,224,614 ✔
120

121
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
1,591,224,614 ✔
122
    auto& rx = sample_fission(i_nuclide, p);
180,778,708 ✔
123
    if (settings::run_mode == RunMode::EIGENVALUE) {
180,778,708 ✔
124
      create_fission_sites(p, i_nuclide, rx);
155,751,663 ✔
125
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
25,027,045 ✔
126
               settings::create_fission_neutrons) {
127
      create_fission_sites(p, i_nuclide, rx);
580,997 ✔
128

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

142
  // Create secondary photons
143
  if (settings::photon_transport) {
1,591,224,614 ✔
144
    sample_secondary_photons(p, i_nuclide);
77,865,359 ✔
145
  }
146

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

150
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
1,591,224,614 ✔
151
    absorption(p, i_nuclide);
1,591,111,512 ✔
152
  }
153
  if (!p.alive())
1,591,224,614 ✔
154
    return;
155

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

165
  // Advance URR seed stream 'N' times after energy changes
166
  if (p.E() != p.E_last()) {
1,554,519,338 ✔
167
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
1,554,200,107 ✔
168
  }
169

170
  // Play russian roulette if there are no weight windows
171
  if (!settings::weight_windows_on)
1,554,519,338 ✔
172
    apply_russian_roulette(p);
1,165,705,147 ✔
173
}
174

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

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

186
  // Sample the number of neutrons produced
187
  int nu = static_cast<int>(nu_t);
156,332,660 ✔
188
  if (prn(p.current_seed()) <= (nu_t - nu))
156,332,660 ✔
189
    ++nu;
25,705,262 ✔
190

191
  // If no neutrons were produced then don't continue
192
  if (nu == 0)
156,332,660 ✔
193
    return;
124,129,561 ✔
194

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

199
  // Clear out particle's nu fission bank
200
  p.nu_bank().clear();
32,203,737 ✔
201

202
  p.fission() = true;
32,203,737 ✔
203

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

208
  // Counter for the number of fission sites successfully stored to the shared
209
  // fission bank or the secondary particle bank
210
  int n_sites_stored = 0;
32,203,737 ✔
211

212
  for (int i = 0; i < nu; i++) {
73,803,738 ✔
213
    // Initialize fission site object with particle data
214
    SourceSite site;
41,600,001 ✔
215
    site.r = p.r();
41,600,001 ✔
216
    site.particle = ParticleType::neutron();
41,600,001 ✔
217
    site.time = p.time();
41,600,001 ✔
218
    site.wgt = 1. / weight;
41,600,001 ✔
219
    site.surf_id = 0;
41,600,001 ✔
220

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

224
    // If a delayed neutron is sampled
225
    if (site.delayed_group > 0) {
41,600,001 ✔
226

227
      // Explicit transport of delayed neutron precursor (DNP)
228
      if (settings::dnp_drift_on) {
275,063 ✔
229
        double dnp_decay_time = site.time - p.time();
12,892 ✔
230

231
        // Reject site if it is longer inside the model
232
        if (!transport_dnp(site, dnp_decay_time, p.current_seed()))
12,892 ✔
233
          continue;
880 ✔
234

235
        // Reject site if it is not usable for the next generation
236
        if (!reconcile_precursor_drift(site))
12,012 !
NEW
237
          continue;
×
238
      }
239

240
      // Reject site if it exceeds time cutoff
241
      double t_cutoff = settings::time_cutoff[site.particle.transport_index()];
274,183 !
242
      if (site.time > t_cutoff) {
274,183 !
243
        continue;
×
244
      }
245
    }
246

247
    // Set parent and progeny IDs
248
    site.parent_id = p.current_work();
41,599,121 ✔
249
    site.progeny_id = p.n_progeny()++;
41,599,121 ✔
250

251
    // Store fission site in bank
252
    if (use_fission_bank) {
41,599,121 ✔
253
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
41,384,336 ✔
254
      if (idx == -1) {
41,384,336 !
255
        warning(
×
256
          "The shared fission bank is full. Additional fission sites created "
257
          "in this generation will not be banked. Results may be "
258
          "non-deterministic.");
259

260
        // Decrement number of particle progeny as storage was unsuccessful.
261
        // This step is needed so that the sum of all progeny is equal to the
262
        // size of the shared fission bank.
263
        p.n_progeny()--;
×
264

265
        // Break out of loop as no more sites can be added to fission bank
266
        break;
×
267
      }
268
      // Iterated Fission Probability (IFP) method
269
      if (settings::ifp_on) {
41,384,336 ✔
270
        ifp(p, idx);
1,352,626 ✔
271
      }
272
    } else {
273
      site.wgt_born = p.wgt_born();
214,785 ✔
274
      site.wgt_ww_born = p.wgt_ww_born();
214,785 ✔
275
      site.n_split = p.n_split();
214,785 ✔
276
      p.local_secondary_bank().push_back(site);
214,785 ✔
277
      p.n_secondaries()++;
214,785 ✔
278
    }
279

280
    n_sites_stored++;
41,599,121 ✔
281

282
    // Increment the number of neutrons born delayed
283
    if (site.delayed_group > 0) {
41,599,121 ✔
284
      nu_d[site.delayed_group - 1]++;
274,183 ✔
285
    }
286

287
    // Write fission particles to nuBank
288
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
41,599,121 ✔
289
    nu_bank_entry.wgt = site.wgt;
41,599,121 ✔
290
    nu_bank_entry.E = site.E;
41,599,121 ✔
291
    nu_bank_entry.delayed_group = site.delayed_group;
41,599,121 ✔
292
  }
293

294
  // If shared fission bank was full, and no fissions could be added,
295
  // set the particle fission flag to false.
296
  if (n_sites_stored == 0) {
32,203,737 ✔
297
    p.fission() = false;
638 ✔
298
    return;
638 ✔
299
  }
300

301
  // Set nu to the number of fission sites successfully stored. If the fission
302
  // bank was not found to be full then these values are already equivalent.
303
  nu = n_sites_stored;
32,203,099 ✔
304

305
  // Store the total weight banked for analog fission tallies
306
  p.n_bank() = nu;
32,203,099 ✔
307
  p.wgt_bank() = nu / weight;
32,203,099 ✔
308
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
289,827,891 ✔
309
    p.n_delayed_bank(d) = nu_d[d];
257,624,792 ✔
310
  }
311
}
312

313
void sample_photon_reaction(Particle& p)
34,340,143 ✔
314
{
315
  // Kill photon if below energy cutoff -- an extra check is made here because
316
  // photons with energy below the cutoff may have been produced by neutrons
317
  // reactions or atomic relaxation
318
  int photon = ParticleType::photon().transport_index();
34,340,143 ✔
319
  if (p.E() < settings::energy_cutoff[photon]) {
34,340,143 ✔
320
    p.E() = 0.0;
55 ✔
321
    p.wgt() = 0.0;
55 ✔
322
    return;
55 ✔
323
  }
324

325
  // Sample element within material
326
  int i_element = sample_element(p);
34,340,088 ✔
327
  const auto& micro {p.photon_xs(i_element)};
34,340,088 ✔
328
  const auto& element {*data::elements[i_element]};
34,340,088 ✔
329

330
  // Calculate photon energy over electron rest mass equivalent
331
  double alpha = p.E() / MASS_ELECTRON_EV;
34,340,088 ✔
332

333
  // For tallying purposes, this routine might be called directly. In that
334
  // case, we need to sample a reaction via the cutoff variable
335
  double prob = 0.0;
34,340,088 ✔
336
  double cutoff = prn(p.current_seed()) * micro.total;
34,340,088 ✔
337

338
  // Coherent (Rayleigh) scattering
339
  prob += micro.coherent;
34,340,088 ✔
340
  if (prob > cutoff) {
34,340,088 ✔
341
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
1,667,198 ✔
342
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
1,667,198 ✔
343
    p.event() = TallyEvent::SCATTER;
1,667,198 ✔
344
    p.event_mt() = COHERENT;
1,667,198 ✔
345
    return;
1,667,198 ✔
346
  }
347

348
  // Incoherent (Compton) scattering
349
  prob += micro.incoherent;
32,672,890 ✔
350
  if (prob > cutoff) {
32,672,890 ✔
351
    double alpha_out;
24,582,665 ✔
352
    int i_shell;
24,582,665 ✔
353
    element.compton_scatter(
24,582,665 ✔
354
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
24,582,665 ✔
355

356
    // Determine binding energy of shell. The binding energy is 0.0 if
357
    // doppler broadening is not used.
358
    double e_b;
24,582,665 ✔
359
    if (i_shell == -1) {
24,582,665 !
360
      e_b = 0.0;
361
    } else {
362
      e_b = element.binding_energy_[i_shell];
24,582,665 ✔
363
    }
364

365
    // Create Compton electron
366
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
24,582,665 ✔
367
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
24,582,665 ✔
368
    int electron = ParticleType::electron().transport_index();
24,582,665 ✔
369
    if (E_electron >= settings::energy_cutoff[electron]) {
24,582,665 !
370
      double mu_electron = (alpha - alpha_out * p.mu()) /
24,582,665 ✔
371
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
24,582,665 ✔
372
                                     2.0 * alpha * alpha_out * p.mu());
24,582,665 ✔
373
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
24,582,665 ✔
374
      process_charged_secondary(p, u, E_electron, ParticleType::electron());
24,582,665 ✔
375
    }
376

377
    // Allow electrons to fill orbital and produce Auger electrons and
378
    // fluorescent photons. Since Compton subshell data does not match atomic
379
    // relaxation data, use the mapping between the data to find the subshell
380
    if (settings::atomic_relaxation && element.has_atomic_relaxation_ &&
24,425,607 !
381
        i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
49,008,272 !
382
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
24,425,607 ✔
383
    }
384

385
    phi += PI;
24,582,665 ✔
386
    p.E() = alpha_out * MASS_ELECTRON_EV;
24,582,665 ✔
387
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
24,582,665 ✔
388
    p.event() = TallyEvent::SCATTER;
24,582,665 ✔
389
    p.event_mt() = INCOHERENT;
24,582,665 ✔
390
    return;
24,582,665 ✔
391
  }
392

393
  // Photoelectric effect
394
  double prob_after = prob + micro.photoelectric;
8,090,225 ✔
395

396
  if (prob_after > cutoff) {
8,090,225 ✔
397
    // Get grid index, interpolation factor, and bounding subshell
398
    // cross sections
399
    int i_grid = micro.index_grid;
7,892,720 ✔
400
    double f = micro.interp_factor;
7,892,720 ✔
401
    tensor::View<const double> xs_lower = element.cross_sections_.slice(i_grid);
7,892,720 ✔
402
    tensor::View<const double> xs_upper =
7,892,720 ✔
403
      element.cross_sections_.slice(i_grid + 1);
7,892,720 ✔
404

405
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
27,649,051 !
406
      const auto& shell {element.shells_[i_shell]};
27,649,051 ✔
407

408
      // Check threshold of reaction
409
      if (xs_lower(i_shell) == 0)
27,649,051 ✔
410
        continue;
10,383,304 ✔
411

412
      //  Evaluation subshell photoionization cross section
413
      prob += std::exp(
17,265,747 ✔
414
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
17,265,747 ✔
415

416
      if (prob > cutoff) {
17,265,747 ✔
417
        // Determine binding energy based on whether atomic relaxation data is
418
        // present (if not, use value from Compton profile data)
419
        double binding_energy = element.has_atomic_relaxation_
7,892,720 ✔
420
                                  ? shell.binding_energy
7,892,720 !
421
                                  : element.binding_energy_[i_shell];
×
422

423
        // Determine energy of secondary electron
424
        double E_electron = p.E() - binding_energy;
7,892,720 ✔
425

426
        // Sample mu using non-relativistic Sauter distribution.
427
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
428
        // model in Serpent 2" by Toni Kaltiaisenaho
429
        double mu;
11,845,514 ✔
430
        while (true) {
11,845,514 ✔
431
          double r = prn(p.current_seed());
11,845,514 ✔
432
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
11,845,514 ✔
433
            double rel_vel =
7,892,720 ✔
434
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
7,892,720 ✔
435
              (E_electron + MASS_ELECTRON_EV);
7,892,720 ✔
436
            mu =
7,892,720 ✔
437
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
7,892,720 ✔
438
            break;
7,892,720 ✔
439
          }
440
        }
441

442
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
7,892,720 ✔
443
        Direction u;
7,892,720 ✔
444
        u.x = mu;
7,892,720 ✔
445
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
7,892,720 ✔
446
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
7,892,720 ✔
447

448
        // Process secondary electron at the photon collision site.
449
        process_charged_secondary(p, u, E_electron, ParticleType::electron());
7,892,720 ✔
450

451
        // Allow electrons to fill orbital and produce auger electrons
452
        // and fluorescent photons
453
        if (settings::atomic_relaxation) {
7,892,720 ✔
454
          element.atomic_relaxation(i_shell, p);
7,672,720 ✔
455
        }
456
        p.event() = TallyEvent::ABSORB;
7,892,720 ✔
457
        p.event_mt() = 533 + shell.index_subshell;
7,892,720 ✔
458
        p.wgt() = 0.0;
7,892,720 ✔
459
        p.E() = 0.0;
7,892,720 ✔
460
        return;
7,892,720 ✔
461
      }
462
    }
463
  }
15,785,440 ✔
464
  prob = prob_after;
197,505 ✔
465

466
  // Pair production
467
  prob += micro.pair_production;
197,505 ✔
468
  if (prob > cutoff) {
197,505 !
469
    double E_electron, E_positron;
197,505 ✔
470
    double mu_electron, mu_positron;
197,505 ✔
471
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
197,505 ✔
472
      &mu_positron, p.current_seed());
473

474
    // Process secondary electron at the photon collision site.
475
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
197,505 ✔
476
    process_charged_secondary(p, u, E_electron, ParticleType::electron());
197,505 ✔
477

478
    // Process secondary positron at the photon collision site.
479
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
197,505 ✔
480
    process_charged_secondary(p, u, E_positron, ParticleType::positron());
197,505 ✔
481
    p.event() = TallyEvent::ABSORB;
197,505 ✔
482
    p.event_mt() = PAIR_PROD;
197,505 ✔
483
    p.wgt() = 0.0;
197,505 ✔
484
    p.E() = 0.0;
197,505 ✔
485
  }
486
}
487

488
void process_charged_secondary(
87,555,186 ✔
489
  Particle& p, Direction u, double E, ParticleType type)
490
{
491
  int idx = type.transport_index();
87,555,186 ✔
492
  if (idx == C_NONE || E < settings::energy_cutoff[idx])
87,555,186 !
493
    return;
494

495
  if (settings::electron_treatment == ElectronTreatment::TTB) {
87,555,186 ✔
496
    thick_target_bremsstrahlung(p, type, u, E);
87,059,724 ✔
497
  }
498

499
  if (type == ParticleType::positron()) {
87,555,186 ✔
500
    Direction photon_u = isotropic_direction(p.current_seed());
197,505 ✔
501
    p.create_secondary(
197,505 ✔
502
      p.wgt(), photon_u, MASS_ELECTRON_EV, ParticleType::photon());
197,505 ✔
503
    p.create_secondary(
197,505 ✔
504
      p.wgt(), -photon_u, MASS_ELECTRON_EV, ParticleType::photon());
197,505 ✔
505

506
    // The annihilation photons are now emitted during the parent photon
507
    // collision. Offset the pair-production Q value in the energy balance so
508
    // heating matches the prior explicit positron slowing-down sequence.
509
    p.bank_second_E() -= 2 * MASS_ELECTRON_EV;
197,505 ✔
510
  }
511
}
512

513
void sample_electron_reaction(Particle& p)
110,000 ✔
514
{
515
  // TODO: create reaction types
516

517
  if (settings::electron_treatment == ElectronTreatment::TTB) {
110,000 !
518
    thick_target_bremsstrahlung(p);
110,000 ✔
519
  }
520

521
  p.E() = 0.0;
110,000 ✔
522
  p.wgt() = 0.0;
110,000 ✔
523
  p.event() = TallyEvent::ABSORB;
110,000 ✔
524
}
110,000 ✔
525

526
void sample_positron_reaction(Particle& p)
×
527
{
528
  // TODO: create reaction types
529

530
  if (settings::electron_treatment == ElectronTreatment::TTB) {
×
531
    thick_target_bremsstrahlung(p);
×
532
  }
533

534
  // Sample angle isotropically
535
  Direction u = isotropic_direction(p.current_seed());
×
536

537
  // Create annihilation photon pair traveling in opposite directions
538
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
×
539
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
×
540

541
  p.E() = 0.0;
×
542
  p.wgt() = 0.0;
×
543
  p.event() = TallyEvent::ABSORB;
×
544
}
×
545

546
int sample_nuclide(Particle& p)
1,591,224,614 ✔
547
{
548
  // Sample cumulative distribution function
549
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
1,591,224,614 ✔
550

551
  // Get pointers to nuclide/density arrays
552
  const auto& mat {model::materials[p.material()]};
1,591,224,614 ✔
553
  int n = mat->nuclide_.size();
1,591,224,614 ✔
554

555
  double prob = 0.0;
1,591,224,614 ✔
556
  for (int i = 0; i < n; ++i) {
2,147,483,647 !
557
    // Get atom density
558
    int i_nuclide = mat->nuclide_[i];
2,147,483,647 ✔
559
    double atom_density = mat->atom_density(i, p.density_mult());
2,147,483,647 ✔
560

561
    // Increment probability to compare to cutoff
562
    prob += atom_density * p.neutron_xs(i_nuclide).total;
2,147,483,647 ✔
563
    if (prob >= cutoff)
2,147,483,647 ✔
564
      return i_nuclide;
1,591,224,614 ✔
565
  }
566

567
  // If we reach here, no nuclide was sampled
568
  p.write_restart();
×
569
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
570
}
571

572
int sample_element(Particle& p)
34,340,088 ✔
573
{
574
  // Sample cumulative distribution function
575
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
34,340,088 ✔
576

577
  // Get pointers to elements, densities
578
  const auto& mat {model::materials[p.material()]};
34,340,088 ✔
579

580
  double prob = 0.0;
34,340,088 ✔
581
  for (int i = 0; i < mat->element_.size(); ++i) {
138,553,846 !
582
    // Find atom density
583
    int i_element = mat->element_[i];
138,553,846 ✔
584
    double atom_density = mat->atom_density(i, p.density_mult());
138,553,846 ✔
585

586
    // Determine microscopic cross section
587
    double sigma = atom_density * p.photon_xs(i_element).total;
138,553,846 ✔
588

589
    // Increment probability to compare to cutoff
590
    prob += sigma;
138,553,846 ✔
591
    if (prob > cutoff) {
138,553,846 ✔
592
      // Save which nuclide particle had collision with for tally purpose
593
      p.event_nuclide() = mat->nuclide_[i];
34,340,088 ✔
594

595
      return i_element;
34,340,088 ✔
596
    }
597
  }
598

599
  // If we made it here, no element was sampled
600
  p.write_restart();
×
601
  fatal_error("Did not sample any element during collision.");
×
602
}
603

604
Reaction& sample_fission(int i_nuclide, Particle& p)
180,778,708 ✔
605
{
606
  // Get pointer to nuclide
607
  const auto& nuc {data::nuclides[i_nuclide]};
180,778,708 ✔
608

609
  // If we're in the URR, by default use the first fission reaction. We also
610
  // default to the first reaction if we know that there are no partial fission
611
  // reactions
612
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
180,778,708 ✔
613
    return *nuc->fission_rx_[0];
180,720,541 ✔
614
  }
615

616
  // Check to see if we are in a windowed multipole range.  WMP only supports
617
  // the first fission reaction.
618
  if (nuc->multipole_) {
58,167 ✔
619
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
2,849 !
620
      return *nuc->fission_rx_[0];
1,991 ✔
621
    }
622
  }
623

624
  // Get grid index and interpolation factor and sample fission cdf
625
  const auto& micro = p.neutron_xs(i_nuclide);
56,176 ✔
626
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
56,176 ✔
627
  double prob = 0.0;
56,176 ✔
628

629
  // Loop through each partial fission reaction type
630
  for (auto& rx : nuc->fission_rx_) {
56,247 !
631
    // add to cumulative probability
632
    prob += rx->xs(micro);
56,247 ✔
633

634
    // Create fission bank sites if fission occurs
635
    if (prob > cutoff)
56,247 ✔
636
      return *rx;
56,176 ✔
637
  }
638

639
  // If we reached here, no reaction was sampled
640
  throw std::runtime_error {
×
641
    "No fission reaction was sampled for " + nuc->name_};
×
642
}
643

644
void sample_photon_product(
2,936,868 ✔
645
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
646
{
647
  // Get grid index and interpolation factor and sample photon production cdf
648
  const auto& micro = p.neutron_xs(i_nuclide);
2,936,868 ✔
649
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
2,936,868 ✔
650
  double prob = 0.0;
2,936,868 ✔
651

652
  // Loop through each reaction type
653
  const auto& nuc {data::nuclides[i_nuclide]};
2,936,868 ✔
654
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
54,189,135 !
655
    // Evaluate neutron cross section
656
    const auto& rx = nuc->reactions_[i];
54,189,135 ✔
657
    double xs = rx->xs(micro);
54,189,135 ✔
658

659
    // if cross section is zero for this reaction, skip it
660
    if (xs == 0.0)
54,189,135 ✔
661
      continue;
33,772,475 ✔
662

663
    for (int j = 0; j < rx->products_.size(); ++j) {
148,910,608 ✔
664
      if (rx->products_[j].particle_.is_photon()) {
131,430,816 ✔
665
        // For fission, artificially increase the photon yield to account
666
        // for delayed photons
667
        double f = 1.0;
115,952,287 ✔
668
        if (settings::delayed_photon_scaling) {
115,952,287 !
669
          if (is_fission(rx->mt_)) {
115,952,287 ✔
670
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
540,826 !
671
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
540,826 ✔
672
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
540,826 ✔
673
              f = (energy_prompt + energy_delayed) / (energy_prompt);
540,826 ✔
674
            }
675
          }
676
        }
677

678
        // add to cumulative probability
679
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
115,952,287 ✔
680

681
        *i_rx = i;
115,952,287 ✔
682
        *i_product = j;
115,952,287 ✔
683
        if (prob > cutoff)
115,952,287 ✔
684
          return;
685
      }
686
    }
687
  }
688
}
689

690
void absorption(Particle& p, int i_nuclide)
1,591,111,512 ✔
691
{
692
  if (settings::survival_biasing) {
1,591,111,512 ✔
693
    // Determine weight absorbed in survival biasing
694
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
41,632,481 ✔
695
                              p.neutron_xs(i_nuclide).total;
41,632,481 ✔
696

697
    // Adjust weight of particle by probability of absorption
698
    p.wgt() -= wgt_absorb;
41,632,481 ✔
699

700
    // Score implicit absorption estimate of keff
701
    if (settings::run_mode == RunMode::EIGENVALUE) {
41,632,481 ✔
702
      p.keff_tally_absorption() += wgt_absorb *
499,950 ✔
703
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950 ✔
704
                                   p.neutron_xs(i_nuclide).absorption;
499,950 ✔
705
    }
706
  } else {
707
    // See if disappearance reaction happens
708
    if (p.neutron_xs(i_nuclide).absorption >
1,549,479,031 ✔
709
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
1,549,479,031 ✔
710
      // Score absorption estimate of keff
711
      if (settings::run_mode == RunMode::EIGENVALUE) {
36,703,758 ✔
712
        p.keff_tally_absorption() += p.wgt() *
26,540,155 ✔
713
                                     p.neutron_xs(i_nuclide).nu_fission /
26,540,155 ✔
714
                                     p.neutron_xs(i_nuclide).absorption;
26,540,155 ✔
715
      }
716

717
      p.wgt() = 0.0;
36,703,758 ✔
718
      p.event() = TallyEvent::ABSORB;
36,703,758 ✔
719
      if (!p.fission()) {
36,703,758 ✔
720
        p.event_mt() = N_DISAPPEAR;
23,029,789 ✔
721
      }
722
    }
723
  }
724
}
1,591,111,512 ✔
725

726
void scatter(Particle& p, int i_nuclide)
1,554,360,509 ✔
727
{
728
  // copy incoming direction
729
  Direction u_old {p.u()};
1,554,360,509 ✔
730

731
  // Get pointer to nuclide and grid index/interpolation factor
732
  const auto& nuc {data::nuclides[i_nuclide]};
1,554,360,509 ✔
733
  const auto& micro {p.neutron_xs(i_nuclide)};
1,554,360,509 ✔
734
  int i_temp = micro.index_temp;
1,554,360,509 ✔
735

736
  // For tallying purposes, this routine might be called directly. In that
737
  // case, we need to sample a reaction via the cutoff variable
738
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
1,554,360,509 ✔
739
  bool sampled = false;
1,554,360,509 ✔
740

741
  // Calculate elastic cross section if it wasn't precalculated
742
  if (micro.elastic == CACHE_INVALID) {
1,554,360,509 ✔
743
    nuc->calculate_elastic_xs(p);
1,291,588,829 ✔
744
  }
745

746
  double prob = micro.elastic - micro.thermal;
1,554,360,509 ✔
747
  if (prob > cutoff) {
1,554,360,509 ✔
748
    // =======================================================================
749
    // NON-S(A,B) ELASTIC SCATTERING
750

751
    // Determine temperature
752
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
1,400,472,029 ✔
753

754
    // Perform collision physics for elastic scattering
755
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
1,400,472,029 ✔
756

757
    p.event_mt() = ELASTIC;
1,400,472,029 ✔
758
    sampled = true;
1,400,472,029 ✔
759
  }
760

761
  prob = micro.elastic;
1,554,360,509 ✔
762
  if (prob > cutoff && !sampled) {
1,554,360,509 ✔
763
    // =======================================================================
764
    // S(A,B) SCATTERING
765

766
    sab_scatter(i_nuclide, micro.index_sab, p);
128,157,410 ✔
767

768
    p.event_mt() = ELASTIC;
128,157,410 ✔
769
    sampled = true;
128,157,410 ✔
770
  }
771

772
  if (!sampled) {
1,554,360,509 ✔
773
    // =======================================================================
774
    // INELASTIC SCATTERING
775

776
    int n = nuc->index_inelastic_scatter_.size();
25,731,070 ✔
777
    int i = 0;
25,731,070 ✔
778
    for (int j = 0; j < n && prob < cutoff; ++j) {
479,620,537 ✔
779
      i = nuc->index_inelastic_scatter_[j];
453,889,467 ✔
780

781
      // add to cumulative probability
782
      prob += nuc->reactions_[i]->xs(micro);
453,889,467 ✔
783
    }
784

785
    // Perform collision physics for inelastic scattering
786
    const auto& rx {nuc->reactions_[i]};
25,731,070 ✔
787
    inelastic_scatter(*nuc, *rx, p);
25,731,070 ✔
788
    p.event_mt() = rx->mt_;
25,731,070 ✔
789
  }
790

791
  // Set event component
792
  p.event() = TallyEvent::SCATTER;
1,554,360,509 ✔
793

794
  // Sample new outgoing angle for isotropic-in-lab scattering
795
  const auto& mat {model::materials[p.material()]};
1,554,360,509 !
796
  if (!mat->p0_.empty()) {
1,554,360,509 !
797
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
326,370 ✔
798
    if (mat->p0_[i_nuc_mat]) {
326,370 !
799
      // Sample isotropic-in-lab outgoing direction
800
      p.u() = isotropic_direction(p.current_seed());
326,370 ✔
801
      p.mu() = u_old.dot(p.u());
326,370 ✔
802
    }
803
  }
804
}
1,554,360,509 ✔
805

806
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
1,400,472,029 ✔
807
{
808
  // get pointer to nuclide
809
  const auto& nuc {data::nuclides[i_nuclide]};
1,400,472,029 ✔
810

811
  double vel = std::sqrt(p.E());
1,400,472,029 ✔
812
  double awr = nuc->awr_;
1,400,472,029 ✔
813

814
  // Neutron velocity in LAB
815
  Direction v_n = vel * p.u();
1,400,472,029 ✔
816

817
  // Sample velocity of target nucleus
818
  Direction v_t {};
1,400,472,029 ✔
819
  if (!p.neutron_xs(i_nuclide).use_ptable) {
1,400,472,029 ✔
820
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,351,529,577 ✔
821
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
1,351,529,577 ✔
822
  }
823

824
  // Velocity of center-of-mass
825
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
1,400,472,029 ✔
826

827
  // Transform to CM frame
828
  v_n -= v_cm;
1,400,472,029 ✔
829

830
  // Find speed of neutron in CM
831
  vel = v_n.norm();
1,400,472,029 ✔
832

833
  // Sample scattering angle, checking if angle distribution is present (assume
834
  // isotropic otherwise)
835
  double mu_cm;
1,400,472,029 ✔
836
  auto& d = rx.products_[0].distribution_[0];
1,400,472,029 !
837
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
1,400,472,029 !
838
  if (!d_->angle().empty()) {
1,400,472,029 !
839
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
1,400,472,029 ✔
840
  } else {
841
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
842
  }
843

844
  // Determine direction cosines in CM
845
  Direction u_cm = v_n / vel;
1,400,472,029 ✔
846

847
  // Rotate neutron velocity vector to new angle -- note that the speed of the
848
  // neutron in CM does not change in elastic scattering. However, the speed
849
  // will change when we convert back to LAB
850
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
1,400,472,029 ✔
851

852
  // Transform back to LAB frame
853
  v_n += v_cm;
1,400,472,029 ✔
854

855
  p.E() = v_n.dot(v_n);
1,400,472,029 ✔
856
  vel = std::sqrt(p.E());
1,400,472,029 ✔
857

858
  // compute cosine of scattering angle in LAB frame by taking dot product of
859
  // neutron's pre- and post-collision angle
860
  p.mu() = p.u().dot(v_n) / vel;
1,400,472,029 ✔
861

862
  // Set energy and direction of particle in LAB frame
863
  p.u() = v_n / vel;
1,400,472,029 !
864

865
  // Because of floating-point roundoff, it may be possible for mu_lab to be
866
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
867
  // -1 or 1
868
  if (std::abs(p.mu()) > 1.0)
1,400,472,029 !
869
    p.mu() = std::copysign(1.0, p.mu());
×
870
}
1,400,472,029 ✔
871

872
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
128,157,410 ✔
873
{
874
  // Determine temperature index
875
  const auto& micro {p.neutron_xs(i_nuclide)};
128,157,410 ✔
876
  int i_temp = micro.index_temp_sab;
128,157,410 ✔
877

878
  // Sample energy and angle
879
  double E_out;
128,157,410 ✔
880
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
128,157,410 ✔
881
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
128,157,410 ✔
882

883
  // Set energy to outgoing, change direction of particle
884
  p.E() = E_out;
128,157,410 ✔
885
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
128,157,410 ✔
886
}
128,157,410 ✔
887

888
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
1,351,529,577 ✔
889
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
890
{
891
  // check if nuclide is a resonant scatterer
892
  ResScatMethod sampling_method;
1,351,529,577 ✔
893
  if (nuc.resonant_) {
1,351,529,577 ✔
894

895
    // sampling method to use
896
    sampling_method = settings::res_scat_method;
84,557 ✔
897

898
    // upper resonance scattering energy bound (target is at rest above this E)
899
    if (E > settings::res_scat_energy_max) {
84,557 ✔
900
      return {};
40,755 ✔
901

902
      // lower resonance scattering energy bound (should be no resonances below)
903
    } else if (E < settings::res_scat_energy_min) {
43,802 ✔
904
      sampling_method = ResScatMethod::cxs;
905
    }
906

907
    // otherwise, use free gas model
908
  } else {
909
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
1,351,445,020 ✔
910
      return {};
502,826,841 ✔
911
    } else {
912
      sampling_method = ResScatMethod::cxs;
913
    }
914
  }
915

916
  // use appropriate target velocity sampling method
917
  switch (sampling_method) {
18,810 !
918
  case ResScatMethod::cxs:
848,643,171 ✔
919

920
    // sample target velocity with the constant cross section (cxs) approx.
921
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
848,643,171 ✔
922

923
  case ResScatMethod::dbrc:
18,810 ✔
924
  case ResScatMethod::rvs: {
18,810 ✔
925
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810 ✔
926
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
37,620 !
927
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810 ✔
928

929
    // find lower and upper energy bound indices
930
    // lower index
931
    int i_E_low;
18,810 ✔
932
    if (E_low < nuc.energy_0K_.front()) {
18,810 !
933
      i_E_low = 0;
934
    } else if (E_low > nuc.energy_0K_.back()) {
18,810 !
935
      i_E_low = nuc.energy_0K_.size() - 2;
×
936
    } else {
937
      i_E_low =
18,810 ✔
938
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
18,810 ✔
939
    }
940

941
    // upper index
942
    int i_E_up;
18,810 ✔
943
    if (E_up < nuc.energy_0K_.front()) {
18,810 !
944
      i_E_up = 0;
945
    } else if (E_up > nuc.energy_0K_.back()) {
18,810 !
946
      i_E_up = nuc.energy_0K_.size() - 2;
×
947
    } else {
948
      i_E_up =
18,810 ✔
949
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
18,810 ✔
950
    }
951

952
    if (i_E_up == i_E_low) {
18,810 ✔
953
      // Handle degenerate case -- if the upper/lower bounds occur for the same
954
      // index, then using cxs is probably a good approximation
955
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
18,810 ✔
956
    }
957

958
    if (sampling_method == ResScatMethod::dbrc) {
15,532 !
959
      // interpolate xs since we're not exactly at the energy indices
960
      double xs_low = nuc.elastic_0K_[i_E_low];
×
961
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
962
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
963
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
964
      double xs_up = nuc.elastic_0K_[i_E_up];
×
965
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
966
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
967
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
968

969
      // get max 0K xs value over range of practical relative energies
970
      double xs_max = *std::max_element(
×
971
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
972
      xs_max = std::max({xs_low, xs_max, xs_up});
×
973

974
      while (true) {
×
975
        double E_rel;
×
976
        Direction v_target;
×
977
        while (true) {
×
978
          // sample target velocity with the constant cross section (cxs)
979
          // approx.
980
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
981
          Direction v_rel = v_neut - v_target;
×
982
          E_rel = v_rel.dot(v_rel);
×
983
          if (E_rel < E_up)
×
984
            break;
985
        }
986

987
        // perform Doppler broadening rejection correction (dbrc)
988
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
989
        double R = xs_0K / xs_max;
×
990
        if (prn(seed) < R)
×
991
          return v_target;
×
992
      }
993

994
    } else if (sampling_method == ResScatMethod::rvs) {
15,532 ✔
995
      // interpolate xs CDF since we're not exactly at the energy indices
996
      // cdf value at lower bound attainable energy
997
      double cdf_low = 0.0;
15,532 ✔
998
      if (E_low > nuc.energy_0K_.front()) {
15,532 !
999
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
15,532 ✔
1000
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
15,532 ✔
1001
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
15,532 ✔
1002
      }
1003

1004
      // cdf value at upper bound attainable energy
1005
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
15,532 ✔
1006
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
15,532 ✔
1007
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
15,532 ✔
1008

1009
      while (true) {
325,908 ✔
1010
        // directly sample Maxwellian
1011
        double E_t = -kT * std::log(prn(seed));
170,720 ✔
1012

1013
        // sample a relative energy using the xs cdf
1014
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
170,720 ✔
1015
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
170,720 ✔
1016
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
170,720 ✔
1017
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
170,720 ✔
1018
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
170,720 ✔
1019
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
170,720 ✔
1020
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
170,720 ✔
1021
                     nuc.energy_0K_[i_E_low + i_E_rel]);
170,720 ✔
1022
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
170,720 ✔
1023

1024
        // perform rejection sampling on cosine between
1025
        // neutron and target velocities
1026
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
170,720 ✔
1027
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
170,720 ✔
1028

1029
        if (std::abs(mu) < 1.0) {
170,720 ✔
1030
          // set and accept target velocity
1031
          E_t /= nuc.awr_;
15,532 ✔
1032
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,532 ✔
1033
        }
1034
      }
155,188 ✔
1035
    }
1036
  } // case RVS, DBRC
1037
  } // switch (sampling_method)
1038

1039
  UNREACHABLE();
×
1040
}
1041

1042
Direction sample_cxs_target_velocity(
848,646,449 ✔
1043
  double awr, double E, Direction u, double kT, uint64_t* seed)
1044
{
1045
  double beta_vn = std::sqrt(awr * E / kT);
848,646,449 ✔
1046
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
848,646,449 ✔
1047

1048
  double beta_vt_sq;
1,032,973,926 ✔
1049
  double mu;
1,032,973,926 ✔
1050
  while (true) {
1,032,973,926 ✔
1051
    // Sample two random numbers
1052
    double r1 = prn(seed);
1,032,973,926 ✔
1053
    double r2 = prn(seed);
1,032,973,926 ✔
1054

1055
    if (prn(seed) < alpha) {
1,032,973,926 ✔
1056
      // With probability alpha, we sample the distribution p(y) =
1057
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1058
      // Carlo sampler
1059

1060
      beta_vt_sq = -std::log(r1 * r2);
293,212,307 ✔
1061

1062
    } else {
1063
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1064
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1065
      // Carlo sampler
1066

1067
      double c = std::cos(PI / 2.0 * prn(seed));
739,761,619 ✔
1068
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
739,761,619 ✔
1069
    }
1070

1071
    // Determine beta * vt
1072
    double beta_vt = std::sqrt(beta_vt_sq);
1,032,973,926 ✔
1073

1074
    // Sample cosine of angle between neutron and target velocity
1075
    mu = uniform_distribution(-1., 1., seed);
1,032,973,926 ✔
1076

1077
    // Determine rejection probability
1078
    double accept_prob =
1,032,973,926 ✔
1079
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
1,032,973,926 ✔
1080
      (beta_vn + beta_vt);
1,032,973,926 ✔
1081

1082
    // Perform rejection sampling on vt and mu
1083
    if (prn(seed) < accept_prob)
1,032,973,926 ✔
1084
      break;
1085
  }
1086

1087
  // Determine speed of target nucleus
1088
  double vt = std::sqrt(beta_vt_sq * kT / awr);
848,646,449 ✔
1089

1090
  // Determine velocity vector of target nucleus based on neutron's velocity
1091
  // and the sampled angle between them
1092
  return vt * rotate_angle(u, mu, nullptr, seed);
848,646,449 ✔
1093
}
1094

1095
void sample_fission_neutron(
41,600,001 ✔
1096
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1097
{
1098
  // Get attributes of particle
1099
  double E_in = p.E();
41,600,001 ✔
1100
  uint64_t* seed = p.current_seed();
41,600,001 ✔
1101

1102
  // Determine total nu, delayed nu, and delayed neutron fraction
1103
  const auto& nuc {data::nuclides[i_nuclide]};
41,600,001 ✔
1104
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
41,600,001 ✔
1105
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
41,600,001 ✔
1106
  double beta = nu_d / nu_t;
41,600,001 ✔
1107

1108
  if (prn(seed) < beta) {
41,600,001 ✔
1109
    // ====================================================================
1110
    // DELAYED NEUTRON SAMPLED
1111

1112
    // sampled delayed precursor group
1113
    double xi = prn(seed) * nu_d;
275,063 ✔
1114
    double prob = 0.0;
275,063 ✔
1115
    int group;
275,063 ✔
1116
    for (group = 1; group < nuc->n_precursor_; ++group) {
1,024,047 ✔
1117
      // determine delayed neutron precursor yield for group j
1118
      double yield = (*rx.products_[group].yield_)(E_in);
1,004,308 ✔
1119

1120
      // Check if this group is sampled
1121
      prob += yield;
1,004,308 ✔
1122
      if (xi < prob)
1,004,308 ✔
1123
        break;
1124
    }
1125

1126
    // if the sum of the probabilities is slightly less than one and the
1127
    // random number is greater, j will be greater than nuc %
1128
    // n_precursor -- check for this condition
1129
    group = std::min(group, nuc->n_precursor_);
275,063 !
1130

1131
    // set the delayed group for the particle born from fission
1132
    site->delayed_group = group;
275,063 ✔
1133

1134
    // Sample time of emission based on decay constant of precursor
1135
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
275,063 ✔
1136
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
275,063 ✔
1137

1138
  } else {
1139
    // ====================================================================
1140
    // PROMPT NEUTRON SAMPLED
1141

1142
    // set the delayed group for the particle born from fission to 0
1143
    site->delayed_group = 0;
41,324,938 ✔
1144
  }
1145

1146
  // sample from prompt neutron energy distribution
1147
  int n_sample = 0;
1148
  double mu;
41,600,001 ✔
1149
  while (true) {
41,600,001 ✔
1150
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
41,600,001 ✔
1151

1152
    // resample if energy is greater than maximum neutron energy
1153
    int neutron = ParticleType::neutron().transport_index();
41,600,001 ✔
1154
    if (site->E < data::energy_max[neutron])
41,600,001 !
1155
      break;
1156

1157
    // check for large number of resamples
1158
    ++n_sample;
×
1159
    if (n_sample == MAX_SAMPLE) {
×
1160
      // particle_write_restart(p)
1161
      fatal_error("Resampled energy distribution maximum number of times "
×
1162
                  "for nuclide " +
×
1163
                  nuc->name_);
×
1164
    }
1165
  }
1166

1167
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1168
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
41,600,001 ✔
1169
}
41,600,001 ✔
1170

1171
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
25,731,070 ✔
1172
{
1173
  // copy energy of neutron
1174
  double E_in = p.E();
25,731,070 ✔
1175

1176
  // sample outgoing energy and scattering cosine
1177
  double E;
25,731,070 ✔
1178
  double mu;
25,731,070 ✔
1179
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
25,731,070 ✔
1180

1181
  // if scattering system is in center-of-mass, transfer cosine of scattering
1182
  // angle and outgoing energy from CM to LAB
1183
  if (rx.scatter_in_cm_) {
25,731,070 ✔
1184
    double E_cm = E;
25,637,200 ✔
1185

1186
    // determine outgoing energy in lab
1187
    double A = nuc.awr_;
25,637,200 ✔
1188
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
25,637,200 ✔
1189
                 ((A + 1.0) * (A + 1.0));
25,637,200 ✔
1190

1191
    // determine outgoing angle in lab
1192
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
25,637,200 ✔
1193
  }
1194

1195
  // Because of floating-point roundoff, it may be possible for mu to be
1196
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1197
  // or 1
1198
  if (std::abs(mu) > 1.0)
25,731,070 !
1199
    mu = std::copysign(1.0, mu);
×
1200

1201
  // Set outgoing energy and scattering angle
1202
  p.E() = E;
25,731,070 ✔
1203
  p.mu() = mu;
25,731,070 ✔
1204

1205
  // change direction of particle
1206
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
25,731,070 ✔
1207

1208
  // evaluate yield
1209
  double yield = (*rx.products_[0].yield_)(E_in);
25,731,070 ✔
1210
  if (std::floor(yield) == yield && yield > 0) {
25,731,070 !
1211
    // If yield is integral, create exactly that many secondary particles
1212
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
25,870,843 ✔
1213
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
139,827 ✔
1214
    }
1215
  } else {
1216
    // Otherwise, change weight of particle based on yield
1217
    p.wgt() *= yield;
54 ✔
1218
  }
1219
}
25,731,070 ✔
1220

1221
void sample_secondary_photons(Particle& p, int i_nuclide)
77,865,359 ✔
1222
{
1223
  // Sample the number of photons produced
1224
  double y_t =
77,865,359 ✔
1225
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
77,865,359 ✔
1226
  double photon_wgt = p.wgt();
77,865,359 ✔
1227
  int y = 1;
77,865,359 ✔
1228

1229
  if (settings::use_decay_photons) {
77,865,359 ✔
1230
    // For decay photons, sample a single photon and modify the weight
1231
    if (y_t <= 0.0)
72,006 ✔
1232
      return;
1233
    photon_wgt *= y_t;
54,725 ✔
1234
  } else {
1235
    // For prompt photons, sample an integral number of photons with weight
1236
    // equal to the neutron's weight
1237
    y = static_cast<int>(y_t);
77,793,353 ✔
1238
    if (prn(p.current_seed()) <= y_t - y)
77,793,353 ✔
1239
      ++y;
2,118,952 ✔
1240
  }
1241

1242
  // Sample each secondary photon
1243
  for (int i = 0; i < y; ++i) {
80,784,946 ✔
1244
    // Sample the reaction and product
1245
    int i_rx;
2,936,868 ✔
1246
    int i_product;
2,936,868 ✔
1247
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
2,936,868 ✔
1248

1249
    // Sample the outgoing energy and angle
1250
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
2,936,868 ✔
1251
    double E;
2,936,868 ✔
1252
    double mu;
2,936,868 ✔
1253
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
2,936,868 ✔
1254

1255
    // Sample the new direction
1256
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
2,936,868 ✔
1257

1258
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1259
    // secondary photons from non-fission reactions to properly balance energy
1260
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1261
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1262
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1263
    double wgt = photon_wgt;
2,936,868 ✔
1264
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
2,936,868 ✔
1265
      wgt *= simulation::keff;
351,406 ✔
1266
    }
1267

1268
    // Create the secondary photon
1269
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
2,936,868 ✔
1270

1271
    // Pre-add photon energy to pht_storage so pht_secondary_particles()
1272
    // subtraction results in net zero
1273
    if (created_photon && !model::active_pulse_height_tallies.empty()) {
2,936,868 ✔
1274
      auto it = std::find(model::pulse_height_cells.begin(),
528 ✔
1275
        model::pulse_height_cells.end(), p.lowest_coord().cell());
528 !
1276
      if (it != model::pulse_height_cells.end()) {
528 !
1277
        int index = std::distance(model::pulse_height_cells.begin(), it);
528 ✔
1278
        p.pht_storage()[index] += E;
528 ✔
1279
      }
1280
    }
1281

1282
    // Tag secondary particle with parent nuclide
1283
    if (created_photon && settings::use_decay_photons) {
2,936,868 ✔
1284
      p.local_secondary_bank().back().parent_nuclide =
52,844 ✔
1285
        rx->products_[i_product].parent_nuclide_;
52,844 ✔
1286
    }
1287
  }
1288
}
1289

1290
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc