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

openmc-dev / openmc / 21991279157

13 Feb 2026 02:53PM UTC coverage: 81.82% (-0.06%) from 81.875%
21991279157

Pull #3805

github

web-flow
Merge 0a7a80411 into bcb939520
Pull Request #3805: Remove xtensor and xtl Dependencies

17242 of 24268 branches covered (71.05%)

Branch coverage included in aggregate %.

977 of 1013 new or added lines in 39 files covered. (96.45%)

404 existing lines in 8 files now uncovered.

57420 of 66983 relevant lines covered (85.72%)

45458907.73 hits per line

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

89.82
/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!
67
    dset = open_dataset(rgroup, "anomalous_real");
×
68
    coherent_anomalous_real_ = Tabulated1D {dset};
×
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 {
NEW
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 {
NEW
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 {
NEW
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!
125
    throw std::runtime_error {
×
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 = cross_sections_.slice(
172
      tensor::range(static_cast<size_t>(shell.threshold)), i);
4,778✔
173
    cross_section = tensor::where(xs > 0, tensor::log(xs), 0);
4,778✔
174

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

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

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

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

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

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

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

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

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

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

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

267
  // Calculate total pair production
268
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
624✔
269

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

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

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

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

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

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

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

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

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

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

345
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
102,654✔
346
    }
347
  }
514✔
348

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

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

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

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

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

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

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

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

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

433
    // Calculate S(x, Z) and S(x_max, Z)
434
    double form_factor_x = incoherent_form_factor_(x);
10,534,949✔
435
    if (form_factor_xmax == 0.0) {
10,534,949✔
436
      form_factor_xmax =
437
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
10,287,204✔
438
    }
439

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

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

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

470
    // Determine binding energy of shell
471
    double E_b = binding_energy_(shell);
10,287,204✔
472

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

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

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

510
    // Sample value on bounded cdf
511
    c = prn(seed) * c_max;
10,190,610✔
512

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

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

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

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

568
  *i_shell = shell;
10,287,204✔
569
}
10,287,204✔
570

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

588
  // check for case where two energy points are the same
589
  if (energy_(i_grid) == energy_(i_grid + 1))
39,036,761!
590
    ++i_grid;
×
591

592
  // calculate interpolation factor
593
  double f =
594
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
39,036,761✔
595

596
  auto& xs {p.photon_xs(index_)};
39,036,761✔
597
  xs.index_grid = i_grid;
39,036,761✔
598
  xs.interp_factor = f;
39,036,761✔
599

600
  // Calculate microscopic coherent cross section
601
  xs.coherent = std::exp(
39,036,761✔
602
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
39,036,761✔
603

604
  // Calculate microscopic incoherent cross section
605
  xs.incoherent = std::exp(
39,036,761✔
606
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
39,036,761✔
607

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

613
  for (int i = 0; i < xs_upper.size(); ++i)
304,055,305✔
614
    if (xs_lower(i) != 0)
265,018,544✔
615
      xs.photoelectric +=
255,517,342✔
616
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
255,517,342✔
617

618
  // Calculate microscopic pair production cross section
619
  xs.pair_production = std::exp(
39,036,761✔
620
    pair_production_total_(i_grid) +
39,036,761✔
621
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
39,036,761✔
622

623
  // Calculate microscopic total cross section
624
  xs.total =
39,036,761✔
625
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
39,036,761✔
626
  xs.last_E = p.E();
39,036,761✔
627
}
39,036,761✔
628

629
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
877,261✔
630
{
631
  double mu;
632
  while (true) {
633
    // Determine maximum value of x^2
634
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
1,006,922✔
635

636
    // Determine F(x^2_max, Z)
637
    double F_max = coherent_int_form_factor_(x2_max);
1,006,922✔
638

639
    // Sample cumulative distribution
640
    double F = prn(seed) * F_max;
1,006,922✔
641

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

649
    // Calculate mu
650
    mu = 1.0 - 2.0 * x2 / x2_max;
1,006,922✔
651

652
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
1,006,922✔
653
      break;
877,261✔
654
  }
129,661✔
655
  return mu;
877,261✔
656
}
657

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

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

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

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

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

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

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

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

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

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

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

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

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

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

791
  // Stack for unprocessed holes left by transitioning electrons
792
  int n_holes = 0;
14,947,037✔
793
  array<int, MAX_STACK_SIZE> holes;
794

795
  // Push the initial hole onto the stack
796
  holes[n_holes++] = i_shell;
14,947,037✔
797

798
  while (n_holes > 0) {
99,355,356✔
799
    // Pop the next hole off the stack
800
    int i_hole = holes[--n_holes];
84,408,319✔
801
    const auto& shell {shells_[i_hole]};
84,408,319✔
802

803
    // If no transitions, assume fluorescent photon from captured free electron
804
    if (shell.transitions.empty()) {
84,408,319✔
805
      Direction u = isotropic_direction(p.current_seed());
48,846,127✔
806
      double E = shell.binding_energy;
48,846,127✔
807
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
48,846,127✔
808
      continue;
48,846,127✔
809
    }
48,846,127✔
810

811
    // Sample transition
812
    double c = -prn(p.current_seed());
35,562,192✔
813
    int i_trans;
814
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
900,276,735!
815
      c += shell.transitions[i_trans].probability;
900,276,735✔
816
      if (c > 0)
900,276,735✔
817
        break;
35,562,192✔
818
    }
819
    const auto& transition = shell.transitions[i_trans];
35,562,192✔
820

821
    // Sample angle isotropically
822
    Direction u = isotropic_direction(p.current_seed());
35,562,192✔
823

824
    // Push the hole created by the electron transitioning to the photoelectron
825
    // hole onto the stack
826
    holes[n_holes++] = transition.primary_subshell;
35,562,192✔
827

828
    if (transition.secondary_subshell != -1) {
35,562,192✔
829
      // Non-radiative transition -- Auger/Coster-Kronig effect
830

831
      // Push the hole left by emitted auger electron onto the stack
832
      holes[n_holes++] = transition.secondary_subshell;
33,899,090✔
833

834
      // Create auger electron
835
      p.create_secondary(
67,798,180✔
836
        p.wgt(), u, transition.energy, ParticleType::electron());
33,899,090✔
837
    } else {
838
      // Radiative transition -- get X-ray energy
839

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

846
//==============================================================================
847
// Non-member functions
848
//==============================================================================
849

850
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
10,534,949✔
851
{
852
  double alpha_out, mu;
853
  double beta = 1.0 + 2.0 * alpha;
10,534,949✔
854
  if (alpha < 3.0) {
10,534,949✔
855
    // Kahn's rejection method
856
    double t = beta / (beta + 8.0);
10,026,992✔
857
    double x;
858
    while (true) {
859
      if (prn(seed) < t) {
16,904,499✔
860
        // Left branch of flow chart
861
        double r = uniform_distribution(0.0, 2.0, seed);
2,842,285✔
862
        x = 1.0 + alpha * r;
2,842,285✔
863
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
2,842,285✔
864
          mu = 1 - r;
1,614,043✔
865
          break;
1,614,043✔
866
        }
867
      } else {
868
        // Right branch of flow chart
869
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
14,062,214✔
870
        mu = 1.0 + (1.0 - x) / alpha;
14,062,214✔
871
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
14,062,214✔
872
          break;
8,412,949✔
873
      }
874
    }
6,877,507✔
875
    alpha_out = alpha / x;
10,026,992✔
876

877
  } else {
878
    // Koblinger's direct method
879
    double gamma = 1.0 - std::pow(beta, -2);
507,957✔
880
    double s =
881
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
507,957✔
882
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
507,957✔
883
    if (s <= 2.0 / alpha) {
507,957✔
884
      // For first term, x = 1 + 2ar
885
      // Therefore, a' = a/(1 + 2ar)
886
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
73,719✔
887
    } else if (s <= 4.0 / alpha) {
434,238✔
888
      // For third term, x = beta/(1 + 2ar)
889
      // Therefore, a' = a(1 + 2ar)/beta
890
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
72,275✔
891
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
361,963✔
892
      // For fourth term, x = 1/sqrt(1 - gamma*r)
893
      // Therefore, a' = a*sqrt(1 - gamma*r)
894
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
91,305✔
895
    } else {
896
      // For third term, x = beta^r
897
      // Therefore, a' = a/beta^r
898
      alpha_out = alpha / std::pow(beta, prn(seed));
270,658✔
899
    }
900

901
    // Calculate cosine of scattering angle based on basic relation
902
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
507,957✔
903
  }
904
  return {alpha_out, mu};
10,534,949✔
905
}
906

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

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