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

openmc-dev / openmc / 13763261523

10 Mar 2025 11:12AM UTC coverage: 85.133% (+0.004%) from 85.129%
13763261523

Pull #3252

github

web-flow
Merge bdc3a6ead into 906548db2
Pull Request #3252: Adding vtkhdf option to write vtk data

83 of 101 new or added lines in 1 file covered. (82.18%)

529 existing lines in 15 files now uncovered.

51616 of 60630 relevant lines covered (85.13%)

37229330.6 hits per line

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

88.93
/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)
695,175,325✔
43
{
44
  // Add to collision counter for particle
45
  ++(p.n_collision());
695,175,325✔
46

47
  // Sample reaction for the material the particle is in
48
  switch (p.type()) {
695,175,325✔
49
  case ParticleType::neutron:
632,927,816✔
50
    sample_neutron_reaction(p);
632,927,816✔
51
    break;
632,927,816✔
52
  case ParticleType::photon:
13,419,198✔
53
    sample_photon_reaction(p);
13,419,198✔
54
    break;
13,419,198✔
55
  case ParticleType::electron:
48,743,094✔
56
    sample_electron_reaction(p);
48,743,094✔
57
    break;
48,743,094✔
58
  case ParticleType::positron:
85,217✔
59
    sample_positron_reaction(p);
85,217✔
60
    break;
85,217✔
61
  }
62

63
  if (settings::weight_window_checkpoint_collision)
695,175,325✔
64
    apply_weight_windows(p);
695,175,325✔
65

66
  // Kill particle if energy falls below cutoff
67
  int type = static_cast<int>(p.type());
695,175,325✔
68
  if (p.E() < settings::energy_cutoff[type]) {
695,175,325✔
69
    p.wgt() = 0.0;
4,752,267✔
70
  }
71

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

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

97
  // Save which nuclide particle had collision with
98
  p.event_nuclide() = i_nuclide;
632,927,816✔
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]};
632,927,816✔
106

107
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
632,927,816✔
108
    auto& rx = sample_fission(i_nuclide, p);
82,706,393✔
109
    if (settings::run_mode == RunMode::EIGENVALUE) {
82,706,393✔
110
      create_fission_sites(p, i_nuclide, rx);
82,134,834✔
111
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
571,559✔
112
               settings::create_fission_neutrons) {
113
      create_fission_sites(p, i_nuclide, rx);
557,655✔
114

115
      // Make sure particle population doesn't grow out of control for
116
      // subcritical multiplication problems.
117
      if (p.secondary_bank().size() >= 10000) {
557,655✔
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) {
632,927,816✔
128
    sample_secondary_photons(p, i_nuclide);
9,099,442✔
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) {
632,927,816✔
135
    absorption(p, i_nuclide);
632,923,969✔
136
  }
137
  if (!p.alive())
632,927,816✔
138
    return;
15,876,332✔
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();
617,051,484✔
143
  if (ncrystal_mat && p.E() < NCRYSTAL_MAX_ENERGY) {
617,051,484✔
144
    ncrystal_mat.scatter(p);
158,829✔
145
  } else {
146
    scatter(p, i_nuclide);
616,892,655✔
147
  }
148

149
  // Advance URR seed stream 'N' times after energy changes
150
  if (p.E() != p.E_last()) {
617,051,484✔
151
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
616,728,315✔
152
  }
153

154
  // Play russian roulette if survival biasing is turned on
155
  if (settings::survival_biasing) {
617,051,484✔
156
    // if survival normalization is on, use normalized weight cutoff and
157
    // normalized weight survive
158
    if (settings::survival_normalization) {
511,401✔
UNCOV
159
      if (p.wgt() < settings::weight_cutoff * p.wgt_born()) {
×
UNCOV
160
        russian_roulette(p, settings::weight_survive * p.wgt_born());
×
161
      }
162
    } else if (p.wgt() < settings::weight_cutoff) {
511,401✔
163
      russian_roulette(p, settings::weight_survive);
58,652✔
164
    }
165
  }
166
}
167

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

174
  // Determine the expected number of neutrons produced
175
  double nu_t = p.wgt() / simulation::keff * weight *
82,692,489✔
176
                p.neutron_xs(i_nuclide).nu_fission /
82,692,489✔
177
                p.neutron_xs(i_nuclide).total;
82,692,489✔
178

179
  // Sample the number of neutrons produced
180
  int nu = static_cast<int>(nu_t);
82,692,489✔
181
  if (prn(p.current_seed()) <= (nu_t - nu))
82,692,489✔
182
    ++nu;
13,681,258✔
183

184
  // If no neutrons were produced then don't continue
185
  if (nu == 0)
82,692,489✔
186
    return;
65,125,999✔
187

188
  // Initialize the counter of delayed neutrons encountered for each delayed
189
  // group.
190
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
17,566,490✔
191

192
  // Clear out particle's nu fission bank
193
  p.nu_bank().clear();
17,566,490✔
194

195
  p.fission() = true;
17,566,490✔
196

197
  // Determine whether to place fission sites into the shared fission bank
198
  // or the secondary particle bank.
199
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
17,566,490✔
200

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

205
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
40,152,284✔
206
    // Initialize fission site object with particle data
207
    SourceSite site;
22,585,794✔
208
    site.r = p.r();
22,585,794✔
209
    site.particle = ParticleType::neutron;
22,585,794✔
210
    site.time = p.time();
22,585,794✔
211
    site.wgt = 1. / weight;
22,585,794✔
212
    site.parent_id = p.id();
22,585,794✔
213
    site.progeny_id = p.n_progeny()++;
22,585,794✔
214
    site.surf_id = 0;
22,585,794✔
215

216
    // Sample delayed group and angle/energy for fission reaction
217
    sample_fission_neutron(i_nuclide, rx, &site, p);
22,585,794✔
218

219
    // Store fission site in bank
220
    if (use_fission_bank) {
22,585,794✔
221
      int64_t idx = simulation::fission_bank.thread_safe_append(site);
22,382,361✔
222
      if (idx == -1) {
22,382,361✔
UNCOV
223
        warning(
×
224
          "The shared fission bank is full. Additional fission sites created "
225
          "in this generation will not be banked. Results may be "
226
          "non-deterministic.");
227

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

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

240
    // Set the delayed group on the particle as well
241
    p.delayed_group() = site.delayed_group;
22,585,794✔
242

243
    // Increment the number of neutrons born delayed
244
    if (p.delayed_group() > 0) {
22,585,794✔
245
      nu_d[p.delayed_group() - 1]++;
138,357✔
246
    }
247

248
    // Write fission particles to nuBank
249
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
22,585,794✔
250
    nu_bank_entry.wgt = site.wgt;
22,585,794✔
251
    nu_bank_entry.E = site.E;
22,585,794✔
252
    nu_bank_entry.delayed_group = site.delayed_group;
22,585,794✔
253
  }
254

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

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

266
  // Store the total weight banked for analog fission tallies
267
  p.n_bank() = nu;
17,566,490✔
268
  p.wgt_bank() = nu / weight;
17,566,490✔
269
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
158,098,410✔
270
    p.n_delayed_bank(d) = nu_d[d];
140,531,920✔
271
  }
272
}
273

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

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

291
  // Calculate photon energy over electron rest mass equivalent
292
  double alpha = p.E() / MASS_ELECTRON_EV;
13,419,198✔
293

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

299
  // Coherent (Rayleigh) scattering
300
  prob += micro.coherent;
13,419,198✔
301
  if (prob > cutoff) {
13,419,198✔
302
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
710,259✔
303
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
710,259✔
304
    p.event() = TallyEvent::SCATTER;
710,259✔
305
    p.event_mt() = COHERENT;
710,259✔
306
    return;
710,259✔
307
  }
308

309
  // Incoherent (Compton) scattering
310
  prob += micro.incoherent;
12,708,939✔
311
  if (prob > cutoff) {
12,708,939✔
312
    double alpha_out;
313
    int i_shell;
314
    element.compton_scatter(
15,935,190✔
315
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
7,967,595✔
316

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

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

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

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

353
  // Photoelectric effect
354
  double prob_after = prob + micro.photoelectric;
4,741,344✔
355

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

364
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
23,760,980✔
365
      const auto& shell {element.shells_[i_shell]};
23,760,980✔
366

367
      // Check threshold of reaction
368
      if (xs_lower(i_shell) == 0)
23,760,980✔
369
        continue;
10,238,998✔
370

371
      //  Evaluation subshell photoionization cross section
372
      prob += std::exp(
13,521,982✔
373
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
13,521,982✔
374

375
      if (prob > cutoff) {
13,521,982✔
376
        // Determine binding energy based on whether atomic relaxation data is
377
        // present (if not, use value from Compton profile data)
378
        double binding_energy = element.has_atomic_relaxation_
4,656,127✔
379
                                  ? shell.binding_energy
4,656,127✔
UNCOV
380
                                  : element.binding_energy_[i_shell];
×
381

382
        // Determine energy of secondary electron
383
        double E_electron = p.E() - binding_energy;
4,656,127✔
384

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

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

407
        // Create secondary electron
408
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
4,656,127✔
409

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

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

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

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

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

446
void sample_electron_reaction(Particle& p)
48,743,094✔
447
{
448
  // TODO: create reaction types
449

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

455
  p.E() = 0.0;
48,743,094✔
456
  p.wgt() = 0.0;
48,743,094✔
457
  p.event() = TallyEvent::ABSORB;
48,743,094✔
458
}
48,743,094✔
459

460
void sample_positron_reaction(Particle& p)
85,217✔
461
{
462
  // TODO: create reaction types
463

464
  if (settings::electron_treatment == ElectronTreatment::TTB) {
85,217✔
465
    double E_lost;
466
    thick_target_bremsstrahlung(p, &E_lost);
85,217✔
467
  }
468

469
  // Sample angle isotropically
470
  Direction u = isotropic_direction(p.current_seed());
85,217✔
471

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

476
  p.E() = 0.0;
85,217✔
477
  p.wgt() = 0.0;
85,217✔
478
  p.event() = TallyEvent::ABSORB;
85,217✔
479
}
85,217✔
480

481
int sample_nuclide(Particle& p)
632,927,816✔
482
{
483
  // Sample cumulative distribution function
484
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
632,927,816✔
485

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

490
  double prob = 0.0;
632,927,816✔
491
  for (int i = 0; i < n; ++i) {
1,419,054,010✔
492
    // Get atom density
493
    int i_nuclide = mat->nuclide_[i];
1,419,054,010✔
494
    double atom_density = mat->atom_density_[i];
1,419,054,010✔
495

496
    // Increment probability to compare to cutoff
497
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,419,054,010✔
498
    if (prob >= cutoff)
1,419,054,010✔
499
      return i_nuclide;
632,927,816✔
500
  }
501

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

507
int sample_element(Particle& p)
13,419,198✔
508
{
509
  // Sample cumulative distribution function
510
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
13,419,198✔
511

512
  // Get pointers to elements, densities
513
  const auto& mat {model::materials[p.material()]};
13,419,198✔
514

515
  double prob = 0.0;
13,419,198✔
516
  for (int i = 0; i < mat->element_.size(); ++i) {
34,464,225✔
517
    // Find atom density
518
    int i_element = mat->element_[i];
34,464,225✔
519
    double atom_density = mat->atom_density_[i];
34,464,225✔
520

521
    // Determine microscopic cross section
522
    double sigma = atom_density * p.photon_xs(i_element).total;
34,464,225✔
523

524
    // Increment probability to compare to cutoff
525
    prob += sigma;
34,464,225✔
526
    if (prob > cutoff) {
34,464,225✔
527
      // Save which nuclide particle had collision with for tally purpose
528
      p.event_nuclide() = mat->nuclide_[i];
13,419,198✔
529

530
      return i_element;
13,419,198✔
531
    }
532
  }
533

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

539
Reaction& sample_fission(int i_nuclide, Particle& p)
82,706,393✔
540
{
541
  // Get pointer to nuclide
542
  const auto& nuc {data::nuclides[i_nuclide]};
82,706,393✔
543

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

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

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

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

569
    // Create fission bank sites if fission occurs
570
    if (prob > cutoff)
24,356✔
571
      return *rx;
24,342✔
572
  }
573

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

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

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

594
    // if cross section is zero for this reaction, skip it
595
    if (xs == 0.0)
22,096,646✔
596
      continue;
7,285,740✔
597

598
    for (int j = 0; j < rx->products_.size(); ++j) {
69,888,368✔
599
      if (rx->products_[j].particle_ == ParticleType::photon) {
56,418,582✔
600
        // For fission, artificially increase the photon yield to account
601
        // for delayed photons
602
        double f = 1.0;
43,979,221✔
603
        if (settings::delayed_photon_scaling) {
43,979,221✔
604
          if (is_fission(rx->mt_)) {
43,979,221✔
605
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
537,746✔
606
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
537,746✔
607
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
537,746✔
608
              f = (energy_prompt + energy_delayed) / (energy_prompt);
537,746✔
609
            }
610
          }
611
        }
612

613
        // add to cumulative probability
614
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
43,979,221✔
615

616
        *i_rx = i;
43,979,221✔
617
        *i_product = j;
43,979,221✔
618
        if (prob > cutoff)
43,979,221✔
619
          return;
1,341,120✔
620
      }
621
    }
622
  }
623
}
624

625
void absorption(Particle& p, int i_nuclide)
632,923,969✔
626
{
627
  if (settings::survival_biasing) {
632,923,969✔
628
    // Determine weight absorbed in survival biasing
629
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
511,401✔
630
                              p.neutron_xs(i_nuclide).total;
511,401✔
631

632
    // Adjust weight of particle by probability of absorption
633
    p.wgt() -= wgt_absorb;
511,401✔
634

635
    // Score implicit absorption estimate of keff
636
    if (settings::run_mode == RunMode::EIGENVALUE) {
511,401✔
637
      p.keff_tally_absorption() += wgt_absorb *
511,401✔
638
                                   p.neutron_xs(i_nuclide).nu_fission /
511,401✔
639
                                   p.neutron_xs(i_nuclide).absorption;
511,401✔
640
    }
641
  } else {
642
    // See if disappearance reaction happens
643
    if (p.neutron_xs(i_nuclide).absorption >
632,412,568✔
644
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
632,412,568✔
645
      // Score absorption estimate of keff
646
      if (settings::run_mode == RunMode::EIGENVALUE) {
15,876,332✔
647
        p.keff_tally_absorption() += p.wgt() *
27,297,536✔
648
                                     p.neutron_xs(i_nuclide).nu_fission /
13,648,768✔
649
                                     p.neutron_xs(i_nuclide).absorption;
13,648,768✔
650
      }
651

652
      p.wgt() = 0.0;
15,876,332✔
653
      p.event() = TallyEvent::ABSORB;
15,876,332✔
654
      p.event_mt() = N_DISAPPEAR;
15,876,332✔
655
    }
656
  }
657
}
632,923,969✔
658

659
void scatter(Particle& p, int i_nuclide)
616,892,655✔
660
{
661
  // copy incoming direction
662
  Direction u_old {p.u()};
616,892,655✔
663

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

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

674
  // Calculate elastic cross section if it wasn't precalculated
675
  if (micro.elastic == CACHE_INVALID) {
616,892,655✔
676
    nuc->calculate_elastic_xs(p);
516,914,153✔
677
  }
678

679
  double prob = micro.elastic - micro.thermal;
616,892,655✔
680
  if (prob > cutoff) {
616,892,655✔
681
    // =======================================================================
682
    // NON-S(A,B) ELASTIC SCATTERING
683

684
    // Determine temperature
685
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
537,094,566✔
686

687
    // Perform collision physics for elastic scattering
688
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
537,094,566✔
689

690
    p.event_mt() = ELASTIC;
537,094,566✔
691
    sampled = true;
537,094,566✔
692
  }
693

694
  prob = micro.elastic;
616,892,655✔
695
  if (prob > cutoff && !sampled) {
616,892,655✔
696
    // =======================================================================
697
    // S(A,B) SCATTERING
698

699
    sab_scatter(i_nuclide, micro.index_sab, p);
67,874,588✔
700

701
    p.event_mt() = ELASTIC;
67,874,588✔
702
    sampled = true;
67,874,588✔
703
  }
704

705
  if (!sampled) {
616,892,655✔
706
    // =======================================================================
707
    // INELASTIC SCATTERING
708

709
    int n = nuc->index_inelastic_scatter_.size();
11,923,501✔
710
    int i = 0;
11,923,501✔
711
    for (int j = 0; j < n && prob < cutoff; ++j) {
211,827,692✔
712
      i = nuc->index_inelastic_scatter_[j];
199,904,191✔
713

714
      // add to cumulative probability
715
      prob += nuc->reactions_[i]->xs(micro);
199,904,191✔
716
    }
717

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

724
  // Set event component
725
  p.event() = TallyEvent::SCATTER;
616,892,655✔
726

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

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

744
  double vel = std::sqrt(p.E());
537,094,566✔
745
  double awr = nuc->awr_;
537,094,566✔
746

747
  // Neutron velocity in LAB
748
  Direction v_n = vel * p.u();
537,094,566✔
749

750
  // Sample velocity of target nucleus
751
  Direction v_t {};
537,094,566✔
752
  if (!p.neutron_xs(i_nuclide).use_ptable) {
537,094,566✔
753
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,041,656,174✔
754
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
520,828,087✔
755
  }
756

757
  // Velocity of center-of-mass
758
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
537,094,566✔
759

760
  // Transform to CM frame
761
  v_n -= v_cm;
537,094,566✔
762

763
  // Find speed of neutron in CM
764
  vel = v_n.norm();
537,094,566✔
765

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

777
  // Determine direction cosines in CM
778
  Direction u_cm = v_n / vel;
537,094,566✔
779

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

785
  // Transform back to LAB frame
786
  v_n += v_cm;
537,094,566✔
787

788
  p.E() = v_n.dot(v_n);
537,094,566✔
789
  vel = std::sqrt(p.E());
537,094,566✔
790

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

795
  // Set energy and direction of particle in LAB frame
796
  p.u() = v_n / vel;
537,094,566✔
797

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

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

811
  // Sample energy and angle
812
  double E_out;
813
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
135,749,176✔
814
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
67,874,588✔
815

816
  // Set energy to outgoing, change direction of particle
817
  p.E() = E_out;
67,874,588✔
818
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
67,874,588✔
819
}
67,874,588✔
820

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

828
    // sampling method to use
829
    sampling_method = settings::res_scat_method;
83,050✔
830

831
    // upper resonance scattering energy bound (target is at rest above this E)
832
    if (E > settings::res_scat_energy_max) {
83,050✔
833
      return {};
40,513✔
834

835
      // lower resonance scattering energy bound (should be no resonances below)
836
    } else if (E < settings::res_scat_energy_min) {
42,537✔
837
      sampling_method = ResScatMethod::cxs;
23,881✔
838
    }
839

840
    // otherwise, use free gas model
841
  } else {
842
    if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) {
520,745,037✔
843
      return {};
201,134,755✔
844
    } else {
845
      sampling_method = ResScatMethod::cxs;
319,610,282✔
846
    }
847
  }
848

849
  // use appropriate target velocity sampling method
850
  switch (sampling_method) {
319,652,819✔
851
  case ResScatMethod::cxs:
319,634,163✔
852

853
    // sample target velocity with the constant cross section (cxs) approx.
854
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
319,634,163✔
855

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

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

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

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

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

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

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

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

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

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

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

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

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

962
        if (std::abs(mu) < 1.0) {
162,547✔
963
          // set and accept target velocity
964
          E_t /= nuc.awr_;
15,455✔
965
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
15,455✔
966
        }
967
      }
147,092✔
968
    }
969
  } // case RVS, DBRC
970
  } // switch (sampling_method)
971

UNCOV
972
  UNREACHABLE();
×
973
}
974

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

981
  double beta_vt_sq;
982
  double mu;
983
  while (true) {
984
    // Sample two random numbers
985
    double r1 = prn(seed);
379,917,867✔
986
    double r2 = prn(seed);
379,917,867✔
987

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

993
      beta_vt_sq = -std::log(r1 * r2);
98,092,670✔
994

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

1000
      double c = std::cos(PI / 2.0 * prn(seed));
281,825,197✔
1001
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
281,825,197✔
1002
    }
1003

1004
    // Determine beta * vt
1005
    double beta_vt = std::sqrt(beta_vt_sq);
379,917,867✔
1006

1007
    // Sample cosine of angle between neutron and target velocity
1008
    mu = uniform_distribution(-1., 1., seed);
379,917,867✔
1009

1010
    // Determine rejection probability
1011
    double accept_prob =
1012
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
379,917,867✔
1013
      (beta_vn + beta_vt);
379,917,867✔
1014

1015
    // Perform rejection sampling on vt and mu
1016
    if (prn(seed) < accept_prob)
379,917,867✔
1017
      break;
319,637,364✔
1018
  }
60,280,503✔
1019

1020
  // Determine speed of target nucleus
1021
  double vt = std::sqrt(beta_vt_sq * kT / awr);
319,637,364✔
1022

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

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

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

1041
  if (prn(seed) < beta) {
22,585,794✔
1042
    // ====================================================================
1043
    // DELAYED NEUTRON SAMPLED
1044

1045
    // sampled delayed precursor group
1046
    double xi = prn(seed) * nu_d;
138,357✔
1047
    double prob = 0.0;
138,357✔
1048
    int group;
1049
    for (group = 1; group < nuc->n_precursor_; ++group) {
519,679✔
1050
      // determine delayed neutron precursor yield for group j
1051
      double yield = (*rx.products_[group].yield_)(E_in);
508,726✔
1052

1053
      // Check if this group is sampled
1054
      prob += yield;
508,726✔
1055
      if (xi < prob)
508,726✔
1056
        break;
127,404✔
1057
    }
1058

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

1064
    // set the delayed group for the particle born from fission
1065
    site->delayed_group = group;
138,357✔
1066

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

1071
    // set the delayed group for the particle born from fission to 0
1072
    site->delayed_group = 0;
22,447,437✔
1073
  }
1074

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

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

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

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

1100
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
11,923,501✔
1101
{
1102
  // copy energy of neutron
1103
  double E_in = p.E();
11,923,501✔
1104

1105
  // sample outgoing energy and scattering cosine
1106
  double E;
1107
  double mu;
1108
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
11,923,501✔
1109

1110
  // if scattering system is in center-of-mass, transfer cosine of scattering
1111
  // angle and outgoing energy from CM to LAB
1112
  if (rx.scatter_in_cm_) {
11,923,501✔
1113
    double E_cm = E;
11,889,614✔
1114

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

1120
    // determine outgoing angle in lab
1121
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
11,889,614✔
1122
  }
1123

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

1130
  // Set outgoing energy and scattering angle
1131
  p.E() = E;
11,923,501✔
1132
  p.mu() = mu;
11,923,501✔
1133

1134
  // change direction of particle
1135
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
11,923,501✔
1136

1137
  // evaluate yield
1138
  double yield = (*rx.products_[0].yield_)(E_in);
11,923,501✔
1139
  if (std::floor(yield) == yield && yield > 0) {
11,923,501✔
1140
    // If yield is integral, create exactly that many secondary particles
1141
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
11,976,353✔
1142
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
52,928✔
1143
    }
1144
  } else {
11,923,425✔
1145
    // Otherwise, change weight of particle based on yield
1146
    p.wgt() *= yield;
76✔
1147
  }
1148
}
11,923,501✔
1149

1150
void sample_secondary_photons(Particle& p, int i_nuclide)
9,099,442✔
1151
{
1152
  // Sample the number of photons produced
1153
  double y_t =
1154
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
9,099,442✔
1155
  double photon_wgt = p.wgt();
9,099,442✔
1156
  int y = 1;
9,099,442✔
1157

1158
  if (settings::use_decay_photons) {
9,099,442✔
1159
    // For decay photons, sample a single photon and modify the weight
1160
    if (y_t <= 0.0)
72,006✔
1161
      return;
17,281✔
1162
    photon_wgt *= y_t;
54,725✔
1163
  } else {
1164
    // For prompt photons, sample an integral number of photons with weight
1165
    // equal to the neutron's weight
1166
    y = static_cast<int>(y_t);
9,027,436✔
1167
    if (prn(p.current_seed()) <= y_t - y)
9,027,436✔
1168
      ++y;
578,248✔
1169
  }
1170

1171
  // Sample each secondary photon
1172
  for (int i = 0; i < y; ++i) {
10,423,281✔
1173
    // Sample the reaction and product
1174
    int i_rx;
1175
    int i_product;
1176
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,341,120✔
1177

1178
    // Sample the outgoing energy and angle
1179
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,341,120✔
1180
    double E;
1181
    double mu;
1182
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,341,120✔
1183

1184
    // Sample the new direction
1185
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,341,120✔
1186

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

1197
    // Create the secondary photon
1198
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,341,120✔
1199

1200
    // Tag secondary particle with parent nuclide
1201
    if (created_photon && settings::use_decay_photons) {
1,341,120✔
1202
      p.secondary_bank().back().parent_nuclide =
52,844✔
1203
        rx->products_[i_product].parent_nuclide_;
52,844✔
1204
    }
1205
  }
1206
}
1207

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