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

openmc-dev / openmc / 9789289404

04 Jul 2024 06:02AM UTC coverage: 84.763% (-0.003%) from 84.766%
9789289404

Pull #3070

github

web-flow
Merge d5ce44080 into 1b22dd28d
Pull Request #3070: Survival Normalization 0.15.1

8 of 13 new or added lines in 4 files covered. (61.54%)

1 existing line in 1 file now uncovered.

49186 of 58028 relevant lines covered (84.76%)

31248475.07 hits per line

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

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

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

29
#include <fmt/core.h>
30

31
#include <algorithm> // for max, min, max_element
32
#include <cmath>     // for sqrt, exp, log, abs, copysign
33
#include <xtensor/xview.hpp>
34

35
namespace openmc {
36

37
//==============================================================================
38
// Non-member functions
39
//==============================================================================
40

41
void collision(Particle& p)
788,076,473✔
42
{
43
  // Add to collision counter for particle
44
  ++(p.n_collision());
788,076,473✔
45

46
  // Sample reaction for the material the particle is in
47
  switch (p.type()) {
788,076,473✔
48
  case ParticleType::neutron:
728,142,173✔
49
    sample_neutron_reaction(p);
728,142,173✔
50
    break;
728,142,173✔
51
  case ParticleType::photon:
11,121,456✔
52
    sample_photon_reaction(p);
11,121,456✔
53
    break;
11,121,456✔
54
  case ParticleType::electron:
48,720,552✔
55
    sample_electron_reaction(p);
48,720,552✔
56
    break;
48,720,552✔
57
  case ParticleType::positron:
92,292✔
58
    sample_positron_reaction(p);
92,292✔
59
    break;
92,292✔
60
  }
61

62
  if (settings::weight_window_checkpoint_collision)
788,076,473✔
63
    apply_weight_windows(p);
788,076,473✔
64

65
  // Kill particle if energy falls below cutoff
66
  int type = static_cast<int>(p.type());
788,076,473✔
67
  if (p.E() < settings::energy_cutoff[type]) {
788,076,473✔
68
    p.wgt() = 0.0;
4,809,228✔
69
  }
70

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

91
void sample_neutron_reaction(Particle& p)
728,142,173✔
92
{
93
  // Sample a nuclide within the material
94
  int i_nuclide = sample_nuclide(p);
728,142,173✔
95

96
  // Save which nuclide particle had collision with
97
  p.event_nuclide() = i_nuclide;
728,142,173✔
98

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

104
  const auto& nuc {data::nuclides[i_nuclide]};
728,142,173✔
105

106
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
728,142,173✔
107
    auto& rx = sample_fission(i_nuclide, p);
110,347,095✔
108
    if (settings::run_mode == RunMode::EIGENVALUE) {
110,347,095✔
109
      create_fission_sites(p, i_nuclide, rx);
109,744,124✔
110
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
602,971✔
111
               settings::create_fission_neutrons) {
112
      create_fission_sites(p, i_nuclide, rx);
587,803✔
113

114
      // Make sure particle population doesn't grow out of control for
115
      // subcritical multiplication problems.
116
      if (p.secondary_bank().size() >= 10000) {
587,803✔
117
        fatal_error(
×
118
          "The secondary particle bank appears to be growing without "
119
          "bound. You are likely running a subcritical multiplication problem "
120
          "with k-effective close to or greater than one.");
121
      }
122
    }
123
  }
124

125
  // Create secondary photons
126
  if (settings::photon_transport) {
728,142,173✔
127
    sample_secondary_photons(p, i_nuclide);
9,848,112✔
128
  }
129

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

133
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
728,142,173✔
134
    absorption(p, i_nuclide);
728,137,909✔
135
  }
136
  if (!p.alive())
728,142,173✔
137
    return;
18,647,263✔
138

139
  // Sample a scattering reaction and determine the secondary energy of the
140
  // exiting neutron
141
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
709,494,910✔
142
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
709,494,910✔
143
    ncrystal_mat.scatter(p);
14,438✔
144
  } else {
145
    scatter(p, i_nuclide);
709,480,472✔
146
  }
147

148
  // Advance URR seed stream 'N' times after energy changes
149
  if (p.E() != p.E_last()) {
709,494,910✔
150
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
709,282,238✔
151
  }
152

153
  // Play russian roulette if survival biasing is turned on
154
  if (settings::survival_biasing) {
709,494,910✔
155
    // if survival normalization is on, use normalized weight cutoff and normalized weight survive
156
    if (settings::survival_normalization) {
557,892✔
NEW
157
      if (p.wgt() < settings::weight_cutoff*p.wgt0()) {
×
NEW
158
        russian_roulette(p, settings::weight_survive*p.wgt0());
×
159
      } 
160
    } else if (p.wgt() < settings::weight_cutoff) {
557,892✔
161
            russian_roulette(p, settings::weight_survive);
63,984✔
162
    }
163
  }
164
}
165

166
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
110,331,927✔
167
{
168
  // If uniform fission source weighting is turned on, we increase or decrease
169
  // the expected number of fission sites produced
170
  double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
110,331,927✔
171

172
  // Determine the expected number of neutrons produced
173
  double nu_t = p.wgt() / simulation::keff * weight *
110,331,927✔
174
                p.neutron_xs(i_nuclide).nu_fission /
110,331,927✔
175
                p.neutron_xs(i_nuclide).total;
110,331,927✔
176

177
  // Sample the number of neutrons produced
178
  int nu = static_cast<int>(nu_t);
110,331,927✔
179
  if (prn(p.current_seed()) <= (nu_t - nu))
110,331,927✔
180
    ++nu;
16,651,185✔
181

182
  // If no neutrons were produced then don't continue
183
  if (nu == 0)
110,331,927✔
184
    return;
89,440,550✔
185

186
  // Initialize the counter of delayed neutrons encountered for each delayed
187
  // group.
188
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
20,891,377✔
189

190
  // Clear out particle's nu fission bank
191
  p.nu_bank().clear();
20,891,377✔
192

193
  p.fission() = true;
20,891,377✔
194

195
  // Determine whether to place fission sites into the shared fission bank
196
  // or the secondary particle bank.
197
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
20,891,377✔
198

199
  // Counter for the number of fission sites successfully stored to the shared
200
  // fission bank or the secondary particle bank
201
  int n_sites_stored;
202

203
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
47,303,170✔
204
    // Initialize fission site object with particle data
205
    SourceSite site;
26,411,793✔
206
    site.r = p.r();
26,411,793✔
207
    site.particle = ParticleType::neutron;
26,411,793✔
208
    site.time = p.time();
26,411,793✔
209
    site.wgt = 1. / weight;
26,411,793✔
210
    site.parent_id = p.id();
26,411,793✔
211
    site.progeny_id = p.n_progeny()++;
26,411,793✔
212
    site.surf_id = 0;
26,411,793✔
213

214
    // Sample delayed group and angle/energy for fission reaction
215
    sample_fission_neutron(i_nuclide, rx, &site, p);
26,411,793✔
216

217
    // Store fission site in bank
218
    if (use_fission_bank) {
26,411,793✔
219
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
26,208,536✔
220
      if (idx == -1) {
26,208,536✔
221
        warning(
×
222
          "The shared fission bank is full. Additional fission sites created "
223
          "in this generation will not be banked. Results may be "
224
          "non-deterministic.");
225

226
        // Decrement number of particle progeny as storage was unsuccessful.
227
        // This step is needed so that the sum of all progeny is equal to the
228
        // size of the shared fission bank.
229
        p.n_progeny()--;
×
230

231
        // Break out of loop as no more sites can be added to fission bank
232
        break;
×
233
      }
234
    } else {
235
      p.secondary_bank().push_back(site);
203,257✔
236
    }
237

238
    // Set the delayed group on the particle as well
239
    p.delayed_group() = site.delayed_group;
26,411,793✔
240

241
    // Increment the number of neutrons born delayed
242
    if (p.delayed_group() > 0) {
26,411,793✔
243
      nu_d[p.delayed_group() - 1]++;
165,832✔
244
    }
245

246
    // Write fission particles to nuBank
247
    p.nu_bank().emplace_back();
26,411,793✔
248
    NuBank* nu_bank_entry = &p.nu_bank().back();
26,411,793✔
249
    nu_bank_entry->wgt = site.wgt;
26,411,793✔
250
    nu_bank_entry->E = site.E;
26,411,793✔
251
    nu_bank_entry->delayed_group = site.delayed_group;
26,411,793✔
252
  }
253

254
  // If shared fission bank was full, and no fissions could be added,
255
  // set the particle fission flag to false.
256
  if (n_sites_stored == 0) {
20,891,377✔
257
    p.fission() = false;
×
258
    return;
×
259
  }
260

261
  // Set nu to the number of fission sites successfully stored. If the fission
262
  // bank was not found to be full then these values are already equivalent.
263
  nu = n_sites_stored;
20,891,377✔
264

265
  // Store the total weight banked for analog fission tallies
266
  p.n_bank() = nu;
20,891,377✔
267
  p.wgt_bank() = nu / weight;
20,891,377✔
268
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
188,022,393✔
269
    p.n_delayed_bank(d) = nu_d[d];
167,131,016✔
270
  }
271
}
272

273
void sample_photon_reaction(Particle& p)
11,121,456✔
274
{
275
  // Kill photon if below energy cutoff -- an extra check is made here because
276
  // photons with energy below the cutoff may have been produced by neutrons
277
  // reactions or atomic relaxation
278
  int photon = static_cast<int>(ParticleType::photon);
11,121,456✔
279
  if (p.E() < settings::energy_cutoff[photon]) {
11,121,456✔
280
    p.E() = 0.0;
×
281
    p.wgt() = 0.0;
×
282
    return;
×
283
  }
284

285
  // Sample element within material
286
  int i_element = sample_element(p);
11,121,456✔
287
  const auto& micro {p.photon_xs(i_element)};
11,121,456✔
288
  const auto& element {*data::elements[i_element]};
11,121,456✔
289

290
  // Calculate photon energy over electron rest mass equivalent
291
  double alpha = p.E() / MASS_ELECTRON_EV;
11,121,456✔
292

293
  // For tallying purposes, this routine might be called directly. In that
294
  // case, we need to sample a reaction via the cutoff variable
295
  double prob = 0.0;
11,121,456✔
296
  double cutoff = prn(p.current_seed()) * micro.total;
11,121,456✔
297

298
  // Coherent (Rayleigh) scattering
299
  prob += micro.coherent;
11,121,456✔
300
  if (prob > cutoff) {
11,121,456✔
301
    double mu = element.rayleigh_scatter(alpha, p.current_seed());
559,692✔
302
    p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
559,692✔
303
    p.event() = TallyEvent::SCATTER;
559,692✔
304
    p.event_mt() = COHERENT;
559,692✔
305
    return;
559,692✔
306
  }
307

308
  // Incoherent (Compton) scattering
309
  prob += micro.incoherent;
10,561,764✔
310
  if (prob > cutoff) {
10,561,764✔
311
    double alpha_out, mu;
312
    int i_shell;
313
    element.compton_scatter(
5,764,452✔
314
      alpha, true, &alpha_out, &mu, &i_shell, p.current_seed());
315

316
    // Determine binding energy of shell. The binding energy is 0.0 if
317
    // doppler broadening is not used.
318
    double e_b;
319
    if (i_shell == -1) {
5,764,452✔
320
      e_b = 0.0;
×
321
    } else {
322
      e_b = element.binding_energy_[i_shell];
5,764,452✔
323
    }
324

325
    // Create Compton electron
326
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
5,764,452✔
327
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
5,764,452✔
328
    int electron = static_cast<int>(ParticleType::electron);
5,764,452✔
329
    if (E_electron >= settings::energy_cutoff[electron]) {
5,764,452✔
330
      double mu_electron = (alpha - alpha_out * mu) /
5,714,628✔
331
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
5,714,628✔
332
                                     2.0 * alpha * alpha_out * mu);
5,714,628✔
333
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
5,714,628✔
334
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
5,714,628✔
335
    }
336

337
    // TODO: Compton subshell data does not match atomic relaxation data
338
    // Allow electrons to fill orbital and produce auger electrons
339
    // and fluorescent photons
340
    if (i_shell >= 0) {
5,764,452✔
341
      element.atomic_relaxation(i_shell, p);
5,764,452✔
342
    }
343

344
    phi += PI;
5,764,452✔
345
    p.E() = alpha_out * MASS_ELECTRON_EV;
5,764,452✔
346
    p.u() = rotate_angle(p.u(), mu, &phi, p.current_seed());
5,764,452✔
347
    p.event() = TallyEvent::SCATTER;
5,764,452✔
348
    p.event_mt() = INCOHERENT;
5,764,452✔
349
    return;
5,764,452✔
350
  }
351

352
  // Photoelectric effect
353
  double prob_after = prob + micro.photoelectric;
4,797,312✔
354

355
  if (prob_after > cutoff) {
4,797,312✔
356
    // Get grid index, interpolation factor, and bounding subshell
357
    // cross sections
358
    int i_grid = micro.index_grid;
4,705,020✔
359
    double f = micro.interp_factor;
4,705,020✔
360
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
4,705,020✔
361
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
4,705,020✔
362

363
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
25,406,244✔
364
      const auto& shell {element.shells_[i_shell]};
25,406,244✔
365

366
      // Check threshold of reaction
367
      if (xs_lower(i_shell) == 0)
25,406,244✔
368
        continue;
11,113,200✔
369

370
      //  Evaluation subshell photoionization cross section
371
      prob += std::exp(
14,293,044✔
372
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
14,293,044✔
373

374
      if (prob > cutoff) {
14,293,044✔
375
        // Determine binding energy based on whether atomic relaxation data is
376
        // present (if not, use value from Compton profile data)
377
        double binding_energy = element.has_atomic_relaxation_
4,705,020✔
378
                                  ? shell.binding_energy
4,705,020✔
379
                                  : element.binding_energy_[i_shell];
×
380

381
        // Determine energy of secondary electron
382
        double E_electron = p.E() - binding_energy;
4,705,020✔
383

384
        // Sample mu using non-relativistic Sauter distribution.
385
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
386
        // model in Serpent 2" by Toni Kaltiaisenaho
387
        double mu;
388
        while (true) {
389
          double r = prn(p.current_seed());
7,071,492✔
390
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
7,071,492✔
391
            double rel_vel =
392
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
4,705,020✔
393
              (E_electron + MASS_ELECTRON_EV);
4,705,020✔
394
            mu =
4,705,020✔
395
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
4,705,020✔
396
            break;
4,705,020✔
397
          }
398
        }
2,366,472✔
399

400
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
4,705,020✔
401
        Direction u;
4,705,020✔
402
        u.x = mu;
4,705,020✔
403
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
4,705,020✔
404
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
4,705,020✔
405

406
        // Create secondary electron
407
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
4,705,020✔
408

409
        // Allow electrons to fill orbital and produce auger electrons
410
        // and fluorescent photons
411
        element.atomic_relaxation(i_shell, p);
4,705,020✔
412
        p.event() = TallyEvent::ABSORB;
4,705,020✔
413
        p.event_mt() = 533 + shell.index_subshell;
4,705,020✔
414
        p.wgt() = 0.0;
4,705,020✔
415
        p.E() = 0.0;
4,705,020✔
416
        return;
4,705,020✔
417
      }
418
    }
419
  }
9,410,040✔
420
  prob = prob_after;
92,292✔
421

422
  // Pair production
423
  prob += micro.pair_production;
92,292✔
424
  if (prob > cutoff) {
92,292✔
425
    double E_electron, E_positron;
426
    double mu_electron, mu_positron;
427
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
92,292✔
428
      &mu_positron, p.current_seed());
429

430
    // Create secondary electron
431
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
92,292✔
432
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
92,292✔
433

434
    // Create secondary positron
435
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
92,292✔
436
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
92,292✔
437

438
    p.event() = TallyEvent::ABSORB;
92,292✔
439
    p.event_mt() = PAIR_PROD;
92,292✔
440
    p.wgt() = 0.0;
92,292✔
441
    p.E() = 0.0;
92,292✔
442
  }
443
}
444

445
void sample_electron_reaction(Particle& p)
48,720,552✔
446
{
447
  // TODO: create reaction types
448

449
  if (settings::electron_treatment == ElectronTreatment::TTB) {
48,720,552✔
450
    double E_lost;
451
    thick_target_bremsstrahlung(p, &E_lost);
48,720,552✔
452
  }
453

454
  p.E() = 0.0;
48,720,552✔
455
  p.wgt() = 0.0;
48,720,552✔
456
  p.event() = TallyEvent::ABSORB;
48,720,552✔
457
}
48,720,552✔
458

459
void sample_positron_reaction(Particle& p)
92,292✔
460
{
461
  // TODO: create reaction types
462

463
  if (settings::electron_treatment == ElectronTreatment::TTB) {
92,292✔
464
    double E_lost;
465
    thick_target_bremsstrahlung(p, &E_lost);
92,292✔
466
  }
467

468
  // Sample angle isotropically
469
  Direction u = isotropic_direction(p.current_seed());
92,292✔
470

471
  // Create annihilation photon pair traveling in opposite directions
472
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
92,292✔
473
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
92,292✔
474

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

480
int sample_nuclide(Particle& p)
728,142,173✔
481
{
482
  // Sample cumulative distribution function
483
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
728,142,173✔
484

485
  // Get pointers to nuclide/density arrays
486
  const auto& mat {model::materials[p.material()]};
728,142,173✔
487
  int n = mat->nuclide_.size();
728,142,173✔
488

489
  double prob = 0.0;
728,142,173✔
490
  for (int i = 0; i < n; ++i) {
1,649,587,588✔
491
    // Get atom density
492
    int i_nuclide = mat->nuclide_[i];
1,649,587,588✔
493
    double atom_density = mat->atom_density_[i];
1,649,587,588✔
494

495
    // Increment probability to compare to cutoff
496
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,649,587,588✔
497
    if (prob >= cutoff)
1,649,587,588✔
498
      return i_nuclide;
728,142,173✔
499
  }
500

501
  // If we reach here, no nuclide was sampled
502
  p.write_restart();
×
503
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
504
}
505

506
int sample_element(Particle& p)
11,121,456✔
507
{
508
  // Sample cumulative distribution function
509
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
11,121,456✔
510

511
  // Get pointers to elements, densities
512
  const auto& mat {model::materials[p.material()]};
11,121,456✔
513

514
  double prob = 0.0;
11,121,456✔
515
  for (int i = 0; i < mat->element_.size(); ++i) {
30,779,700✔
516
    // Find atom density
517
    int i_element = mat->element_[i];
30,779,700✔
518
    double atom_density = mat->atom_density_[i];
30,779,700✔
519

520
    // Determine microscopic cross section
521
    double sigma = atom_density * p.photon_xs(i_element).total;
30,779,700✔
522

523
    // Increment probability to compare to cutoff
524
    prob += sigma;
30,779,700✔
525
    if (prob > cutoff) {
30,779,700✔
526
      // Save which nuclide particle had collision with for tally purpose
527
      p.event_nuclide() = mat->nuclide_[i];
11,121,456✔
528

529
      return i_element;
11,121,456✔
530
    }
531
  }
532

533
  // If we made it here, no element was sampled
534
  p.write_restart();
×
535
  fatal_error("Did not sample any element during collision.");
×
536
}
537

538
Reaction& sample_fission(int i_nuclide, Particle& p)
110,347,095✔
539
{
540
  // Get pointer to nuclide
541
  const auto& nuc {data::nuclides[i_nuclide]};
110,347,095✔
542

543
  // If we're in the URR, by default use the first fission reaction. We also
544
  // default to the first reaction if we know that there are no partial fission
545
  // reactions
546
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
110,347,095✔
547
    return *nuc->fission_rx_[0];
110,320,026✔
548
  }
549

550
  // Check to see if we are in a windowed multipole range.  WMP only supports
551
  // the first fission reaction.
552
  if (nuc->multipole_) {
27,069✔
553
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
554
      return *nuc->fission_rx_[0];
×
555
    }
556
  }
557

558
  // Get grid index and interpolation factor and sample fission cdf
559
  const auto& micro = p.neutron_xs(i_nuclide);
27,069✔
560
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
27,069✔
561
  double prob = 0.0;
27,069✔
562

563
  // Loop through each partial fission reaction type
564
  for (auto& rx : nuc->fission_rx_) {
27,083✔
565
    // add to cumulative probability
566
    prob += rx->xs(micro);
27,083✔
567

568
    // Create fission bank sites if fission occurs
569
    if (prob > cutoff)
27,083✔
570
      return *rx;
27,069✔
571
  }
572

573
  // If we reached here, no reaction was sampled
574
  throw std::runtime_error {
×
575
    "No fission reaction was sampled for " + nuc->name_};
×
576
}
577

578
void sample_photon_product(
1,403,340✔
579
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
580
{
581
  // Get grid index and interpolation factor and sample photon production cdf
582
  const auto& micro = p.neutron_xs(i_nuclide);
1,403,340✔
583
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,403,340✔
584
  double prob = 0.0;
1,403,340✔
585

586
  // Loop through each reaction type
587
  const auto& nuc {data::nuclides[i_nuclide]};
1,403,340✔
588
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
23,211,156✔
589
    // Evaluate neutron cross section
590
    const auto& rx = nuc->reactions_[i];
23,211,156✔
591
    double xs = rx->xs(micro);
23,211,156✔
592

593
    // if cross section is zero for this reaction, skip it
594
    if (xs == 0.0)
23,211,156✔
595
      continue;
7,180,140✔
596

597
    for (int j = 0; j < rx->products_.size(); ++j) {
76,049,580✔
598
      if (rx->products_[j].particle_ == ParticleType::photon) {
61,421,904✔
599
        // For fission, artificially increase the photon yield to account
600
        // for delayed photons
601
        double f = 1.0;
47,915,244✔
602
        if (settings::delayed_photon_scaling) {
47,915,244✔
603
          if (is_fission(rx->mt_)) {
47,915,244✔
604
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
586,632✔
605
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
586,632✔
606
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
586,632✔
607
              f = (energy_prompt + energy_delayed) / (energy_prompt);
586,632✔
608
            }
609
          }
610
        }
611

612
        // add to cumulative probability
613
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
47,915,244✔
614

615
        *i_rx = i;
47,915,244✔
616
        *i_product = j;
47,915,244✔
617
        if (prob > cutoff)
47,915,244✔
618
          return;
1,403,340✔
619
      }
620
    }
621
  }
622
}
623

624
void absorption(Particle& p, int i_nuclide)
728,137,909✔
625
{
626
  if (settings::survival_biasing) {
728,137,909✔
627
    // Determine weight absorbed in survival biasing
628
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
557,892✔
629
                              p.neutron_xs(i_nuclide).total;
557,892✔
630

631
    // Adjust weight of particle by probability of absorption
632
    p.wgt() -= wgt_absorb;
557,892✔
633

634
    // Score implicit absorption estimate of keff
635
    if (settings::run_mode == RunMode::EIGENVALUE) {
557,892✔
636
      p.keff_tally_absorption() += wgt_absorb *
557,892✔
637
                                   p.neutron_xs(i_nuclide).nu_fission /
557,892✔
638
                                   p.neutron_xs(i_nuclide).absorption;
557,892✔
639
    }
640
  } else {
641
    // See if disappearance reaction happens
642
    if (p.neutron_xs(i_nuclide).absorption >
727,580,017✔
643
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
727,580,017✔
644
      // Score absorption estimate of keff
645
      if (settings::run_mode == RunMode::EIGENVALUE) {
18,647,263✔
646
        p.keff_tally_absorption() += p.wgt() *
32,600,164✔
647
                                     p.neutron_xs(i_nuclide).nu_fission /
16,300,082✔
648
                                     p.neutron_xs(i_nuclide).absorption;
16,300,082✔
649
      }
650

651
      p.wgt() = 0.0;
18,647,263✔
652
      p.event() = TallyEvent::ABSORB;
18,647,263✔
653
      p.event_mt() = N_DISAPPEAR;
18,647,263✔
654
    }
655
  }
656
}
728,137,909✔
657

658
void scatter(Particle& p, int i_nuclide)
709,480,472✔
659
{
660
  // copy incoming direction
661
  Direction u_old {p.u()};
709,480,472✔
662

663
  // Get pointer to nuclide and grid index/interpolation factor
664
  const auto& nuc {data::nuclides[i_nuclide]};
709,480,472✔
665
  const auto& micro {p.neutron_xs(i_nuclide)};
709,480,472✔
666
  int i_temp = micro.index_temp;
709,480,472✔
667

668
  // For tallying purposes, this routine might be called directly. In that
669
  // case, we need to sample a reaction via the cutoff variable
670
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
709,480,472✔
671
  bool sampled = false;
709,480,472✔
672

673
  // Calculate elastic cross section if it wasn't precalculated
674
  if (micro.elastic == CACHE_INVALID) {
709,480,472✔
675
    nuc->calculate_elastic_xs(p);
591,941,641✔
676
  }
677

678
  double prob = micro.elastic - micro.thermal;
709,480,472✔
679
  if (prob > cutoff) {
709,480,472✔
680
    // =======================================================================
681
    // NON-S(A,B) ELASTIC SCATTERING
682

683
    // Determine temperature
684
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
617,374,964✔
685

686
    // Perform collision physics for elastic scattering
687
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
617,374,964✔
688

689
    p.event_mt() = ELASTIC;
617,374,964✔
690
    sampled = true;
617,374,964✔
691
  }
692

693
  prob = micro.elastic;
709,480,472✔
694
  if (prob > cutoff && !sampled) {
709,480,472✔
695
    // =======================================================================
696
    // S(A,B) SCATTERING
697

698
    sab_scatter(i_nuclide, micro.index_sab, p);
76,650,411✔
699

700
    p.event_mt() = ELASTIC;
76,650,411✔
701
    sampled = true;
76,650,411✔
702
  }
703

704
  if (!sampled) {
709,480,472✔
705
    // =======================================================================
706
    // INELASTIC SCATTERING
707

708
    int n = nuc->index_inelastic_scatter_.size();
15,455,097✔
709
    int i = 0;
15,455,097✔
710
    for (int j = 0; j < n && prob < cutoff; ++j) {
268,491,999✔
711
      i = nuc->index_inelastic_scatter_[j];
253,036,902✔
712

713
      // add to cumulative probability
714
      prob += nuc->reactions_[i]->xs(micro);
253,036,902✔
715
    }
716

717
    // Perform collision physics for inelastic scattering
718
    const auto& rx {nuc->reactions_[i]};
15,455,097✔
719
    inelastic_scatter(*nuc, *rx, p);
15,455,097✔
720
    p.event_mt() = rx->mt_;
15,455,097✔
721
  }
722

723
  // Set event component
724
  p.event() = TallyEvent::SCATTER;
709,480,472✔
725

726
  // Sample new outgoing angle for isotropic-in-lab scattering
727
  const auto& mat {model::materials[p.material()]};
709,480,472✔
728
  if (!mat->p0_.empty()) {
709,480,472✔
729
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
339,852✔
730
    if (mat->p0_[i_nuc_mat]) {
339,852✔
731
      // Sample isotropic-in-lab outgoing direction
732
      p.u() = isotropic_direction(p.current_seed());
339,852✔
733
      p.mu() = u_old.dot(p.u());
339,852✔
734
    }
735
  }
736
}
709,480,472✔
737

738
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
617,374,964✔
739
{
740
  // get pointer to nuclide
741
  const auto& nuc {data::nuclides[i_nuclide]};
617,374,964✔
742

743
  double vel = std::sqrt(p.E());
617,374,964✔
744
  double awr = nuc->awr_;
617,374,964✔
745

746
  // Neutron velocity in LAB
747
  Direction v_n = vel * p.u();
617,374,964✔
748

749
  // Sample velocity of target nucleus
750
  Direction v_t {};
617,374,964✔
751
  if (!p.neutron_xs(i_nuclide).use_ptable) {
617,374,964✔
752
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,187,816,978✔
753
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
593,908,489✔
754
  }
755

756
  // Velocity of center-of-mass
757
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
617,374,964✔
758

759
  // Transform to CM frame
760
  v_n -= v_cm;
617,374,964✔
761

762
  // Find speed of neutron in CM
763
  vel = v_n.norm();
617,374,964✔
764

765
  // Sample scattering angle, checking if angle distribution is present (assume
766
  // isotropic otherwise)
767
  double mu_cm;
768
  auto& d = rx.products_[0].distribution_[0];
617,374,964✔
769
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
617,374,964✔
770
  if (!d_->angle().empty()) {
617,374,964✔
771
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
617,374,964✔
772
  } else {
773
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
774
  }
775

776
  // Determine direction cosines in CM
777
  Direction u_cm = v_n / vel;
617,374,964✔
778

779
  // Rotate neutron velocity vector to new angle -- note that the speed of the
780
  // neutron in CM does not change in elastic scattering. However, the speed
781
  // will change when we convert back to LAB
782
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
617,374,964✔
783

784
  // Transform back to LAB frame
785
  v_n += v_cm;
617,374,964✔
786

787
  p.E() = v_n.dot(v_n);
617,374,964✔
788
  vel = std::sqrt(p.E());
617,374,964✔
789

790
  // compute cosine of scattering angle in LAB frame by taking dot product of
791
  // neutron's pre- and post-collision angle
792
  p.mu() = p.u().dot(v_n) / vel;
617,374,964✔
793

794
  // Set energy and direction of particle in LAB frame
795
  p.u() = v_n / vel;
617,374,964✔
796

797
  // Because of floating-point roundoff, it may be possible for mu_lab to be
798
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
799
  // -1 or 1
800
  if (std::abs(p.mu()) > 1.0)
617,374,964✔
801
    p.mu() = std::copysign(1.0, p.mu());
×
802
}
617,374,964✔
803

804
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
76,650,411✔
805
{
806
  // Determine temperature index
807
  const auto& micro {p.neutron_xs(i_nuclide)};
76,650,411✔
808
  int i_temp = micro.index_temp_sab;
76,650,411✔
809

810
  // Sample energy and angle
811
  double E_out;
812
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
153,300,822✔
813
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
76,650,411✔
814

815
  // Set energy to outgoing, change direction of particle
816
  p.E() = E_out;
76,650,411✔
817
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
76,650,411✔
818
}
76,650,411✔
819

820
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
593,908,489✔
821
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
822
{
823
  // check if nuclide is a resonant scatterer
824
  ResScatMethod sampling_method;
825
  if (nuc.resonant_) {
593,908,489✔
826

827
    // sampling method to use
828
    sampling_method = settings::res_scat_method;
90,600✔
829

830
    // upper resonance scattering energy bound (target is at rest above this E)
831
    if (E > settings::res_scat_energy_max) {
90,600✔
832
      return {};
44,196✔
833

834
      // lower resonance scattering energy bound (should be no resonances below)
835
    } else if (E < settings::res_scat_energy_min) {
46,404✔
836
      sampling_method = ResScatMethod::cxs;
26,052✔
837
    }
838

839
    // otherwise, use free gas model
840
  } else {
841
    if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) {
593,817,889✔
842
      return {};
243,089,649✔
843
    } else {
844
      sampling_method = ResScatMethod::cxs;
350,728,240✔
845
    }
846
  }
847

848
  // use appropriate target velocity sampling method
849
  switch (sampling_method) {
350,774,644✔
850
  case ResScatMethod::cxs:
350,754,292✔
851

852
    // sample target velocity with the constant cross section (cxs) approx.
853
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
350,754,292✔
854

855
  case ResScatMethod::dbrc:
20,352✔
856
  case ResScatMethod::rvs: {
857
    double E_red = std::sqrt(nuc.awr_ * E / kT);
20,352✔
858
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
20,352✔
859
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
20,352✔
860

861
    // find lower and upper energy bound indices
862
    // lower index
863
    int i_E_low;
864
    if (E_low < nuc.energy_0K_.front()) {
20,352✔
865
      i_E_low = 0;
×
866
    } else if (E_low > nuc.energy_0K_.back()) {
20,352✔
867
      i_E_low = nuc.energy_0K_.size() - 2;
×
868
    } else {
869
      i_E_low =
20,352✔
870
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
20,352✔
871
    }
872

873
    // upper index
874
    int i_E_up;
875
    if (E_up < nuc.energy_0K_.front()) {
20,352✔
876
      i_E_up = 0;
×
877
    } else if (E_up > nuc.energy_0K_.back()) {
20,352✔
878
      i_E_up = nuc.energy_0K_.size() - 2;
×
879
    } else {
880
      i_E_up =
20,352✔
881
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
20,352✔
882
    }
883

884
    if (i_E_up == i_E_low) {
20,352✔
885
      // Handle degenerate case -- if the upper/lower bounds occur for the same
886
      // index, then using cxs is probably a good approximation
887
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
20,352✔
888
    }
889

890
    if (sampling_method == ResScatMethod::dbrc) {
16,860✔
891
      // interpolate xs since we're not exactly at the energy indices
892
      double xs_low = nuc.elastic_0K_[i_E_low];
×
893
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
894
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
895
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
896
      double xs_up = nuc.elastic_0K_[i_E_up];
×
897
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
898
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
899
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
900

901
      // get max 0K xs value over range of practical relative energies
902
      double xs_max = *std::max_element(
×
903
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
904
      xs_max = std::max({xs_low, xs_max, xs_up});
×
905

906
      while (true) {
907
        double E_rel;
908
        Direction v_target;
×
909
        while (true) {
910
          // sample target velocity with the constant cross section (cxs)
911
          // approx.
912
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
913
          Direction v_rel = v_neut - v_target;
×
914
          E_rel = v_rel.dot(v_rel);
×
915
          if (E_rel < E_up)
×
916
            break;
×
917
        }
918

919
        // perform Doppler broadening rejection correction (dbrc)
920
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
921
        double R = xs_0K / xs_max;
×
922
        if (prn(seed) < R)
×
923
          return v_target;
×
924
      }
925

926
    } else if (sampling_method == ResScatMethod::rvs) {
16,860✔
927
      // interpolate xs CDF since we're not exactly at the energy indices
928
      // cdf value at lower bound attainable energy
929
      double cdf_low = 0.0;
16,860✔
930
      if (E_low > nuc.energy_0K_.front()) {
16,860✔
931
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
16,860✔
932
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
16,860✔
933
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
16,860✔
934
      }
935

936
      // cdf value at upper bound attainable energy
937
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
16,860✔
938
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
16,860✔
939
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
16,860✔
940

941
      while (true) {
942
        // directly sample Maxwellian
943
        double E_t = -kT * std::log(prn(seed));
177,324✔
944

945
        // sample a relative energy using the xs cdf
946
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
177,324✔
947
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
177,324✔
948
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
177,324✔
949
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
177,324✔
950
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
177,324✔
951
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
177,324✔
952
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
177,324✔
953
                     nuc.energy_0K_[i_E_low + i_E_rel]);
177,324✔
954
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
177,324✔
955

956
        // perform rejection sampling on cosine between
957
        // neutron and target velocities
958
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
177,324✔
959
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
177,324✔
960

961
        if (std::abs(mu) < 1.0) {
177,324✔
962
          // set and accept target velocity
963
          E_t /= nuc.awr_;
16,860✔
964
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
16,860✔
965
        }
966
      }
160,464✔
967
    }
968
  } // case RVS, DBRC
969
  } // switch (sampling_method)
970

971
  UNREACHABLE();
×
972
}
973

974
Direction sample_cxs_target_velocity(
350,757,784✔
975
  double awr, double E, Direction u, double kT, uint64_t* seed)
976
{
977
  double beta_vn = std::sqrt(awr * E / kT);
350,757,784✔
978
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
350,757,784✔
979

980
  double beta_vt_sq;
981
  double mu;
982
  while (true) {
983
    // Sample two random numbers
984
    double r1 = prn(seed);
416,102,847✔
985
    double r2 = prn(seed);
416,102,847✔
986

987
    if (prn(seed) < alpha) {
416,102,847✔
988
      // With probability alpha, we sample the distribution p(y) =
989
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
990
      // Carlo sampler
991

992
      beta_vt_sq = -std::log(r1 * r2);
106,252,974✔
993

994
    } else {
995
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
996
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
997
      // Carlo sampler
998

999
      double c = std::cos(PI / 2.0 * prn(seed));
309,849,873✔
1000
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
309,849,873✔
1001
    }
1002

1003
    // Determine beta * vt
1004
    double beta_vt = std::sqrt(beta_vt_sq);
416,102,847✔
1005

1006
    // Sample cosine of angle between neutron and target velocity
1007
    mu = uniform_distribution(-1., 1., seed);
416,102,847✔
1008

1009
    // Determine rejection probability
1010
    double accept_prob =
1011
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
416,102,847✔
1012
      (beta_vn + beta_vt);
416,102,847✔
1013

1014
    // Perform rejection sampling on vt and mu
1015
    if (prn(seed) < accept_prob)
416,102,847✔
1016
      break;
350,757,784✔
1017
  }
65,345,063✔
1018

1019
  // Determine speed of target nucleus
1020
  double vt = std::sqrt(beta_vt_sq * kT / awr);
350,757,784✔
1021

1022
  // Determine velocity vector of target nucleus based on neutron's velocity
1023
  // and the sampled angle between them
1024
  return vt * rotate_angle(u, mu, nullptr, seed);
350,757,784✔
1025
}
1026

1027
void sample_fission_neutron(
26,411,793✔
1028
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1029
{
1030
  // Get attributes of particle
1031
  double E_in = p.E();
26,411,793✔
1032
  uint64_t* seed = p.current_seed();
26,411,793✔
1033

1034
  // Determine total nu, delayed nu, and delayed neutron fraction
1035
  const auto& nuc {data::nuclides[i_nuclide]};
26,411,793✔
1036
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
26,411,793✔
1037
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
26,411,793✔
1038
  double beta = nu_d / nu_t;
26,411,793✔
1039

1040
  if (prn(seed) < beta) {
26,411,793✔
1041
    // ====================================================================
1042
    // DELAYED NEUTRON SAMPLED
1043

1044
    // sampled delayed precursor group
1045
    double xi = prn(seed) * nu_d;
165,832✔
1046
    double prob = 0.0;
165,832✔
1047
    int group;
1048
    for (group = 1; group < nuc->n_precursor_; ++group) {
622,454✔
1049
      // determine delayed neutron precursor yield for group j
1050
      double yield = (*rx.products_[group].yield_)(E_in);
609,417✔
1051

1052
      // Check if this group is sampled
1053
      prob += yield;
609,417✔
1054
      if (xi < prob)
609,417✔
1055
        break;
152,795✔
1056
    }
1057

1058
    // if the sum of the probabilities is slightly less than one and the
1059
    // random number is greater, j will be greater than nuc %
1060
    // n_precursor -- check for this condition
1061
    group = std::min(group, nuc->n_precursor_);
165,832✔
1062

1063
    // set the delayed group for the particle born from fission
1064
    site->delayed_group = group;
165,832✔
1065

1066
  } else {
1067
    // ====================================================================
1068
    // PROMPT NEUTRON SAMPLED
1069

1070
    // set the delayed group for the particle born from fission to 0
1071
    site->delayed_group = 0;
26,245,961✔
1072
  }
1073

1074
  // sample from prompt neutron energy distribution
1075
  int n_sample = 0;
26,411,793✔
1076
  double mu;
1077
  while (true) {
1078
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
26,411,793✔
1079

1080
    // resample if energy is greater than maximum neutron energy
1081
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
26,411,793✔
1082
    if (site->E < data::energy_max[neutron])
26,411,793✔
1083
      break;
26,411,793✔
1084

1085
    // check for large number of resamples
1086
    ++n_sample;
×
1087
    if (n_sample == MAX_SAMPLE) {
×
1088
      // particle_write_restart(p)
1089
      fatal_error("Resampled energy distribution maximum number of times "
×
1090
                  "for nuclide " +
×
1091
                  nuc->name_);
×
1092
    }
1093
  }
1094

1095
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1096
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
26,411,793✔
1097
}
26,411,793✔
1098

1099
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
15,455,097✔
1100
{
1101
  // copy energy of neutron
1102
  double E_in = p.E();
15,455,097✔
1103

1104
  // sample outgoing energy and scattering cosine
1105
  double E;
1106
  double mu;
1107
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
15,455,097✔
1108

1109
  // if scattering system is in center-of-mass, transfer cosine of scattering
1110
  // angle and outgoing energy from CM to LAB
1111
  if (rx.scatter_in_cm_) {
15,455,097✔
1112
    double E_cm = E;
15,421,641✔
1113

1114
    // determine outgoing energy in lab
1115
    double A = nuc.awr_;
15,421,641✔
1116
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
15,421,641✔
1117
                 ((A + 1.0) * (A + 1.0));
15,421,641✔
1118

1119
    // determine outgoing angle in lab
1120
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
15,421,641✔
1121
  }
1122

1123
  // Because of floating-point roundoff, it may be possible for mu to be
1124
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1125
  // or 1
1126
  if (std::abs(mu) > 1.0)
15,455,097✔
1127
    mu = std::copysign(1.0, mu);
×
1128

1129
  // Set outgoing energy and scattering angle
1130
  p.E() = E;
15,455,097✔
1131
  p.mu() = mu;
15,455,097✔
1132

1133
  // change direction of particle
1134
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
15,455,097✔
1135

1136
  // evaluate yield
1137
  double yield = (*rx.products_[0].yield_)(E_in);
15,455,097✔
1138
  if (std::floor(yield) == yield && yield > 0) {
15,455,097✔
1139
    // If yield is integral, create exactly that many secondary particles
1140
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
15,513,559✔
1141
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
58,540✔
1142
    }
1143
  } else {
15,455,019✔
1144
    // Otherwise, change weight of particle based on yield
1145
    p.wgt() *= yield;
78✔
1146
  }
1147
}
15,455,097✔
1148

1149
void sample_secondary_photons(Particle& p, int i_nuclide)
9,848,112✔
1150
{
1151
  // Sample the number of photons produced
1152
  double y_t =
1153
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
9,848,112✔
1154
  int y = static_cast<int>(y_t);
9,848,112✔
1155
  if (prn(p.current_seed()) <= y_t - y)
9,848,112✔
1156
    ++y;
630,816✔
1157

1158
  // Sample each secondary photon
1159
  for (int i = 0; i < y; ++i) {
11,251,452✔
1160
    // Sample the reaction and product
1161
    int i_rx;
1162
    int i_product;
1163
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,403,340✔
1164

1165
    // Sample the outgoing energy and angle
1166
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,403,340✔
1167
    double E;
1168
    double mu;
1169
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,403,340✔
1170

1171
    // Sample the new direction
1172
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,403,340✔
1173

1174
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1175
    // secondary photons from non-fission reactions to properly balance energy
1176
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1177
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1178
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1179
    double wgt;
1180
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
1,403,340✔
1181
      wgt = simulation::keff * p.wgt();
378,540✔
1182
    } else {
1183
      wgt = p.wgt();
1,024,800✔
1184
    }
1185

1186
    // Create the secondary photon
1187
    p.create_secondary(wgt, u, E, ParticleType::photon);
1,403,340✔
1188
  }
1189
}
9,848,112✔
1190

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