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

openmc-dev / openmc / 21489137916

29 Jan 2026 05:58PM UTC coverage: 81.248% (-0.7%) from 81.953%
21489137916

Pull #3757

github

web-flow
Merge 9d16542dd into f7a734189
Pull Request #3757: Testing point detectors

17267 of 24417 branches covered (70.72%)

Branch coverage included in aggregate %.

94 of 512 new or added lines in 26 files covered. (18.36%)

3 existing lines in 2 files now uncovered.

55822 of 65541 relevant lines covered (85.17%)

43735001.71 hits per line

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

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

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

32
#include <fmt/core.h>
33

34
#include <algorithm> // for max, min, max_element
35
#include <cmath>     // for sqrt, exp, log, abs, copysign
36
#include <xtensor/xview.hpp>
37

38
namespace openmc {
39

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

44
void collision(Particle& p)
943,847,945✔
45
{
46
  // Add to collision counter for particle
47
  ++(p.n_collision());
943,847,945✔
48

49
  // Sample reaction for the material the particle is in
50
  switch (p.type()) {
943,847,945!
51
  case ParticleType::neutron:
873,124,173✔
52
    sample_neutron_reaction(p);
873,124,173✔
53
    break;
873,124,173✔
54
  case ParticleType::photon:
17,256,831✔
55
    sample_photon_reaction(p);
17,256,831✔
56
    break;
17,256,831✔
57
  case ParticleType::electron:
53,379,678✔
58
    sample_electron_reaction(p);
53,379,678✔
59
    break;
53,379,678✔
60
  case ParticleType::positron:
87,263✔
61
    sample_positron_reaction(p);
87,263✔
62
    break;
87,263✔
63
  }
64

65
  if (settings::weight_window_checkpoint_collision)
943,847,945!
66
    apply_weight_windows(p);
943,847,945✔
67

68
  // Kill particle if energy falls below cutoff
69
  int type = static_cast<int>(p.type());
943,847,945✔
70
  if (p.E() < settings::energy_cutoff[type]) {
943,847,945✔
71
    p.wgt() = 0.0;
5,186,399✔
72
  }
73

74
  // Display information about collision
75
  if (settings::verbosity >= 10 || p.trace()) {
943,847,945!
76
    std::string msg;
66✔
77
    if (p.event() == TallyEvent::KILL) {
66!
78
      msg = fmt::format("    Killed. Energy = {} eV.", p.E());
×
79
    } else if (p.type() == ParticleType::neutron) {
66!
80
      msg = fmt::format("    {} with {}. Energy = {} eV.",
186✔
81
        reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_,
132✔
82
        p.E());
66✔
83
    } else if (p.type() == ParticleType::photon) {
×
84
      msg = fmt::format("    {} with {}. Energy = {} eV.",
×
85
        reaction_name(p.event_mt()),
×
86
        to_element(data::nuclides[p.event_nuclide()]->name_), p.E());
×
87
    } else {
88
      msg = fmt::format("    Disappeared. Energy = {} eV.", p.E());
×
89
    }
90
    write_message(msg, 1);
66✔
91
  }
66✔
92
}
943,847,945✔
93

94
void sample_neutron_reaction(Particle& p)
873,124,173✔
95
{
96
  // Sample a nuclide within the material
97
  int i_nuclide = sample_nuclide(p);
873,124,173✔
98

99
  // Save which nuclide particle had collision with
100
  p.event_nuclide() = i_nuclide;
873,124,173✔
101

102
  // Create fission bank sites. Note that while a fission reaction is sampled,
103
  // it never actually "happens", i.e. the weight of the particle does not
104
  // change when sampling fission sites. The following block handles all
105
  // absorption (including fission)
106

107
  const auto& nuc {data::nuclides[i_nuclide]};
873,124,173✔
108

109
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
873,124,173✔
110
    auto& rx = sample_fission(i_nuclide, p);
114,589,708✔
111
    if (settings::run_mode == RunMode::EIGENVALUE) {
114,589,708✔
112
      create_fission_sites(p, i_nuclide, rx);
98,474,752✔
113
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
16,114,956✔
114
               settings::create_fission_neutrons) {
115
      create_fission_sites(p, i_nuclide, rx);
558,074✔
116

117
      // Make sure particle population doesn't grow out of control for
118
      // subcritical multiplication problems.
119
      if (p.secondary_bank().size() >= settings::max_secondaries) {
558,074!
120
        fatal_error(
×
121
          "The secondary particle bank appears to be growing without "
122
          "bound. You are likely running a subcritical multiplication problem "
123
          "with k-effective close to or greater than one.");
124
      }
125
    }
126
    p.event_mt() = rx.mt_;
114,589,708✔
127
  }
128

129
  // Create secondary photons
130
  if (settings::photon_transport) {
873,124,173✔
131
    sample_secondary_photons(p, i_nuclide);
8,809,548✔
132
  }
133

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

137
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
873,124,173✔
138
    absorption(p, i_nuclide);
873,120,653✔
139
  }
140
  if (!p.alive())
873,124,173✔
141
    return;
21,836,933✔
142

143
  // Sample a scattering reaction and determine the secondary energy of the
144
  // exiting neutron
145
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
851,287,240✔
146
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
851,287,240!
147
    ncrystal_mat.scatter(p);
158,829✔
148
  } else {
149
    scatter(p, i_nuclide);
851,128,411✔
150
  }
151

152
  // Advance URR seed stream 'N' times after energy changes
153
  if (p.E() != p.E_last()) {
851,287,240✔
154
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
850,968,009✔
155
  }
156

157
  // Play russian roulette if survival biasing is turned on
158
  if (settings::survival_biasing) {
851,287,240✔
159
    // if survival normalization is on, use normalized weight cutoff and
160
    // normalized weight survive
161
    if (settings::survival_normalization) {
499,950!
162
      if (p.wgt() < settings::weight_cutoff * p.wgt_born()) {
×
163
        russian_roulette(p, settings::weight_survive * p.wgt_born());
×
164
      }
165
    } else if (p.wgt() < settings::weight_cutoff) {
499,950✔
166
      russian_roulette(p, settings::weight_survive);
56,991✔
167
    }
168
  }
169
}
170

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

177
  // Determine the expected number of neutrons produced
178
  double nu_t = p.wgt() / simulation::keff * weight *
99,032,826✔
179
                p.neutron_xs(i_nuclide).nu_fission /
99,032,826✔
180
                p.neutron_xs(i_nuclide).total;
99,032,826✔
181

182
  // Sample the number of neutrons produced
183
  int nu = static_cast<int>(nu_t);
99,032,826✔
184
  if (prn(p.current_seed()) <= (nu_t - nu))
99,032,826✔
185
    ++nu;
20,417,397✔
186

187
  // If no neutrons were produced then don't continue
188
  if (nu == 0)
99,032,826✔
189
    return;
74,683,903✔
190

191
  // Initialize the counter of delayed neutrons encountered for each delayed
192
  // group.
193
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
24,348,923✔
194

195
  // Clear out particle's nu fission bank
196
  p.nu_bank().clear();
24,348,923✔
197

198
  p.fission() = true;
24,348,923✔
199

200
  // Determine whether to place fission sites into the shared fission bank
201
  // or the secondary particle bank.
202
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
24,348,923✔
203

204
  // Counter for the number of fission sites successfully stored to the shared
205
  // fission bank or the secondary particle bank
206
  int n_sites_stored;
207

208
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
54,346,599✔
209
    // Initialize fission site object with particle data
210
    SourceSite site;
29,997,676✔
211
    site.r = p.r();
29,997,676✔
212
    site.particle = ParticleType::neutron;
29,997,676✔
213
    site.time = p.time();
29,997,676✔
214
    site.wgt = 1. / weight;
29,997,676✔
215
    site.surf_id = 0;
29,997,676✔
216

217
    // Sample delayed group and angle/energy for fission reaction
218
    sample_fission_neutron(i_nuclide, rx, &site, p);
29,997,676✔
219

220
    // Reject site if it exceeds time cutoff
221
    if (site.delayed_group > 0) {
29,997,676✔
222
      double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
187,943✔
223
      if (site.time > t_cutoff) {
187,943!
224
        continue;
×
225
      }
226
    }
227

228
    // Set parent and progeny IDs
229
    site.parent_id = p.id();
29,997,676✔
230
    site.progeny_id = p.n_progeny()++;
29,997,676✔
231

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

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

246
        // Break out of loop as no more sites can be added to fission bank
247
        break;
×
248
      }
249
      // Iterated Fission Probability (IFP) method
250
      if (settings::ifp_on) {
29,794,157✔
251
        ifp(p, idx);
1,352,626✔
252
      }
253
    } else {
254
      p.secondary_bank().push_back(site);
203,519✔
255
    }
256

257
    // Increment the number of neutrons born delayed
258
    if (site.delayed_group > 0) {
29,997,676✔
259
      nu_d[site.delayed_group - 1]++;
187,943✔
260
    }
261

262
    // Write fission particles to nuBank
263
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
29,997,676✔
264
    nu_bank_entry.wgt = site.wgt;
29,997,676✔
265
    nu_bank_entry.E = site.E;
29,997,676✔
266
    nu_bank_entry.delayed_group = site.delayed_group;
29,997,676✔
267
  }
268

269
  // If shared fission bank was full, and no fissions could be added,
270
  // set the particle fission flag to false.
271
  if (n_sites_stored == 0) {
24,348,923!
272
    p.fission() = false;
×
273
    return;
×
274
  }
275

276
  // Set nu to the number of fission sites successfully stored. If the fission
277
  // bank was not found to be full then these values are already equivalent.
278
  nu = n_sites_stored;
24,348,923✔
279

280
  // Store the total weight banked for analog fission tallies
281
  p.n_bank() = nu;
24,348,923✔
282
  p.wgt_bank() = nu / weight;
24,348,923✔
283
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
219,140,307✔
284
    p.n_delayed_bank(d) = nu_d[d];
194,791,384✔
285
  }
286
}
287

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

300
  // Sample element within material
301
  int i_element = sample_element(p);
17,256,831✔
302
  const auto& micro {p.photon_xs(i_element)};
17,256,831✔
303
  const auto& element {*data::elements[i_element]};
17,256,831✔
304

305
  // Calculate photon energy over electron rest mass equivalent
306
  double alpha = p.E() / MASS_ELECTRON_EV;
17,256,831✔
307

308
  // For tallying purposes, this routine might be called directly. In that
309
  // case, we need to sample a reaction via the cutoff variable
310
  double prob = 0.0;
17,256,831✔
311
  double cutoff = prn(p.current_seed()) * micro.total;
17,256,831✔
312

313
  // Coherent (Rayleigh) scattering
314
  prob += micro.coherent;
17,256,831✔
315
  if (prob > cutoff) {
17,256,831✔
316
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
950,073✔
317
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
950,073✔
318
    p.event() = TallyEvent::SCATTER;
950,073✔
319
    p.event_mt() = COHERENT;
950,073✔
320
    return;
950,073✔
321
  }
322

323
  // Incoherent (Compton) scattering
324
  prob += micro.incoherent;
16,306,758✔
325
  if (prob > cutoff) {
16,306,758✔
326
    double alpha_out;
327
    int i_shell;
328
    element.compton_scatter(
22,262,564✔
329
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
11,131,282✔
330

331
    // Determine binding energy of shell. The binding energy is 0.0 if
332
    // doppler broadening is not used.
333
    double e_b;
334
    if (i_shell == -1) {
11,131,282!
335
      e_b = 0.0;
×
336
    } else {
337
      e_b = element.binding_energy_[i_shell];
11,131,282✔
338
    }
339

340
    // Create Compton electron
341
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
11,131,282✔
342
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
11,131,282✔
343
    int electron = static_cast<int>(ParticleType::electron);
11,131,282✔
344
    if (E_electron >= settings::energy_cutoff[electron]) {
11,131,282✔
345
      double mu_electron = (alpha - alpha_out * p.mu()) /
11,026,641✔
346
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
22,053,282✔
347
                                     2.0 * alpha * alpha_out * p.mu());
11,026,641✔
348
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
11,026,641✔
349
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
11,026,641✔
350
    }
351

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

359
    phi += PI;
11,131,282✔
360
    p.E() = alpha_out * MASS_ELECTRON_EV;
11,131,282✔
361
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
11,131,282✔
362
    p.event() = TallyEvent::SCATTER;
11,131,282✔
363
    p.event_mt() = INCOHERENT;
11,131,282✔
364
    return;
11,131,282✔
365
  }
366

367
  // Photoelectric effect
368
  double prob_after = prob + micro.photoelectric;
5,175,476✔
369

370
  if (prob_after > cutoff) {
5,175,476✔
371
    // Get grid index, interpolation factor, and bounding subshell
372
    // cross sections
373
    int i_grid = micro.index_grid;
5,088,213✔
374
    double f = micro.interp_factor;
5,088,213✔
375
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
5,088,213✔
376
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
5,088,213✔
377

378
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
24,286,709!
379
      const auto& shell {element.shells_[i_shell]};
24,286,709✔
380

381
      // Check threshold of reaction
382
      if (xs_lower(i_shell) == 0)
24,286,709✔
383
        continue;
10,266,766✔
384

385
      //  Evaluation subshell photoionization cross section
386
      prob += std::exp(
14,019,943✔
387
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
14,019,943✔
388

389
      if (prob > cutoff) {
14,019,943✔
390
        // Determine binding energy based on whether atomic relaxation data is
391
        // present (if not, use value from Compton profile data)
392
        double binding_energy = element.has_atomic_relaxation_
5,088,213✔
393
                                  ? shell.binding_energy
5,088,213!
394
                                  : element.binding_energy_[i_shell];
×
395

396
        // Determine energy of secondary electron
397
        double E_electron = p.E() - binding_energy;
5,088,213✔
398

399
        // Sample mu using non-relativistic Sauter distribution.
400
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
401
        // model in Serpent 2" by Toni Kaltiaisenaho
402
        double mu;
403
        while (true) {
404
          double r = prn(p.current_seed());
7,632,442✔
405
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
7,632,442✔
406
            double rel_vel =
407
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
5,088,213✔
408
              (E_electron + MASS_ELECTRON_EV);
5,088,213✔
409
            mu =
5,088,213✔
410
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
5,088,213✔
411
            break;
5,088,213✔
412
          }
413
        }
2,544,229✔
414

415
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
5,088,213✔
416
        Direction u;
5,088,213✔
417
        u.x = mu;
5,088,213✔
418
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
5,088,213✔
419
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
5,088,213✔
420

421
        // Create secondary electron
422
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
5,088,213✔
423

424
        // Allow electrons to fill orbital and produce auger electrons
425
        // and fluorescent photons
426
        element.atomic_relaxation(i_shell, p);
5,088,213✔
427
        p.event() = TallyEvent::ABSORB;
5,088,213✔
428
        p.event_mt() = 533 + shell.index_subshell;
5,088,213✔
429
        p.wgt() = 0.0;
5,088,213✔
430
        p.E() = 0.0;
5,088,213✔
431
        return;
5,088,213✔
432
      }
433
    }
434
  }
10,176,426!
435
  prob = prob_after;
87,263✔
436

437
  // Pair production
438
  prob += micro.pair_production;
87,263✔
439
  if (prob > cutoff) {
87,263!
440
    double E_electron, E_positron;
441
    double mu_electron, mu_positron;
442
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
87,263✔
443
      &mu_positron, p.current_seed());
444

445
    // Create secondary electron
446
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
87,263✔
447
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
87,263✔
448

449
    // Create secondary positron
450
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
87,263✔
451
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
87,263✔
452

453
    p.event() = TallyEvent::ABSORB;
87,263✔
454
    p.event_mt() = PAIR_PROD;
87,263✔
455
    p.wgt() = 0.0;
87,263✔
456
    p.E() = 0.0;
87,263✔
457
  }
458
}
459

460
void sample_electron_reaction(Particle& p)
53,379,678✔
461
{
462
  // TODO: create reaction types
463

464
  if (settings::electron_treatment == ElectronTreatment::TTB) {
53,379,678✔
465
    double E_lost;
466
    thick_target_bremsstrahlung(p, &E_lost);
53,076,023✔
467
  }
468

469
  p.E() = 0.0;
53,379,678✔
470
  p.wgt() = 0.0;
53,379,678✔
471
  p.event() = TallyEvent::ABSORB;
53,379,678✔
472
}
53,379,678✔
473

474
void sample_positron_reaction(Particle& p)
87,263✔
475
{
476
  // TODO: create reaction types
477

478
  if (settings::electron_treatment == ElectronTreatment::TTB) {
87,263✔
479
    double E_lost;
480
    thick_target_bremsstrahlung(p, &E_lost);
85,998✔
481
  }
482

483
  // Sample angle isotropically
484
  Direction u = isotropic_direction(p.current_seed());
87,263✔
485

486
  // Create annihilation photon pair traveling in opposite directions
487
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
87,263✔
488
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
87,263✔
489

490
  p.E() = 0.0;
87,263✔
491
  p.wgt() = 0.0;
87,263✔
492
  p.event() = TallyEvent::ABSORB;
87,263✔
493
}
87,263✔
494

495
int sample_nuclide(Particle& p)
873,124,173✔
496
{
497
  // Sample cumulative distribution function
498
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
873,124,173✔
499

500
  // Get pointers to nuclide/density arrays
501
  const auto& mat {model::materials[p.material()]};
873,124,173✔
502
  int n = mat->nuclide_.size();
873,124,173✔
503

504
  double prob = 0.0;
873,124,173✔
505
  for (int i = 0; i < n; ++i) {
1,860,465,621!
506
    // Get atom density
507
    int i_nuclide = mat->nuclide_[i];
1,860,465,621✔
508
    double atom_density = mat->atom_density(i, p.density_mult());
1,860,465,621✔
509

510
    // Increment probability to compare to cutoff
511
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,860,465,621✔
512
    if (prob >= cutoff)
1,860,465,621✔
513
      return i_nuclide;
873,124,173✔
514
  }
515

516
  // If we reach here, no nuclide was sampled
517
  p.write_restart();
×
518
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
519
}
520

521
int sample_element(Particle& p)
17,256,831✔
522
{
523
  // Sample cumulative distribution function
524
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
17,256,831✔
525

526
  // Get pointers to elements, densities
527
  const auto& mat {model::materials[p.material()]};
17,256,831✔
528

529
  double prob = 0.0;
17,256,831✔
530
  for (int i = 0; i < mat->element_.size(); ++i) {
41,249,039!
531
    // Find atom density
532
    int i_element = mat->element_[i];
41,249,039✔
533
    double atom_density = mat->atom_density(i, p.density_mult());
41,249,039✔
534

535
    // Determine microscopic cross section
536
    double sigma = atom_density * p.photon_xs(i_element).total;
41,249,039✔
537

538
    // Increment probability to compare to cutoff
539
    prob += sigma;
41,249,039✔
540
    if (prob > cutoff) {
41,249,039✔
541
      // Save which nuclide particle had collision with for tally purpose
542
      p.event_nuclide() = mat->nuclide_[i];
17,256,831✔
543

544
      return i_element;
17,256,831✔
545
    }
546
  }
547

548
  // If we made it here, no element was sampled
549
  p.write_restart();
×
550
  fatal_error("Did not sample any element during collision.");
×
551
}
552

553
Reaction& sample_fission(int i_nuclide, Particle& p)
114,589,708✔
554
{
555
  // Get pointer to nuclide
556
  const auto& nuc {data::nuclides[i_nuclide]};
114,589,708✔
557

558
  // If we're in the URR, by default use the first fission reaction. We also
559
  // default to the first reaction if we know that there are no partial fission
560
  // reactions
561
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
114,589,708✔
562
    return *nuc->fission_rx_[0];
114,561,944✔
563
  }
564

565
  // Check to see if we are in a windowed multipole range.  WMP only supports
566
  // the first fission reaction.
567
  if (nuc->multipole_) {
27,764!
568
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
569
      return *nuc->fission_rx_[0];
×
570
    }
571
  }
572

573
  // Get grid index and interpolation factor and sample fission cdf
574
  const auto& micro = p.neutron_xs(i_nuclide);
27,764✔
575
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
27,764✔
576
  double prob = 0.0;
27,764✔
577

578
  // Loop through each partial fission reaction type
579
  for (auto& rx : nuc->fission_rx_) {
27,824!
580
    // add to cumulative probability
581
    prob += rx->xs(micro);
27,824✔
582

583
    // Create fission bank sites if fission occurs
584
    if (prob > cutoff)
27,824✔
585
      return *rx;
27,764✔
586
  }
587

588
  // If we reached here, no reaction was sampled
589
  throw std::runtime_error {
×
590
    "No fission reaction was sampled for " + nuc->name_};
×
591
}
592

593
void sample_photon_product(
1,337,644✔
594
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
595
{
596
  // Get grid index and interpolation factor and sample photon production cdf
597
  const auto& micro = p.neutron_xs(i_nuclide);
1,337,644✔
598
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,337,644✔
599
  double prob = 0.0;
1,337,644✔
600

601
  // Loop through each reaction type
602
  const auto& nuc {data::nuclides[i_nuclide]};
1,337,644✔
603
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
21,995,831!
604
    // Evaluate neutron cross section
605
    const auto& rx = nuc->reactions_[i];
21,995,831✔
606
    double xs = rx->xs(micro);
21,995,831✔
607

608
    // if cross section is zero for this reaction, skip it
609
    if (xs == 0.0)
21,995,831✔
610
      continue;
7,191,415✔
611

612
    for (int j = 0; j < rx->products_.size(); ++j) {
69,577,497✔
613
      if (rx->products_[j].particle_ == ParticleType::photon) {
56,110,725✔
614
        // For fission, artificially increase the photon yield to account
615
        // for delayed photons
616
        double f = 1.0;
43,656,910✔
617
        if (settings::delayed_photon_scaling) {
43,656,910!
618
          if (is_fission(rx->mt_)) {
43,656,910✔
619
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
540,221!
620
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
540,221✔
621
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
540,221✔
622
              f = (energy_prompt + energy_delayed) / (energy_prompt);
540,221✔
623
            }
624
          }
625
        }
626

627
        // add to cumulative probability
628
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
43,656,910✔
629

630
        *i_rx = i;
43,656,910✔
631
        *i_product = j;
43,656,910✔
632
        if (prob > cutoff)
43,656,910✔
633
          return;
1,337,644✔
634
      }
635
    }
636
  }
637
}
638

639
void absorption(Particle& p, int i_nuclide)
873,120,653✔
640
{
641
  if (settings::survival_biasing) {
873,120,653✔
642
    // Determine weight absorbed in survival biasing
643
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
499,950✔
644
                              p.neutron_xs(i_nuclide).total;
499,950✔
645

646
    // Adjust weight of particle by probability of absorption
647
    p.wgt() -= wgt_absorb;
499,950✔
648

649
    // Score implicit absorption estimate of keff
650
    if (settings::run_mode == RunMode::EIGENVALUE) {
499,950!
651
      p.keff_tally_absorption() += wgt_absorb *
499,950✔
652
                                   p.neutron_xs(i_nuclide).nu_fission /
499,950✔
653
                                   p.neutron_xs(i_nuclide).absorption;
499,950✔
654
    }
655
  } else {
656
    // See if disappearance reaction happens
657
    if (p.neutron_xs(i_nuclide).absorption >
872,620,703✔
658
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
872,620,703✔
659
      // Score absorption estimate of keff
660
      if (settings::run_mode == RunMode::EIGENVALUE) {
21,836,933✔
661
        p.keff_tally_absorption() += p.wgt() *
34,485,488✔
662
                                     p.neutron_xs(i_nuclide).nu_fission /
17,242,744✔
663
                                     p.neutron_xs(i_nuclide).absorption;
17,242,744✔
664
      }
665

666
      p.wgt() = 0.0;
21,836,933✔
667
      p.event() = TallyEvent::ABSORB;
21,836,933✔
668
      if (!p.fission()) {
21,836,933✔
669
        p.event_mt() = N_DISAPPEAR;
13,299,874✔
670
      }
671
    }
672
  }
673
}
873,120,653✔
674

675
void scatter(Particle& p, int i_nuclide)
851,128,411✔
676
{
677
  // copy incoming direction
678
  Direction u_old {p.u()};
851,128,411✔
679

680
  // Get pointer to nuclide and grid index/interpolation factor
681
  const auto& nuc {data::nuclides[i_nuclide]};
851,128,411✔
682
  const auto& micro {p.neutron_xs(i_nuclide)};
851,128,411✔
683
  int i_temp = micro.index_temp;
851,128,411✔
684

685
  // For tallying purposes, this routine might be called directly. In that
686
  // case, we need to sample a reaction via the cutoff variable
687
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
851,128,411✔
688
  bool sampled = false;
851,128,411✔
689

690
  // Calculate elastic cross section if it wasn't precalculated
691
  if (micro.elastic == CACHE_INVALID) {
851,128,411✔
692
    nuc->calculate_elastic_xs(p);
702,146,800✔
693
  }
694

695
  double prob = micro.elastic - micro.thermal;
851,128,411✔
696
  if (prob > cutoff) {
851,128,411✔
697
    // =======================================================================
698
    // NON-S(A,B) ELASTIC SCATTERING
699

700
    // Determine temperature
701
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
728,341,159✔
702

703
    // Perform collision physics for elastic scattering
704
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
728,341,159✔
705

706
    p.event_mt() = ELASTIC;
728,341,159✔
707
    sampled = true;
728,341,159✔
708
  }
709

710
  prob = micro.elastic;
851,128,411✔
711
  if (prob > cutoff && !sampled) {
851,128,411✔
712
    // =======================================================================
713
    // S(A,B) SCATTERING
714

715
    sab_scatter(i_nuclide, micro.index_sab, p);
105,302,963✔
716

717
    p.event_mt() = ELASTIC;
105,302,963✔
718
    sampled = true;
105,302,963✔
719
  }
720

721
  if (!sampled) {
851,128,411✔
722
    // =======================================================================
723
    // INELASTIC SCATTERING
724

725
    int n = nuc->index_inelastic_scatter_.size();
17,484,289✔
726
    int i = 0;
17,484,289✔
727
    for (int j = 0; j < n && prob < cutoff; ++j) {
335,463,625✔
728
      i = nuc->index_inelastic_scatter_[j];
317,979,336✔
729

730
      // add to cumulative probability
731
      prob += nuc->reactions_[i]->xs(micro);
317,979,336✔
732
    }
733

734
    // Perform collision physics for inelastic scattering
735
    const auto& rx {nuc->reactions_[i]};
17,484,289✔
736
    inelastic_scatter(i_nuclide, *rx, p);
17,484,289✔
737
    p.event_mt() = rx->mt_;
17,484,289✔
738
  }
739

740
  // Set event component
741
  p.event() = TallyEvent::SCATTER;
851,128,411✔
742

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

755
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
728,341,159✔
756
{
757
  // get pointer to nuclide
758
  const auto& nuc {data::nuclides[i_nuclide]};
728,341,159✔
759

760
  double vel = std::sqrt(p.E());
728,341,159✔
761
  double awr = nuc->awr_;
728,341,159✔
762

763
  // Neutron velocity in LAB
764
  Direction v_n = vel * p.u();
728,341,159✔
765

766
  // Sample velocity of target nucleus
767
  Direction v_t {};
728,341,159✔
768
  if (!p.neutron_xs(i_nuclide).use_ptable) {
728,341,159✔
769
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,401,108,292✔
770
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
700,554,146✔
771
  }
772

773
  // Velocity of center-of-mass
774
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
728,341,159✔
775

776
  // Transform to CM frame
777
  v_n -= v_cm;
728,341,159✔
778

779
  // Find speed of neutron in CM
780
  vel = v_n.norm();
728,341,159✔
781

782
  if (!model::active_point_tallies.empty()) {
728,341,159!
NEW
783
    score_point_tally(p, i_nuclide, rx, 0, &v_t);
×
784
  }
785

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

797
  // Determine direction cosines in CM
798
  Direction u_cm = v_n / vel;
728,341,159✔
799

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

805
  // Transform back to LAB frame
806
  v_n += v_cm;
728,341,159✔
807

808
  p.E() = v_n.dot(v_n);
728,341,159✔
809
  vel = std::sqrt(p.E());
728,341,159✔
810

811
  // compute cosine of scattering angle in LAB frame by taking dot product of
812
  // neutron's pre- and post-collision angle
813
  p.mu() = p.u().dot(v_n) / vel;
728,341,159✔
814

815
  // Set energy and direction of particle in LAB frame
816
  p.u() = v_n / vel;
728,341,159✔
817

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

825
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
105,302,963✔
826
{
827
  // Determine temperature index
828
  const auto& micro {p.neutron_xs(i_nuclide)};
105,302,963✔
829
  int i_temp = micro.index_temp_sab;
105,302,963✔
830

831
  // Sample energy and angle
832
  double E_out;
833
  auto& sab = data::thermal_scatt[i_sab]->data_[i_temp];
105,302,963✔
834
  sab.sample(micro, p.E(), &E_out, &p.mu(), p.current_seed());
105,302,963✔
835

836
  if (!model::active_point_tallies.empty()) {
105,302,963!
NEW
837
    score_point_tally(p, i_nuclide, sab, micro);
×
838
  }
839

840
  // Set energy to outgoing, change direction of particle
841
  p.E() = E_out;
105,302,963✔
842
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
105,302,963✔
843
}
105,302,963✔
844

845
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
700,554,146✔
846
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
847
{
848
  // check if nuclide is a resonant scatterer
849
  ResScatMethod sampling_method;
850
  if (nuc.resonant_) {
700,554,146✔
851

852
    // sampling method to use
853
    sampling_method = settings::res_scat_method;
84,557✔
854

855
    // upper resonance scattering energy bound (target is at rest above this E)
856
    if (E > settings::res_scat_energy_max) {
84,557✔
857
      return {};
40,755✔
858

859
      // lower resonance scattering energy bound (should be no resonances below)
860
    } else if (E < settings::res_scat_energy_min) {
43,802✔
861
      sampling_method = ResScatMethod::cxs;
24,992✔
862
    }
863

864
    // otherwise, use free gas model
865
  } else {
866
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
700,469,589✔
867
      return {};
336,135,873✔
868
    } else {
869
      sampling_method = ResScatMethod::cxs;
364,333,716✔
870
    }
871
  }
872

873
  // use appropriate target velocity sampling method
874
  switch (sampling_method) {
364,377,518!
875
  case ResScatMethod::cxs:
364,358,708✔
876

877
    // sample target velocity with the constant cross section (cxs) approx.
878
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
364,358,708✔
879

880
  case ResScatMethod::dbrc:
18,810✔
881
  case ResScatMethod::rvs: {
882
    double E_red = std::sqrt(nuc.awr_ * E / kT);
18,810✔
883
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
18,810✔
884
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
18,810✔
885

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

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

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

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

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

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

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

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

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

966
      while (true) {
967
        // directly sample Maxwellian
968
        double E_t = -kT * std::log(prn(seed));
170,720✔
969

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

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

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

996
  UNREACHABLE();
×
997
}
998

999
Direction sample_cxs_target_velocity(
364,361,986✔
1000
  double awr, double E, Direction u, double kT, uint64_t* seed)
1001
{
1002
  double beta_vn = std::sqrt(awr * E / kT);
364,361,986✔
1003
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
364,361,986✔
1004

1005
  double beta_vt_sq;
1006
  double mu;
1007
  while (true) {
1008
    // Sample two random numbers
1009
    double r1 = prn(seed);
428,138,699✔
1010
    double r2 = prn(seed);
428,138,699✔
1011

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

1017
      beta_vt_sq = -std::log(r1 * r2);
103,286,895✔
1018

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

1024
      double c = std::cos(PI / 2.0 * prn(seed));
324,851,804✔
1025
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
324,851,804✔
1026
    }
1027

1028
    // Determine beta * vt
1029
    double beta_vt = std::sqrt(beta_vt_sq);
428,138,699✔
1030

1031
    // Sample cosine of angle between neutron and target velocity
1032
    mu = uniform_distribution(-1., 1., seed);
428,138,699✔
1033

1034
    // Determine rejection probability
1035
    double accept_prob =
1036
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
428,138,699✔
1037
      (beta_vn + beta_vt);
428,138,699✔
1038

1039
    // Perform rejection sampling on vt and mu
1040
    if (prn(seed) < accept_prob)
428,138,699✔
1041
      break;
364,361,986✔
1042
  }
63,776,713✔
1043

1044
  // Determine speed of target nucleus
1045
  double vt = std::sqrt(beta_vt_sq * kT / awr);
364,361,986✔
1046

1047
  // Determine velocity vector of target nucleus based on neutron's velocity
1048
  // and the sampled angle between them
1049
  return vt * rotate_angle(u, mu, nullptr, seed);
364,361,986✔
1050
}
1051

1052
void sample_fission_neutron(
29,997,676✔
1053
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1054
{
1055
  // Get attributes of particle
1056
  double E_in = p.E();
29,997,676✔
1057
  uint64_t* seed = p.current_seed();
29,997,676✔
1058

1059
  // Determine total nu, delayed nu, and delayed neutron fraction
1060
  const auto& nuc {data::nuclides[i_nuclide]};
29,997,676✔
1061
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
29,997,676✔
1062
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
29,997,676✔
1063
  double beta = nu_d / nu_t;
29,997,676✔
1064

1065
  if (prn(seed) < beta) {
29,997,676✔
1066
    // ====================================================================
1067
    // DELAYED NEUTRON SAMPLED
1068

1069
    // sampled delayed precursor group
1070
    double xi = prn(seed) * nu_d;
187,943✔
1071
    double prob = 0.0;
187,943✔
1072
    int group;
1073
    for (group = 1; group < nuc->n_precursor_; ++group) {
699,340✔
1074
      // determine delayed neutron precursor yield for group j
1075
      double yield = (*rx.products_[group].yield_)(E_in);
686,158✔
1076

1077
      // Check if this group is sampled
1078
      prob += yield;
686,158✔
1079
      if (xi < prob)
686,158✔
1080
        break;
174,761✔
1081
    }
1082

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

1088
    // set the delayed group for the particle born from fission
1089
    site->delayed_group = group;
187,943✔
1090

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

1095
  } else {
1096
    // ====================================================================
1097
    // PROMPT NEUTRON SAMPLED
1098

1099
    // set the delayed group for the particle born from fission to 0
1100
    site->delayed_group = 0;
29,809,733✔
1101
  }
1102

1103
  if (!model::active_point_tallies.empty()) {
29,997,676!
NEW
1104
    score_point_tally(p, i_nuclide, rx, site->delayed_group, nullptr);
×
1105
  }
1106

1107
  // sample from prompt neutron energy distribution
1108
  int n_sample = 0;
29,997,676✔
1109
  double mu;
1110
  while (true) {
1111
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
29,997,676✔
1112

1113
    // resample if energy is greater than maximum neutron energy
1114
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
29,997,676✔
1115
    if (site->E < data::energy_max[neutron])
29,997,676!
1116
      break;
29,997,676✔
1117

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

1128
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1129
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
29,997,676✔
1130
}
29,997,676✔
1131

1132
void inelastic_scatter(int i_nuclide, const Reaction& rx, Particle& p)
17,484,289✔
1133
{
1134
  // Get pointer to nuclide
1135
  const auto& nuc {data::nuclides[i_nuclide]};
17,484,289✔
1136

1137
  // copy energy of neutron
1138
  double E_in = p.E();
17,484,289✔
1139

1140
  // sample outgoing energy and scattering cosine
1141
  double E;
1142
  double mu;
1143
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
17,484,289✔
1144

1145
  if (!model::active_point_tallies.empty()) {
17,484,289!
NEW
1146
    score_point_tally(p, i_nuclide, rx, 0, nullptr);
×
1147
  }
1148

1149
  // if scattering system is in center-of-mass, transfer cosine of scattering
1150
  // angle and outgoing energy from CM to LAB
1151
  if (rx.scatter_in_cm_) {
17,484,289✔
1152
    double E_cm = E;
17,430,295✔
1153

1154
    // determine outgoing energy in lab
1155
    double A = nuc->awr_;
17,430,295✔
1156
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
17,430,295✔
1157
                 ((A + 1.0) * (A + 1.0));
17,430,295✔
1158

1159
    // determine outgoing angle in lab
1160
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
17,430,295✔
1161
  }
1162

1163
  // Because of floating-point roundoff, it may be possible for mu to be
1164
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1165
  // or 1
1166
  if (std::abs(mu) > 1.0)
17,484,289!
1167
    mu = std::copysign(1.0, mu);
×
1168

1169
  // Set outgoing energy and scattering angle
1170
  p.E() = E;
17,484,289✔
1171
  p.mu() = mu;
17,484,289✔
1172

1173
  // change direction of particle
1174
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
17,484,289✔
1175

1176
  // evaluate yield
1177
  double yield = (*rx.products_[0].yield_)(E_in);
17,484,289✔
1178
  if (std::floor(yield) == yield && yield > 0) {
17,484,289!
1179
    // If yield is integral, create exactly that many secondary particles
1180
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
17,586,337✔
1181
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
102,102✔
1182
    }
1183
  } else {
17,484,235✔
1184
    // Otherwise, change weight of particle based on yield
1185
    p.wgt() *= yield;
54✔
1186
  }
1187
}
17,484,289✔
1188

1189
void sample_secondary_photons(Particle& p, int i_nuclide)
8,809,548✔
1190
{
1191
  // Sample the number of photons produced
1192
  double y_t =
1193
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,809,548✔
1194
  double photon_wgt = p.wgt();
8,809,548✔
1195
  int y = 1;
8,809,548✔
1196

1197
  if (settings::use_decay_photons) {
8,809,548✔
1198
    // For decay photons, sample a single photon and modify the weight
1199
    if (y_t <= 0.0)
72,006✔
1200
      return;
17,281✔
1201
    photon_wgt *= y_t;
54,725✔
1202
  } else {
1203
    // For prompt photons, sample an integral number of photons with weight
1204
    // equal to the neutron's weight
1205
    y = static_cast<int>(y_t);
8,737,542✔
1206
    if (prn(p.current_seed()) <= y_t - y)
8,737,542✔
1207
      ++y;
574,860✔
1208
  }
1209

1210
  // Sample each secondary photon
1211
  for (int i = 0; i < y; ++i) {
10,129,911✔
1212
    // Sample the reaction and product
1213
    int i_rx;
1214
    int i_product;
1215
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,337,644✔
1216

1217
    // Sample the outgoing energy and angle
1218
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,337,644✔
1219
    double E;
1220
    double mu;
1221
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,337,644✔
1222

1223
    // Sample the new direction
1224
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,337,644✔
1225

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

1236
    // Create the secondary photon
1237
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,337,644✔
1238

1239
    // Tag secondary particle with parent nuclide
1240
    if (created_photon && settings::use_decay_photons) {
1,337,644✔
1241
      p.secondary_bank().back().parent_nuclide =
52,844✔
1242
        rx->products_[i_product].parent_nuclide_;
52,844✔
1243
    }
1244
  }
1245
}
1246

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

© 2026 Coveralls, Inc