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

openmc-dev / openmc / 13591584831

28 Feb 2025 03:46PM UTC coverage: 85.051% (+0.3%) from 84.722%
13591584831

Pull #3067

github

web-flow
Merge 08055e996 into c26fde666
Pull Request #3067: Implement user-configurable random number stride

36 of 44 new or added lines in 8 files covered. (81.82%)

3588 existing lines in 111 files now uncovered.

51062 of 60037 relevant lines covered (85.05%)

32650986.73 hits per line

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

89.23
/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/material.h"
12
#include "openmc/math_functions.h"
13
#include "openmc/message_passing.h"
14
#include "openmc/ncrystal_interface.h"
15
#include "openmc/nuclide.h"
16
#include "openmc/photon.h"
17
#include "openmc/physics_common.h"
18
#include "openmc/random_dist.h"
19
#include "openmc/random_lcg.h"
20
#include "openmc/reaction.h"
21
#include "openmc/search.h"
22
#include "openmc/secondary_uncorrelated.h"
23
#include "openmc/settings.h"
24
#include "openmc/simulation.h"
25
#include "openmc/string_utils.h"
26
#include "openmc/tallies/tally.h"
27
#include "openmc/thermal.h"
28
#include "openmc/weight_windows.h"
29

30
#include <fmt/core.h>
31

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

36
namespace openmc {
37

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

42
void collision(Particle& p)
744,898,699✔
43
{
44
  // Add to collision counter for particle
45
  ++(p.n_collision());
744,898,699✔
46

47
  // Sample reaction for the material the particle is in
48
  switch (p.type()) {
744,898,699✔
49
  case ParticleType::neutron:
676,999,892✔
50
    sample_neutron_reaction(p);
676,999,892✔
51
    break;
676,999,892✔
52
  case ParticleType::photon:
14,635,666✔
53
    sample_photon_reaction(p);
14,635,666✔
54
    break;
14,635,666✔
55
  case ParticleType::electron:
53,170,177✔
56
    sample_electron_reaction(p);
53,170,177✔
57
    break;
53,170,177✔
58
  case ParticleType::positron:
92,964✔
59
    sample_positron_reaction(p);
92,964✔
60
    break;
92,964✔
61
  }
62

63
  if (settings::weight_window_checkpoint_collision)
744,898,699✔
64
    apply_weight_windows(p);
744,898,699✔
65

66
  // Kill particle if energy falls below cutoff
67
  int type = static_cast<int>(p.type());
744,898,699✔
68
  if (p.E() < settings::energy_cutoff[type]) {
744,898,699✔
69
    p.wgt() = 0.0;
5,183,999✔
70
  }
71

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

92
void sample_neutron_reaction(Particle& p)
676,999,892✔
93
{
94
  // Sample a nuclide within the material
95
  int i_nuclide = sample_nuclide(p);
676,999,892✔
96

97
  // Save which nuclide particle had collision with
98
  p.event_nuclide() = i_nuclide;
676,999,892✔
99

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

105
  const auto& nuc {data::nuclides[i_nuclide]};
676,999,892✔
106

107
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
676,999,892✔
108
    auto& rx = sample_fission(i_nuclide, p);
83,569,389✔
109
    if (settings::run_mode == RunMode::EIGENVALUE) {
83,569,389✔
110
      create_fission_sites(p, i_nuclide, rx);
82,965,866✔
111
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
603,523✔
112
               settings::create_fission_neutrons) {
113
      create_fission_sites(p, i_nuclide, rx);
588,355✔
114

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

126
  // Create secondary photons
127
  if (settings::photon_transport) {
676,999,892✔
128
    sample_secondary_photons(p, i_nuclide);
9,926,664✔
129
  }
130

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

134
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
676,999,892✔
135
    absorption(p, i_nuclide);
676,995,718✔
136
  }
137
  if (!p.alive())
676,999,892✔
138
    return;
16,804,713✔
139

140
  // Sample a scattering reaction and determine the secondary energy of the
141
  // exiting neutron
142
  const auto& ncrystal_mat = model::materials[p.material()]->ncrystal_mat();
660,195,179✔
143
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
660,195,179✔
144
    ncrystal_mat.scatter(p);
14,439✔
145
  } else {
146
    scatter(p, i_nuclide);
660,180,740✔
147
  }
148

149
  // Advance URR seed stream 'N' times after energy changes
150
  if (p.E() != p.E_last()) {
660,195,179✔
151
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
659,982,507✔
152
  }
153

154
  // Play russian roulette if survival biasing is turned on
155
  if (settings::survival_biasing) {
660,195,179✔
156
    if (p.wgt() < settings::weight_cutoff) {
557,892✔
157
      russian_roulette(p, settings::weight_survive);
63,984✔
158
    }
159
  }
160
}
161

162
void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx)
83,554,221✔
163
{
164
  // If uniform fission source weighting is turned on, we increase or decrease
165
  // the expected number of fission sites produced
166
  double weight = settings::ufs_on ? ufs_get_weight(p) : 1.0;
83,554,221✔
167

168
  // Determine the expected number of neutrons produced
169
  double nu_t = p.wgt() / simulation::keff * weight *
83,554,221✔
170
                p.neutron_xs(i_nuclide).nu_fission /
83,554,221✔
171
                p.neutron_xs(i_nuclide).total;
83,554,221✔
172

173
  // Sample the number of neutrons produced
174
  int nu = static_cast<int>(nu_t);
83,554,221✔
175
  if (prn(p.current_seed()) <= (nu_t - nu))
83,554,221✔
176
    ++nu;
14,325,574✔
177

178
  // If no neutrons were produced then don't continue
179
  if (nu == 0)
83,554,221✔
180
    return;
65,008,725✔
181

182
  // Initialize the counter of delayed neutrons encountered for each delayed
183
  // group.
184
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
18,545,496✔
185

186
  // Clear out particle's nu fission bank
187
  p.nu_bank().clear();
18,545,496✔
188

189
  p.fission() = true;
18,545,496✔
190

191
  // Determine whether to place fission sites into the shared fission bank
192
  // or the secondary particle bank.
193
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
18,545,496✔
194

195
  // Counter for the number of fission sites successfully stored to the shared
196
  // fission bank or the secondary particle bank
197
  int n_sites_stored;
198

199
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
42,529,852✔
200
    // Initialize fission site object with particle data
201
    SourceSite site;
23,984,356✔
202
    site.r = p.r();
23,984,356✔
203
    site.particle = ParticleType::neutron;
23,984,356✔
204
    site.time = p.time();
23,984,356✔
205
    site.wgt = 1. / weight;
23,984,356✔
206
    site.parent_id = p.id();
23,984,356✔
207
    site.progeny_id = p.n_progeny()++;
23,984,356✔
208
    site.surf_id = 0;
23,984,356✔
209

210
    // Sample delayed group and angle/energy for fission reaction
211
    sample_fission_neutron(i_nuclide, rx, &site, p);
23,984,356✔
212

213
    // Store fission site in bank
214
    if (use_fission_bank) {
23,984,356✔
215
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
23,780,907✔
216
      if (idx == -1) {
23,780,907✔
UNCOV
217
        warning(
×
218
          "The shared fission bank is full. Additional fission sites created "
219
          "in this generation will not be banked. Results may be "
220
          "non-deterministic.");
221

222
        // Decrement number of particle progeny as storage was unsuccessful.
223
        // This step is needed so that the sum of all progeny is equal to the
224
        // size of the shared fission bank.
UNCOV
225
        p.n_progeny()--;
×
226

227
        // Break out of loop as no more sites can be added to fission bank
UNCOV
228
        break;
×
229
      }
230
    } else {
231
      p.secondary_bank().push_back(site);
203,449✔
232
    }
233

234
    // Set the delayed group on the particle as well
235
    p.delayed_group() = site.delayed_group;
23,984,356✔
236

237
    // Increment the number of neutrons born delayed
238
    if (p.delayed_group() > 0) {
23,984,356✔
239
      nu_d[p.delayed_group() - 1]++;
145,784✔
240
    }
241

242
    // Write fission particles to nuBank
243
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
23,984,356✔
244
    nu_bank_entry.wgt = site.wgt;
23,984,356✔
245
    nu_bank_entry.E = site.E;
23,984,356✔
246
    nu_bank_entry.delayed_group = site.delayed_group;
23,984,356✔
247
  }
248

249
  // If shared fission bank was full, and no fissions could be added,
250
  // set the particle fission flag to false.
251
  if (n_sites_stored == 0) {
18,545,496✔
252
    p.fission() = false;
×
253
    return;
×
254
  }
255

256
  // Set nu to the number of fission sites successfully stored. If the fission
257
  // bank was not found to be full then these values are already equivalent.
258
  nu = n_sites_stored;
18,545,496✔
259

260
  // Store the total weight banked for analog fission tallies
261
  p.n_bank() = nu;
18,545,496✔
262
  p.wgt_bank() = nu / weight;
18,545,496✔
263
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
166,909,464✔
264
    p.n_delayed_bank(d) = nu_d[d];
148,363,968✔
265
  }
266
}
267

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

280
  // Sample element within material
281
  int i_element = sample_element(p);
14,635,666✔
282
  const auto& micro {p.photon_xs(i_element)};
14,635,666✔
283
  const auto& element {*data::elements[i_element]};
14,635,666✔
284

285
  // Calculate photon energy over electron rest mass equivalent
286
  double alpha = p.E() / MASS_ELECTRON_EV;
14,635,666✔
287

288
  // For tallying purposes, this routine might be called directly. In that
289
  // case, we need to sample a reaction via the cutoff variable
290
  double prob = 0.0;
14,635,666✔
291
  double cutoff = prn(p.current_seed()) * micro.total;
14,635,666✔
292

293
  // Coherent (Rayleigh) scattering
294
  prob += micro.coherent;
14,635,666✔
295
  if (prob > cutoff) {
14,635,666✔
296
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
774,591✔
297
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
774,591✔
298
    p.event() = TallyEvent::SCATTER;
774,591✔
299
    p.event_mt() = COHERENT;
774,591✔
300
    return;
774,591✔
301
  }
302

303
  // Incoherent (Compton) scattering
304
  prob += micro.incoherent;
13,861,075✔
305
  if (prob > cutoff) {
13,861,075✔
306
    double alpha_out;
307
    int i_shell;
308
    element.compton_scatter(
17,377,984✔
309
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
8,688,992✔
310

311
    // Determine binding energy of shell. The binding energy is 0.0 if
312
    // doppler broadening is not used.
313
    double e_b;
314
    if (i_shell == -1) {
8,688,992✔
315
      e_b = 0.0;
×
316
    } else {
317
      e_b = element.binding_energy_[i_shell];
8,688,992✔
318
    }
319

320
    // Create Compton electron
321
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
8,688,992✔
322
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
8,688,992✔
323
    int electron = static_cast<int>(ParticleType::electron);
8,688,992✔
324
    if (E_electron >= settings::energy_cutoff[electron]) {
8,688,992✔
325
      double mu_electron = (alpha - alpha_out * p.mu()) /
8,610,140✔
326
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
17,220,280✔
327
                                     2.0 * alpha * alpha_out * p.mu());
8,610,140✔
328
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
8,610,140✔
329
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
8,610,140✔
330
    }
331

332
    // TODO: Compton subshell data does not match atomic relaxation data
333
    // Allow electrons to fill orbital and produce auger electrons
334
    // and fluorescent photons
335
    if (i_shell >= 0) {
8,688,992✔
336
      element.atomic_relaxation(i_shell, p);
8,688,992✔
337
    }
338

339
    phi += PI;
8,688,992✔
340
    p.E() = alpha_out * MASS_ELECTRON_EV;
8,688,992✔
341
    p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed());
8,688,992✔
342
    p.event() = TallyEvent::SCATTER;
8,688,992✔
343
    p.event_mt() = INCOHERENT;
8,688,992✔
344
    return;
8,688,992✔
345
  }
346

347
  // Photoelectric effect
348
  double prob_after = prob + micro.photoelectric;
5,172,083✔
349

350
  if (prob_after > cutoff) {
5,172,083✔
351
    // Get grid index, interpolation factor, and bounding subshell
352
    // cross sections
353
    int i_grid = micro.index_grid;
5,079,119✔
354
    double f = micro.interp_factor;
5,079,119✔
355
    const auto& xs_lower = xt::row(element.cross_sections_, i_grid);
5,079,119✔
356
    const auto& xs_upper = xt::row(element.cross_sections_, i_grid + 1);
5,079,119✔
357

358
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
25,920,767✔
359
      const auto& shell {element.shells_[i_shell]};
25,920,767✔
360

361
      // Check threshold of reaction
362
      if (xs_lower(i_shell) == 0)
25,920,767✔
363
        continue;
11,169,816✔
364

365
      //  Evaluation subshell photoionization cross section
366
      prob += std::exp(
14,750,951✔
367
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
14,750,951✔
368

369
      if (prob > cutoff) {
14,750,951✔
370
        // Determine binding energy based on whether atomic relaxation data is
371
        // present (if not, use value from Compton profile data)
372
        double binding_energy = element.has_atomic_relaxation_
5,079,119✔
373
                                  ? shell.binding_energy
5,079,119✔
374
                                  : element.binding_energy_[i_shell];
×
375

376
        // Determine energy of secondary electron
377
        double E_electron = p.E() - binding_energy;
5,079,119✔
378

379
        // Sample mu using non-relativistic Sauter distribution.
380
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
381
        // model in Serpent 2" by Toni Kaltiaisenaho
382
        double mu;
383
        while (true) {
384
          double r = prn(p.current_seed());
7,630,560✔
385
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
7,630,560✔
386
            double rel_vel =
387
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
5,079,119✔
388
              (E_electron + MASS_ELECTRON_EV);
5,079,119✔
389
            mu =
5,079,119✔
390
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
5,079,119✔
391
            break;
5,079,119✔
392
          }
393
        }
2,551,441✔
394

395
        double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
5,079,119✔
396
        Direction u;
5,079,119✔
397
        u.x = mu;
5,079,119✔
398
        u.y = std::sqrt(1.0 - mu * mu) * std::cos(phi);
5,079,119✔
399
        u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi);
5,079,119✔
400

401
        // Create secondary electron
402
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
5,079,119✔
403

404
        // Allow electrons to fill orbital and produce auger electrons
405
        // and fluorescent photons
406
        element.atomic_relaxation(i_shell, p);
5,079,119✔
407
        p.event() = TallyEvent::ABSORB;
5,079,119✔
408
        p.event_mt() = 533 + shell.index_subshell;
5,079,119✔
409
        p.wgt() = 0.0;
5,079,119✔
410
        p.E() = 0.0;
5,079,119✔
411
        return;
5,079,119✔
412
      }
413
    }
414
  }
10,158,238✔
415
  prob = prob_after;
92,964✔
416

417
  // Pair production
418
  prob += micro.pair_production;
92,964✔
419
  if (prob > cutoff) {
92,964✔
420
    double E_electron, E_positron;
421
    double mu_electron, mu_positron;
422
    element.pair_production(alpha, &E_electron, &E_positron, &mu_electron,
92,964✔
423
      &mu_positron, p.current_seed());
424

425
    // Create secondary electron
426
    Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed());
92,964✔
427
    p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
92,964✔
428

429
    // Create secondary positron
430
    u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed());
92,964✔
431
    p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron);
92,964✔
432

433
    p.event() = TallyEvent::ABSORB;
92,964✔
434
    p.event_mt() = PAIR_PROD;
92,964✔
435
    p.wgt() = 0.0;
92,964✔
436
    p.E() = 0.0;
92,964✔
437
  }
438
}
439

440
void sample_electron_reaction(Particle& p)
53,170,177✔
441
{
442
  // TODO: create reaction types
443

444
  if (settings::electron_treatment == ElectronTreatment::TTB) {
53,170,177✔
445
    double E_lost;
446
    thick_target_bremsstrahlung(p, &E_lost);
53,170,177✔
447
  }
448

449
  p.E() = 0.0;
53,170,177✔
450
  p.wgt() = 0.0;
53,170,177✔
451
  p.event() = TallyEvent::ABSORB;
53,170,177✔
452
}
53,170,177✔
453

454
void sample_positron_reaction(Particle& p)
92,964✔
455
{
456
  // TODO: create reaction types
457

458
  if (settings::electron_treatment == ElectronTreatment::TTB) {
92,964✔
459
    double E_lost;
460
    thick_target_bremsstrahlung(p, &E_lost);
92,964✔
461
  }
462

463
  // Sample angle isotropically
464
  Direction u = isotropic_direction(p.current_seed());
92,964✔
465

466
  // Create annihilation photon pair traveling in opposite directions
467
  p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon);
92,964✔
468
  p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon);
92,964✔
469

470
  p.E() = 0.0;
92,964✔
471
  p.wgt() = 0.0;
92,964✔
472
  p.event() = TallyEvent::ABSORB;
92,964✔
473
}
92,964✔
474

475
int sample_nuclide(Particle& p)
676,999,892✔
476
{
477
  // Sample cumulative distribution function
478
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
676,999,892✔
479

480
  // Get pointers to nuclide/density arrays
481
  const auto& mat {model::materials[p.material()]};
676,999,892✔
482
  int n = mat->nuclide_.size();
676,999,892✔
483

484
  double prob = 0.0;
676,999,892✔
485
  for (int i = 0; i < n; ++i) {
1,515,060,282✔
486
    // Get atom density
487
    int i_nuclide = mat->nuclide_[i];
1,515,060,282✔
488
    double atom_density = mat->atom_density_[i];
1,515,060,282✔
489

490
    // Increment probability to compare to cutoff
491
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,515,060,282✔
492
    if (prob >= cutoff)
1,515,060,282✔
493
      return i_nuclide;
676,999,892✔
494
  }
495

496
  // If we reach here, no nuclide was sampled
497
  p.write_restart();
×
498
  throw std::runtime_error {"Did not sample any nuclide during collision."};
×
499
}
500

501
int sample_element(Particle& p)
14,635,666✔
502
{
503
  // Sample cumulative distribution function
504
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
14,635,666✔
505

506
  // Get pointers to elements, densities
507
  const auto& mat {model::materials[p.material()]};
14,635,666✔
508

509
  double prob = 0.0;
14,635,666✔
510
  for (int i = 0; i < mat->element_.size(); ++i) {
37,590,621✔
511
    // Find atom density
512
    int i_element = mat->element_[i];
37,590,621✔
513
    double atom_density = mat->atom_density_[i];
37,590,621✔
514

515
    // Determine microscopic cross section
516
    double sigma = atom_density * p.photon_xs(i_element).total;
37,590,621✔
517

518
    // Increment probability to compare to cutoff
519
    prob += sigma;
37,590,621✔
520
    if (prob > cutoff) {
37,590,621✔
521
      // Save which nuclide particle had collision with for tally purpose
522
      p.event_nuclide() = mat->nuclide_[i];
14,635,666✔
523

524
      return i_element;
14,635,666✔
525
    }
526
  }
527

528
  // If we made it here, no element was sampled
529
  p.write_restart();
×
530
  fatal_error("Did not sample any element during collision.");
×
531
}
532

533
Reaction& sample_fission(int i_nuclide, Particle& p)
83,569,389✔
534
{
535
  // Get pointer to nuclide
536
  const auto& nuc {data::nuclides[i_nuclide]};
83,569,389✔
537

538
  // If we're in the URR, by default use the first fission reaction. We also
539
  // default to the first reaction if we know that there are no partial fission
540
  // reactions
541
  if (p.neutron_xs(i_nuclide).use_ptable || !nuc->has_partial_fission_) {
83,569,389✔
542
    return *nuc->fission_rx_[0];
83,543,391✔
543
  }
544

545
  // Check to see if we are in a windowed multipole range.  WMP only supports
546
  // the first fission reaction.
547
  if (nuc->multipole_) {
25,998✔
548
    if (p.E() >= nuc->multipole_->E_min_ && p.E() <= nuc->multipole_->E_max_) {
×
549
      return *nuc->fission_rx_[0];
×
550
    }
551
  }
552

553
  // Get grid index and interpolation factor and sample fission cdf
554
  const auto& micro = p.neutron_xs(i_nuclide);
25,998✔
555
  double cutoff = prn(p.current_seed()) * p.neutron_xs(i_nuclide).fission;
25,998✔
556
  double prob = 0.0;
25,998✔
557

558
  // Loop through each partial fission reaction type
559
  for (auto& rx : nuc->fission_rx_) {
26,012✔
560
    // add to cumulative probability
561
    prob += rx->xs(micro);
26,012✔
562

563
    // Create fission bank sites if fission occurs
564
    if (prob > cutoff)
26,012✔
565
      return *rx;
25,998✔
566
  }
567

568
  // If we reached here, no reaction was sampled
569
  throw std::runtime_error {
×
570
    "No fission reaction was sampled for " + nuc->name_};
×
571
}
572

573
void sample_photon_product(
1,463,040✔
574
  int i_nuclide, Particle& p, int* i_rx, int* i_product)
575
{
576
  // Get grid index and interpolation factor and sample photon production cdf
577
  const auto& micro = p.neutron_xs(i_nuclide);
1,463,040✔
578
  double cutoff = prn(p.current_seed()) * micro.photon_prod;
1,463,040✔
579
  double prob = 0.0;
1,463,040✔
580

581
  // Loop through each reaction type
582
  const auto& nuc {data::nuclides[i_nuclide]};
1,463,040✔
583
  for (int i = 0; i < nuc->reactions_.size(); ++i) {
24,105,432✔
584
    // Evaluate neutron cross section
585
    const auto& rx = nuc->reactions_[i];
24,105,432✔
586
    double xs = rx->xs(micro);
24,105,432✔
587

588
    // if cross section is zero for this reaction, skip it
589
    if (xs == 0.0)
24,105,432✔
590
      continue;
7,948,080✔
591

592
    for (int j = 0; j < rx->products_.size(); ++j) {
76,241,856✔
593
      if (rx->products_[j].particle_ == ParticleType::photon) {
61,547,544✔
594
        // For fission, artificially increase the photon yield to account
595
        // for delayed photons
596
        double f = 1.0;
47,977,332✔
597
        if (settings::delayed_photon_scaling) {
47,977,332✔
598
          if (is_fission(rx->mt_)) {
47,977,332✔
599
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
586,632✔
600
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
586,632✔
601
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
586,632✔
602
              f = (energy_prompt + energy_delayed) / (energy_prompt);
586,632✔
603
            }
604
          }
605
        }
606

607
        // add to cumulative probability
608
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
47,977,332✔
609

610
        *i_rx = i;
47,977,332✔
611
        *i_product = j;
47,977,332✔
612
        if (prob > cutoff)
47,977,332✔
613
          return;
1,463,040✔
614
      }
615
    }
616
  }
617
}
618

619
void absorption(Particle& p, int i_nuclide)
676,995,718✔
620
{
621
  if (settings::survival_biasing) {
676,995,718✔
622
    // Determine weight absorbed in survival biasing
623
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
557,892✔
624
                              p.neutron_xs(i_nuclide).total;
557,892✔
625

626
    // Adjust weight of particle by probability of absorption
627
    p.wgt() -= wgt_absorb;
557,892✔
628

629
    // Score implicit absorption estimate of keff
630
    if (settings::run_mode == RunMode::EIGENVALUE) {
557,892✔
631
      p.keff_tally_absorption() += wgt_absorb *
557,892✔
632
                                   p.neutron_xs(i_nuclide).nu_fission /
557,892✔
633
                                   p.neutron_xs(i_nuclide).absorption;
557,892✔
634
    }
635
  } else {
636
    // See if disappearance reaction happens
637
    if (p.neutron_xs(i_nuclide).absorption >
676,437,826✔
638
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
676,437,826✔
639
      // Score absorption estimate of keff
640
      if (settings::run_mode == RunMode::EIGENVALUE) {
16,804,713✔
641
        p.keff_tally_absorption() += p.wgt() *
28,856,868✔
642
                                     p.neutron_xs(i_nuclide).nu_fission /
14,428,434✔
643
                                     p.neutron_xs(i_nuclide).absorption;
14,428,434✔
644
      }
645

646
      p.wgt() = 0.0;
16,804,713✔
647
      p.event() = TallyEvent::ABSORB;
16,804,713✔
648
      p.event_mt() = N_DISAPPEAR;
16,804,713✔
649
    }
650
  }
651
}
676,995,718✔
652

653
void scatter(Particle& p, int i_nuclide)
660,180,740✔
654
{
655
  // copy incoming direction
656
  Direction u_old {p.u()};
660,180,740✔
657

658
  // Get pointer to nuclide and grid index/interpolation factor
659
  const auto& nuc {data::nuclides[i_nuclide]};
660,180,740✔
660
  const auto& micro {p.neutron_xs(i_nuclide)};
660,180,740✔
661
  int i_temp = micro.index_temp;
660,180,740✔
662

663
  // For tallying purposes, this routine might be called directly. In that
664
  // case, we need to sample a reaction via the cutoff variable
665
  double cutoff = prn(p.current_seed()) * (micro.total - micro.absorption);
660,180,740✔
666
  bool sampled = false;
660,180,740✔
667

668
  // Calculate elastic cross section if it wasn't precalculated
669
  if (micro.elastic == CACHE_INVALID) {
660,180,740✔
670
    nuc->calculate_elastic_xs(p);
553,921,998✔
671
  }
672

673
  double prob = micro.elastic - micro.thermal;
660,180,740✔
674
  if (prob > cutoff) {
660,180,740✔
675
    // =======================================================================
676
    // NON-S(A,B) ELASTIC SCATTERING
677

678
    // Determine temperature
679
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
574,826,866✔
680

681
    // Perform collision physics for elastic scattering
682
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
574,826,866✔
683

684
    p.event_mt() = ELASTIC;
574,826,866✔
685
    sampled = true;
574,826,866✔
686
  }
687

688
  prob = micro.elastic;
660,180,740✔
689
  if (prob > cutoff && !sampled) {
660,180,740✔
690
    // =======================================================================
691
    // S(A,B) SCATTERING
692

693
    sab_scatter(i_nuclide, micro.index_sab, p);
73,182,409✔
694

695
    p.event_mt() = ELASTIC;
73,182,409✔
696
    sampled = true;
73,182,409✔
697
  }
698

699
  if (!sampled) {
660,180,740✔
700
    // =======================================================================
701
    // INELASTIC SCATTERING
702

703
    int n = nuc->index_inelastic_scatter_.size();
12,171,465✔
704
    int i = 0;
12,171,465✔
705
    for (int j = 0; j < n && prob < cutoff; ++j) {
218,264,906✔
706
      i = nuc->index_inelastic_scatter_[j];
206,093,441✔
707

708
      // add to cumulative probability
709
      prob += nuc->reactions_[i]->xs(micro);
206,093,441✔
710
    }
711

712
    // Perform collision physics for inelastic scattering
713
    const auto& rx {nuc->reactions_[i]};
12,171,465✔
714
    inelastic_scatter(*nuc, *rx, p);
12,171,465✔
715
    p.event_mt() = rx->mt_;
12,171,465✔
716
  }
717

718
  // Set event component
719
  p.event() = TallyEvent::SCATTER;
660,180,740✔
720

721
  // Sample new outgoing angle for isotropic-in-lab scattering
722
  const auto& mat {model::materials[p.material()]};
660,180,740✔
723
  if (!mat->p0_.empty()) {
660,180,740✔
724
    int i_nuc_mat = mat->mat_nuclide_index_[i_nuclide];
339,852✔
725
    if (mat->p0_[i_nuc_mat]) {
339,852✔
726
      // Sample isotropic-in-lab outgoing direction
727
      p.u() = isotropic_direction(p.current_seed());
339,852✔
728
      p.mu() = u_old.dot(p.u());
339,852✔
729
    }
730
  }
731
}
660,180,740✔
732

733
void elastic_scatter(int i_nuclide, const Reaction& rx, double kT, Particle& p)
574,826,866✔
734
{
735
  // get pointer to nuclide
736
  const auto& nuc {data::nuclides[i_nuclide]};
574,826,866✔
737

738
  double vel = std::sqrt(p.E());
574,826,866✔
739
  double awr = nuc->awr_;
574,826,866✔
740

741
  // Neutron velocity in LAB
742
  Direction v_n = vel * p.u();
574,826,866✔
743

744
  // Sample velocity of target nucleus
745
  Direction v_t {};
574,826,866✔
746
  if (!p.neutron_xs(i_nuclide).use_ptable) {
574,826,866✔
747
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,117,959,614✔
748
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
558,979,807✔
749
  }
750

751
  // Velocity of center-of-mass
752
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
574,826,866✔
753

754
  // Transform to CM frame
755
  v_n -= v_cm;
574,826,866✔
756

757
  // Find speed of neutron in CM
758
  vel = v_n.norm();
574,826,866✔
759

760
  // Sample scattering angle, checking if angle distribution is present (assume
761
  // isotropic otherwise)
762
  double mu_cm;
763
  auto& d = rx.products_[0].distribution_[0];
574,826,866✔
764
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
574,826,866✔
765
  if (!d_->angle().empty()) {
574,826,866✔
766
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
574,826,866✔
767
  } else {
768
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
769
  }
770

771
  // Determine direction cosines in CM
772
  Direction u_cm = v_n / vel;
574,826,866✔
773

774
  // Rotate neutron velocity vector to new angle -- note that the speed of the
775
  // neutron in CM does not change in elastic scattering. However, the speed
776
  // will change when we convert back to LAB
777
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
574,826,866✔
778

779
  // Transform back to LAB frame
780
  v_n += v_cm;
574,826,866✔
781

782
  p.E() = v_n.dot(v_n);
574,826,866✔
783
  vel = std::sqrt(p.E());
574,826,866✔
784

785
  // compute cosine of scattering angle in LAB frame by taking dot product of
786
  // neutron's pre- and post-collision angle
787
  p.mu() = p.u().dot(v_n) / vel;
574,826,866✔
788

789
  // Set energy and direction of particle in LAB frame
790
  p.u() = v_n / vel;
574,826,866✔
791

792
  // Because of floating-point roundoff, it may be possible for mu_lab to be
793
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
794
  // -1 or 1
795
  if (std::abs(p.mu()) > 1.0)
574,826,866✔
796
    p.mu() = std::copysign(1.0, p.mu());
×
797
}
574,826,866✔
798

799
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
73,182,409✔
800
{
801
  // Determine temperature index
802
  const auto& micro {p.neutron_xs(i_nuclide)};
73,182,409✔
803
  int i_temp = micro.index_temp_sab;
73,182,409✔
804

805
  // Sample energy and angle
806
  double E_out;
807
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
146,364,818✔
808
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
73,182,409✔
809

810
  // Set energy to outgoing, change direction of particle
811
  p.E() = E_out;
73,182,409✔
812
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
73,182,409✔
813
}
73,182,409✔
814

815
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
558,979,807✔
816
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
817
{
818
  // check if nuclide is a resonant scatterer
819
  ResScatMethod sampling_method;
820
  if (nuc.resonant_) {
558,979,807✔
821

822
    // sampling method to use
823
    sampling_method = settings::res_scat_method;
90,600✔
824

825
    // upper resonance scattering energy bound (target is at rest above this E)
826
    if (E > settings::res_scat_energy_max) {
90,600✔
827
      return {};
44,196✔
828

829
      // lower resonance scattering energy bound (should be no resonances below)
830
    } else if (E < settings::res_scat_energy_min) {
46,404✔
831
      sampling_method = ResScatMethod::cxs;
26,052✔
832
    }
833

834
    // otherwise, use free gas model
835
  } else {
836
    if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) {
558,889,207✔
837
      return {};
211,552,566✔
838
    } else {
839
      sampling_method = ResScatMethod::cxs;
347,336,641✔
840
    }
841
  }
842

843
  // use appropriate target velocity sampling method
844
  switch (sampling_method) {
347,383,045✔
845
  case ResScatMethod::cxs:
347,362,693✔
846

847
    // sample target velocity with the constant cross section (cxs) approx.
848
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
347,362,693✔
849

850
  case ResScatMethod::dbrc:
20,352✔
851
  case ResScatMethod::rvs: {
852
    double E_red = std::sqrt(nuc.awr_ * E / kT);
20,352✔
853
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
20,352✔
854
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
20,352✔
855

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

868
    // upper index
869
    int i_E_up;
870
    if (E_up < nuc.energy_0K_.front()) {
20,352✔
871
      i_E_up = 0;
×
872
    } else if (E_up > nuc.energy_0K_.back()) {
20,352✔
873
      i_E_up = nuc.energy_0K_.size() - 2;
×
874
    } else {
875
      i_E_up =
20,352✔
876
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
20,352✔
877
    }
878

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

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

896
      // get max 0K xs value over range of practical relative energies
897
      double xs_max = *std::max_element(
×
898
        &nuc.elastic_0K_[i_E_low + 1], &nuc.elastic_0K_[i_E_up + 1]);
×
899
      xs_max = std::max({xs_low, xs_max, xs_up});
×
900

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

914
        // perform Doppler broadening rejection correction (dbrc)
915
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
916
        double R = xs_0K / xs_max;
×
917
        if (prn(seed) < R)
×
918
          return v_target;
×
919
      }
920

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

931
      // cdf value at upper bound attainable energy
932
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
16,860✔
933
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
16,860✔
934
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
16,860✔
935

936
      while (true) {
937
        // directly sample Maxwellian
938
        double E_t = -kT * std::log(prn(seed));
177,324✔
939

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

951
        // perform rejection sampling on cosine between
952
        // neutron and target velocities
953
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
177,324✔
954
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
177,324✔
955

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

966
  UNREACHABLE();
×
967
}
968

969
Direction sample_cxs_target_velocity(
347,366,185✔
970
  double awr, double E, Direction u, double kT, uint64_t* seed)
971
{
972
  double beta_vn = std::sqrt(awr * E / kT);
347,366,185✔
973
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
347,366,185✔
974

975
  double beta_vt_sq;
976
  double mu;
977
  while (true) {
978
    // Sample two random numbers
979
    double r1 = prn(seed);
413,029,053✔
980
    double r2 = prn(seed);
413,029,053✔
981

982
    if (prn(seed) < alpha) {
413,029,053✔
983
      // With probability alpha, we sample the distribution p(y) =
984
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
985
      // Carlo sampler
986

987
      beta_vt_sq = -std::log(r1 * r2);
106,865,397✔
988

989
    } else {
990
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
991
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
992
      // Carlo sampler
993

994
      double c = std::cos(PI / 2.0 * prn(seed));
306,163,656✔
995
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
306,163,656✔
996
    }
997

998
    // Determine beta * vt
999
    double beta_vt = std::sqrt(beta_vt_sq);
413,029,053✔
1000

1001
    // Sample cosine of angle between neutron and target velocity
1002
    mu = uniform_distribution(-1., 1., seed);
413,029,053✔
1003

1004
    // Determine rejection probability
1005
    double accept_prob =
1006
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
413,029,053✔
1007
      (beta_vn + beta_vt);
413,029,053✔
1008

1009
    // Perform rejection sampling on vt and mu
1010
    if (prn(seed) < accept_prob)
413,029,053✔
1011
      break;
347,366,185✔
1012
  }
65,662,868✔
1013

1014
  // Determine speed of target nucleus
1015
  double vt = std::sqrt(beta_vt_sq * kT / awr);
347,366,185✔
1016

1017
  // Determine velocity vector of target nucleus based on neutron's velocity
1018
  // and the sampled angle between them
1019
  return vt * rotate_angle(u, mu, nullptr, seed);
347,366,185✔
1020
}
1021

1022
void sample_fission_neutron(
23,984,356✔
1023
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1024
{
1025
  // Get attributes of particle
1026
  double E_in = p.E();
23,984,356✔
1027
  uint64_t* seed = p.current_seed();
23,984,356✔
1028

1029
  // Determine total nu, delayed nu, and delayed neutron fraction
1030
  const auto& nuc {data::nuclides[i_nuclide]};
23,984,356✔
1031
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
23,984,356✔
1032
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
23,984,356✔
1033
  double beta = nu_d / nu_t;
23,984,356✔
1034

1035
  if (prn(seed) < beta) {
23,984,356✔
1036
    // ====================================================================
1037
    // DELAYED NEUTRON SAMPLED
1038

1039
    // sampled delayed precursor group
1040
    double xi = prn(seed) * nu_d;
145,784✔
1041
    double prob = 0.0;
145,784✔
1042
    int group;
1043
    for (group = 1; group < nuc->n_precursor_; ++group) {
547,756✔
1044
      // determine delayed neutron precursor yield for group j
1045
      double yield = (*rx.products_[group].yield_)(E_in);
536,177✔
1046

1047
      // Check if this group is sampled
1048
      prob += yield;
536,177✔
1049
      if (xi < prob)
536,177✔
1050
        break;
134,205✔
1051
    }
1052

1053
    // if the sum of the probabilities is slightly less than one and the
1054
    // random number is greater, j will be greater than nuc %
1055
    // n_precursor -- check for this condition
1056
    group = std::min(group, nuc->n_precursor_);
145,784✔
1057

1058
    // set the delayed group for the particle born from fission
1059
    site->delayed_group = group;
145,784✔
1060

1061
  } else {
1062
    // ====================================================================
1063
    // PROMPT NEUTRON SAMPLED
1064

1065
    // set the delayed group for the particle born from fission to 0
1066
    site->delayed_group = 0;
23,838,572✔
1067
  }
1068

1069
  // sample from prompt neutron energy distribution
1070
  int n_sample = 0;
23,984,356✔
1071
  double mu;
1072
  while (true) {
1073
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
23,984,356✔
1074

1075
    // resample if energy is greater than maximum neutron energy
1076
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
23,984,356✔
1077
    if (site->E < data::energy_max[neutron])
23,984,356✔
1078
      break;
23,984,356✔
1079

1080
    // check for large number of resamples
1081
    ++n_sample;
×
1082
    if (n_sample == MAX_SAMPLE) {
×
1083
      // particle_write_restart(p)
1084
      fatal_error("Resampled energy distribution maximum number of times "
×
1085
                  "for nuclide " +
×
1086
                  nuc->name_);
×
1087
    }
1088
  }
1089

1090
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1091
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
23,984,356✔
1092
}
23,984,356✔
1093

1094
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
12,171,465✔
1095
{
1096
  // copy energy of neutron
1097
  double E_in = p.E();
12,171,465✔
1098

1099
  // sample outgoing energy and scattering cosine
1100
  double E;
1101
  double mu;
1102
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
12,171,465✔
1103

1104
  // if scattering system is in center-of-mass, transfer cosine of scattering
1105
  // angle and outgoing energy from CM to LAB
1106
  if (rx.scatter_in_cm_) {
12,171,465✔
1107
    double E_cm = E;
12,134,511✔
1108

1109
    // determine outgoing energy in lab
1110
    double A = nuc.awr_;
12,134,511✔
1111
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
12,134,511✔
1112
                 ((A + 1.0) * (A + 1.0));
12,134,511✔
1113

1114
    // determine outgoing angle in lab
1115
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
12,134,511✔
1116
  }
1117

1118
  // Because of floating-point roundoff, it may be possible for mu to be
1119
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1120
  // or 1
1121
  if (std::abs(mu) > 1.0)
12,171,465✔
1122
    mu = std::copysign(1.0, mu);
×
1123

1124
  // Set outgoing energy and scattering angle
1125
  p.E() = E;
12,171,465✔
1126
  p.mu() = mu;
12,171,465✔
1127

1128
  // change direction of particle
1129
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
12,171,465✔
1130

1131
  // evaluate yield
1132
  double yield = (*rx.products_[0].yield_)(E_in);
12,171,465✔
1133
  if (std::floor(yield) == yield && yield > 0) {
12,171,465✔
1134
    // If yield is integral, create exactly that many secondary particles
1135
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
12,225,873✔
1136
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
54,486✔
1137
    }
1138
  } else {
12,171,387✔
1139
    // Otherwise, change weight of particle based on yield
1140
    p.wgt() *= yield;
78✔
1141
  }
1142
}
12,171,465✔
1143

1144
void sample_secondary_photons(Particle& p, int i_nuclide)
9,926,664✔
1145
{
1146
  // Sample the number of photons produced
1147
  double y_t =
1148
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
9,926,664✔
1149
  double photon_wgt = p.wgt();
9,926,664✔
1150
  int y = 1;
9,926,664✔
1151

1152
  if (settings::use_decay_photons) {
9,926,664✔
1153
    // For decay photons, sample a single photon and modify the weight
1154
    if (y_t <= 0.0)
78,552✔
1155
      return;
18,852✔
1156
    photon_wgt *= y_t;
59,700✔
1157
  } else {
1158
    // For prompt photons, sample an integral number of photons with weight
1159
    // equal to the neutron's weight
1160
    y = static_cast<int>(y_t);
9,848,112✔
1161
    if (prn(p.current_seed()) <= y_t - y)
9,848,112✔
1162
      ++y;
630,816✔
1163
  }
1164

1165
  // Sample each secondary photon
1166
  for (int i = 0; i < y; ++i) {
11,370,852✔
1167
    // Sample the reaction and product
1168
    int i_rx;
1169
    int i_product;
1170
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,463,040✔
1171

1172
    // Sample the outgoing energy and angle
1173
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,463,040✔
1174
    double E;
1175
    double mu;
1176
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,463,040✔
1177

1178
    // Sample the new direction
1179
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,463,040✔
1180

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

1191
    // Create the secondary photon
1192
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,463,040✔
1193

1194
    // Tag secondary particle with parent nuclide
1195
    if (created_photon && settings::use_decay_photons) {
1,463,040✔
1196
      p.secondary_bank().back().parent_nuclide =
57,648✔
1197
        rx->products_[i_product].parent_nuclide_;
57,648✔
1198
    }
1199
  }
1200
}
1201

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

© 2025 Coveralls, Inc