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

openmc-dev / openmc / 39481612286

20 Oct 2026 07:13PM UTC coverage: 80.772% (-0.7%) from 81.44%
39481612286

Pull #4176

github

web-flow
Merge 596ea72c3 into 9315d8301
Pull Request #4176: Build fine energy group structures

17719 of 25612 branches covered (69.18%)

Branch coverage included in aggregate %.

34 of 34 new or added lines in 2 files covered. (100.0%)

1081 existing lines in 50 files now uncovered.

59639 of 70161 relevant lines covered (85.0%)

18444085.23 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/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)
289,384,294 ✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
289,384,294 ✔
47
  p.secondary_bank_index() = p.local_secondary_bank().size();
289,384,294 !
48

49
  // Sample reaction for the material the particle is in
50
  switch (p.type().pdg_number()) {
289,384,294 !
51
  case PDG_NEUTRON:
283,115,814 ✔
52
    sample_neutron_reaction(p);
283,115,814 ✔
53
    break;
283,115,814 ✔
54
  case PDG_PHOTON:
6,248,480 ✔
55
    sample_photon_reaction(p);
6,248,480 ✔
56
    break;
6,248,480 ✔
57
  case PDG_ELECTRON:
20,000 ✔
58
    sample_electron_reaction(p);
20,000 ✔
59
    break;
20,000 ✔
60
  case PDG_POSITRON:
×
61
    sample_positron_reaction(p);
×
62
    break;
×
63
  default:
×
64
    fatal_error("Unsupported particle PDG for collision sampling.");
×
65
  }
66

67
  if (settings::weight_windows_on) {
289,384,294 ✔
68
    auto [ww_found, ww] = search_weight_window(p);
75,894,450 ✔
69
    if (!ww_found && p.type() == ParticleType::neutron()) {
75,894,450 ✔
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,588 ✔
73
    } else if (settings::weight_window_checkpoint_collision) {
75,884,862 !
74
      // if collision checkpointing is on, apply weight window
75
      apply_weight_window(p, ww);
75,884,862 ✔
76
    }
77
  }
78

79
  // Kill particle if energy falls below cutoff
80
  int type = p.type().transport_index();
289,384,294 !
81
  if (type != C_NONE && p.E() < settings::energy_cutoff[type]) {
289,384,294 !
82
    p.wgt() = 0.0;
1,476,365 ✔
83
  }
84

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

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

110
  // Save which nuclide particle had collision with
111
  p.event_nuclide() = i_nuclide;
283,115,814 ✔
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]};
283,115,814 ✔
119

120
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
283,115,814 ✔
121
    auto& rx = sample_fission(i_nuclide, p);
32,151,203 ✔
122
    if (settings::run_mode == RunMode::EIGENVALUE) {
32,151,203 ✔
123
      create_fission_sites(p, i_nuclide, rx);
27,640,139 ✔
124
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
4,511,064 ✔
125
               settings::create_fission_neutrons) {
126
      create_fission_sites(p, i_nuclide, rx);
66,328 ✔
127

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

141
  // Create secondary photons
142
  if (settings::photon_transport) {
283,115,814 ✔
143
    sample_secondary_photons(p, i_nuclide);
14,157,338 ✔
144
  }
145

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

149
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
283,115,814 ✔
150
    absorption(p, i_nuclide);
283,095,333 ✔
151
  }
152
  if (!p.alive())
283,115,814 ✔
153
    return;
154

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

164
  // Advance URR seed stream 'N' times after energy changes
165
  if (p.E() != p.E_last()) {
276,646,048 ✔
166
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
276,588,006 ✔
167
  }
168

169
  // Play russian roulette if there are no weight windows
170
  if (!settings::weight_windows_on)
276,646,048 ✔
171
    apply_russian_roulette(p);
205,992,006 ✔
172
}
173

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

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

185
  // Sample the number of neutrons produced
186
  int nu = static_cast<int>(nu_t);
27,706,467 ✔
187
  if (prn(p.current_seed()) <= (nu_t - nu))
27,706,467 ✔
188
    ++nu;
4,819,474 ✔
189

190
  // If no neutrons were produced then don't continue
191
  if (nu == 0)
27,706,467 ✔
192
    return;
21,821,027 ✔
193

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

198
  // Clear out particle's nu fission bank
199
  p.nu_bank().clear();
5,885,440 ✔
200

201
  p.fission() = true;
5,885,440 ✔
202

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

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

211
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
13,412,642 ✔
212
    // Initialize fission site object with particle data
213
    SourceSite site;
7,527,202 ✔
214
    site.r = p.r();
7,527,202 ✔
215
    site.particle = ParticleType::neutron();
7,527,202 ✔
216
    site.time = p.time();
7,527,202 ✔
217
    site.wgt = 1. / weight;
7,527,202 ✔
218
    site.surf_id = 0;
7,527,202 ✔
219

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

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

231
    // Set parent and progeny IDs
232
    site.parent_id = p.current_work();
7,527,202 ✔
233
    site.progeny_id = p.n_progeny()++;
7,527,202 ✔
234

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

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

249
        // Break out of loop as no more sites can be added to fission bank
250
        break;
×
251
      }
252
      // Iterated Fission Probability (IFP) method
253
      if (settings::ifp_on()) {
7,525,032 ✔
254
        ifp(p, idx);
445,656 ✔
255
      }
256
    } else {
257
      site.wgt_born = p.wgt_born();
2,170 ✔
258
      site.wgt_ww_born = p.wgt_ww_born();
2,170 ✔
259
      site.n_split = p.n_split();
2,170 ✔
260
      p.local_secondary_bank().push_back(site);
2,170 ✔
261
      p.n_secondaries()++;
2,170 ✔
262
    }
263

264
    // Increment the number of neutrons born delayed
265
    if (site.delayed_group > 0) {
7,527,202 ✔
266
      nu_d[site.delayed_group - 1]++;
49,845 ✔
267
    }
268

269
    // Write fission particles to nuBank
270
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
7,527,202 ✔
271
    nu_bank_entry.wgt = site.wgt;
7,527,202 ✔
272
    nu_bank_entry.E = site.E;
7,527,202 ✔
273
    nu_bank_entry.delayed_group = site.delayed_group;
7,527,202 ✔
274
  }
275

276
  // If shared fission bank was full, and no fissions could be added,
277
  // set the particle fission flag to false.
278
  if (n_sites_stored == 0) {
5,885,440 !
279
    p.fission() = false;
×
280
    return;
×
281
  }
282

283
  // Set nu to the number of fission sites successfully stored. If the fission
284
  // bank was not found to be full then these values are already equivalent.
285
  nu = n_sites_stored;
5,885,440 ✔
286

287
  // Store the total weight banked for analog fission tallies
288
  p.n_bank() = nu;
5,885,440 ✔
289
  p.wgt_bank() = nu / weight;
5,885,440 ✔
290
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
52,968,960 ✔
291
    p.n_delayed_bank(d) = nu_d[d];
47,083,520 ✔
292
  }
293
}
294

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

307
  // Sample element within material
308
  int i_element = sample_element(p);
6,248,470 ✔
309
  const auto& micro {p.photon_xs(i_element)};
6,248,470 ✔
310
  const auto& element {*data::elements[i_element]};
6,248,470 ✔
311

312
  // Calculate photon energy over electron rest mass equivalent
313
  double alpha = p.E() / MASS_ELECTRON_EV;
6,248,470 ✔
314

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

320
  // Coherent (Rayleigh) scattering
321
  prob += micro.coherent;
6,248,470 ✔
322
  if (prob > cutoff) {
6,248,470 ✔
323
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
303,458 ✔
324
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
303,458 ✔
325
    p.event() = TallyEvent::SCATTER;
303,458 ✔
326
    p.event_mt() = COHERENT;
303,458 ✔
327
    return;
303,458 ✔
328
  }
329

330
  // Incoherent (Compton) scattering
331
  prob += micro.incoherent;
5,945,012 ✔
332
  if (prob > cutoff) {
5,945,012 ✔
333
    double alpha_out;
4,470,643 ✔
334
    int i_shell;
4,470,643 ✔
335
    element.compton_scatter(
4,470,643 ✔
336
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
4,470,643 ✔
337

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

347
    // Create Compton electron
348
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
4,470,643 ✔
349
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
4,470,643 ✔
350
    int electron = ParticleType::electron().transport_index();
4,470,643 ✔
351
    if (E_electron >= settings::energy_cutoff[electron]) {
4,470,643 !
352
      double mu_electron = (alpha - alpha_out * p.mu()) /
4,470,643 ✔
353
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
4,470,643 ✔
354
                                     2.0 * alpha * alpha_out * p.mu());
4,470,643 ✔
355
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
4,470,643 ✔
356
      process_charged_secondary(p, u, E_electron, ParticleType::electron());
4,470,643 ✔
357
    }
358

359
    // Allow electrons to fill orbital and produce Auger electrons and
360
    // fluorescent photons. Since Compton subshell data does not match atomic
361
    // relaxation data, use the mapping between the data to find the subshell
362
    if (settings::atomic_relaxation && element.has_atomic_relaxation_ &&
4,442,087 !
363
        i_shell >= 0 && element.subshell_map_[i_shell] >= 0) {
8,912,730 !
364
      element.atomic_relaxation(element.subshell_map_[i_shell], p);
4,442,087 ✔
365
    }
366

367
    phi += PI;
4,470,643 ✔
368
    p.E() = alpha_out * MASS_ELECTRON_EV;
4,470,643 ✔
369
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
4,470,643 ✔
370
    p.event() = TallyEvent::SCATTER;
4,470,643 ✔
371
    p.event_mt() = INCOHERENT;
4,470,643 ✔
372
    return;
4,470,643 ✔
373
  }
374

375
  // Photoelectric effect
376
  double prob_after = prob + micro.photoelectric;
1,474,369 ✔
377

378
  if (prob_after > cutoff) {
1,474,369 ✔
379
    // Get grid index, interpolation factor, and bounding subshell
380
    // cross sections
381
    int i_grid = micro.index_grid;
1,437,851 ✔
382
    double f = micro.interp_factor;
1,437,851 ✔
383
    tensor::View<const double> xs_lower = element.cross_sections_.slice(i_grid);
1,437,851 ✔
384
    tensor::View<const double> xs_upper =
1,437,851 ✔
385
      element.cross_sections_.slice(i_grid + 1);
1,437,851 ✔
386

387
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
5,041,986 !
388
      const auto& shell {element.shells_[i_shell]};
5,041,986 ✔
389

390
      // Check threshold of reaction
391
      if (xs_lower(i_shell) == 0)
5,041,986 ✔
392
        continue;
1,893,653 ✔
393

394
      //  Evaluation subshell photoionization cross section
395
      prob += std::exp(
3,148,333 ✔
396
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
3,148,333 ✔
397

398
      if (prob > cutoff) {
3,148,333 ✔
399
        // Determine binding energy based on whether atomic relaxation data is
400
        // present (if not, use value from Compton profile data)
401
        double binding_energy = element.has_atomic_relaxation_
1,437,851 ✔
402
                                  ? shell.binding_energy
1,437,851 !
403
                                  : element.binding_energy_[i_shell];
×
404

405
        // Determine energy of secondary electron
406
        double E_electron = p.E() - binding_energy;
1,437,851 ✔
407

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

424
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
1,437,851 ✔
425
        Direction u;
1,437,851 ✔
426
        u.x = mu;
1,437,851 ✔
427
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
1,437,851 ✔
428
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
1,437,851 ✔
429

430
        // Process secondary electron at the photon collision site.
431
        process_charged_secondary(p, u, E_electron, ParticleType::electron());
1,437,851 ✔
432

433
        // Allow electrons to fill orbital and produce auger electrons
434
        // and fluorescent photons
435
        if (settings::atomic_relaxation) {
1,437,851 ✔
436
          element.atomic_relaxation(i_shell, p);
1,397,851 ✔
437
        }
438
        p.event() = TallyEvent::ABSORB;
1,437,851 ✔
439
        p.event_mt() = 533 + shell.index_subshell;
1,437,851 ✔
440
        p.wgt() = 0.0;
1,437,851 ✔
441
        p.E() = 0.0;
1,437,851 ✔
442
        return;
1,437,851 ✔
443
      }
444
    }
445
  }
2,875,702 ✔
446
  prob = prob_after;
36,518 ✔
447

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

456
    // Process secondary electron at the photon collision site.
457
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
36,518 ✔
458
    process_charged_secondary(p, u, E_electron, ParticleType::electron());
36,518 ✔
459

460
    // Process secondary positron at the photon collision site.
461
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
36,518 ✔
462
    process_charged_secondary(p, u, E_positron, ParticleType::positron());
36,518 ✔
463
    p.event() = TallyEvent::ABSORB;
36,518 ✔
464
    p.event_mt() = PAIR_PROD;
36,518 ✔
465
    p.wgt() = 0.0;
36,518 ✔
466
    p.E() = 0.0;
36,518 ✔
467
  }
468
}
469

470
void process_charged_secondary(
15,955,430 ✔
471
  Particle& p, Direction u, double E, ParticleType type)
472
{
473
  int idx = type.transport_index();
15,955,430 ✔
474
  if (idx == C_NONE)
15,955,430 !
475
    return;
476

477
  // Below the energy cutoff, the kinetic energy is deposited locally without
478
  // producing bremsstrahlung
479
  if (E >= settings::energy_cutoff[idx] &&
15,955,430 ✔
480
      settings::electron_treatment == ElectronTreatment::TTB) {
15,955,126 ✔
481
    thick_target_bremsstrahlung(p, type, u, E);
15,820,142 ✔
482
  }
483

484
  // A positron annihilates regardless of its kinetic energy
485
  if (type == ParticleType::positron()) {
15,955,430 ✔
486
    Direction photon_u = isotropic_direction(p.current_seed());
36,518 ✔
487
    p.create_secondary(
36,518 ✔
488
      p.wgt(), photon_u, MASS_ELECTRON_EV, ParticleType::photon());
36,518 ✔
489
    p.create_secondary(
36,518 ✔
490
      p.wgt(), -photon_u, MASS_ELECTRON_EV, ParticleType::photon());
36,518 ✔
491

492
    // The annihilation photons are now emitted during the parent photon
493
    // collision. Offset the pair-production Q value in the energy balance so
494
    // heating matches the prior explicit positron slowing-down sequence.
495
    p.bank_second_E() -= 2 * MASS_ELECTRON_EV;
36,518 ✔
496
  }
497
}
498

499
void sample_electron_reaction(Particle& p)
20,000 ✔
500
{
501
  // TODO: create reaction types
502

503
  if (settings::electron_treatment == ElectronTreatment::TTB) {
20,000 !
504
    thick_target_bremsstrahlung(p);
20,000 ✔
505
  }
506

507
  p.E() = 0.0;
20,000 ✔
508
  p.wgt() = 0.0;
20,000 ✔
509
  p.event() = TallyEvent::ABSORB;
20,000 ✔
510
}
20,000 ✔
511

512
void sample_positron_reaction(Particle& p)
×
513
{
514
  // TODO: create reaction types
515

516
  if (settings::electron_treatment == ElectronTreatment::TTB) {
×
517
    thick_target_bremsstrahlung(p);
×
518
  }
519

520
  // Sample angle isotropically
521
  Direction u = isotropic_direction(p.current_seed());
×
522

523
  // Create annihilation photon pair traveling in opposite directions
524
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon());
×
525
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon());
×
526

527
  p.E() = 0.0;
×
528
  p.wgt() = 0.0;
×
529
  p.event() = TallyEvent::ABSORB;
×
530
}
×
531

532
int sample_nuclide(Particle& p)
283,115,814 ✔
533
{
534
  // Sample cumulative distribution function
535
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
283,115,814 ✔
536

537
  // Get pointers to nuclide/density arrays
538
  const auto& mat {model::materials[p.material()]};
283,115,814 ✔
539
  int n = mat->nuclide_.size();
283,115,814 ✔
540

541
  double prob = 0.0;
283,115,814 ✔
542
  for (int i = 0; i < n; ++i) {
597,041,216 !
543
    // Get atom density
544
    int i_nuclide = mat->nuclide_[i];
597,041,216 ✔
545
    double atom_density = mat->atom_density(i, p.density_mult());
597,041,216 ✔
546

547
    // Increment probability to compare to cutoff
548
    prob += atom_density * p.neutron_xs(i_nuclide).total;
597,041,216 ✔
549
    if (prob >= cutoff)
597,041,216 ✔
550
      return i_nuclide;
283,115,814 ✔
551
  }
552

553
  // If we reach here, no nuclide was sampled
554
  p.write_restart();
×
555
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
556
}
557

558
int sample_element(Particle& p)
6,248,470 ✔
559
{
560
  // Sample cumulative distribution function
561
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
6,248,470 ✔
562

563
  // Get pointers to elements, densities
564
  const auto& mat {model::materials[p.material()]};
6,248,470 ✔
565

566
  double prob = 0.0;
6,248,470 ✔
567
  for (int i = 0; i < mat->element_.size(); ++i) {
25,196,426 !
568
    // Find atom density
569
    int i_element = mat->element_[i];
25,196,426 ✔
570
    double atom_density = mat->atom_density(i, p.density_mult());
25,196,426 ✔
571

572
    // Determine microscopic cross section
573
    double sigma = atom_density * p.photon_xs(i_element).total;
25,196,426 ✔
574

575
    // Increment probability to compare to cutoff
576
    prob += sigma;
25,196,426 ✔
577
    if (prob > cutoff) {
25,196,426 ✔
578
      // Save which nuclide particle had collision with for tally purpose
579
      p.event_nuclide() = mat->nuclide_[i];
6,248,470 ✔
580

581
      return i_element;
6,248,470 ✔
582
    }
583
  }
584

585
  // If we made it here, no element was sampled
586
  p.write_restart();
×
587
  fatal_error("Did not sample any element during collision.");
×
588
}
589

590
Reaction& sample_fission(int i_nuclide, Particle& p)
32,151,203 ✔
591
{
592
  // Get pointer to nuclide
593
  const auto& nuc {data::nuclides[i_nuclide]};
32,151,203 ✔
594

595
  // If we're in the URR, by default use the first fission reaction. We also
596
  // default to the first reaction if we know that there are no partial fission
597
  // reactions
598
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
32,151,203 ✔
599
    return *nuc->fission_rx_[0];
32,141,625 ✔
600
  }
601

602
  // Check to see if we are in a windowed multipole range.  WMP only supports
603
  // the first fission reaction.
604
  if (nuc->multipole_) {
9,578 ✔
605
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
518 !
606
      return *nuc->fission_rx_[0];
362 ✔
607
    }
608
  }
609

610
  // Get grid index and interpolation factor and sample fission cdf
611
  const auto& micro = p.neutron_xs(i_nuclide);
9,216 ✔
612
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
9,216 ✔
613
  double prob = 0.0;
9,216 ✔
614

615
  // Loop through each partial fission reaction type
616
  for (auto& rx : nuc->fission_rx_) {
9,232 !
617
    // add to cumulative probability
618
    prob += rx->xs(micro);
9,232 ✔
619

620
    // Create fission bank sites if fission occurs
621
    if (prob > cutoff)
9,232 ✔
622
      return *rx;
9,216 ✔
623
  }
624

625
  // If we reached here, no reaction was sampled
626
  throw std::runtime_error {
×
627
    "No fission reaction was sampled for " + nuc->name_};
×
628
}
629

630
void sample_photon_product(
533,976 ✔
631
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
632
{
633
  // Get grid index and interpolation factor and sample photon production cdf
634
  const auto& micro = p.neutron_xs(i_nuclide);
533,976 ✔
635
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
533,976 ✔
636
  double prob = 0.0;
533,976 ✔
637

638
  // Loop through each reaction type
639
  const auto& nuc {data::nuclides[i_nuclide]};
533,976 ✔
640
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
9,852,570 !
641
    // Evaluate neutron cross section
642
    const auto& rx = nuc->reactions_[i];
9,852,570 ✔
643
    double xs = rx->xs(micro);
9,852,570 ✔
644

645
    // if cross section is zero for this reaction, skip it
646
    if (xs == 0.0)
9,852,570 ✔
647
      continue;
6,140,450 ✔
648

649
    for (int j = 0; j < rx->products_.size(); ++j) {
27,074,656 ✔
650
      if (rx->products_[j].particle_.is_photon()) {
23,896,512 ✔
651
        // For fission, artificially increase the photon yield to account
652
        // for delayed photons
653
        double f = 1.0;
21,082,234 ✔
654
        if (settings::delayed_photon_scaling) {
21,082,234 !
655
          if (is_fission(rx->mt_)) {
21,082,234 ✔
656
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
98,332 !
657
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
98,332 ✔
658
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
98,332 ✔
659
              f = (energy_prompt + energy_delayed) / (energy_prompt);
98,332 ✔
660
            }
661
          }
662
        }
663

664
        // add to cumulative probability
665
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
21,082,234 ✔
666

667
        *i_rx = i;
21,082,234 ✔
668
        *i_product = j;
21,082,234 ✔
669
        if (prob > cutoff)
21,082,234 ✔
670
          return;
671
      }
672
    }
673
  }
674
}
675

676
void absorption(Particle& p, int i_nuclide)
283,095,333 ✔
677
{
678
  if (settings::survival_biasing) {
283,095,333 ✔
679
    // Determine weight absorbed in survival biasing
680
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
7,569,542 ✔
681
                              p.neutron_xs(i_nuclide).total;
7,569,542 ✔
682

683
    // Adjust weight of particle by probability of absorption
684
    p.wgt() -= wgt_absorb;
7,569,542 ✔
685

686
    // Score implicit absorption estimate of keff
687
    if (settings::run_mode == RunMode::EIGENVALUE) {
7,569,542 ✔
688
      p.keff_tally_absorption() += wgt_absorb *
90,900 ✔
689
                                   p.neutron_xs(i_nuclide).nu_fission /
90,900 ✔
690
                                   p.neutron_xs(i_nuclide).absorption;
90,900 ✔
691
    }
692
  } else {
693
    // See if disappearance reaction happens
694
    if (p.neutron_xs(i_nuclide).absorption >
275,525,791 ✔
695
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
275,525,791 ✔
696
      // Score absorption estimate of keff
697
      if (settings::run_mode == RunMode::EIGENVALUE) {
6,469,490 ✔
698
        p.keff_tally_absorption() += p.wgt() *
4,642,250 ✔
699
                                     p.neutron_xs(i_nuclide).nu_fission /
4,642,250 ✔
700
                                     p.neutron_xs(i_nuclide).absorption;
4,642,250 ✔
701
      }
702

703
      p.wgt() = 0.0;
6,469,490 ✔
704
      p.event() = TallyEvent::ABSORB;
6,469,490 ✔
705
      if (!p.fission()) {
6,469,490 ✔
706
        p.event_mt() = N_DISAPPEAR;
4,139,590 ✔
707
      }
708
    }
709
  }
710
}
283,095,333 ✔
711

712
void scatter(Particle& p, int i_nuclide)
276,617,170 ✔
713
{
714
  // copy incoming direction
715
  Direction u_old {p.u()};
276,617,170 ✔
716

717
  // Get pointer to nuclide and grid index/interpolation factor
718
  const auto& nuc {data::nuclides[i_nuclide]};
276,617,170 ✔
719
  const auto& micro {p.neutron_xs(i_nuclide)};
276,617,170 ✔
720
  int i_temp = micro.index_temp;
276,617,170 ✔
721

722
  // For tallying purposes, this routine might be called directly. In that
723
  // case, we need to sample a reaction via the cutoff variable
724
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
276,617,170 ✔
725
  bool sampled = false;
276,617,170 ✔
726

727
  // Calculate elastic cross section if it wasn't precalculated
728
  if (micro.elastic == CACHE_INVALID) {
276,617,170 ✔
729
    nuc->calculate_elastic_xs(p);
229,847,257 ✔
730
  }
731

732
  double prob = micro.elastic - micro.thermal;
276,617,170 ✔
733
  if (prob > cutoff) {
276,617,170 ✔
734
    // =======================================================================
735
    // NON-S(A,B) ELASTIC SCATTERING
736

737
    // Determine temperature
738
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
248,481,812 ✔
739

740
    // Perform collision physics for elastic scattering
741
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
248,481,812 ✔
742

743
    p.event_mt() = ELASTIC;
248,481,812 ✔
744
    sampled = true;
248,481,812 ✔
745
  }
746

747
  prob = micro.elastic;
276,617,170 ✔
748
  if (prob > cutoff && !sampled) {
276,617,170 ✔
749
    // =======================================================================
750
    // S(A,B) SCATTERING
751

752
    sab_scatter(i_nuclide, micro.index_sab, p);
23,474,190 ✔
753

754
    p.event_mt() = ELASTIC;
23,474,190 ✔
755
    sampled = true;
23,474,190 ✔
756
  }
757

758
  if (!sampled) {
276,617,170 ✔
759
    // =======================================================================
760
    // INELASTIC SCATTERING
761

762
    int n = nuc->index_inelastic_scatter_.size();
4,661,168 ✔
763
    int i = 0;
4,661,168 ✔
764
    for (int j = 0; j < n && prob < cutoff; ++j) {
88,805,736 ✔
765
      i = nuc->index_inelastic_scatter_[j];
84,144,568 ✔
766

767
      // add to cumulative probability
768
      prob += nuc->reactions_[i]->xs(micro);
84,144,568 ✔
769
    }
770

771
    // Perform collision physics for inelastic scattering
772
    const auto& rx {nuc->reactions_[i]};
4,661,168 ✔
773
    inelastic_scatter(*nuc, *rx, p);
4,661,168 ✔
774
    p.event_mt() = rx->mt_;
4,661,168 ✔
775
  }
776

777
  // Set event component
778
  p.event() = TallyEvent::SCATTER;
276,617,170 ✔
779

780
  // Sample new outgoing angle for isotropic-in-lab scattering
781
  const auto& mat {model::materials[p.material()]};
276,617,170 !
782
  if (!mat->p0_.empty()) {
276,617,170 !
783
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
59,340 ✔
784
    if (mat->p0_[i_nuc_mat]) {
59,340 !
785
      // Sample isotropic-in-lab outgoing direction
786
      p.u() = isotropic_direction(p.current_seed());
59,340 ✔
787
      p.mu() = u_old.dot(p.u());
59,340 ✔
788
    }
789
  }
790
}
276,617,170 ✔
791

792
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
248,481,812 ✔
793
{
794
  // get pointer to nuclide
795
  const auto& nuc {data::nuclides[i_nuclide]};
248,481,812 ✔
796

797
  double vel = std::sqrt(p.E());
248,481,812 ✔
798
  double awr = nuc->awr_;
248,481,812 ✔
799

800
  // Neutron velocity in LAB
801
  Direction v_n = vel * p.u();
248,481,812 ✔
802

803
  // Sample velocity of target nucleus
804
  Direction v_t {};
248,481,812 ✔
805
  if (!p.neutron_xs(i_nuclide).use_ptable) {
248,481,812 ✔
806
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
239,840,386 ✔
807
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
239,840,386 ✔
808
  }
809

810
  // Velocity of center-of-mass
811
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
248,481,812 ✔
812

813
  // Transform to CM frame
814
  v_n -= v_cm;
248,481,812 ✔
815

816
  // Find speed of neutron in CM
817
  vel = v_n.norm();
248,481,812 ✔
818

819
  // Sample scattering angle, checking if angle distribution is present (assume
820
  // isotropic otherwise)
821
  double mu_cm;
248,481,812 ✔
822
  auto& d = rx.products_[0].distribution_[0];
248,481,812 !
823
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
248,481,812 !
824
  if (!d_->angle().empty()) {
248,481,812 !
825
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
248,481,812 ✔
826
  } else {
827
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
828
  }
829

830
  // Determine direction cosines in CM
831
  Direction u_cm = v_n / vel;
248,481,812 ✔
832

833
  // Rotate neutron velocity vector to new angle -- note that the speed of the
834
  // neutron in CM does not change in elastic scattering. However, the speed
835
  // will change when we convert back to LAB
836
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
248,481,812 ✔
837

838
  // Transform back to LAB frame
839
  v_n += v_cm;
248,481,812 ✔
840

841
  p.E() = v_n.dot(v_n);
248,481,812 ✔
842
  vel = std::sqrt(p.E());
248,481,812 ✔
843

844
  // compute cosine of scattering angle in LAB frame by taking dot product of
845
  // neutron's pre- and post-collision angle
846
  p.mu() = p.u().dot(v_n) / vel;
248,481,812 ✔
847

848
  // Set energy and direction of particle in LAB frame
849
  p.u() = v_n / vel;
248,481,812 !
850

851
  // Because of floating-point roundoff, it may be possible for mu_lab to be
852
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
853
  // -1 or 1
854
  if (std::abs(p.mu()) > 1.0)
248,481,812 !
855
    p.mu() = std::copysign(1.0, p.mu());
×
856
}
248,481,812 ✔
857

858
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
23,474,190 ✔
859
{
860
  // Determine temperature index
861
  const auto& micro {p.neutron_xs(i_nuclide)};
23,474,190 ✔
862
  int i_temp = micro.index_temp_sab;
23,474,190 ✔
863

864
  // Sample energy and angle
865
  double E_out;
23,474,190 ✔
866
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
23,474,190 ✔
867
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
23,474,190 ✔
868

869
  // Set energy to outgoing, change direction of particle
870
  p.E() = E_out;
23,474,190 ✔
871
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
23,474,190 ✔
872
}
23,474,190 ✔
873

874
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
239,840,386 ✔
875
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
876
{
877
  // check if nuclide is a resonant scatterer
878
  ResScatMethod sampling_method;
239,840,386 ✔
879
  if (nuc.resonant_) {
239,840,386 ✔
880

881
    // sampling method to use
882
    sampling_method = settings::res_scat_method;
15,374 ✔
883

884
    // upper resonance scattering energy bound (target is at rest above this E)
885
    if (E > settings::res_scat_energy_max) {
15,374 ✔
886
      return {};
7,410 ✔
887

888
      // lower resonance scattering energy bound (should be no resonances below)
889
    } else if (E < settings::res_scat_energy_min) {
7,964 ✔
890
      sampling_method = ResScatMethod::cxs;
891
    }
892

893
    // otherwise, use free gas model
894
  } else {
895
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
239,825,012 ✔
896
      return {};
89,580,409 ✔
897
    } else {
898
      sampling_method = ResScatMethod::cxs;
899
    }
900
  }
901

902
  // use appropriate target velocity sampling method
903
  switch (sampling_method) {
3,420 !
904
  case ResScatMethod::cxs:
150,249,147 ✔
905

906
    // sample target velocity with the constant cross section (cxs) approx.
907
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
150,249,147 ✔
908

909
  case ResScatMethod::dbrc:
3,420 ✔
910
  case ResScatMethod::rvs: {
3,420 ✔
911
    double E_red = std::sqrt(nuc.awr_ * E / kT);
3,420 ✔
912
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
6,840 !
913
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
3,420 ✔
914

915
    // find lower and upper energy bound indices
916
    // lower index
917
    int i_E_low;
3,420 ✔
918
    if (E_low < nuc.energy_0K_.front()) {
3,420 !
919
      i_E_low = 0;
920
    } else if (E_low > nuc.energy_0K_.back()) {
3,420 !
921
      i_E_low = nuc.energy_0K_.size() - 2;
×
922
    } else {
923
      i_E_low =
3,420 ✔
924
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
3,420 ✔
925
    }
926

927
    // upper index
928
    int i_E_up;
3,420 ✔
929
    if (E_up < nuc.energy_0K_.front()) {
3,420 !
930
      i_E_up = 0;
931
    } else if (E_up > nuc.energy_0K_.back()) {
3,420 !
932
      i_E_up = nuc.energy_0K_.size() - 2;
×
933
    } else {
934
      i_E_up =
3,420 ✔
935
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
3,420 ✔
936
    }
937

938
    if (i_E_up == i_E_low) {
3,420 ✔
939
      // Handle degenerate case -- if the upper/lower bounds occur for the same
940
      // index, then using cxs is probably a good approximation
941
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
3,420 ✔
942
    }
943

944
    if (sampling_method == ResScatMethod::dbrc) {
2,824 !
945
      // interpolate xs since we're not exactly at the energy indices
946
      double xs_low = nuc.elastic_0K_[i_E_low];
×
947
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
948
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
949
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
950
      double xs_up = nuc.elastic_0K_[i_E_up];
×
951
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
952
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
953
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
954

955
      // get max 0K xs value over range of practical relative energies
956
      double xs_max = *std::max_element(
×
957
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
958
      xs_max = std::max({xs_low, xs_max, xs_up});
×
959

960
      while (true) {
×
961
        double E_rel;
×
962
        Direction v_target;
×
963
        while (true) {
×
964
          // sample target velocity with the constant cross section (cxs)
965
          // approx.
966
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
967
          Direction v_rel = v_neut - v_target;
×
968
          E_rel = v_rel.dot(v_rel);
×
969
          if (E_rel < E_up)
×
970
            break;
971
        }
972

973
        // perform Doppler broadening rejection correction (dbrc)
974
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
975
        double R = xs_0K / xs_max;
×
976
        if (prn(seed) < R)
×
977
          return v_target;
×
978
      }
979

980
    } else if (sampling_method == ResScatMethod::rvs) {
2,824 ✔
981
      // interpolate xs CDF since we're not exactly at the energy indices
982
      // cdf value at lower bound attainable energy
983
      double cdf_low = 0.0;
2,824 ✔
984
      if (E_low > nuc.energy_0K_.front()) {
2,824 !
985
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
2,824 ✔
986
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
2,824 ✔
987
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
2,824 ✔
988
      }
989

990
      // cdf value at upper bound attainable energy
991
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
2,824 ✔
992
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
2,824 ✔
993
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
2,824 ✔
994

995
      while (true) {
59,256 ✔
996
        // directly sample Maxwellian
997
        double E_t = -kT * std::log(prn(seed));
31,040 ✔
998

999
        // sample a relative energy using the xs cdf
1000
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
31,040 ✔
1001
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
31,040 ✔
1002
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
31,040 ✔
1003
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
31,040 ✔
1004
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
31,040 ✔
1005
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
31,040 ✔
1006
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
31,040 ✔
1007
                     nuc.energy_0K_[i_E_low + i_E_rel]);
31,040 ✔
1008
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
31,040 ✔
1009

1010
        // perform rejection sampling on cosine between
1011
        // neutron and target velocities
1012
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
31,040 ✔
1013
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
31,040 ✔
1014

1015
        if (std::abs(mu) < 1.0) {
31,040 ✔
1016
          // set and accept target velocity
1017
          E_t /= nuc.awr_;
2,824 ✔
1018
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
2,824 ✔
1019
        }
1020
      }
28,216 ✔
1021
    }
1022
  } // case RVS, DBRC
1023
  } // switch (sampling_method)
1024

1025
  UNREACHABLE();
×
1026
}
1027

1028
Direction sample_cxs_target_velocity(
150,249,743 ✔
1029
  double awr, double E, Direction u, double kT, uint64_t* seed)
1030
{
1031
  double beta_vn = std::sqrt(awr * E / kT);
150,249,743 ✔
1032
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
150,249,743 ✔
1033

1034
  double beta_vt_sq;
183,513,423 ✔
1035
  double mu;
183,513,423 ✔
1036
  while (true) {
183,513,423 ✔
1037
    // Sample two random numbers
1038
    double r1 = prn(seed);
183,513,423 ✔
1039
    double r2 = prn(seed);
183,513,423 ✔
1040

1041
    if (prn(seed) < alpha) {
183,513,423 ✔
1042
      // With probability alpha, we sample the distribution p(y) =
1043
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1044
      // Carlo sampler
1045

1046
      beta_vt_sq = -std::log(r1 * r2);
52,926,848 ✔
1047

1048
    } else {
1049
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1050
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1051
      // Carlo sampler
1052

1053
      double c = std::cos(PI / 2.0 * prn(seed));
130,586,575 ✔
1054
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
130,586,575 ✔
1055
    }
1056

1057
    // Determine beta * vt
1058
    double beta_vt = std::sqrt(beta_vt_sq);
183,513,423 ✔
1059

1060
    // Sample cosine of angle between neutron and target velocity
1061
    mu = uniform_distribution(-1., 1., seed);
183,513,423 ✔
1062

1063
    // Determine rejection probability
1064
    double accept_prob =
183,513,423 ✔
1065
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
183,513,423 ✔
1066
      (beta_vn + beta_vt);
183,513,423 ✔
1067

1068
    // Perform rejection sampling on vt and mu
1069
    if (prn(seed) < accept_prob)
183,513,423 ✔
1070
      break;
1071
  }
1072

1073
  // Determine speed of target nucleus
1074
  double vt = std::sqrt(beta_vt_sq * kT / awr);
150,249,743 ✔
1075

1076
  // Determine velocity vector of target nucleus based on neutron's velocity
1077
  // and the sampled angle between them
1078
  return vt * rotate_angle(u, mu, nullptr, seed);
150,249,743 ✔
1079
}
1080

1081
void sample_fission_neutron(
7,527,202 ✔
1082
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1083
{
1084
  // Get attributes of particle
1085
  double E_in = p.E();
7,527,202 ✔
1086
  uint64_t* seed = p.current_seed();
7,527,202 ✔
1087

1088
  // Determine total nu, delayed nu, and delayed neutron fraction
1089
  const auto& nuc {data::nuclides[i_nuclide]};
7,527,202 ✔
1090
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
7,527,202 ✔
1091
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
7,527,202 ✔
1092
  double beta = nu_d / nu_t;
7,527,202 ✔
1093

1094
  if (prn(seed) < beta) {
7,527,202 ✔
1095
    // ====================================================================
1096
    // DELAYED NEUTRON SAMPLED
1097

1098
    // sampled delayed precursor group
1099
    double xi = prn(seed) * nu_d;
49,845 ✔
1100
    double prob = 0.0;
49,845 ✔
1101
    int group;
49,845 ✔
1102
    for (group = 1; group < nuc->n_precursor_; ++group) {
185,954 ✔
1103
      // determine delayed neutron precursor yield for group j
1104
      double yield = (*rx.products_[group].yield_)(E_in);
182,347 ✔
1105

1106
      // Check if this group is sampled
1107
      prob += yield;
182,347 ✔
1108
      if (xi < prob)
182,347 ✔
1109
        break;
1110
    }
1111

1112
    // if the sum of the probabilities is slightly less than one and the
1113
    // random number is greater, j will be greater than nuc %
1114
    // n_precursor -- check for this condition
1115
    group = std::min(group, nuc->n_precursor_);
49,845 !
1116

1117
    // set the delayed group for the particle born from fission
1118
    site->delayed_group = group;
49,845 ✔
1119

1120
    // Sample time of emission based on decay constant of precursor
1121
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
49,845 ✔
1122
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
49,845 ✔
1123

1124
  } else {
1125
    // ====================================================================
1126
    // PROMPT NEUTRON SAMPLED
1127

1128
    // set the delayed group for the particle born from fission to 0
1129
    site->delayed_group = 0;
7,477,357 ✔
1130
  }
1131

1132
  // sample from prompt neutron energy distribution
1133
  int n_sample = 0;
1134
  double mu;
7,527,203 ✔
1135
  while (true) {
7,527,203 ✔
1136
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
7,527,203 ✔
1137

1138
    // resample if energy is greater than maximum neutron energy
1139
    int neutron = ParticleType::neutron().transport_index();
7,527,203 ✔
1140
    if (site->E < data::energy_max[neutron])
7,527,203 ✔
1141
      break;
1142

1143
    // check for large number of resamples
1144
    ++n_sample;
1 ✔
1145
    if (n_sample == MAX_SAMPLE) {
1 !
1146
      // particle_write_restart(p)
1147
      fatal_error("Resampled energy distribution maximum number of times "
×
1148
                  "for nuclide " +
×
1149
                  nuc->name_);
×
1150
    }
1151
  }
1152

1153
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1154
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
7,527,202 ✔
1155
}
7,527,202 ✔
1156

1157
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
4,661,168 ✔
1158
{
1159
  // copy energy of neutron
1160
  double E_in = p.E();
4,661,168 ✔
1161

1162
  // sample outgoing energy and scattering cosine
1163
  double E;
4,661,168 ✔
1164
  double mu;
4,661,168 ✔
1165
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
4,661,168 ✔
1166

1167
  // if scattering system is in center-of-mass, transfer cosine of scattering
1168
  // angle and outgoing energy from CM to LAB
1169
  if (rx.scatter_in_cm_) {
4,661,168 ✔
1170
    double E_cm = E;
4,644,138 ✔
1171

1172
    // determine outgoing energy in lab
1173
    double A = nuc.awr_;
4,644,138 ✔
1174
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
4,644,138 ✔
1175
                 ((A + 1.0) * (A + 1.0));
4,644,138 ✔
1176

1177
    // determine outgoing angle in lab
1178
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
4,644,138 ✔
1179
  }
1180

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

1187
  // Set outgoing energy and scattering angle
1188
  p.E() = E;
4,661,168 ✔
1189
  p.mu() = mu;
4,661,168 ✔
1190

1191
  // change direction of particle
1192
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
4,661,168 ✔
1193

1194
  // evaluate yield
1195
  double yield = (*rx.products_[0].yield_)(E_in);
4,661,168 ✔
1196
  if (std::floor(yield) == yield && yield > 0) {
4,661,168 !
1197
    // If yield is integral, create exactly that many secondary particles
1198
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
4,683,854 ✔
1199
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron());
22,686 ✔
1200
    }
1201
  } else {
1202
    // Otherwise, change weight of particle based on yield
UNCOV
1203
    p.wgt() *= yield;
×
1204
  }
1205
}
4,661,168 ✔
1206

1207
void sample_secondary_photons(Particle& p, int i_nuclide)
14,157,338 ✔
1208
{
1209
  // Sample the number of photons produced
1210
  double y_t =
14,157,338 ✔
1211
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
14,157,338 ✔
1212
  double photon_wgt = p.wgt();
14,157,338 ✔
1213
  int y = 1;
14,157,338 ✔
1214

1215
  if (settings::use_decay_photons) {
14,157,338 ✔
1216
    // For decay photons, sample a single photon and modify the weight
1217
    if (y_t <= 0.0)
13,092 ✔
1218
      return;
1219
    photon_wgt *= y_t;
9,950 ✔
1220
  } else {
1221
    // For prompt photons, sample an integral number of photons with weight
1222
    // equal to the neutron's weight
1223
    y = static_cast<int>(y_t);
14,144,246 ✔
1224
    if (prn(p.current_seed()) <= y_t - y)
14,144,246 ✔
1225
      ++y;
385,264 ✔
1226
  }
1227

1228
  // Sample each secondary photon
1229
  for (int i = 0; i < y; ++i) {
14,688,172 ✔
1230
    // Sample the reaction and product
1231
    int i_rx;
533,976 ✔
1232
    int i_product;
533,976 ✔
1233
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
533,976 ✔
1234

1235
    // Sample the outgoing energy and angle
1236
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
533,976 ✔
1237
    double E;
533,976 ✔
1238
    double mu;
533,976 ✔
1239
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
533,976 ✔
1240

1241
    // Sample the new direction
1242
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
533,976 ✔
1243

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

1254
    // Create the secondary photon
1255
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon());
533,976 ✔
1256

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

1268
    // Tag secondary particle with parent nuclide
1269
    if (created_photon && settings::use_decay_photons) {
533,976 ✔
1270
      p.local_secondary_bank().back().parent_nuclide =
9,608 ✔
1271
        rx->products_[i_product].parent_nuclide_;
9,608 ✔
1272
    }
1273
  }
1274
}
1275

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