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

openmc-dev / openmc / 22231051182

20 Feb 2026 03:57PM UTC coverage: 81.849% (-0.2%) from 82.058%
22231051182

Pull #2693

github

web-flow
Merge c2c0a6ecd into 53ce1910f
Pull Request #2693: Add reactivity control to coupled transport-depletion analyses

17270 of 24310 branches covered (71.04%)

Branch coverage included in aggregate %.

77 of 82 new or added lines in 4 files covered. (93.9%)

3461 existing lines in 80 files now uncovered.

57624 of 67193 relevant lines covered (85.76%)

49135727.7 hits per line

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

89.84
/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 "openmc/tensor.h"
17

18
#include <cmath>
19
#include <fmt/core.h>
20
#include <tuple> // for tie
21

22
namespace openmc {
23

24
constexpr int PhotonInteraction::MAX_STACK_SIZE;
25

26
//==============================================================================
27
// Global variables
28
//==============================================================================
29

30
namespace data {
31

32
tensor::Tensor<double> compton_profile_pz;
33

34
std::unordered_map<std::string, int> element_map;
35
vector<unique_ptr<PhotonInteraction>> elements;
36

37
} // namespace data
38

39
//==============================================================================
40
// PhotonInteraction implementation
41
//==============================================================================
42

43
PhotonInteraction::PhotonInteraction(hid_t group)
624✔
44
{
45
  // Set index of element in global vector
46
  index_ = data::elements.size();
624✔
47

48
  // Get name of nuclide from group, removing leading '/'
49
  name_ = object_name(group).substr(1);
624✔
50
  data::element_map[name_] = index_;
624✔
51

52
  // Get atomic number
53
  read_attribute(group, "Z", Z_);
624✔
54

55
  // Determine number of energies and read energy grid
56
  read_dataset(group, "energy", energy_);
624✔
57

58
  // Read coherent scattering
59
  hid_t rgroup = open_group(group, "coherent");
624✔
60
  read_dataset(rgroup, "xs", coherent_);
624✔
61

62
  hid_t dset = open_dataset(rgroup, "integrated_scattering_factor");
624✔
63
  coherent_int_form_factor_ = Tabulated1D {dset};
624✔
64
  close_dataset(dset);
624✔
65

66
  if (object_exists(group, "anomalous_real")) {
624!
UNCOV
67
    dset = open_dataset(rgroup, "anomalous_real");
×
UNCOV
68
    coherent_anomalous_real_ = Tabulated1D {dset};
×
UNCOV
69
    close_dataset(dset);
×
70
  }
71

72
  if (object_exists(group, "anomalous_imag")) {
624!
73
    dset = open_dataset(rgroup, "anomalous_imag");
×
74
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
75
    close_dataset(dset);
×
76
  }
77
  close_group(rgroup);
624✔
78

79
  // Read incoherent scattering
80
  rgroup = open_group(group, "incoherent");
624✔
81
  read_dataset(rgroup, "xs", incoherent_);
624✔
82
  dset = open_dataset(rgroup, "scattering_factor");
624✔
83
  incoherent_form_factor_ = Tabulated1D {dset};
624✔
84
  close_dataset(dset);
624✔
85
  close_group(rgroup);
624✔
86

87
  // Read pair production
88
  if (object_exists(group, "pair_production_electron")) {
624!
89
    rgroup = open_group(group, "pair_production_electron");
624✔
90
    read_dataset(rgroup, "xs", pair_production_electron_);
624✔
91
    close_group(rgroup);
624✔
92
  } else {
UNCOV
93
    pair_production_electron_ = tensor::zeros_like(energy_);
×
94
  }
95

96
  // Read pair production
97
  if (object_exists(group, "pair_production_nuclear")) {
624!
98
    rgroup = open_group(group, "pair_production_nuclear");
624✔
99
    read_dataset(rgroup, "xs", pair_production_nuclear_);
624✔
100
    close_group(rgroup);
624✔
101
  } else {
UNCOV
102
    pair_production_nuclear_ = tensor::zeros_like(energy_);
×
103
  }
104

105
  // Read photoelectric
106
  rgroup = open_group(group, "photoelectric");
624✔
107
  read_dataset(rgroup, "xs", photoelectric_total_);
624✔
108
  close_group(rgroup);
624✔
109

110
  // Read heating
111
  if (object_exists(group, "heating")) {
624!
112
    rgroup = open_group(group, "heating");
624✔
113
    read_dataset(rgroup, "xs", heating_);
624✔
114
    close_group(rgroup);
624✔
115
  } else {
UNCOV
116
    heating_ = tensor::zeros_like(energy_);
×
117
  }
118

119
  // Read subshell photoionization cross section and atomic relaxation data
120
  rgroup = open_group(group, "subshells");
624✔
121
  vector<std::string> designators;
624✔
122
  read_attribute(rgroup, "designators", designators);
624✔
123
  auto n_shell = designators.size();
624✔
124
  if (n_shell == 0) {
624!
UNCOV
125
    throw std::runtime_error {
×
UNCOV
126
      "Photoatomic data for " + name_ + " does not have subshell data."};
×
127
  }
128

129
  shells_.resize(n_shell);
624✔
130
  cross_sections_ = tensor::zeros<double>({energy_.size(), n_shell});
624✔
131

132
  // Create mapping from designator to index
133
  std::unordered_map<int, int> shell_map;
624✔
134
  for (int i = 0; i < n_shell; ++i) {
5,402✔
135
    const auto& designator {designators[i]};
4,778✔
136

137
    int j = 1;
4,778✔
138
    for (const auto& subshell : SUBSHELLS) {
34,190!
139
      if (designator == subshell) {
34,190✔
140
        shell_map[j] = i;
4,778✔
141
        shells_[i].index_subshell = j;
4,778✔
142
        break;
4,778✔
143
      }
144
      ++j;
29,412✔
145
    }
146
  }
147
  shell_map[0] = -1;
624✔
148

149
  for (int i = 0; i < n_shell; ++i) {
5,402✔
150
    const auto& designator {designators[i]};
4,778✔
151
    auto& shell {shells_[i]};
4,778✔
152

153
    // TODO: Move to ElectronSubshell constructor
154

155
    hid_t tgroup = open_group(rgroup, designator.c_str());
4,778✔
156

157
    // Read binding energy energy and number of electrons if atomic relaxation
158
    // data is present
159
    if (attribute_exists(tgroup, "binding_energy")) {
4,778!
160
      has_atomic_relaxation_ = true;
4,778✔
161
      read_attribute(tgroup, "binding_energy", shell.binding_energy);
4,778✔
162
    }
163

164
    // Read subshell cross section
165
    tensor::Tensor<double> xs;
4,778✔
166
    dset = open_dataset(tgroup, "xs");
4,778✔
167
    read_attribute(dset, "threshold_idx", shell.threshold);
4,778✔
168
    close_dataset(dset);
4,778✔
169
    read_dataset(tgroup, "xs", xs);
4,778✔
170

171
    auto cross_section =
172
      cross_sections_.slice(tensor::range(static_cast<size_t>(shell.threshold),
4,778✔
173
                              cross_sections_.shape(0)),
174
        i);
4,778✔
175
    cross_section = tensor::where(xs > 0, tensor::log(xs), 0);
4,778✔
176

177
    if (object_exists(tgroup, "transitions")) {
4,778✔
178
      // Determine dimensions of transitions
179
      dset = open_dataset(tgroup, "transitions");
2,898✔
180
      auto dims = object_shape(dset);
2,898✔
181
      close_dataset(dset);
2,898✔
182

183
      int n_transition = dims[0];
2,898✔
184
      if (n_transition > 0) {
2,898!
185
        tensor::Tensor<double> matrix;
2,898✔
186
        read_dataset(tgroup, "transitions", matrix);
2,898✔
187

188
        // Transition probability normalization
189
        double norm =
190
          tensor::Tensor<double>(matrix.slice(tensor::all, 3)).sum();
2,898✔
191

192
        shell.transitions.resize(n_transition);
2,898✔
193
        for (int j = 0; j < n_transition; ++j) {
155,628✔
194
          auto& transition = shell.transitions[j];
152,730✔
195
          transition.primary_subshell = shell_map.at(matrix(j, 0));
152,730✔
196
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
152,730✔
197
          transition.energy = matrix(j, 2);
152,730✔
198
          transition.probability = matrix(j, 3) / norm;
152,730✔
199
        }
200
      }
2,898✔
201
    }
2,898✔
202
    close_group(tgroup);
4,778✔
203
  }
4,778✔
204
  close_group(rgroup);
624✔
205

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

215
  // Determine number of electron shells
216
  rgroup = open_group(group, "compton_profiles");
624✔
217

218
  // Read electron shell PDF and binding energies
219
  read_dataset(rgroup, "num_electrons", electron_pdf_);
624✔
220
  electron_pdf_ /= electron_pdf_.sum();
624✔
221
  read_dataset(rgroup, "binding_energy", binding_energy_);
624✔
222

223
  // Read Compton profiles
224
  read_dataset(rgroup, "J", profile_pdf_);
624✔
225

226
  // Get Compton profile momentum grid
227
  if (data::compton_profile_pz.size() == 0) {
624✔
228
    read_dataset(rgroup, "pz", data::compton_profile_pz);
274✔
229
  }
230
  close_group(rgroup);
624✔
231

232
  // Map Compton subshell data to atomic relaxation data by finding the
233
  // subshell with the equivalent binding energy
234
  if (has_atomic_relaxation_) {
624!
235
    auto is_close = [](double a, double b) {
8,256✔
236
      return std::abs(a - b) / a < FP_REL_PRECISION;
8,256✔
237
    };
238
    subshell_map_ = tensor::Tensor<int>(binding_energy_.shape(), -1);
624✔
239
    for (int i = 0; i < binding_energy_.size(); ++i) {
4,602✔
240
      double E_b = binding_energy_[i];
3,978✔
241
      if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) {
3,978!
242
        subshell_map_[i] = i;
3,300✔
243
      } else {
244
        for (int j = 0; j < n_shell; ++j) {
4,278!
245
          if (is_close(E_b, shells_[j].binding_energy)) {
4,278✔
246
            subshell_map_[i] = j;
678✔
247
            break;
678✔
248
          }
249
        }
250
      }
251
    }
252
  }
253

254
  // Create Compton profile CDF
255
  auto n_profile = data::compton_profile_pz.size();
624✔
256
  auto n_shell_compton = profile_pdf_.shape(0);
624✔
257
  profile_cdf_ = tensor::Tensor<double>({n_shell_compton, n_profile});
624✔
258
  for (int i = 0; i < n_shell_compton; ++i) {
4,602✔
259
    double c = 0.0;
3,978✔
260
    profile_cdf_(i, 0) = 0.0;
3,978✔
261
    for (int j = 0; j < n_profile - 1; ++j) {
123,318✔
262
      c += 0.5 *
238,680✔
263
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
119,340✔
264
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
119,340✔
265
      profile_cdf_(i, j + 1) = c;
119,340✔
266
    }
267
  }
268

269
  // Calculate total pair production
270
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
624✔
271

272
  if (settings::electron_treatment == ElectronTreatment::TTB) {
624✔
273
    // Read bremsstrahlung scaled DCS
274
    rgroup = open_group(group, "bremsstrahlung");
514✔
275
    read_dataset(rgroup, "dcs", dcs_);
514✔
276
    auto n_e = dcs_.shape(0);
514✔
277
    auto n_k = dcs_.shape(1);
514✔
278

279
    // Get energy grids used for bremsstrahlung DCS and for stopping powers
280
    tensor::Tensor<double> electron_energy;
514✔
281
    read_dataset(rgroup, "electron_energy", electron_energy);
514✔
282
    if (data::ttb_k_grid.size() == 0) {
514✔
283
      read_dataset(rgroup, "photon_energy", data::ttb_k_grid);
244✔
284
    }
285

286
    // Get data used for density effect correction
287
    read_dataset(rgroup, "num_electrons", n_electrons_);
514✔
288
    read_dataset(rgroup, "ionization_energy", ionization_energy_);
514✔
289
    read_attribute(rgroup, "I", I_);
514✔
290
    close_group(rgroup);
514✔
291

292
    // Truncate the bremsstrahlung data at the cutoff energy
293
    int photon = ParticleType::photon().transport_index();
514✔
294
    const auto& E {electron_energy};
514✔
295
    double cutoff = settings::energy_cutoff[photon];
514✔
296
    if (cutoff > E(0)) {
514✔
297
      size_t i_grid = lower_bound_index(
10✔
298
        E.cbegin(), E.cend(), settings::energy_cutoff[photon]);
10✔
299

300
      // calculate interpolation factor
301
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
10✔
302
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
10✔
303

304
      // Interpolate bremsstrahlung DCS at the cutoff energy and truncate
305
      tensor::Tensor<double> dcs({n_e - i_grid, n_k});
10✔
306
      for (int i = 0; i < n_k; ++i) {
310✔
307
        double y = std::exp(
300✔
308
          std::log(dcs_(i_grid, i)) +
300✔
309
          f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i))));
300✔
310
        tensor::View<double> col_i = dcs.slice(tensor::all, i);
300✔
311
        col_i(0) = y;
300✔
312
        for (int j = i_grid + 1; j < n_e; ++j) {
55,320✔
313
          col_i(j - i_grid) = dcs_(j, i);
55,020✔
314
        }
315
      }
300✔
316
      dcs_ = dcs;
10✔
317

318
      tensor::Tensor<double> frst({static_cast<size_t>(1)});
10✔
319
      frst(0) = cutoff;
10✔
320
      tensor::Tensor<double> rest(electron_energy.slice(
10✔
321
        tensor::range(i_grid + 1, electron_energy.size())));
10✔
322
      electron_energy = tensor::concatenate(frst, rest);
10✔
323
    }
10✔
324

325
    // Set incident particle energy grid
326
    if (data::ttb_e_grid.size() == 0) {
514✔
327
      data::ttb_e_grid = electron_energy;
244✔
328
    }
329

330
    // Calculate the radiative stopping power
331
    stopping_power_radiative_ =
332
      tensor::Tensor<double>({data::ttb_e_grid.size()});
514✔
333
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
103,158✔
334
      // Integrate over reduced photon energy
335
      double c = 0.0;
102,644✔
336
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,079,320✔
337
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
2,976,676✔
338
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
2,976,676✔
339
      }
340
      double e = data::ttb_e_grid(i);
102,644✔
341

342
      // Square of the ratio of the speed of light to the velocity of the
343
      // charged particle
344
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
102,644✔
345
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
102,644✔
346

347
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
102,644✔
348
    }
349
  }
514✔
350

351
  // Take logarithm of energies and cross sections since they are log-log
352
  // interpolated. Note that cross section libraries converted from ACE files
353
  // represent zero as exp(-500) to avoid log-log interpolation errors. For
354
  // values below exp(-499) we store the log as -900, for which exp(-900)
355
  // evaluates to zero.
356
  double limit = std::exp(-499.0);
624✔
357
  energy_ = tensor::log(energy_);
624✔
358
  coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0);
624✔
359
  incoherent_ =
360
    tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0);
624✔
361
  photoelectric_total_ = tensor::where(
1,248✔
362
    photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0);
1,872✔
363
  pair_production_total_ = tensor::where(pair_production_total_ > limit,
1,248✔
364
    tensor::log(pair_production_total_), -900.0);
1,872✔
365
  heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0);
624✔
366
}
624✔
367

368
PhotonInteraction::~PhotonInteraction()
624✔
369
{
370
  data::element_map.erase(name_);
624✔
371
}
624✔
372

373
int PhotonInteraction::calc_max_stack_size() const
624✔
374
{
375
  // Table to store solutions to sub-problems
376
  std::unordered_map<int, int> visited;
624✔
377

378
  // Find the maximum possible size of the stack used to store holes created
379
  // during atomic relaxation, checking over every subshell the initial hole
380
  // could be in
381
  int max_size = 0;
624✔
382
  for (int i_shell = 0; i_shell < shells_.size(); ++i_shell) {
5,402✔
383
    max_size = std::max(max_size, this->calc_helper(visited, i_shell));
4,778✔
384
  }
385
  return max_size;
624✔
386
}
624✔
387

388
int PhotonInteraction::calc_helper(
295,874✔
389
  std::unordered_map<int, int>& visited, int i_shell) const
390
{
391
  // No transitions for this subshell, so this is the only shell in the stack
392
  const auto& shell {shells_[i_shell]};
295,874✔
393
  if (shell.transitions.empty()) {
295,874✔
394
    return 1;
121,646✔
395
  }
396

397
  // Check the table to see if the maximum stack size has already been
398
  // calculated for this shell
399
  auto it = visited.find(i_shell);
174,228✔
400
  if (it != visited.end()) {
174,228✔
401
    return it->second;
171,330✔
402
  }
403

404
  int max_size = 0;
2,898✔
405
  for (const auto& transition : shell.transitions) {
155,628✔
406
    // If this is a non-radiative transition two vacancies are created and
407
    // the stack grows by one; if this is a radiative transition only one
408
    // vacancy is created and the stack size stays the same
409
    int size = 0;
152,730✔
410
    if (transition.secondary_subshell != -1) {
152,730✔
411
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
138,366✔
412
    }
413
    size =
152,730✔
414
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
152,730✔
415
    max_size = std::max(max_size, size);
152,730✔
416
  }
417
  visited[i_shell] = max_size;
2,898✔
418
  return max_size;
2,898✔
419
}
420

421
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
10,287,240✔
422
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
423
{
424
  double form_factor_xmax = 0.0;
10,287,240✔
425
  while (true) {
426
    // Sample Klein-Nishina distribution for trial energy and angle
427
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
10,535,046✔
428

429
    // Note that the parameter used here does not correspond exactly to the
430
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
431
    // parameter as defined by Hubbell, where the actual data comes from
432
    double x =
433
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
10,535,046✔
434

435
    // Calculate S(x, Z) and S(x_max, Z)
436
    double form_factor_x = incoherent_form_factor_(x);
10,535,046✔
437
    if (form_factor_xmax == 0.0) {
10,535,046✔
438
      form_factor_xmax =
439
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
10,287,240✔
440
    }
441

442
    // Perform rejection on form factor
443
    if (prn(seed) < form_factor_x / form_factor_xmax) {
10,535,046✔
444
      if (doppler) {
10,287,240!
445
        double E_out;
446
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
10,287,240✔
447
        *alpha_out = E_out / MASS_ELECTRON_EV;
10,287,240✔
448
      } else {
UNCOV
449
        *i_shell = -1;
×
450
      }
451
      break;
10,287,240✔
452
    }
453
  }
247,806✔
454
}
10,287,240✔
455

456
void PhotonInteraction::compton_doppler(
10,287,240✔
457
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
458
{
459
  auto n = data::compton_profile_pz.size();
10,287,240✔
460

461
  int shell; // index for shell
462
  while (true) {
463
    // Sample electron shell
464
    double rn = prn(seed);
10,287,240✔
465
    double c = 0.0;
10,287,240✔
466
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
29,770,624!
467
      c += electron_pdf_(shell);
29,770,624✔
468
      if (rn < c)
29,770,624✔
469
        break;
10,287,240✔
470
    }
471

472
    // Determine binding energy of shell
473
    double E_b = binding_energy_(shell);
10,287,240✔
474

475
    // Determine p_z,max
476
    double E = alpha * MASS_ELECTRON_EV;
10,287,240✔
477
    if (E < E_b) {
10,287,240✔
478
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
1,060✔
479
      break;
1,060✔
480
    }
481

482
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
10,286,180✔
483
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
10,286,180✔
484
    if (pz_max < 0.0) {
10,286,180✔
485
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
95,571✔
486
      break;
95,571✔
487
    }
488

489
    // Determine profile cdf value corresponding to p_z,max
490
    double c_max;
491
    if (pz_max > data::compton_profile_pz(n - 1)) {
10,190,609✔
492
      c_max = profile_cdf_(shell, n - 1);
910,690✔
493
    } else {
494
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
9,279,919✔
495
        data::compton_profile_pz.cend(), pz_max);
9,279,919✔
496
      double pz_l = data::compton_profile_pz(i);
9,279,919✔
497
      double pz_r = data::compton_profile_pz(i + 1);
9,279,919✔
498
      double p_l = profile_pdf_(shell, i);
9,279,919✔
499
      double p_r = profile_pdf_(shell, i + 1);
9,279,919✔
500
      double c_l = profile_cdf_(shell, i);
9,279,919✔
501
      if (pz_l == pz_r) {
9,279,919!
UNCOV
502
        c_max = c_l;
×
503
      } else if (p_l == p_r) {
9,279,919✔
504
        c_max = c_l + (pz_max - pz_l) * p_l;
14,957✔
505
      } else {
506
        double m = (p_l - p_r) / (pz_l - pz_r);
9,264,962✔
507
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
9,264,962✔
508
                        (2.0 * m);
9,264,962✔
509
      }
510
    }
511

512
    // Sample value on bounded cdf
513
    c = prn(seed) * c_max;
10,190,609✔
514

515
    // Determine pz corresponding to sampled cdf value
516
    tensor::View<const double> cdf_shell = profile_cdf_.slice(shell);
10,190,609✔
517
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
10,190,609✔
518
    double pz_l = data::compton_profile_pz(i);
10,190,609✔
519
    double pz_r = data::compton_profile_pz(i + 1);
10,190,609✔
520
    double p_l = profile_pdf_(shell, i);
10,190,609✔
521
    double p_r = profile_pdf_(shell, i + 1);
10,190,609✔
522
    double c_l = profile_cdf_(shell, i);
10,190,609✔
523
    double pz;
524
    if (pz_l == pz_r) {
10,190,609!
UNCOV
525
      pz = pz_l;
×
526
    } else if (p_l == p_r) {
10,190,609✔
527
      pz = pz_l + (c - c_l) / p_l;
815,097✔
528
    } else {
529
      double m = (p_l - p_r) / (pz_l - pz_r);
9,375,512✔
530
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
9,375,512✔
531
    }
532

533
    // Determine outgoing photon energy corresponding to electron momentum
534
    // (solve Eq. 39 in LA-UR-04-0487 for E')
535
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
10,190,609✔
536
    double f = 1.0 + alpha * (1.0 - mu);
10,190,609✔
537
    double a = momentum_sq - f * f;
10,190,609✔
538
    double b = 2.0 * E * (f - momentum_sq * mu);
10,190,609✔
539
    c = E * E * (momentum_sq - 1.0);
10,190,609✔
540

541
    double quad = b * b - 4.0 * a * c;
10,190,609✔
542
    if (quad < 0) {
10,190,609!
543
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
×
UNCOV
544
      break;
×
545
    }
546
    quad = std::sqrt(quad);
10,190,609✔
547
    double E_out1 = -(b + quad) / (2.0 * a);
10,190,609✔
548
    double E_out2 = -(b - quad) / (2.0 * a);
10,190,609✔
549

550
    // Determine solution to quadratic equation that is positive
551
    if (E_out1 > 0.0) {
10,190,609!
552
      if (E_out2 > 0.0) {
10,190,609!
553
        // If both are positive, pick one at random
554
        *E_out = prn(seed) < 0.5 ? E_out1 : E_out2;
10,190,609✔
555
      } else {
UNCOV
556
        *E_out = E_out1;
×
557
      }
558
    } else {
559
      if (E_out2 > 0.0) {
×
UNCOV
560
        *E_out = E_out2;
×
561
      } else {
562
        // No positive solution -- resample
UNCOV
563
        continue;
×
564
      }
565
    }
566
    if (*E_out < E - E_b)
10,190,609!
567
      break;
10,190,609✔
568
  }
10,190,609!
569

570
  *i_shell = shell;
10,287,240✔
571
}
10,287,240✔
572

573
void PhotonInteraction::calculate_xs(Particle& p) const
39,038,321✔
574
{
575
  // Perform binary search on the element energy grid in order to determine
576
  // which points to interpolate between
577
  int n_grid = energy_.size();
39,038,321✔
578
  double log_E = std::log(p.E());
39,038,321✔
579
  int i_grid;
580
  if (log_E <= energy_[0]) {
39,038,321!
UNCOV
581
    i_grid = 0;
×
582
  } else if (log_E > energy_(n_grid - 1)) {
39,038,321!
UNCOV
583
    i_grid = n_grid - 2;
×
584
  } else {
585
    // We use upper_bound_index here because sometimes photons are created with
586
    // energies that exactly match a grid point
587
    i_grid = upper_bound_index(energy_.cbegin(), energy_.cend(), log_E);
39,038,321✔
588
  }
589

590
  // check for case where two energy points are the same
591
  if (energy_(i_grid) == energy_(i_grid + 1))
39,038,321!
UNCOV
592
    ++i_grid;
×
593

594
  // calculate interpolation factor
595
  double f =
596
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
39,038,321✔
597

598
  auto& xs {p.photon_xs(index_)};
39,038,321✔
599
  xs.index_grid = i_grid;
39,038,321✔
600
  xs.interp_factor = f;
39,038,321✔
601

602
  // Calculate microscopic coherent cross section
603
  xs.coherent = std::exp(
39,038,321✔
604
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
39,038,321✔
605

606
  // Calculate microscopic incoherent cross section
607
  xs.incoherent = std::exp(
39,038,321✔
608
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
39,038,321✔
609

610
  // Calculate microscopic photoelectric cross section
611
  xs.photoelectric = 0.0;
39,038,321✔
612
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
39,038,321✔
613
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
39,038,321✔
614

615
  for (int i = 0; i < xs_upper.size(); ++i)
304,068,715✔
616
    if (xs_lower(i) != 0)
265,030,394✔
617
      xs.photoelectric +=
255,529,583✔
618
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
255,529,583✔
619

620
  // Calculate microscopic pair production cross section
621
  xs.pair_production = std::exp(
39,038,321✔
622
    pair_production_total_(i_grid) +
39,038,321✔
623
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
39,038,321✔
624

625
  // Calculate microscopic total cross section
626
  xs.total =
39,038,321✔
627
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
39,038,321✔
628
  xs.last_E = p.E();
39,038,321✔
629
}
39,038,321✔
630

631
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
877,622✔
632
{
633
  double mu;
634
  while (true) {
635
    // Determine maximum value of x^2
636
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
1,007,357✔
637

638
    // Determine F(x^2_max, Z)
639
    double F_max = coherent_int_form_factor_(x2_max);
1,007,357✔
640

641
    // Sample cumulative distribution
642
    double F = prn(seed) * F_max;
1,007,357✔
643

644
    // Determine x^2 corresponding to F
645
    const auto& x {coherent_int_form_factor_.x()};
1,007,357✔
646
    const auto& y {coherent_int_form_factor_.y()};
1,007,357✔
647
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
1,007,357✔
648
    double r = (F - y[i]) / (y[i + 1] - y[i]);
1,007,357✔
649
    double x2 = x[i] + r * (x[i + 1] - x[i]);
1,007,357✔
650

651
    // Calculate mu
652
    mu = 1.0 - 2.0 * x2 / x2_max;
1,007,357✔
653

654
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
1,007,357✔
655
      break;
877,622✔
656
  }
129,735✔
657
  return mu;
877,622✔
658
}
659

660
void PhotonInteraction::pair_production(double alpha, double* E_electron,
79,680✔
661
  double* E_positron, double* mu_electron, double* mu_positron,
662
  uint64_t* seed) const
663
{
664
  constexpr double r[] {122.81, 73.167, 69.228, 67.301, 64.696, 61.228, 57.524,
79,680✔
665
    54.033, 50.787, 47.851, 46.373, 45.401, 44.503, 43.815, 43.074, 42.321,
666
    41.586, 40.953, 40.524, 40.256, 39.756, 39.144, 38.462, 37.778, 37.174,
667
    36.663, 35.986, 35.317, 34.688, 34.197, 33.786, 33.422, 33.068, 32.740,
668
    32.438, 32.143, 31.884, 31.622, 31.438, 31.142, 30.950, 30.758, 30.561,
669
    30.285, 30.097, 29.832, 29.581, 29.411, 29.247, 29.085, 28.930, 28.721,
670
    28.580, 28.442, 28.312, 28.139, 27.973, 27.819, 27.675, 27.496, 27.285,
671
    27.093, 26.911, 26.705, 26.516, 26.304, 26.108, 25.929, 25.730, 25.577,
672
    25.403, 25.245, 25.100, 24.941, 24.790, 24.655, 24.506, 24.391, 24.262,
673
    24.145, 24.039, 23.922, 23.813, 23.712, 23.621, 23.523, 23.430, 23.331,
674
    23.238, 23.139, 23.048, 22.967, 22.833, 22.694, 22.624, 22.545, 22.446,
675
    22.358, 22.264};
676

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

688
  // Compute the high-energy Coulomb correction
689
  double a = Z_ / FINE_STRUCTURE;
79,680✔
690
  double c =
79,680✔
691
    a * a *
79,680✔
692
    (1.0 / (1.0 + a * a) + 0.202059 +
79,680✔
693
      a * a *
79,680✔
694
        (-0.03693 +
79,680✔
695
          a * a *
79,680✔
696
            (0.00835 +
79,680✔
697
              a * a *
79,680✔
698
                (-0.00201 +
79,680✔
699
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
79,680✔
700

701
  // The analytical approximation of the DCS underestimates the cross section
702
  // at low energies. The correction factor f compensates for this.
703
  double q = std::sqrt(2.0 / alpha);
79,680✔
704
  double f = q * (-0.1774 - 12.10 * a + 11.18 * a * a) +
79,680✔
705
             q * q * (8.523 + 73.26 * a - 44.41 * a * a) +
79,680✔
706
             q * q * q * (-13.52 - 121.1 * a + 96.41 * a * a) +
79,680✔
707
             q * q * q * q * (8.946 + 62.05 * a - 63.41 * a * a);
79,680✔
708

709
  // Calculate phi_1(1/2) and phi_2(1/2). The unnormalized PDF for the reduced
710
  // energy is given by p = 2*(1/2 - e)^2*phi_1(e) + phi_2(e), where phi_1 and
711
  // phi_2 are non-negative and maximum at e = 1/2.
712
  double b = 2.0 * r[Z_] / alpha;
79,680✔
713
  double t1 = 2.0 * std::log(1.0 + b * b);
79,680✔
714
  double t2 = b * std::atan(1.0 / b);
79,680✔
715
  double t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
79,680✔
716
  double t4 = 4.0 * std::log(r[Z_]) - 4.0 * c + f;
79,680✔
717
  double phi1_max = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
79,680✔
718
  double phi2_max = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
79,680✔
719

720
  // To aid sampling, the unnormalized PDF can be expressed as
721
  // p = u_1*U_1(e)*pi_1(e) + u_2*U_2(e)*pi_2(e), where pi_1 and pi_2 are
722
  // normalized PDFs on the interval (e_min, e_max) from which values of e can
723
  // be sampled using the inverse transform method, and
724
  // U_1 = phi_1(e)/phi_1(1/2) and U_2 = phi_2(e)/phi_2(1/2) are valid
725
  // rejection functions. The reduced energy can now be sampled using a
726
  // combination of the composition and rejection methods.
727
  double u1 = 2.0 / 3.0 * std::pow(0.5 - 1.0 / alpha, 2) * phi1_max;
79,680✔
728
  double u2 = phi2_max;
79,680✔
729
  double e;
730
  while (true) {
731
    double rn = prn(seed);
99,900✔
732

733
    // Sample the index i in (1, 2) using the point probabilities
734
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
735
    int i;
736
    if (prn(seed) < u1 / (u1 + u2)) {
99,900✔
737
      i = 1;
8,990✔
738

739
      // Sample e from pi_1 using the inverse transform method
740
      e = rn >= 0.5
8,990✔
741
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
8,990✔
742
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
4,630✔
743
    } else {
744
      i = 2;
90,910✔
745

746
      // Sample e from pi_2 using the inverse transform method
747
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
90,910✔
748
    }
749

750
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
751
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
99,900✔
752
    t1 = 2.0 * std::log(1.0 + b * b);
99,900✔
753
    t2 = b * std::atan(1.0 / b);
99,900✔
754
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
99,900✔
755
    if (i == 1) {
99,900✔
756
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
8,990✔
757
      if (prn(seed) <= phi1 / phi1_max)
8,990✔
758
        break;
5,940✔
759
    } else {
760
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
90,910✔
761
      if (prn(seed) <= phi2 / phi2_max)
90,910✔
762
        break;
73,740✔
763
    }
764
  }
20,220✔
765

766
  // Compute the kinetic energy of the electron and the positron
767
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
79,680✔
768
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
79,680✔
769

770
  // Sample the scattering angle of the electron. The cosine of the polar
771
  // angle of the direction relative to the incident photon is sampled from
772
  // p(mu) = C/(1 - beta*mu)^2 using the inverse transform method.
773
  double beta =
774
    std::sqrt(*E_electron * (*E_electron + 2.0 * MASS_ELECTRON_EV)) /
79,680✔
775
    (*E_electron + MASS_ELECTRON_EV);
79,680✔
776
  double rn = uniform_distribution(-1., 1., seed);
79,680✔
777
  *mu_electron = (rn + beta) / (rn * beta + 1.0);
79,680✔
778

779
  // Sample the scattering angle of the positron
780
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
79,680✔
781
         (*E_positron + MASS_ELECTRON_EV);
79,680✔
782
  rn = uniform_distribution(-1., 1., seed);
79,680✔
783
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
79,680✔
784
}
79,680✔
785

786
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
14,949,184✔
787
{
788
  // Return if no atomic relaxation data is present or if the binding energy is
789
  // larger than the incident particle energy
790
  if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E())
14,949,184!
791
    return;
1,060✔
792

793
  // Stack for unprocessed holes left by transitioning electrons
794
  int n_holes = 0;
14,948,124✔
795
  array<int, MAX_STACK_SIZE> holes;
796

797
  // Push the initial hole onto the stack
798
  holes[n_holes++] = i_shell;
14,948,124✔
799

800
  while (n_holes > 0) {
99,373,504✔
801
    // Pop the next hole off the stack
802
    int i_hole = holes[--n_holes];
84,425,380✔
803
    const auto& shell {shells_[i_hole]};
84,425,380✔
804

805
    // If no transitions, assume fluorescent photon from captured free electron
806
    if (shell.transitions.empty()) {
84,425,380✔
807
      Direction u = isotropic_direction(p.current_seed());
48,854,847✔
808
      double E = shell.binding_energy;
48,854,847✔
809
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
48,854,847✔
810
      continue;
48,854,847✔
811
    }
48,854,847✔
812

813
    // Sample transition
814
    double c = -prn(p.current_seed());
35,570,533✔
815
    int i_trans;
816
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
900,351,057!
817
      c += shell.transitions[i_trans].probability;
900,351,057✔
818
      if (c > 0)
900,351,057✔
819
        break;
35,570,533✔
820
    }
821
    const auto& transition = shell.transitions[i_trans];
35,570,533✔
822

823
    // Sample angle isotropically
824
    Direction u = isotropic_direction(p.current_seed());
35,570,533✔
825

826
    // Push the hole created by the electron transitioning to the photoelectron
827
    // hole onto the stack
828
    holes[n_holes++] = transition.primary_subshell;
35,570,533✔
829

830
    if (transition.secondary_subshell != -1) {
35,570,533✔
831
      // Non-radiative transition -- Auger/Coster-Kronig effect
832

833
      // Push the hole left by emitted auger electron onto the stack
834
      holes[n_holes++] = transition.secondary_subshell;
33,906,723✔
835

836
      // Create auger electron
837
      p.create_secondary(
67,813,446✔
838
        p.wgt(), u, transition.energy, ParticleType::electron());
33,906,723✔
839
    } else {
840
      // Radiative transition -- get X-ray energy
841

842
      // Create fluorescent photon
843
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
1,663,810✔
844
    }
845
  }
846
}
847

848
//==============================================================================
849
// Non-member functions
850
//==============================================================================
851

852
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
10,535,046✔
853
{
854
  double alpha_out, mu;
855
  double beta = 1.0 + 2.0 * alpha;
10,535,046✔
856
  if (alpha < 3.0) {
10,535,046✔
857
    // Kahn's rejection method
858
    double t = beta / (beta + 8.0);
10,027,026✔
859
    double x;
860
    while (true) {
861
      if (prn(seed) < t) {
16,904,573✔
862
        // Left branch of flow chart
863
        double r = uniform_distribution(0.0, 2.0, seed);
2,841,926✔
864
        x = 1.0 + alpha * r;
2,841,926✔
865
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
2,841,926✔
866
          mu = 1 - r;
1,613,586✔
867
          break;
1,613,586✔
868
        }
869
      } else {
870
        // Right branch of flow chart
871
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
14,062,647✔
872
        mu = 1.0 + (1.0 - x) / alpha;
14,062,647✔
873
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
14,062,647✔
874
          break;
8,413,440✔
875
      }
876
    }
6,877,547✔
877
    alpha_out = alpha / x;
10,027,026✔
878

879
  } else {
880
    // Koblinger's direct method
881
    double gamma = 1.0 - std::pow(beta, -2);
508,020✔
882
    double s =
883
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
508,020✔
884
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
508,020✔
885
    if (s <= 2.0 / alpha) {
508,020✔
886
      // For first term, x = 1 + 2ar
887
      // Therefore, a' = a/(1 + 2ar)
888
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
73,740✔
889
    } else if (s <= 4.0 / alpha) {
434,280✔
890
      // For third term, x = beta/(1 + 2ar)
891
      // Therefore, a' = a(1 + 2ar)/beta
892
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
72,290✔
893
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
361,990✔
894
      // For fourth term, x = 1/sqrt(1 - gamma*r)
895
      // Therefore, a' = a*sqrt(1 - gamma*r)
896
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
91,320✔
897
    } else {
898
      // For third term, x = beta^r
899
      // Therefore, a' = a/beta^r
900
      alpha_out = alpha / std::pow(beta, prn(seed));
270,670✔
901
    }
902

903
    // Calculate cosine of scattering angle based on basic relation
904
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
508,020✔
905
  }
906
  return {alpha_out, mu};
10,535,046✔
907
}
908

909
void free_memory_photon()
7,582✔
910
{
911
  data::elements.clear();
7,582✔
912
  data::compton_profile_pz.resize({0});
7,582✔
913
  data::ttb_e_grid.resize({0});
7,582✔
914
  data::ttb_k_grid.resize({0});
7,582✔
915
}
7,582✔
916

917
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc