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

openmc-dev / openmc / 20278909785

16 Dec 2025 06:41PM UTC coverage: 81.834% (-0.09%) from 81.92%
20278909785

Pull #3493

github

web-flow
Merge 9dc7c7a58 into bbfa18d72
Pull Request #3493: Implement vector fitting to replace external `vectfit` package

17020 of 23572 branches covered (72.2%)

Branch coverage included in aggregate %.

188 of 207 new or added lines in 3 files covered. (90.82%)

3101 existing lines in 56 files now uncovered.

54979 of 64410 relevant lines covered (85.36%)

41388074.54 hits per line

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

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

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

31
#include <fmt/core.h>
32

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

37
namespace openmc {
38

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

43
void collision(Particle& p)
800,630,299✔
44
{
45
  // Add to collision counter for particle
46
  ++(p.n_collision());
800,630,299✔
47

48
  // Sample reaction for the material the particle is in
49
  switch (p.type()) {
800,630,299!
50
  case ParticleType::neutron:
735,673,366✔
51
    sample_neutron_reaction(p);
735,673,366✔
52
    break;
735,673,366✔
53
  case ParticleType::photon:
15,880,764✔
54
    sample_photon_reaction(p);
15,880,764✔
55
    break;
15,880,764✔
56
  case ParticleType::electron:
48,994,429✔
57
    sample_electron_reaction(p);
48,994,429✔
58
    break;
48,994,429✔
59
  case ParticleType::positron:
81,740✔
60
    sample_positron_reaction(p);
81,740✔
61
    break;
81,740✔
62
  }
63

64
  if (settings::weight_window_checkpoint_collision)
800,630,299!
65
    apply_weight_windows(p);
800,630,299✔
66

67
  // Kill particle if energy falls below cutoff
68
  int type = static_cast<int>(p.type());
800,630,299✔
69
  if (p.E() < settings::energy_cutoff[type]) {
800,630,299✔
70
    p.wgt() = 0.0;
4,752,370✔
71
  }
72

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

93
void sample_neutron_reaction(Particle& p)
735,673,366✔
94
{
95
  // Sample a nuclide within the material
96
  int i_nuclide = sample_nuclide(p);
735,673,366✔
97

98
  // Save which nuclide particle had collision with
99
  p.event_nuclide() = i_nuclide;
735,673,366✔
100

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

106
  const auto& nuc {data::nuclides[i_nuclide]};
735,673,366✔
107

108
  if (nuc->fissionable_ && p.neutron_xs(i_nuclide).fission > 0.0) {
735,673,366✔
109
    auto& rx = sample_fission(i_nuclide, p);
98,973,586✔
110
    if (settings::run_mode == RunMode::EIGENVALUE) {
98,973,586✔
111
      create_fission_sites(p, i_nuclide, rx);
88,349,708✔
112
    } else if (settings::run_mode == RunMode::FIXED_SOURCE &&
10,623,878✔
113
               settings::create_fission_neutrons) {
114
      create_fission_sites(p, i_nuclide, rx);
527,328✔
115

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

128
  // Create secondary photons
129
  if (settings::photon_transport) {
735,673,366✔
130
    sample_secondary_photons(p, i_nuclide);
8,008,680✔
131
  }
132

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

136
  if (p.neutron_xs(i_nuclide).absorption > 0.0) {
735,673,366✔
137
    absorption(p, i_nuclide);
735,670,166✔
138
  }
139
  if (!p.alive())
735,673,366✔
140
    return;
19,000,689✔
141

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

151
  // Advance URR seed stream 'N' times after energy changes
152
  if (p.E() != p.E_last()) {
716,672,677✔
153
    advance_prn_seed(data::nuclides.size(), &p.seeds(STREAM_URR_PTABLE));
716,382,467✔
154
  }
155

156
  // Play russian roulette if survival biasing is turned on
157
  if (settings::survival_biasing) {
716,672,677✔
158
    // if survival normalization is on, use normalized weight cutoff and
159
    // normalized weight survive
160
    if (settings::survival_normalization) {
454,500!
161
      if (p.wgt() < settings::weight_cutoff * p.wgt_born()) {
×
UNCOV
162
        russian_roulette(p, settings::weight_survive * p.wgt_born());
×
163
      }
164
    } else if (p.wgt() < settings::weight_cutoff) {
454,500✔
165
      russian_roulette(p, settings::weight_survive);
51,810✔
166
    }
167
  }
168
}
169

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

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

181
  // Sample the number of neutrons produced
182
  int nu = static_cast<int>(nu_t);
88,877,036✔
183
  if (prn(p.current_seed()) <= (nu_t - nu))
88,877,036✔
184
    ++nu;
18,313,684✔
185

186
  // If no neutrons were produced then don't continue
187
  if (nu == 0)
88,877,036✔
188
    return;
67,087,435✔
189

190
  // Initialize the counter of delayed neutrons encountered for each delayed
191
  // group.
192
  double nu_d[MAX_DELAYED_GROUPS] = {0.};
21,789,601✔
193

194
  // Clear out particle's nu fission bank
195
  p.nu_bank().clear();
21,789,601✔
196

197
  p.fission() = true;
21,789,601✔
198

199
  // Determine whether to place fission sites into the shared fission bank
200
  // or the secondary particle bank.
201
  bool use_fission_bank = (settings::run_mode == RunMode::EIGENVALUE);
21,789,601✔
202

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

207
  for (n_sites_stored = 0; n_sites_stored < nu; n_sites_stored++) {
48,246,179✔
208
    // Initialize fission site object with particle data
209
    SourceSite site;
26,456,578✔
210
    site.r = p.r();
26,456,578✔
211
    site.particle = ParticleType::neutron;
26,456,578✔
212
    site.time = p.time();
26,456,578✔
213
    site.wgt = 1. / weight;
26,456,578✔
214
    site.surf_id = 0;
26,456,578✔
215

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

219
    // Reject site if it exceeds time cutoff
220
    if (site.delayed_group > 0) {
26,456,578✔
221
      double t_cutoff = settings::time_cutoff[static_cast<int>(site.particle)];
165,338✔
222
      if (site.time > t_cutoff) {
165,338!
UNCOV
223
        continue;
×
224
      }
225
    }
226

227
    // Set parent and progeny IDs
228
    site.parent_id = p.id();
26,456,578✔
229
    site.progeny_id = p.n_progeny()++;
26,456,578✔
230

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

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

245
        // Break out of loop as no more sites can be added to fission bank
UNCOV
246
        break;
×
247
      }
248
      // Iterated Fission Probability (IFP) method
249
      if (settings::ifp_on) {
26,253,091✔
250
        ifp(p, idx);
1,229,660✔
251
      }
252
    } else {
253
      p.secondary_bank().push_back(site);
203,487✔
254
    }
255

256
    // Increment the number of neutrons born delayed
257
    if (site.delayed_group > 0) {
26,456,578✔
258
      nu_d[site.delayed_group - 1]++;
165,338✔
259
    }
260

261
    // Write fission particles to nuBank
262
    NuBank& nu_bank_entry = p.nu_bank().emplace_back();
26,456,578✔
263
    nu_bank_entry.wgt = site.wgt;
26,456,578✔
264
    nu_bank_entry.E = site.E;
26,456,578✔
265
    nu_bank_entry.delayed_group = site.delayed_group;
26,456,578✔
266
  }
267

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

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

279
  // Store the total weight banked for analog fission tallies
280
  p.n_bank() = nu;
21,789,601✔
281
  p.wgt_bank() = nu / weight;
21,789,601✔
282
  for (size_t d = 0; d < MAX_DELAYED_GROUPS; d++) {
196,106,409✔
283
    p.n_delayed_bank(d) = nu_d[d];
174,316,808✔
284
  }
285
}
286

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

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

304
  // Calculate photon energy over electron rest mass equivalent
305
  double alpha = p.E() / MASS_ELECTRON_EV;
15,880,764✔
306

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

312
  // Coherent (Rayleigh) scattering
313
  prob += micro.coherent;
15,880,764✔
314
  if (prob > cutoff) {
15,880,764✔
315
    p.mu() = element.rayleigh_scatter(alpha, p.current_seed());
871,103✔
316
    p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
871,103✔
317
    p.event() = TallyEvent::SCATTER;
871,103✔
318
    p.event_mt() = COHERENT;
871,103✔
319
    return;
871,103✔
320
  }
321

322
  // Incoherent (Compton) scattering
323
  prob += micro.incoherent;
15,009,661✔
324
  if (prob > cutoff) {
15,009,661✔
325
    double alpha_out;
326
    int i_shell;
327
    element.compton_scatter(
20,534,442✔
328
      alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed());
10,267,221✔
329

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

339
    // Create Compton electron
340
    double phi = uniform_distribution(0., 2.0 * PI, p.current_seed());
10,267,221✔
341
    double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b;
10,267,221✔
342
    int electron = static_cast<int>(ParticleType::electron);
10,267,221✔
343
    if (E_electron >= settings::energy_cutoff[electron]) {
10,267,221✔
344
      double mu_electron = (alpha - alpha_out * p.mu()) /
10,171,477✔
345
                           std::sqrt(alpha * alpha + alpha_out * alpha_out -
20,342,954✔
346
                                     2.0 * alpha * alpha_out * p.mu());
10,171,477✔
347
      Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed());
10,171,477✔
348
      p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
10,171,477✔
349
    }
350

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

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

366
  // Photoelectric effect
367
  double prob_after = prob + micro.photoelectric;
4,742,440✔
368

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

377
    for (int i_shell = 0; i_shell < element.shells_.size(); ++i_shell) {
22,143,285!
378
      const auto& shell {element.shells_[i_shell]};
22,143,285✔
379

380
      // Check threshold of reaction
381
      if (xs_lower(i_shell) == 0)
22,143,285✔
382
        continue;
9,345,274✔
383

384
      //  Evaluation subshell photoionization cross section
385
      prob += std::exp(
12,798,011✔
386
        xs_lower(i_shell) + f * (xs_upper(i_shell) - xs_lower(i_shell)));
12,798,011✔
387

388
      if (prob > cutoff) {
12,798,011✔
389
        // Determine binding energy based on whether atomic relaxation data is
390
        // present (if not, use value from Compton profile data)
391
        double binding_energy = element.has_atomic_relaxation_
4,660,700✔
392
                                  ? shell.binding_energy
4,660,700!
UNCOV
393
                                  : element.binding_energy_[i_shell];
×
394

395
        // Determine energy of secondary electron
396
        double E_electron = p.E() - binding_energy;
4,660,700✔
397

398
        // Sample mu using non-relativistic Sauter distribution.
399
        // See Eqns 3.19 and 3.20 in "Implementing a photon physics
400
        // model in Serpent 2" by Toni Kaltiaisenaho
401
        double mu;
402
        while (true) {
403
          double r = prn(p.current_seed());
6,990,858✔
404
          if (4.0 * (1.0 - r) * r >= prn(p.current_seed())) {
6,990,858✔
405
            double rel_vel =
406
              std::sqrt(E_electron * (E_electron + 2.0 * MASS_ELECTRON_EV)) /
4,660,700✔
407
              (E_electron + MASS_ELECTRON_EV);
4,660,700✔
408
            mu =
4,660,700✔
409
              (2.0 * r + rel_vel - 1.0) / (2.0 * rel_vel * r - rel_vel + 1.0);
4,660,700✔
410
            break;
4,660,700✔
411
          }
412
        }
2,330,158✔
413

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

420
        // Create secondary electron
421
        p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron);
4,660,700✔
422

423
        // Allow electrons to fill orbital and produce auger electrons
424
        // and fluorescent photons
425
        element.atomic_relaxation(i_shell, p);
4,660,700✔
426
        p.event() = TallyEvent::ABSORB;
4,660,700✔
427
        p.event_mt() = 533 + shell.index_subshell;
4,660,700✔
428
        p.wgt() = 0.0;
4,660,700✔
429
        p.E() = 0.0;
4,660,700✔
430
        return;
4,660,700✔
431
      }
432
    }
433
  }
9,321,400!
434
  prob = prob_after;
81,740✔
435

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

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

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

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

459
void sample_electron_reaction(Particle& p)
48,994,429✔
460
{
461
  // TODO: create reaction types
462

463
  if (settings::electron_treatment == ElectronTreatment::TTB) {
48,994,429✔
464
    double E_lost;
465
    thick_target_bremsstrahlung(p, &E_lost);
48,241,609✔
466
  }
467

468
  p.E() = 0.0;
48,994,429✔
469
  p.wgt() = 0.0;
48,994,429✔
470
  p.event() = TallyEvent::ABSORB;
48,994,429✔
471
}
48,994,429✔
472

473
void sample_positron_reaction(Particle& p)
81,740✔
474
{
475
  // TODO: create reaction types
476

477
  if (settings::electron_treatment == ElectronTreatment::TTB) {
81,740✔
478
    double E_lost;
479
    thick_target_bremsstrahlung(p, &E_lost);
78,180✔
480
  }
481

482
  // Sample angle isotropically
483
  Direction u = isotropic_direction(p.current_seed());
81,740✔
484

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

489
  p.E() = 0.0;
81,740✔
490
  p.wgt() = 0.0;
81,740✔
491
  p.event() = TallyEvent::ABSORB;
81,740✔
492
}
81,740✔
493

494
int sample_nuclide(Particle& p)
735,673,366✔
495
{
496
  // Sample cumulative distribution function
497
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
735,673,366✔
498

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

503
  double prob = 0.0;
735,673,366✔
504
  for (int i = 0; i < n; ++i) {
1,576,454,400!
505
    // Get atom density
506
    int i_nuclide = mat->nuclide_[i];
1,576,454,400✔
507
    double atom_density = mat->atom_density(i, p.density_mult());
1,576,454,400✔
508

509
    // Increment probability to compare to cutoff
510
    prob += atom_density * p.neutron_xs(i_nuclide).total;
1,576,454,400✔
511
    if (prob >= cutoff)
1,576,454,400✔
512
      return i_nuclide;
735,673,366✔
513
  }
514

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

520
int sample_element(Particle& p)
15,880,764✔
521
{
522
  // Sample cumulative distribution function
523
  double cutoff = prn(p.current_seed()) * p.macro_xs().total;
15,880,764✔
524

525
  // Get pointers to elements, densities
526
  const auto& mat {model::materials[p.material()]};
15,880,764✔
527

528
  double prob = 0.0;
15,880,764✔
529
  for (int i = 0; i < mat->element_.size(); ++i) {
37,788,556!
530
    // Find atom density
531
    int i_element = mat->element_[i];
37,788,556✔
532
    double atom_density = mat->atom_density(i, p.density_mult());
37,788,556✔
533

534
    // Determine microscopic cross section
535
    double sigma = atom_density * p.photon_xs(i_element).total;
37,788,556✔
536

537
    // Increment probability to compare to cutoff
538
    prob += sigma;
37,788,556✔
539
    if (prob > cutoff) {
37,788,556✔
540
      // Save which nuclide particle had collision with for tally purpose
541
      p.event_nuclide() = mat->nuclide_[i];
15,880,764✔
542

543
      return i_element;
15,880,764✔
544
    }
545
  }
546

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

552
Reaction& sample_fission(int i_nuclide, Particle& p)
98,973,586✔
553
{
554
  // Get pointer to nuclide
555
  const auto& nuc {data::nuclides[i_nuclide]};
98,973,586✔
556

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

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

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

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

582
    // Create fission bank sites if fission occurs
583
    if (prob > cutoff)
24,542✔
584
      return *rx;
24,485✔
585
  }
586

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

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

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

607
    // if cross section is zero for this reaction, skip it
608
    if (xs == 0.0)
19,996,210✔
609
      continue;
6,537,650✔
610

611
    for (int j = 0; j < rx->products_.size(); ++j) {
63,252,270✔
612
      if (rx->products_[j].particle_ == ParticleType::photon) {
51,009,750✔
613
        // For fission, artificially increase the photon yield to account
614
        // for delayed photons
615
        double f = 1.0;
39,688,100✔
616
        if (settings::delayed_photon_scaling) {
39,688,100!
617
          if (is_fission(rx->mt_)) {
39,688,100✔
618
            if (nuc->prompt_photons_ && nuc->delayed_photons_) {
491,110!
619
              double energy_prompt = (*nuc->prompt_photons_)(p.E());
491,110✔
620
              double energy_delayed = (*nuc->delayed_photons_)(p.E());
491,110✔
621
              f = (energy_prompt + energy_delayed) / (energy_prompt);
491,110✔
622
            }
623
          }
624
        }
625

626
        // add to cumulative probability
627
        prob += f * (*rx->products_[j].yield_)(p.E()) * xs;
39,688,100✔
628

629
        *i_rx = i;
39,688,100✔
630
        *i_product = j;
39,688,100✔
631
        if (prob > cutoff)
39,688,100✔
632
          return;
1,216,040✔
633
      }
634
    }
635
  }
636
}
637

638
void absorption(Particle& p, int i_nuclide)
735,670,166✔
639
{
640
  if (settings::survival_biasing) {
735,670,166✔
641
    // Determine weight absorbed in survival biasing
642
    const double wgt_absorb = p.wgt() * p.neutron_xs(i_nuclide).absorption /
454,500✔
643
                              p.neutron_xs(i_nuclide).total;
454,500✔
644

645
    // Adjust weight of particle by probability of absorption
646
    p.wgt() -= wgt_absorb;
454,500✔
647

648
    // Score implicit absorption estimate of keff
649
    if (settings::run_mode == RunMode::EIGENVALUE) {
454,500!
650
      p.keff_tally_absorption() += wgt_absorb *
454,500✔
651
                                   p.neutron_xs(i_nuclide).nu_fission /
454,500✔
652
                                   p.neutron_xs(i_nuclide).absorption;
454,500✔
653
    }
654
  } else {
655
    // See if disappearance reaction happens
656
    if (p.neutron_xs(i_nuclide).absorption >
735,215,666✔
657
        prn(p.current_seed()) * p.neutron_xs(i_nuclide).total) {
735,215,666✔
658
      // Score absorption estimate of keff
659
      if (settings::run_mode == RunMode::EIGENVALUE) {
19,000,689✔
660
        p.keff_tally_absorption() += p.wgt() *
30,659,708✔
661
                                     p.neutron_xs(i_nuclide).nu_fission /
15,329,854✔
662
                                     p.neutron_xs(i_nuclide).absorption;
15,329,854✔
663
      }
664

665
      p.wgt() = 0.0;
19,000,689✔
666
      p.event() = TallyEvent::ABSORB;
19,000,689✔
667
      if (!p.fission()) {
19,000,689✔
668
        p.event_mt() = N_DISAPPEAR;
11,441,605✔
669
      }
670
    }
671
  }
672
}
735,670,166✔
673

674
void scatter(Particle& p, int i_nuclide)
716,528,287✔
675
{
676
  // copy incoming direction
677
  Direction u_old {p.u()};
716,528,287✔
678

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

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

689
  // Calculate elastic cross section if it wasn't precalculated
690
  if (micro.elastic == CACHE_INVALID) {
716,528,287✔
691
    nuc->calculate_elastic_xs(p);
595,534,657✔
692
  }
693

694
  double prob = micro.elastic - micro.thermal;
716,528,287✔
695
  if (prob > cutoff) {
716,528,287✔
696
    // =======================================================================
697
    // NON-S(A,B) ELASTIC SCATTERING
698

699
    // Determine temperature
700
    double kT = nuc->multipole_ ? p.sqrtkT() * p.sqrtkT() : nuc->kTs_[i_temp];
615,792,514✔
701

702
    // Perform collision physics for elastic scattering
703
    elastic_scatter(i_nuclide, *nuc->reactions_[0], kT, p);
615,792,514✔
704

705
    p.event_mt() = ELASTIC;
615,792,514✔
706
    sampled = true;
615,792,514✔
707
  }
708

709
  prob = micro.elastic;
716,528,287✔
710
  if (prob > cutoff && !sampled) {
716,528,287✔
711
    // =======================================================================
712
    // S(A,B) SCATTERING
713

714
    sab_scatter(i_nuclide, micro.index_sab, p);
85,229,151✔
715

716
    p.event_mt() = ELASTIC;
85,229,151✔
717
    sampled = true;
85,229,151✔
718
  }
719

720
  if (!sampled) {
716,528,287✔
721
    // =======================================================================
722
    // INELASTIC SCATTERING
723

724
    int n = nuc->index_inelastic_scatter_.size();
15,506,622✔
725
    int i = 0;
15,506,622✔
726
    for (int j = 0; j < n && prob < cutoff; ++j) {
298,102,754✔
727
      i = nuc->index_inelastic_scatter_[j];
282,596,132✔
728

729
      // add to cumulative probability
730
      prob += nuc->reactions_[i]->xs(micro);
282,596,132✔
731
    }
732

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

739
  // Set event component
740
  p.event() = TallyEvent::SCATTER;
716,528,287✔
741

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

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

759
  double vel = std::sqrt(p.E());
615,792,514✔
760
  double awr = nuc->awr_;
615,792,514✔
761

762
  // Neutron velocity in LAB
763
  Direction v_n = vel * p.u();
615,792,514✔
764

765
  // Sample velocity of target nucleus
766
  Direction v_t {};
615,792,514✔
767
  if (!p.neutron_xs(i_nuclide).use_ptable) {
615,792,514✔
768
    v_t = sample_target_velocity(*nuc, p.E(), p.u(), v_n,
1,188,888,498✔
769
      p.neutron_xs(i_nuclide).elastic, kT, p.current_seed());
594,444,249✔
770
  }
771

772
  // Velocity of center-of-mass
773
  Direction v_cm = (v_n + awr * v_t) / (awr + 1.0);
615,792,514✔
774

775
  // Transform to CM frame
776
  v_n -= v_cm;
615,792,514✔
777

778
  // Find speed of neutron in CM
779
  vel = v_n.norm();
615,792,514✔
780

781
  // Sample scattering angle, checking if angle distribution is present (assume
782
  // isotropic otherwise)
783
  double mu_cm;
784
  auto& d = rx.products_[0].distribution_[0];
615,792,514✔
785
  auto d_ = dynamic_cast<UncorrelatedAngleEnergy*>(d.get());
615,792,514!
786
  if (!d_->angle().empty()) {
615,792,514!
787
    mu_cm = d_->angle().sample(p.E(), p.current_seed());
615,792,514✔
788
  } else {
UNCOV
789
    mu_cm = uniform_distribution(-1., 1., p.current_seed());
×
790
  }
791

792
  // Determine direction cosines in CM
793
  Direction u_cm = v_n / vel;
615,792,514✔
794

795
  // Rotate neutron velocity vector to new angle -- note that the speed of the
796
  // neutron in CM does not change in elastic scattering. However, the speed
797
  // will change when we convert back to LAB
798
  v_n = vel * rotate_angle(u_cm, mu_cm, nullptr, p.current_seed());
615,792,514✔
799

800
  // Transform back to LAB frame
801
  v_n += v_cm;
615,792,514✔
802

803
  p.E() = v_n.dot(v_n);
615,792,514✔
804
  vel = std::sqrt(p.E());
615,792,514✔
805

806
  // compute cosine of scattering angle in LAB frame by taking dot product of
807
  // neutron's pre- and post-collision angle
808
  p.mu() = p.u().dot(v_n) / vel;
615,792,514✔
809

810
  // Set energy and direction of particle in LAB frame
811
  p.u() = v_n / vel;
615,792,514✔
812

813
  // Because of floating-point roundoff, it may be possible for mu_lab to be
814
  // outside of the range [-1,1). In these cases, we just set mu_lab to exactly
815
  // -1 or 1
816
  if (std::abs(p.mu()) > 1.0)
615,792,514!
UNCOV
817
    p.mu() = std::copysign(1.0, p.mu());
×
818
}
615,792,514✔
819

820
void sab_scatter(int i_nuclide, int i_sab, Particle& p)
85,229,151✔
821
{
822
  // Determine temperature index
823
  const auto& micro {p.neutron_xs(i_nuclide)};
85,229,151✔
824
  int i_temp = micro.index_temp_sab;
85,229,151✔
825

826
  // Sample energy and angle
827
  double E_out;
828
  data::thermal_scatt[i_sab]->data_[i_temp].sample(
170,458,302✔
829
    micro, p.E(), &E_out, &p.mu(), p.current_seed());
85,229,151✔
830

831
  // Set energy to outgoing, change direction of particle
832
  p.E() = E_out;
85,229,151✔
833
  p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed());
85,229,151✔
834
}
85,229,151✔
835

836
Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u,
594,444,249✔
837
  Direction v_neut, double xs_eff, double kT, uint64_t* seed)
838
{
839
  // check if nuclide is a resonant scatterer
840
  ResScatMethod sampling_method;
841
  if (nuc.resonant_) {
594,444,249✔
842

843
    // sampling method to use
844
    sampling_method = settings::res_scat_method;
76,870✔
845

846
    // upper resonance scattering energy bound (target is at rest above this E)
847
    if (E > settings::res_scat_energy_max) {
76,870✔
848
      return {};
37,050✔
849

850
      // lower resonance scattering energy bound (should be no resonances below)
851
    } else if (E < settings::res_scat_energy_min) {
39,820✔
852
      sampling_method = ResScatMethod::cxs;
22,720✔
853
    }
854

855
    // otherwise, use free gas model
856
  } else {
857
    if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) {
594,367,379✔
858
      return {};
274,771,081✔
859
    } else {
860
      sampling_method = ResScatMethod::cxs;
319,596,298✔
861
    }
862
  }
863

864
  // use appropriate target velocity sampling method
865
  switch (sampling_method) {
319,636,118!
866
  case ResScatMethod::cxs:
319,619,018✔
867

868
    // sample target velocity with the constant cross section (cxs) approx.
869
    return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
319,619,018✔
870

871
  case ResScatMethod::dbrc:
17,100✔
872
  case ResScatMethod::rvs: {
873
    double E_red = std::sqrt(nuc.awr_ * E / kT);
17,100✔
874
    double E_low = std::pow(std::max(0.0, E_red - 4.0), 2) * kT / nuc.awr_;
17,100✔
875
    double E_up = (E_red + 4.0) * (E_red + 4.0) * kT / nuc.awr_;
17,100✔
876

877
    // find lower and upper energy bound indices
878
    // lower index
879
    int i_E_low;
880
    if (E_low < nuc.energy_0K_.front()) {
17,100!
UNCOV
881
      i_E_low = 0;
×
882
    } else if (E_low > nuc.energy_0K_.back()) {
17,100!
UNCOV
883
      i_E_low = nuc.energy_0K_.size() - 2;
×
884
    } else {
885
      i_E_low =
17,100✔
886
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_low);
17,100✔
887
    }
888

889
    // upper index
890
    int i_E_up;
891
    if (E_up < nuc.energy_0K_.front()) {
17,100!
UNCOV
892
      i_E_up = 0;
×
893
    } else if (E_up > nuc.energy_0K_.back()) {
17,100!
UNCOV
894
      i_E_up = nuc.energy_0K_.size() - 2;
×
895
    } else {
896
      i_E_up =
17,100✔
897
        lower_bound_index(nuc.energy_0K_.begin(), nuc.energy_0K_.end(), E_up);
17,100✔
898
    }
899

900
    if (i_E_up == i_E_low) {
17,100✔
901
      // Handle degenerate case -- if the upper/lower bounds occur for the same
902
      // index, then using cxs is probably a good approximation
903
      return sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
17,100✔
904
    }
905

906
    if (sampling_method == ResScatMethod::dbrc) {
14,120!
907
      // interpolate xs since we're not exactly at the energy indices
908
      double xs_low = nuc.elastic_0K_[i_E_low];
×
909
      double m = (nuc.elastic_0K_[i_E_low + 1] - xs_low) /
×
910
                 (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
×
911
      xs_low += m * (E_low - nuc.energy_0K_[i_E_low]);
×
912
      double xs_up = nuc.elastic_0K_[i_E_up];
×
UNCOV
913
      m = (nuc.elastic_0K_[i_E_up + 1] - xs_up) /
×
UNCOV
914
          (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
×
915
      xs_up += m * (E_up - nuc.energy_0K_[i_E_up]);
×
916

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

922
      while (true) {
923
        double E_rel;
UNCOV
924
        Direction v_target;
×
925
        while (true) {
926
          // sample target velocity with the constant cross section (cxs)
927
          // approx.
928
          v_target = sample_cxs_target_velocity(nuc.awr_, E, u, kT, seed);
×
929
          Direction v_rel = v_neut - v_target;
×
930
          E_rel = v_rel.dot(v_rel);
×
UNCOV
931
          if (E_rel < E_up)
×
UNCOV
932
            break;
×
933
        }
×
934

935
        // perform Doppler broadening rejection correction (dbrc)
936
        double xs_0K = nuc.elastic_xs_0K(E_rel);
×
937
        double R = xs_0K / xs_max;
×
UNCOV
938
        if (prn(seed) < R)
×
UNCOV
939
          return v_target;
×
UNCOV
940
      }
×
941

942
    } else if (sampling_method == ResScatMethod::rvs) {
14,120!
943
      // interpolate xs CDF since we're not exactly at the energy indices
944
      // cdf value at lower bound attainable energy
945
      double cdf_low = 0.0;
14,120✔
946
      if (E_low > nuc.energy_0K_.front()) {
14,120!
947
        double m = (nuc.xs_cdf_[i_E_low + 1] - nuc.xs_cdf_[i_E_low]) /
14,120✔
948
                   (nuc.energy_0K_[i_E_low + 1] - nuc.energy_0K_[i_E_low]);
14,120✔
949
        cdf_low = nuc.xs_cdf_[i_E_low] + m * (E_low - nuc.energy_0K_[i_E_low]);
14,120✔
950
      }
951

952
      // cdf value at upper bound attainable energy
953
      double m = (nuc.xs_cdf_[i_E_up + 1] - nuc.xs_cdf_[i_E_up]) /
14,120✔
954
                 (nuc.energy_0K_[i_E_up + 1] - nuc.energy_0K_[i_E_up]);
14,120✔
955
      double cdf_up = nuc.xs_cdf_[i_E_up] + m * (E_up - nuc.energy_0K_[i_E_up]);
14,120✔
956

957
      while (true) {
958
        // directly sample Maxwellian
959
        double E_t = -kT * std::log(prn(seed));
155,200✔
960

961
        // sample a relative energy using the xs cdf
962
        double cdf_rel = cdf_low + prn(seed) * (cdf_up - cdf_low);
155,200✔
963
        int i_E_rel = lower_bound_index(nuc.xs_cdf_.begin() + i_E_low,
155,200✔
964
          nuc.xs_cdf_.begin() + i_E_up + 2, cdf_rel);
155,200✔
965
        double E_rel = nuc.energy_0K_[i_E_low + i_E_rel];
155,200✔
966
        double m = (nuc.xs_cdf_[i_E_low + i_E_rel + 1] -
155,200✔
967
                     nuc.xs_cdf_[i_E_low + i_E_rel]) /
155,200✔
968
                   (nuc.energy_0K_[i_E_low + i_E_rel + 1] -
155,200✔
969
                     nuc.energy_0K_[i_E_low + i_E_rel]);
155,200✔
970
        E_rel += (cdf_rel - nuc.xs_cdf_[i_E_low + i_E_rel]) / m;
155,200✔
971

972
        // perform rejection sampling on cosine between
973
        // neutron and target velocities
974
        double mu = (E_t + nuc.awr_ * (E - E_rel)) /
155,200✔
975
                    (2.0 * std::sqrt(nuc.awr_ * E * E_t));
155,200✔
976

977
        if (std::abs(mu) < 1.0) {
155,200✔
978
          // set and accept target velocity
979
          E_t /= nuc.awr_;
14,120✔
980
          return std::sqrt(E_t) * rotate_angle(u, mu, nullptr, seed);
14,120✔
981
        }
982
      }
141,080✔
983
    }
984
  } // case RVS, DBRC
985
  } // switch (sampling_method)
986

UNCOV
987
  UNREACHABLE();
×
988
}
989

990
Direction sample_cxs_target_velocity(
319,621,998✔
991
  double awr, double E, Direction u, double kT, uint64_t* seed)
992
{
993
  double beta_vn = std::sqrt(awr * E / kT);
319,621,998✔
994
  double alpha = 1.0 / (1.0 + std::sqrt(PI) * beta_vn / 2.0);
319,621,998✔
995

996
  double beta_vt_sq;
997
  double mu;
998
  while (true) {
999
    // Sample two random numbers
1000
    double r1 = prn(seed);
376,956,327✔
1001
    double r2 = prn(seed);
376,956,327✔
1002

1003
    if (prn(seed) < alpha) {
376,956,327✔
1004
      // With probability alpha, we sample the distribution p(y) =
1005
      // y*e^(-y). This can be done with sampling scheme C45 from the Monte
1006
      // Carlo sampler
1007

1008
      beta_vt_sq = -std::log(r1 * r2);
92,997,484✔
1009

1010
    } else {
1011
      // With probability 1-alpha, we sample the distribution p(y) = y^2 *
1012
      // e^(-y^2). This can be done with sampling scheme C61 from the Monte
1013
      // Carlo sampler
1014

1015
      double c = std::cos(PI / 2.0 * prn(seed));
283,958,843✔
1016
      beta_vt_sq = -std::log(r1) - std::log(r2) * c * c;
283,958,843✔
1017
    }
1018

1019
    // Determine beta * vt
1020
    double beta_vt = std::sqrt(beta_vt_sq);
376,956,327✔
1021

1022
    // Sample cosine of angle between neutron and target velocity
1023
    mu = uniform_distribution(-1., 1., seed);
376,956,327✔
1024

1025
    // Determine rejection probability
1026
    double accept_prob =
1027
      std::sqrt(beta_vn * beta_vn + beta_vt_sq - 2 * beta_vn * beta_vt * mu) /
376,956,327✔
1028
      (beta_vn + beta_vt);
376,956,327✔
1029

1030
    // Perform rejection sampling on vt and mu
1031
    if (prn(seed) < accept_prob)
376,956,327✔
1032
      break;
319,621,998✔
1033
  }
57,334,329✔
1034

1035
  // Determine speed of target nucleus
1036
  double vt = std::sqrt(beta_vt_sq * kT / awr);
319,621,998✔
1037

1038
  // Determine velocity vector of target nucleus based on neutron's velocity
1039
  // and the sampled angle between them
1040
  return vt * rotate_angle(u, mu, nullptr, seed);
319,621,998✔
1041
}
1042

1043
void sample_fission_neutron(
26,456,578✔
1044
  int i_nuclide, const Reaction& rx, SourceSite* site, Particle& p)
1045
{
1046
  // Get attributes of particle
1047
  double E_in = p.E();
26,456,578✔
1048
  uint64_t* seed = p.current_seed();
26,456,578✔
1049

1050
  // Determine total nu, delayed nu, and delayed neutron fraction
1051
  const auto& nuc {data::nuclides[i_nuclide]};
26,456,578✔
1052
  double nu_t = nuc->nu(E_in, Nuclide::EmissionMode::total);
26,456,578✔
1053
  double nu_d = nuc->nu(E_in, Nuclide::EmissionMode::delayed);
26,456,578✔
1054
  double beta = nu_d / nu_t;
26,456,578✔
1055

1056
  if (prn(seed) < beta) {
26,456,578✔
1057
    // ====================================================================
1058
    // DELAYED NEUTRON SAMPLED
1059

1060
    // sampled delayed precursor group
1061
    double xi = prn(seed) * nu_d;
165,338✔
1062
    double prob = 0.0;
165,338✔
1063
    int group;
1064
    for (group = 1; group < nuc->n_precursor_; ++group) {
615,988✔
1065
      // determine delayed neutron precursor yield for group j
1066
      double yield = (*rx.products_[group].yield_)(E_in);
604,377✔
1067

1068
      // Check if this group is sampled
1069
      prob += yield;
604,377✔
1070
      if (xi < prob)
604,377✔
1071
        break;
153,727✔
1072
    }
1073

1074
    // if the sum of the probabilities is slightly less than one and the
1075
    // random number is greater, j will be greater than nuc %
1076
    // n_precursor -- check for this condition
1077
    group = std::min(group, nuc->n_precursor_);
165,338✔
1078

1079
    // set the delayed group for the particle born from fission
1080
    site->delayed_group = group;
165,338✔
1081

1082
    // Sample time of emission based on decay constant of precursor
1083
    double decay_rate = rx.products_[site->delayed_group].decay_rate_;
165,338✔
1084
    site->time -= std::log(prn(p.current_seed())) / decay_rate;
165,338✔
1085

1086
  } else {
1087
    // ====================================================================
1088
    // PROMPT NEUTRON SAMPLED
1089

1090
    // set the delayed group for the particle born from fission to 0
1091
    site->delayed_group = 0;
26,291,240✔
1092
  }
1093

1094
  // sample from prompt neutron energy distribution
1095
  int n_sample = 0;
26,456,578✔
1096
  double mu;
1097
  while (true) {
1098
    rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed);
26,456,578✔
1099

1100
    // resample if energy is greater than maximum neutron energy
1101
    constexpr int neutron = static_cast<int>(ParticleType::neutron);
26,456,578✔
1102
    if (site->E < data::energy_max[neutron])
26,456,578!
1103
      break;
26,456,578✔
1104

1105
    // check for large number of resamples
1106
    ++n_sample;
×
1107
    if (n_sample == MAX_SAMPLE) {
×
1108
      // particle_write_restart(p)
UNCOV
1109
      fatal_error("Resampled energy distribution maximum number of times "
×
1110
                  "for nuclide " +
×
UNCOV
1111
                  nuc->name_);
×
1112
    }
UNCOV
1113
  }
×
1114

1115
  // Sample azimuthal angle uniformly in [0, 2*pi) and assign angle
1116
  site->u = rotate_angle(p.u(), mu, nullptr, seed);
26,456,578✔
1117
}
26,456,578✔
1118

1119
void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p)
15,506,622✔
1120
{
1121
  // copy energy of neutron
1122
  double E_in = p.E();
15,506,622✔
1123

1124
  // sample outgoing energy and scattering cosine
1125
  double E;
1126
  double mu;
1127
  rx.products_[0].sample(E_in, E, mu, p.current_seed());
15,506,622✔
1128

1129
  // if scattering system is in center-of-mass, transfer cosine of scattering
1130
  // angle and outgoing energy from CM to LAB
1131
  if (rx.scatter_in_cm_) {
15,506,622✔
1132
    double E_cm = E;
15,458,776✔
1133

1134
    // determine outgoing energy in lab
1135
    double A = nuc.awr_;
15,458,776✔
1136
    E = E_cm + (E_in + 2.0 * mu * (A + 1.0) * std::sqrt(E_in * E_cm)) /
15,458,776✔
1137
                 ((A + 1.0) * (A + 1.0));
15,458,776✔
1138

1139
    // determine outgoing angle in lab
1140
    mu = mu * std::sqrt(E_cm / E) + 1.0 / (A + 1.0) * std::sqrt(E_in / E);
15,458,776✔
1141
  }
1142

1143
  // Because of floating-point roundoff, it may be possible for mu to be
1144
  // outside of the range [-1,1). In these cases, we just set mu to exactly -1
1145
  // or 1
1146
  if (std::abs(mu) > 1.0)
15,506,622!
UNCOV
1147
    mu = std::copysign(1.0, mu);
×
1148

1149
  // Set outgoing energy and scattering angle
1150
  p.E() = E;
15,506,622✔
1151
  p.mu() = mu;
15,506,622✔
1152

1153
  // change direction of particle
1154
  p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed());
15,506,622✔
1155

1156
  // evaluate yield
1157
  double yield = (*rx.products_[0].yield_)(E_in);
15,506,622✔
1158
  if (std::floor(yield) == yield && yield > 0) {
15,506,622!
1159
    // If yield is integral, create exactly that many secondary particles
1160
    for (int i = 0; i < static_cast<int>(std::round(yield)) - 1; ++i) {
15,598,942✔
1161
      p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron);
92,374✔
1162
    }
1163
  } else {
15,506,568✔
1164
    // Otherwise, change weight of particle based on yield
1165
    p.wgt() *= yield;
54✔
1166
  }
1167
}
15,506,622✔
1168

1169
void sample_secondary_photons(Particle& p, int i_nuclide)
8,008,680✔
1170
{
1171
  // Sample the number of photons produced
1172
  double y_t =
1173
    p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total;
8,008,680✔
1174
  double photon_wgt = p.wgt();
8,008,680✔
1175
  int y = 1;
8,008,680✔
1176

1177
  if (settings::use_decay_photons) {
8,008,680✔
1178
    // For decay photons, sample a single photon and modify the weight
1179
    if (y_t <= 0.0)
65,460✔
1180
      return;
15,710✔
1181
    photon_wgt *= y_t;
49,750✔
1182
  } else {
1183
    // For prompt photons, sample an integral number of photons with weight
1184
    // equal to the neutron's weight
1185
    y = static_cast<int>(y_t);
7,943,220✔
1186
    if (prn(p.current_seed()) <= y_t - y)
7,943,220✔
1187
      ++y;
522,600✔
1188
  }
1189

1190
  // Sample each secondary photon
1191
  for (int i = 0; i < y; ++i) {
9,209,010✔
1192
    // Sample the reaction and product
1193
    int i_rx;
1194
    int i_product;
1195
    sample_photon_product(i_nuclide, p, &i_rx, &i_product);
1,216,040✔
1196

1197
    // Sample the outgoing energy and angle
1198
    auto& rx = data::nuclides[i_nuclide]->reactions_[i_rx];
1,216,040✔
1199
    double E;
1200
    double mu;
1201
    rx->products_[i_product].sample(p.E(), E, mu, p.current_seed());
1,216,040✔
1202

1203
    // Sample the new direction
1204
    Direction u = rotate_angle(p.u(), mu, nullptr, p.current_seed());
1,216,040✔
1205

1206
    // In a k-eigenvalue simulation, it's necessary to provide higher weight to
1207
    // secondary photons from non-fission reactions to properly balance energy
1208
    // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H.
1209
    // Stedry, "Self-consistent energy normalization for quasistatic reactor
1210
    // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020.
1211
    double wgt = photon_wgt;
1,216,040✔
1212
    if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) {
1,216,040✔
1213
      wgt *= simulation::keff;
316,480✔
1214
    }
1215

1216
    // Create the secondary photon
1217
    bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon);
1,216,040✔
1218

1219
    // Tag secondary particle with parent nuclide
1220
    if (created_photon && settings::use_decay_photons) {
1,216,040✔
1221
      p.secondary_bank().back().parent_nuclide =
48,040✔
1222
        rx->products_[i_product].parent_nuclide_;
48,040✔
1223
    }
1224
  }
1225
}
1226

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