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

openmc-dev / openmc / 12621974951

05 Jan 2025 06:29PM UTC coverage: 84.831% (+0.004%) from 84.827%
12621974951

Pull #3242

github

web-flow
Merge 170fe2608 into 775c41512
Pull Request #3242: Fix for erroneously non-zero tally results of photon threshold reactions

8 of 8 new or added lines in 1 file covered. (100.0%)

12 existing lines in 1 file now uncovered.

49861 of 58777 relevant lines covered (84.83%)

34045547.86 hits per line

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

94.36
/src/photon.cpp
1
#include "openmc/photon.h"
2

3
#include "openmc/array.h"
4
#include "openmc/bremsstrahlung.h"
5
#include "openmc/constants.h"
6
#include "openmc/distribution_multi.h"
7
#include "openmc/hdf5_interface.h"
8
#include "openmc/message_passing.h"
9
#include "openmc/nuclide.h"
10
#include "openmc/particle.h"
11
#include "openmc/random_dist.h"
12
#include "openmc/random_lcg.h"
13
#include "openmc/search.h"
14
#include "openmc/settings.h"
15

16
#include "xtensor/xbuilder.hpp"
17
#include "xtensor/xmath.hpp"
18
#include "xtensor/xoperation.hpp"
19
#include "xtensor/xslice.hpp"
20
#include "xtensor/xview.hpp"
21

22
#include <cmath>
23
#include <fmt/core.h>
24
#include <tuple> // for tie
25

26
namespace openmc {
27

28
constexpr int PhotonInteraction::MAX_STACK_SIZE;
29

30
//==============================================================================
31
// Global variables
32
//==============================================================================
33

34
namespace data {
35

36
xt::xtensor<double, 1> compton_profile_pz;
37

38
std::unordered_map<std::string, int> element_map;
39
vector<unique_ptr<PhotonInteraction>> elements;
40

41
} // namespace data
42

43
//==============================================================================
44
// PhotonInteraction implementation
45
//==============================================================================
46

47
PhotonInteraction::PhotonInteraction(hid_t group)
623✔
48
{
49
  using namespace xt::placeholders;
50

51
  // Set index of element in global vector
52
  index_ = data::elements.size();
623✔
53

54
  // Get name of nuclide from group, removing leading '/'
55
  name_ = object_name(group).substr(1);
623✔
56
  data::element_map[name_] = index_;
623✔
57

58
  // Get atomic number
59
  read_attribute(group, "Z", Z_);
623✔
60

61
  // Determine number of energies and read energy grid
62
  read_dataset(group, "energy", energy_);
623✔
63

64
  // Read coherent scattering
65
  hid_t rgroup = open_group(group, "coherent");
623✔
66
  read_dataset(rgroup, "xs", coherent_);
623✔
67

68
  hid_t dset = open_dataset(rgroup, "integrated_scattering_factor");
623✔
69
  coherent_int_form_factor_ = Tabulated1D {dset};
623✔
70
  close_dataset(dset);
623✔
71

72
  if (object_exists(group, "anomalous_real")) {
623✔
73
    dset = open_dataset(rgroup, "anomalous_real");
×
74
    coherent_anomalous_real_ = Tabulated1D {dset};
×
75
    close_dataset(dset);
×
76
  }
77

78
  if (object_exists(group, "anomalous_imag")) {
623✔
79
    dset = open_dataset(rgroup, "anomalous_imag");
×
80
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
81
    close_dataset(dset);
×
82
  }
83
  close_group(rgroup);
623✔
84

85
  // Read incoherent scattering
86
  rgroup = open_group(group, "incoherent");
623✔
87
  read_dataset(rgroup, "xs", incoherent_);
623✔
88
  dset = open_dataset(rgroup, "scattering_factor");
623✔
89
  incoherent_form_factor_ = Tabulated1D {dset};
623✔
90
  close_dataset(dset);
623✔
91
  close_group(rgroup);
623✔
92

93
  // Read pair production
94
  if (object_exists(group, "pair_production_electron")) {
623✔
95
    rgroup = open_group(group, "pair_production_electron");
623✔
96
    read_dataset(rgroup, "xs", pair_production_electron_);
623✔
97
    close_group(rgroup);
623✔
98
  } else {
99
    pair_production_electron_ = xt::zeros_like(energy_);
×
100
  }
101

102
  // Read pair production
103
  if (object_exists(group, "pair_production_nuclear")) {
623✔
104
    rgroup = open_group(group, "pair_production_nuclear");
623✔
105
    read_dataset(rgroup, "xs", pair_production_nuclear_);
623✔
106
    close_group(rgroup);
623✔
107
  } else {
108
    pair_production_nuclear_ = xt::zeros_like(energy_);
×
109
  }
110

111
  // Read photoelectric
112
  rgroup = open_group(group, "photoelectric");
623✔
113
  read_dataset(rgroup, "xs", photoelectric_total_);
623✔
114
  close_group(rgroup);
623✔
115

116
  // Read heating
117
  if (object_exists(group, "heating")) {
623✔
118
    rgroup = open_group(group, "heating");
623✔
119
    read_dataset(rgroup, "xs", heating_);
623✔
120
    close_group(rgroup);
623✔
121
  } else {
122
    heating_ = xt::zeros_like(energy_);
×
123
  }
124

125
  // Read subshell photoionization cross section and atomic relaxation data
126
  rgroup = open_group(group, "subshells");
623✔
127
  vector<std::string> designators;
623✔
128
  read_attribute(rgroup, "designators", designators);
623✔
129
  auto n_shell = designators.size();
623✔
130
  if (n_shell == 0) {
623✔
131
    throw std::runtime_error {
×
132
      "Photoatomic data for " + name_ + " does not have subshell data."};
×
133
  }
134

135
  shells_.resize(n_shell);
623✔
136
  cross_sections_ = xt::zeros<double>({energy_.size(), n_shell});
623✔
137

138
  // Create mapping from designator to index
139
  std::unordered_map<int, int> shell_map;
623✔
140
  for (int i = 0; i < n_shell; ++i) {
5,902✔
141
    const auto& designator {designators[i]};
5,279✔
142

143
    int j = 1;
5,279✔
144
    for (const auto& subshell : SUBSHELLS) {
39,525✔
145
      if (designator == subshell) {
39,525✔
146
        shell_map[j] = i;
5,279✔
147
        shells_[i].index_subshell = j;
5,279✔
148
        break;
5,279✔
149
      }
150
      ++j;
34,246✔
151
    }
152
  }
153
  shell_map[0] = -1;
623✔
154

155
  for (int i = 0; i < n_shell; ++i) {
5,902✔
156
    const auto& designator {designators[i]};
5,279✔
157
    auto& shell {shells_[i]};
5,279✔
158

159
    // TODO: Move to ElectronSubshell constructor
160

161
    hid_t tgroup = open_group(rgroup, designator.c_str());
5,279✔
162

163
    // Read binding energy energy and number of electrons if atomic relaxation
164
    // data is present
165
    if (attribute_exists(tgroup, "binding_energy")) {
5,279✔
166
      has_atomic_relaxation_ = true;
5,279✔
167
      read_attribute(tgroup, "binding_energy", shell.binding_energy);
5,279✔
168
      read_attribute(tgroup, "num_electrons", shell.n_electrons);
5,279✔
169
    }
170

171
    // Read subshell cross section
172
    xt::xtensor<double, 1> xs;
5,279✔
173
    dset = open_dataset(tgroup, "xs");
5,279✔
174
    read_attribute(dset, "threshold_idx", shell.threshold);
5,279✔
175
    close_dataset(dset);
5,279✔
176
    read_dataset(tgroup, "xs", xs);
5,279✔
177

178
    auto cross_section =
179
      xt::view(cross_sections_, xt::range(shell.threshold, _), i);
5,279✔
180
    cross_section = xt::where(xs > 0, xt::log(xs), 0);
5,279✔
181

182
    if (object_exists(tgroup, "transitions")) {
5,279✔
183
      // Determine dimensions of transitions
184
      dset = open_dataset(tgroup, "transitions");
3,293✔
185
      auto dims = object_shape(dset);
3,293✔
186
      close_dataset(dset);
3,293✔
187

188
      int n_transition = dims[0];
3,293✔
189
      if (n_transition > 0) {
3,293✔
190
        xt::xtensor<double, 2> matrix;
3,293✔
191
        read_dataset(tgroup, "transitions", matrix);
3,293✔
192

193
        // Transition probability normalization
194
        double norm = xt::sum(xt::col(matrix, 3))();
3,293✔
195

196
        shell.transitions.resize(n_transition);
3,293✔
197
        for (int j = 0; j < n_transition; ++j) {
184,004✔
198
          auto& transition = shell.transitions[j];
180,711✔
199
          transition.primary_subshell = shell_map.at(matrix(j, 0));
180,711✔
200
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
180,711✔
201
          transition.energy = matrix(j, 2);
180,711✔
202
          transition.probability = matrix(j, 3) / norm;
180,711✔
203
        }
204
      }
3,293✔
205
    }
3,293✔
206
    close_group(tgroup);
5,279✔
207
  }
5,279✔
208
  close_group(rgroup);
623✔
209

210
  // Check the maximum size of the atomic relaxation stack
211
  auto max_size = this->calc_max_stack_size();
623✔
212
  if (max_size > MAX_STACK_SIZE && mpi::master) {
623✔
213
    warning(fmt::format(
×
214
      "The subshell vacancy stack in atomic relaxation can grow up to {}, but "
215
      "the stack size limit is set to {}.",
216
      max_size, MAX_STACK_SIZE));
217
  }
218

219
  // Determine number of electron shells
220
  rgroup = open_group(group, "compton_profiles");
623✔
221

222
  // Read electron shell PDF and binding energies
223
  read_dataset(rgroup, "num_electrons", electron_pdf_);
623✔
224
  electron_pdf_ /= xt::sum(electron_pdf_);
623✔
225
  read_dataset(rgroup, "binding_energy", binding_energy_);
623✔
226

227
  // Read Compton profiles
228
  read_dataset(rgroup, "J", profile_pdf_);
623✔
229

230
  // Get Compton profile momentum grid
231
  if (data::compton_profile_pz.size() == 0) {
623✔
232
    read_dataset(rgroup, "pz", data::compton_profile_pz);
254✔
233
  }
234
  close_group(rgroup);
623✔
235

236
  // Create Compton profile CDF
237
  auto n_profile = data::compton_profile_pz.size();
623✔
238
  auto n_shell_compton = profile_pdf_.shape(0);
623✔
239
  profile_cdf_ = xt::empty<double>({n_shell_compton, n_profile});
623✔
240
  for (int i = 0; i < n_shell_compton; ++i) {
5,050✔
241
    double c = 0.0;
4,427✔
242
    profile_cdf_(i, 0) = 0.0;
4,427✔
243
    for (int j = 0; j < n_profile - 1; ++j) {
137,237✔
244
      c += 0.5 *
265,620✔
245
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
132,810✔
246
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
132,810✔
247
      profile_cdf_(i, j + 1) = c;
132,810✔
248
    }
249
  }
250

251
  // Calculate total pair production
252
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
623✔
253

254
  if (settings::electron_treatment == ElectronTreatment::TTB) {
623✔
255
    // Read bremsstrahlung scaled DCS
256
    rgroup = open_group(group, "bremsstrahlung");
539✔
257
    read_dataset(rgroup, "dcs", dcs_);
539✔
258
    auto n_e = dcs_.shape()[0];
539✔
259
    auto n_k = dcs_.shape()[1];
539✔
260

261
    // Get energy grids used for bremsstrahlung DCS and for stopping powers
262
    xt::xtensor<double, 1> electron_energy;
539✔
263
    read_dataset(rgroup, "electron_energy", electron_energy);
539✔
264
    if (data::ttb_k_grid.size() == 0) {
539✔
265
      read_dataset(rgroup, "photon_energy", data::ttb_k_grid);
242✔
266
    }
267

268
    // Get data used for density effect correction
269
    read_dataset(rgroup, "num_electrons", n_electrons_);
539✔
270
    read_dataset(rgroup, "ionization_energy", ionization_energy_);
539✔
271
    read_attribute(rgroup, "I", I_);
539✔
272
    close_group(rgroup);
539✔
273

274
    // Truncate the bremsstrahlung data at the cutoff energy
275
    int photon = static_cast<int>(ParticleType::photon);
539✔
276
    const auto& E {electron_energy};
539✔
277
    double cutoff = settings::energy_cutoff[photon];
539✔
278
    if (cutoff > E(0)) {
539✔
279
      size_t i_grid = lower_bound_index(
12✔
280
        E.cbegin(), E.cend(), settings::energy_cutoff[photon]);
12✔
281

282
      // calculate interpolation factor
283
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
12✔
284
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
12✔
285

286
      // Interpolate bremsstrahlung DCS at the cutoff energy and truncate
287
      xt::xtensor<double, 2> dcs({n_e - i_grid, n_k});
12✔
288
      for (int i = 0; i < n_k; ++i) {
372✔
289
        double y = std::exp(
360✔
290
          std::log(dcs_(i_grid, i)) +
360✔
291
          f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i))));
360✔
292
        auto col_i = xt::view(dcs, xt::all(), i);
360✔
293
        col_i(0) = y;
360✔
294
        for (int j = i_grid + 1; j < n_e; ++j) {
67,320✔
295
          col_i(j - i_grid) = dcs_(j, i);
66,960✔
296
        }
297
      }
360✔
298
      dcs_ = dcs;
12✔
299

300
      xt::xtensor<double, 1> frst {cutoff};
12✔
301
      electron_energy = xt::concatenate(xt::xtuple(
24✔
302
        frst, xt::view(electron_energy, xt::range(i_grid + 1, n_e))));
36✔
303
    }
12✔
304

305
    // Set incident particle energy grid
306
    if (data::ttb_e_grid.size() == 0) {
539✔
307
      data::ttb_e_grid = electron_energy;
242✔
308
    }
309

310
    // Calculate the radiative stopping power
311
    stopping_power_radiative_ = xt::empty<double>({data::ttb_e_grid.size()});
539✔
312
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
108,183✔
313
      // Integrate over reduced photon energy
314
      double c = 0.0;
107,644✔
315
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,229,320✔
316
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
3,121,676✔
317
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
3,121,676✔
318
      }
319
      double e = data::ttb_e_grid(i);
107,644✔
320

321
      // Square of the ratio of the speed of light to the velocity of the
322
      // charged particle
323
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
107,644✔
324
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
107,644✔
325

326
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
107,644✔
327
    }
328
  }
539✔
329

330
  // Take logarithm of energies and cross sections since they are log-log
331
  // interpolated
332
  energy_ = xt::log(energy_);
623✔
333
  coherent_ = xt::where(coherent_ > 0.0, xt::log(coherent_), -900.0);
623✔
334
  incoherent_ = xt::where(incoherent_ > 0.0, xt::log(incoherent_), -900.0);
623✔
335
  photoelectric_total_ = xt::where(
1,246✔
336
    photoelectric_total_ > 0.0, xt::log(photoelectric_total_), -900.0);
1,869✔
337
  pair_production_total_ = xt::where(
1,246✔
338
    pair_production_total_ > 0.0, xt::log(pair_production_total_), -900.0);
1,869✔
339
  heating_ = xt::where(heating_ > 0.0, xt::log(heating_), -900.0);
623✔
340
}
623✔
341

342
PhotonInteraction::~PhotonInteraction()
623✔
343
{
344
  data::element_map.erase(name_);
623✔
345
}
623✔
346

347
int PhotonInteraction::calc_max_stack_size() const
623✔
348
{
349
  // Table to store solutions to sub-problems
350
  std::unordered_map<int, int> visited;
623✔
351

352
  // Find the maximum possible size of the stack used to store holes created
353
  // during atomic relaxation, checking over every subshell the initial hole
354
  // could be in
355
  int max_size = 0;
623✔
356
  for (int i_shell = 0; i_shell < shells_.size(); ++i_shell) {
5,902✔
357
    max_size = std::max(max_size, this->calc_helper(visited, i_shell));
5,279✔
358
  }
359
  return max_size;
623✔
360
}
623✔
361

362
int PhotonInteraction::calc_helper(
350,130✔
363
  std::unordered_map<int, int>& visited, int i_shell) const
364
{
365
  // No transitions for this subshell, so this is the only shell in the stack
366
  const auto& shell {shells_[i_shell]};
350,130✔
367
  if (shell.transitions.empty()) {
350,130✔
368
    return 1;
143,146✔
369
  }
370

371
  // Check the table to see if the maximum stack size has already been
372
  // calculated for this shell
373
  auto it = visited.find(i_shell);
206,984✔
374
  if (it != visited.end()) {
206,984✔
375
    return it->second;
203,691✔
376
  }
377

378
  int max_size = 0;
3,293✔
379
  for (const auto& transition : shell.transitions) {
184,004✔
380
    // If this is a non-radiative transition two vacancies are created and
381
    // the stack grows by one; if this is a radiative transition only one
382
    // vacancy is created and the stack size stays the same
383
    int size = 0;
180,711✔
384
    if (transition.secondary_subshell != -1) {
180,711✔
385
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
164,140✔
386
    }
387
    size =
180,711✔
388
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
180,711✔
389
    max_size = std::max(max_size, size);
180,711✔
390
  }
391
  visited[i_shell] = max_size;
3,293✔
392
  return max_size;
3,293✔
393
}
394

395
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
5,764,452✔
396
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
397
{
398
  double form_factor_xmax = 0.0;
5,764,452✔
399
  while (true) {
400
    // Sample Klein-Nishina distribution for trial energy and angle
401
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
5,880,324✔
402

403
    // Note that the parameter used here does not correspond exactly to the
404
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
405
    // parameter as defined by Hubbell, where the actual data comes from
406
    double x =
407
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
5,880,324✔
408

409
    // Calculate S(x, Z) and S(x_max, Z)
410
    double form_factor_x = incoherent_form_factor_(x);
5,880,324✔
411
    if (form_factor_xmax == 0.0) {
5,880,324✔
412
      form_factor_xmax =
413
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
5,764,452✔
414
    }
415

416
    // Perform rejection on form factor
417
    if (prn(seed) < form_factor_x / form_factor_xmax) {
5,880,324✔
418
      if (doppler) {
5,764,452✔
419
        double E_out;
420
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
5,764,452✔
421
        *alpha_out = E_out / MASS_ELECTRON_EV;
5,764,452✔
422

423
        // It's possible for the Compton profile data to have more shells than
424
        // there are in the ENDF data. Make sure the shell index doesn't end up
425
        // out of bounds.
426
        if (*i_shell >= shells_.size()) {
5,764,452✔
UNCOV
427
          *i_shell = -1;
×
428
        }
429
      } else {
UNCOV
430
        *i_shell = -1;
×
431
      }
432
      break;
5,764,452✔
433
    }
434
  }
115,872✔
435
}
5,764,452✔
436

437
void PhotonInteraction::compton_doppler(
5,764,452✔
438
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
439
{
440
  auto n = data::compton_profile_pz.size();
5,764,452✔
441

442
  int shell; // index for shell
443
  while (true) {
444
    // Sample electron shell
445
    double rn = prn(seed);
5,764,452✔
446
    double c = 0.0;
5,764,452✔
447
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
21,876,876✔
448
      c += electron_pdf_(shell);
21,876,876✔
449
      if (rn < c)
21,876,876✔
450
        break;
5,764,452✔
451
    }
452

453
    // Determine binding energy of shell
454
    double E_b = binding_energy_(shell);
5,764,452✔
455

456
    // Determine p_z,max
457
    double E = alpha * MASS_ELECTRON_EV;
5,764,452✔
458
    if (E < E_b) {
5,764,452✔
459
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
1,200✔
460
      break;
1,200✔
461
    }
462

463
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
5,763,252✔
464
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
5,763,252✔
465
    if (pz_max < 0.0) {
5,763,252✔
466
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
48,624✔
467
      break;
48,624✔
468
    }
469

470
    // Determine profile cdf value corresponding to p_z,max
471
    double c_max;
472
    if (pz_max > data::compton_profile_pz(n - 1)) {
5,714,628✔
473
      c_max = profile_cdf_(shell, n - 1);
946,116✔
474
    } else {
475
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
4,768,512✔
476
        data::compton_profile_pz.cend(), pz_max);
4,768,512✔
477
      double pz_l = data::compton_profile_pz(i);
4,768,512✔
478
      double pz_r = data::compton_profile_pz(i + 1);
4,768,512✔
479
      double p_l = profile_pdf_(shell, i);
4,768,512✔
480
      double p_r = profile_pdf_(shell, i + 1);
4,768,512✔
481
      double c_l = profile_cdf_(shell, i);
4,768,512✔
482
      if (pz_l == pz_r) {
4,768,512✔
UNCOV
483
        c_max = c_l;
×
484
      } else if (p_l == p_r) {
4,768,512✔
485
        c_max = c_l + (pz_max - pz_l) * p_l;
8,628✔
486
      } else {
487
        double m = (p_l - p_r) / (pz_l - pz_r);
4,759,884✔
488
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
4,759,884✔
489
                        (2.0 * m);
4,759,884✔
490
      }
491
    }
492

493
    // Sample value on bounded cdf
494
    c = prn(seed) * c_max;
5,714,628✔
495

496
    // Determine pz corresponding to sampled cdf value
497
    auto cdf_shell = xt::view(profile_cdf_, shell, xt::all());
5,714,628✔
498
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
5,714,628✔
499
    double pz_l = data::compton_profile_pz(i);
5,714,628✔
500
    double pz_r = data::compton_profile_pz(i + 1);
5,714,628✔
501
    double p_l = profile_pdf_(shell, i);
5,714,628✔
502
    double p_r = profile_pdf_(shell, i + 1);
5,714,628✔
503
    double c_l = profile_cdf_(shell, i);
5,714,628✔
504
    double pz;
505
    if (pz_l == pz_r) {
5,714,628✔
UNCOV
506
      pz = pz_l;
×
507
    } else if (p_l == p_r) {
5,714,628✔
508
      pz = pz_l + (c - c_l) / p_l;
525,444✔
509
    } else {
510
      double m = (p_l - p_r) / (pz_l - pz_r);
5,189,184✔
511
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
5,189,184✔
512
    }
513

514
    // Determine outgoing photon energy corresponding to electron momentum
515
    // (solve Eq. 39 in LA-UR-04-0487 for E')
516
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
5,714,628✔
517
    double f = 1.0 + alpha * (1.0 - mu);
5,714,628✔
518
    double a = momentum_sq - f * f;
5,714,628✔
519
    double b = 2.0 * E * (f - momentum_sq * mu);
5,714,628✔
520
    c = E * E * (momentum_sq - 1.0);
5,714,628✔
521

522
    double quad = b * b - 4.0 * a * c;
5,714,628✔
523
    if (quad < 0) {
5,714,628✔
UNCOV
524
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
×
UNCOV
525
      break;
×
526
    }
527
    quad = std::sqrt(quad);
5,714,628✔
528
    double E_out1 = -(b + quad) / (2.0 * a);
5,714,628✔
529
    double E_out2 = -(b - quad) / (2.0 * a);
5,714,628✔
530

531
    // Determine solution to quadratic equation that is positive
532
    if (E_out1 > 0.0) {
5,714,628✔
533
      if (E_out2 > 0.0) {
5,714,628✔
534
        // If both are positive, pick one at random
535
        *E_out = prn(seed) < 0.5 ? E_out1 : E_out2;
5,714,628✔
536
      } else {
UNCOV
537
        *E_out = E_out1;
×
538
      }
539
    } else {
UNCOV
540
      if (E_out2 > 0.0) {
×
541
        *E_out = E_out2;
×
542
      } else {
543
        // No positive solution -- resample
544
        continue;
×
545
      }
546
    }
547
    if (*E_out < E - E_b)
5,714,628✔
548
      break;
5,714,628✔
549
  }
5,714,628✔
550

551
  *i_shell = shell;
5,764,452✔
552
}
5,764,452✔
553

554
void PhotonInteraction::calculate_xs(Particle& p) const
31,751,192✔
555
{
556
  // Perform binary search on the element energy grid in order to determine
557
  // which points to interpolate between
558
  int n_grid = energy_.size();
31,751,192✔
559
  double log_E = std::log(p.E());
31,751,192✔
560
  int i_grid;
561
  if (log_E <= energy_[0]) {
31,751,192✔
UNCOV
562
    i_grid = 0;
×
563
  } else if (log_E > energy_(n_grid - 1)) {
31,751,192✔
UNCOV
564
    i_grid = n_grid - 2;
×
565
  } else {
566
    // We use upper_bound_index here because sometimes photons are created with
567
    // energies that exactly match a grid point
568
    i_grid = upper_bound_index(energy_.cbegin(), energy_.cend(), log_E);
31,751,192✔
569
  }
570

571
  // check for case where two energy points are the same
572
  if (energy_(i_grid) == energy_(i_grid + 1))
31,751,192✔
UNCOV
573
    ++i_grid;
×
574

575
  // calculate interpolation factor
576
  double f =
577
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
31,751,192✔
578

579
  auto& xs {p.photon_xs(index_)};
31,751,192✔
580
  xs.index_grid = i_grid;
31,751,192✔
581
  xs.interp_factor = f;
31,751,192✔
582

583
  // Calculate microscopic coherent cross section
584
  xs.coherent = std::exp(
31,751,192✔
585
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
31,751,192✔
586

587
  // Calculate microscopic incoherent cross section
588
  xs.incoherent = std::exp(
31,751,192✔
589
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
31,751,192✔
590

591
  // Calculate microscopic photoelectric cross section
592
  xs.photoelectric = 0.0;
31,751,192✔
593
  const auto& xs_lower = xt::row(cross_sections_, i_grid);
31,751,192✔
594
  const auto& xs_upper = xt::row(cross_sections_, i_grid + 1);
31,751,192✔
595

596
  for (int i = 0; i < xs_upper.size(); ++i)
307,215,468✔
597
    if (xs_lower(i) != 0)
275,464,276✔
598
      xs.photoelectric +=
264,132,748✔
599
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
264,132,748✔
600

601
  // Calculate microscopic pair production cross section
602
  xs.pair_production = std::exp(
31,751,192✔
603
    pair_production_total_(i_grid) +
31,751,192✔
604
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
31,751,192✔
605

606
  // Calculate microscopic total cross section
607
  xs.total =
31,751,192✔
608
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
31,751,192✔
609
  xs.last_E = p.E();
31,751,192✔
610
}
31,751,192✔
611

612
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
644,100✔
613
{
614
  double mu;
615
  while (true) {
616
    // Determine maximum value of x^2
617
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
644,100✔
618

619
    // Determine F(x^2_max, Z)
620
    double F_max = coherent_int_form_factor_(x2_max);
644,100✔
621

622
    // Sample cumulative distribution
623
    double F = prn(seed) * F_max;
644,100✔
624

625
    // Determine x^2 corresponding to F
626
    const auto& x {coherent_int_form_factor_.x()};
644,100✔
627
    const auto& y {coherent_int_form_factor_.y()};
644,100✔
628
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
644,100✔
629
    double r = (F - y[i]) / (y[i + 1] - y[i]);
644,100✔
630
    double x2 = x[i] + r * (x[i + 1] - x[i]);
644,100✔
631

632
    // Calculate mu
633
    mu = 1.0 - 2.0 * x2 / x2_max;
644,100✔
634

635
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
644,100✔
636
      break;
559,692✔
637
  }
84,408✔
638
  return mu;
559,692✔
639
}
640

641
void PhotonInteraction::pair_production(double alpha, double* E_electron,
92,292✔
642
  double* E_positron, double* mu_electron, double* mu_positron,
643
  uint64_t* seed) const
644
{
645
  constexpr double r[] {122.81, 73.167, 69.228, 67.301, 64.696, 61.228, 57.524,
92,292✔
646
    54.033, 50.787, 47.851, 46.373, 45.401, 44.503, 43.815, 43.074, 42.321,
647
    41.586, 40.953, 40.524, 40.256, 39.756, 39.144, 38.462, 37.778, 37.174,
648
    36.663, 35.986, 35.317, 34.688, 34.197, 33.786, 33.422, 33.068, 32.740,
649
    32.438, 32.143, 31.884, 31.622, 31.438, 31.142, 30.950, 30.758, 30.561,
650
    30.285, 30.097, 29.832, 29.581, 29.411, 29.247, 29.085, 28.930, 28.721,
651
    28.580, 28.442, 28.312, 28.139, 27.973, 27.819, 27.675, 27.496, 27.285,
652
    27.093, 26.911, 26.705, 26.516, 26.304, 26.108, 25.929, 25.730, 25.577,
653
    25.403, 25.245, 25.100, 24.941, 24.790, 24.655, 24.506, 24.391, 24.262,
654
    24.145, 24.039, 23.922, 23.813, 23.712, 23.621, 23.523, 23.430, 23.331,
655
    23.238, 23.139, 23.048, 22.967, 22.833, 22.694, 22.624, 22.545, 22.446,
656
    22.358, 22.264};
657

658
  // The reduced screening radius r is the ratio of the screening radius to
659
  // the Compton wavelength of the electron, where the screening radius is
660
  // obtained under the assumption that the Coulomb field of the nucleus is
661
  // exponentially screened by atomic electrons. This allows us to use a
662
  // simplified atomic form factor and analytical approximations of the
663
  // screening functions in the pair production DCS instead of computing the
664
  // screening functions numerically. The reduced screening radii above for
665
  // Z = 1-99 come from F. Salvat, J. M. Fernández-Varea, and J. Sempau,
666
  // "PENELOPE-2011: A Code System for Monte Carlo Simulation of Electron and
667
  // Photon Transport," OECD-NEA, Issy-les-Moulineaux, France (2011).
668

669
  // Compute the high-energy Coulomb correction
670
  double a = Z_ / FINE_STRUCTURE;
92,292✔
671
  double c =
92,292✔
672
    a * a *
92,292✔
673
    (1.0 / (1.0 + a * a) + 0.202059 +
92,292✔
674
      a * a *
92,292✔
675
        (-0.03693 +
92,292✔
676
          a * a *
92,292✔
677
            (0.00835 +
92,292✔
678
              a * a *
92,292✔
679
                (-0.00201 +
92,292✔
680
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
92,292✔
681

682
  // The analytical approximation of the DCS underestimates the cross section
683
  // at low energies. The correction factor f compensates for this.
684
  double q = std::sqrt(2.0 / alpha);
92,292✔
685
  double f = q * (-0.1774 - 12.10 * a + 11.18 * a * a) +
92,292✔
686
             q * q * (8.523 + 73.26 * a - 44.41 * a * a) +
92,292✔
687
             q * q * q * (-13.52 - 121.1 * a + 96.41 * a * a) +
92,292✔
688
             q * q * q * q * (8.946 + 62.05 * a - 63.41 * a * a);
92,292✔
689

690
  // Calculate phi_1(1/2) and phi_2(1/2). The unnormalized PDF for the reduced
691
  // energy is given by p = 2*(1/2 - e)^2*phi_1(e) + phi_2(e), where phi_1 and
692
  // phi_2 are non-negative and maximum at e = 1/2.
693
  double b = 2.0 * r[Z_] / alpha;
92,292✔
694
  double t1 = 2.0 * std::log(1.0 + b * b);
92,292✔
695
  double t2 = b * std::atan(1.0 / b);
92,292✔
696
  double t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
92,292✔
697
  double t4 = 4.0 * std::log(r[Z_]) - 4.0 * c + f;
92,292✔
698
  double phi1_max = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
92,292✔
699
  double phi2_max = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
92,292✔
700

701
  // To aid sampling, the unnormalized PDF can be expressed as
702
  // p = u_1*U_1(e)*pi_1(e) + u_2*U_2(e)*pi_2(e), where pi_1 and pi_2 are
703
  // normalized PDFs on the interval (e_min, e_max) from which values of e can
704
  // be sampled using the inverse transform method, and
705
  // U_1 = phi_1(e)/phi_1(1/2) and U_2 = phi_2(e)/phi_2(1/2) are valid
706
  // rejection functions. The reduced energy can now be sampled using a
707
  // combination of the composition and rejection methods.
708
  double u1 = 2.0 / 3.0 * std::pow(0.5 - 1.0 / alpha, 2) * phi1_max;
92,292✔
709
  double u2 = phi2_max;
92,292✔
710
  double e;
711
  while (true) {
712
    double rn = prn(seed);
115,620✔
713

714
    // Sample the index i in (1, 2) using the point probabilities
715
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
716
    int i;
717
    if (prn(seed) < u1 / (u1 + u2)) {
115,620✔
718
      i = 1;
10,560✔
719

720
      // Sample e from pi_1 using the inverse transform method
721
      e = rn >= 0.5
10,560✔
722
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
10,560✔
723
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
5,268✔
724
    } else {
725
      i = 2;
105,060✔
726

727
      // Sample e from pi_2 using the inverse transform method
728
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
105,060✔
729
    }
730

731
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
732
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
115,620✔
733
    t1 = 2.0 * std::log(1.0 + b * b);
115,620✔
734
    t2 = b * std::atan(1.0 / b);
115,620✔
735
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
115,620✔
736
    if (i == 1) {
115,620✔
737
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
10,560✔
738
      if (prn(seed) <= phi1 / phi1_max)
10,560✔
739
        break;
6,780✔
740
    } else {
741
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
105,060✔
742
      if (prn(seed) <= phi2 / phi2_max)
105,060✔
743
        break;
85,512✔
744
    }
745
  }
23,328✔
746

747
  // Compute the kinetic energy of the electron and the positron
748
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
92,292✔
749
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
92,292✔
750

751
  // Sample the scattering angle of the electron. The cosine of the polar
752
  // angle of the direction relative to the incident photon is sampled from
753
  // p(mu) = C/(1 - beta*mu)^2 using the inverse transform method.
754
  double beta =
755
    std::sqrt(*E_electron * (*E_electron + 2.0 * MASS_ELECTRON_EV)) /
92,292✔
756
    (*E_electron + MASS_ELECTRON_EV);
92,292✔
757
  double rn = uniform_distribution(-1., 1., seed);
92,292✔
758
  *mu_electron = (rn + beta) / (rn * beta + 1.0);
92,292✔
759

760
  // Sample the scattering angle of the positron
761
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
92,292✔
762
         (*E_positron + MASS_ELECTRON_EV);
92,292✔
763
  rn = uniform_distribution(-1., 1., seed);
92,292✔
764
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
92,292✔
765
}
92,292✔
766

767
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
10,469,472✔
768
{
769
  // Return if no atomic relaxation data is present
770
  if (!has_atomic_relaxation_)
10,469,472✔
UNCOV
771
    return;
×
772

773
  // Stack for unprocessed holes left by transitioning electrons
774
  int n_holes = 0;
10,469,472✔
775
  array<int, MAX_STACK_SIZE> holes;
776

777
  // Push the initial hole onto the stack
778
  holes[n_holes++] = i_shell;
10,469,472✔
779

780
  while (n_holes > 0) {
99,318,804✔
781
    // Pop the next hole off the stack
782
    int i_hole = holes[--n_holes];
88,849,332✔
783
    const auto& shell {shells_[i_hole]};
88,849,332✔
784

785
    // If no transitions, assume fluorescent photon from captured free electron
786
    if (shell.transitions.empty()) {
88,849,332✔
787
      Direction u = isotropic_direction(p.current_seed());
48,678,084✔
788
      double E = shell.binding_energy;
48,678,084✔
789
      p.create_secondary(p.wgt(), u, E, ParticleType::photon);
48,678,084✔
790
      continue;
48,678,084✔
791
    }
48,678,084✔
792

793
    // Sample transition
794
    double c = -prn(p.current_seed());
40,171,248✔
795
    int i_trans;
796
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,061,793,564✔
797
      c += shell.transitions[i_trans].probability;
1,061,793,564✔
798
      if (c > 0)
1,061,793,564✔
799
        break;
40,171,248✔
800
    }
801
    const auto& transition = shell.transitions[i_trans];
40,171,248✔
802

803
    // Sample angle isotropically
804
    Direction u = isotropic_direction(p.current_seed());
40,171,248✔
805

806
    // Push the hole created by the electron transitioning to the photoelectron
807
    // hole onto the stack
808
    holes[n_holes++] = transition.primary_subshell;
40,171,248✔
809

810
    if (transition.secondary_subshell != -1) {
40,171,248✔
811
      // Non-radiative transition -- Auger/Coster-Kronig effect
812

813
      // Push the hole left by emitted auger electron onto the stack
814
      holes[n_holes++] = transition.secondary_subshell;
38,208,612✔
815

816
      // Create auger electron
817
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron);
38,208,612✔
818
    } else {
819
      // Radiative transition -- get X-ray energy
820

821
      // Create fluorescent photon
822
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon);
1,962,636✔
823
    }
824
  }
825
}
826

827
//==============================================================================
828
// Non-member functions
829
//==============================================================================
830

831
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
5,880,324✔
832
{
833
  double alpha_out, mu;
834
  double beta = 1.0 + 2.0 * alpha;
5,880,324✔
835
  if (alpha < 3.0) {
5,880,324✔
836
    // Kahn's rejection method
837
    double t = beta / (beta + 8.0);
5,325,252✔
838
    double x;
839
    while (true) {
840
      if (prn(seed) < t) {
8,909,856✔
841
        // Left branch of flow chart
842
        double r = uniform_distribution(0.0, 2.0, seed);
1,682,280✔
843
        x = 1.0 + alpha * r;
1,682,280✔
844
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
1,682,280✔
845
          mu = 1 - r;
1,081,896✔
846
          break;
1,081,896✔
847
        }
848
      } else {
849
        // Right branch of flow chart
850
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
7,227,576✔
851
        mu = 1.0 + (1.0 - x) / alpha;
7,227,576✔
852
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
7,227,576✔
853
          break;
4,243,356✔
854
      }
855
    }
3,584,604✔
856
    alpha_out = alpha / x;
5,325,252✔
857

858
  } else {
859
    // Koblinger's direct method
860
    double gamma = 1.0 - std::pow(beta, -2);
555,072✔
861
    double s =
862
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
555,072✔
863
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
555,072✔
864
    if (s <= 2.0 / alpha) {
555,072✔
865
      // For first term, x = 1 + 2ar
866
      // Therefore, a' = a/(1 + 2ar)
867
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
79,128✔
868
    } else if (s <= 4.0 / alpha) {
475,944✔
869
      // For third term, x = beta/(1 + 2ar)
870
      // Therefore, a' = a(1 + 2ar)/beta
871
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
80,256✔
872
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
395,688✔
873
      // For fourth term, x = 1/sqrt(1 - gamma*r)
874
      // Therefore, a' = a*sqrt(1 - gamma*r)
875
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
100,992✔
876
    } else {
877
      // For third term, x = beta^r
878
      // Therefore, a' = a/beta^r
879
      alpha_out = alpha / std::pow(beta, prn(seed));
294,696✔
880
    }
881

882
    // Calculate cosine of scattering angle based on basic relation
883
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
555,072✔
884
  }
885
  return {alpha_out, mu};
5,880,324✔
886
}
887

888
void free_memory_photon()
6,869✔
889
{
890
  data::elements.clear();
6,869✔
891
  data::compton_profile_pz.resize({0});
6,869✔
892
  data::ttb_e_grid.resize({0});
6,869✔
893
  data::ttb_k_grid.resize({0});
6,869✔
894
}
6,869✔
895

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