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

openmc-dev / openmc / 14843042946

05 May 2025 06:05PM UTC coverage: 85.193% (+0.002%) from 85.191%
14843042946

push

github

web-flow
Map Compton subshell data to atomic relaxation data (#3392)

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>

14 of 14 new or added lines in 2 files covered. (100.0%)

1 existing line in 1 file now uncovered.

52185 of 61255 relevant lines covered (85.19%)

37308041.46 hits per line

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

94.9
/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)
612✔
48
{
49
  using namespace xt::placeholders;
50

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

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

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

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

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

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

72
  if (object_exists(group, "anomalous_real")) {
612✔
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")) {
612✔
79
    dset = open_dataset(rgroup, "anomalous_imag");
×
80
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
81
    close_dataset(dset);
×
82
  }
83
  close_group(rgroup);
612✔
84

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

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

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

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

116
  // Read heating
117
  if (object_exists(group, "heating")) {
612✔
118
    rgroup = open_group(group, "heating");
612✔
119
    read_dataset(rgroup, "xs", heating_);
612✔
120
    close_group(rgroup);
612✔
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");
612✔
127
  vector<std::string> designators;
612✔
128
  read_attribute(rgroup, "designators", designators);
612✔
129
  auto n_shell = designators.size();
612✔
130
  if (n_shell == 0) {
612✔
131
    throw std::runtime_error {
×
132
      "Photoatomic data for " + name_ + " does not have subshell data."};
×
133
  }
134

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

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

143
    int j = 1;
5,082✔
144
    for (const auto& subshell : SUBSHELLS) {
37,661✔
145
      if (designator == subshell) {
37,661✔
146
        shell_map[j] = i;
5,082✔
147
        shells_[i].index_subshell = j;
5,082✔
148
        break;
5,082✔
149
      }
150
      ++j;
32,579✔
151
    }
152
  }
153
  shell_map[0] = -1;
612✔
154

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

159
    // TODO: Move to ElectronSubshell constructor
160

161
    hid_t tgroup = open_group(rgroup, designator.c_str());
5,082✔
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,082✔
166
      has_atomic_relaxation_ = true;
5,082✔
167
      read_attribute(tgroup, "binding_energy", shell.binding_energy);
5,082✔
168
    }
169

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

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

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

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

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

195
        shell.transitions.resize(n_transition);
3,157✔
196
        for (int j = 0; j < n_transition; ++j) {
174,359✔
197
          auto& transition = shell.transitions[j];
171,202✔
198
          transition.primary_subshell = shell_map.at(matrix(j, 0));
171,202✔
199
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
171,202✔
200
          transition.energy = matrix(j, 2);
171,202✔
201
          transition.probability = matrix(j, 3) / norm;
171,202✔
202
        }
203
      }
3,157✔
204
    }
3,157✔
205
    close_group(tgroup);
5,082✔
206
  }
5,082✔
207
  close_group(rgroup);
612✔
208

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

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

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

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

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

235
  // Map Compton subshell data to atomic relaxation data by finding the
236
  // subshell with the equivalent binding energy
237
  if (has_atomic_relaxation_) {
612✔
238
    auto is_close = [](double a, double b) {
8,684✔
239
      return std::abs(a - b) / a < FP_REL_PRECISION;
8,684✔
240
    };
241
    subshell_map_ = xt::full_like(binding_energy_, -1);
612✔
242
    for (int i = 0; i < binding_energy_.size(); ++i) {
4,859✔
243
      double E_b = binding_energy_[i];
4,247✔
244
      if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) {
4,247✔
245
        subshell_map_[i] = i;
3,535✔
246
      } else {
247
        for (int j = 0; j < n_shell; ++j) {
4,437✔
248
          if (is_close(E_b, shells_[j].binding_energy)) {
4,437✔
249
            subshell_map_[i] = j;
712✔
250
            break;
712✔
251
          }
252
        }
253
      }
254
    }
255
  }
256

257
  // Create Compton profile CDF
258
  auto n_profile = data::compton_profile_pz.size();
612✔
259
  auto n_shell_compton = profile_pdf_.shape(0);
612✔
260
  profile_cdf_ = xt::empty<double>({n_shell_compton, n_profile});
612✔
261
  for (int i = 0; i < n_shell_compton; ++i) {
4,859✔
262
    double c = 0.0;
4,247✔
263
    profile_cdf_(i, 0) = 0.0;
4,247✔
264
    for (int j = 0; j < n_profile - 1; ++j) {
131,657✔
265
      c += 0.5 *
254,820✔
266
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
127,410✔
267
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
127,410✔
268
      profile_cdf_(i, j + 1) = c;
127,410✔
269
    }
270
  }
271

272
  // Calculate total pair production
273
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
612✔
274

275
  if (settings::electron_treatment == ElectronTreatment::TTB) {
612✔
276
    // Read bremsstrahlung scaled DCS
277
    rgroup = open_group(group, "bremsstrahlung");
535✔
278
    read_dataset(rgroup, "dcs", dcs_);
535✔
279
    auto n_e = dcs_.shape()[0];
535✔
280
    auto n_k = dcs_.shape()[1];
535✔
281

282
    // Get energy grids used for bremsstrahlung DCS and for stopping powers
283
    xt::xtensor<double, 1> electron_energy;
535✔
284
    read_dataset(rgroup, "electron_energy", electron_energy);
535✔
285
    if (data::ttb_k_grid.size() == 0) {
535✔
286
      read_dataset(rgroup, "photon_energy", data::ttb_k_grid);
248✔
287
    }
288

289
    // Get data used for density effect correction
290
    read_dataset(rgroup, "num_electrons", n_electrons_);
535✔
291
    read_dataset(rgroup, "ionization_energy", ionization_energy_);
535✔
292
    read_attribute(rgroup, "I", I_);
535✔
293
    close_group(rgroup);
535✔
294

295
    // Truncate the bremsstrahlung data at the cutoff energy
296
    int photon = static_cast<int>(ParticleType::photon);
535✔
297
    const auto& E {electron_energy};
535✔
298
    double cutoff = settings::energy_cutoff[photon];
535✔
299
    if (cutoff > E(0)) {
535✔
300
      size_t i_grid = lower_bound_index(
11✔
301
        E.cbegin(), E.cend(), settings::energy_cutoff[photon]);
11✔
302

303
      // calculate interpolation factor
304
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
11✔
305
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
11✔
306

307
      // Interpolate bremsstrahlung DCS at the cutoff energy and truncate
308
      xt::xtensor<double, 2> dcs({n_e - i_grid, n_k});
11✔
309
      for (int i = 0; i < n_k; ++i) {
341✔
310
        double y = std::exp(
330✔
311
          std::log(dcs_(i_grid, i)) +
330✔
312
          f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i))));
330✔
313
        auto col_i = xt::view(dcs, xt::all(), i);
330✔
314
        col_i(0) = y;
330✔
315
        for (int j = i_grid + 1; j < n_e; ++j) {
61,710✔
316
          col_i(j - i_grid) = dcs_(j, i);
61,380✔
317
        }
318
      }
330✔
319
      dcs_ = dcs;
11✔
320

321
      xt::xtensor<double, 1> frst {cutoff};
11✔
322
      electron_energy = xt::concatenate(xt::xtuple(
22✔
323
        frst, xt::view(electron_energy, xt::range(i_grid + 1, n_e))));
33✔
324
    }
11✔
325

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

331
    // Calculate the radiative stopping power
332
    stopping_power_radiative_ = xt::empty<double>({data::ttb_e_grid.size()});
535✔
333
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
107,392✔
334
      // Integrate over reduced photon energy
335
      double c = 0.0;
106,857✔
336
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,205,710✔
337
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
3,098,853✔
338
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
3,098,853✔
339
      }
340
      double e = data::ttb_e_grid(i);
106,857✔
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) /
106,857✔
345
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
106,857✔
346

347
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
106,857✔
348
    }
349
  }
535✔
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);
612✔
357
  energy_ = xt::log(energy_);
612✔
358
  coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0);
612✔
359
  incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0);
612✔
360
  photoelectric_total_ = xt::where(
1,224✔
361
    photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0);
1,836✔
362
  pair_production_total_ = xt::where(
1,224✔
363
    pair_production_total_ > limit, xt::log(pair_production_total_), -900.0);
1,836✔
364
  heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0);
612✔
365
}
612✔
366

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

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

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

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

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

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

420
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
7,917,282✔
421
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
422
{
423
  double form_factor_xmax = 0.0;
7,917,282✔
424
  while (true) {
425
    // Sample Klein-Nishina distribution for trial energy and angle
426
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
8,091,585✔
427

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

434
    // Calculate S(x, Z) and S(x_max, Z)
435
    double form_factor_x = incoherent_form_factor_(x);
8,091,585✔
436
    if (form_factor_xmax == 0.0) {
8,091,585✔
437
      form_factor_xmax =
438
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
7,917,282✔
439
    }
440

441
    // Perform rejection on form factor
442
    if (prn(seed) < form_factor_x / form_factor_xmax) {
8,091,585✔
443
      if (doppler) {
7,917,282✔
444
        double E_out;
445
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
7,917,282✔
446
        *alpha_out = E_out / MASS_ELECTRON_EV;
7,917,282✔
447
      } else {
UNCOV
448
        *i_shell = -1;
×
449
      }
450
      break;
7,917,282✔
451
    }
452
  }
174,303✔
453
}
7,917,282✔
454

455
void PhotonInteraction::compton_doppler(
7,917,282✔
456
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
457
{
458
  auto n = data::compton_profile_pz.size();
7,917,282✔
459

460
  int shell; // index for shell
461
  while (true) {
462
    // Sample electron shell
463
    double rn = prn(seed);
7,917,282✔
464
    double c = 0.0;
7,917,282✔
465
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
25,768,878✔
466
      c += electron_pdf_(shell);
25,768,878✔
467
      if (rn < c)
25,768,878✔
468
        break;
7,917,282✔
469
    }
470

471
    // Determine binding energy of shell
472
    double E_b = binding_energy_(shell);
7,917,282✔
473

474
    // Determine p_z,max
475
    double E = alpha * MASS_ELECTRON_EV;
7,917,282✔
476
    if (E < E_b) {
7,917,282✔
477
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
1,111✔
478
      break;
1,111✔
479
    }
480

481
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
7,916,171✔
482
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
7,916,171✔
483
    if (pz_max < 0.0) {
7,916,171✔
484
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
71,377✔
485
      break;
71,377✔
486
    }
487

488
    // Determine profile cdf value corresponding to p_z,max
489
    double c_max;
490
    if (pz_max > data::compton_profile_pz(n - 1)) {
7,844,794✔
491
      c_max = profile_cdf_(shell, n - 1);
909,608✔
492
    } else {
493
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
6,935,186✔
494
        data::compton_profile_pz.cend(), pz_max);
6,935,186✔
495
      double pz_l = data::compton_profile_pz(i);
6,935,186✔
496
      double pz_r = data::compton_profile_pz(i + 1);
6,935,186✔
497
      double p_l = profile_pdf_(shell, i);
6,935,186✔
498
      double p_r = profile_pdf_(shell, i + 1);
6,935,186✔
499
      double c_l = profile_cdf_(shell, i);
6,935,186✔
500
      if (pz_l == pz_r) {
6,935,186✔
501
        c_max = c_l;
×
502
      } else if (p_l == p_r) {
6,935,186✔
503
        c_max = c_l + (pz_max - pz_l) * p_l;
11,393✔
504
      } else {
505
        double m = (p_l - p_r) / (pz_l - pz_r);
6,923,793✔
506
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
6,923,793✔
507
                        (2.0 * m);
6,923,793✔
508
      }
509
    }
510

511
    // Sample value on bounded cdf
512
    c = prn(seed) * c_max;
7,844,794✔
513

514
    // Determine pz corresponding to sampled cdf value
515
    auto cdf_shell = xt::view(profile_cdf_, shell, xt::all());
7,844,794✔
516
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
7,844,794✔
517
    double pz_l = data::compton_profile_pz(i);
7,844,794✔
518
    double pz_r = data::compton_profile_pz(i + 1);
7,844,794✔
519
    double p_l = profile_pdf_(shell, i);
7,844,794✔
520
    double p_r = profile_pdf_(shell, i + 1);
7,844,794✔
521
    double c_l = profile_cdf_(shell, i);
7,844,794✔
522
    double pz;
523
    if (pz_l == pz_r) {
7,844,794✔
524
      pz = pz_l;
×
525
    } else if (p_l == p_r) {
7,844,794✔
526
      pz = pz_l + (c - c_l) / p_l;
678,293✔
527
    } else {
528
      double m = (p_l - p_r) / (pz_l - pz_r);
7,166,501✔
529
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
7,166,501✔
530
    }
531

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

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

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

569
  *i_shell = shell;
7,917,282✔
570
}
7,917,282✔
571

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

589
  // check for case where two energy points are the same
590
  if (energy_(i_grid) == energy_(i_grid + 1))
34,571,370✔
591
    ++i_grid;
×
592

593
  // calculate interpolation factor
594
  double f =
595
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
34,571,370✔
596

597
  auto& xs {p.photon_xs(index_)};
34,571,370✔
598
  xs.index_grid = i_grid;
34,571,370✔
599
  xs.interp_factor = f;
34,571,370✔
600

601
  // Calculate microscopic coherent cross section
602
  xs.coherent = std::exp(
34,571,370✔
603
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
34,571,370✔
604

605
  // Calculate microscopic incoherent cross section
606
  xs.incoherent = std::exp(
34,571,370✔
607
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
34,571,370✔
608

609
  // Calculate microscopic photoelectric cross section
610
  xs.photoelectric = 0.0;
34,571,370✔
611
  const auto& xs_lower = xt::row(cross_sections_, i_grid);
34,571,370✔
612
  const auto& xs_upper = xt::row(cross_sections_, i_grid + 1);
34,571,370✔
613

614
  for (int i = 0; i < xs_upper.size(); ++i)
299,552,589✔
615
    if (xs_lower(i) != 0)
264,981,219✔
616
      xs.photoelectric +=
254,574,702✔
617
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
254,574,702✔
618

619
  // Calculate microscopic pair production cross section
620
  xs.pair_production = std::exp(
34,571,370✔
621
    pair_production_total_(i_grid) +
34,571,370✔
622
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
34,571,370✔
623

624
  // Calculate microscopic total cross section
625
  xs.total =
34,571,370✔
626
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
34,571,370✔
627
  xs.last_E = p.E();
34,571,370✔
628
}
34,571,370✔
629

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

637
    // Determine F(x^2_max, Z)
638
    double F_max = coherent_int_form_factor_(x2_max);
808,970✔
639

640
    // Sample cumulative distribution
641
    double F = prn(seed) * F_max;
808,970✔
642

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

650
    // Calculate mu
651
    mu = 1.0 - 2.0 * x2 / x2_max;
808,970✔
652

653
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
808,970✔
654
      break;
704,506✔
655
  }
104,464✔
656
  return mu;
704,506✔
657
}
658

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

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

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

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

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

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

732
    // Sample the index i in (1, 2) using the point probabilities
733
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
734
    int i;
735
    if (prn(seed) < u1 / (u1 + u2)) {
105,600✔
736
      i = 1;
9,471✔
737

738
      // Sample e from pi_1 using the inverse transform method
739
      e = rn >= 0.5
9,471✔
740
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
9,471✔
741
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
4,741✔
742
    } else {
743
      i = 2;
96,129✔
744

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

749
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
750
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
105,600✔
751
    t1 = 2.0 * std::log(1.0 + b * b);
105,600✔
752
    t2 = b * std::atan(1.0 / b);
105,600✔
753
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
105,600✔
754
    if (i == 1) {
105,600✔
755
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
9,471✔
756
      if (prn(seed) <= phi1 / phi1_max)
9,471✔
757
        break;
6,270✔
758
    } else {
759
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
96,129✔
760
      if (prn(seed) <= phi2 / phi2_max)
96,129✔
761
        break;
78,254✔
762
    }
763
  }
21,076✔
764

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

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

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

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

792
  // Stack for unprocessed holes left by transitioning electrons
793
  int n_holes = 0;
12,561,958✔
794
  array<int, MAX_STACK_SIZE> holes;
795

796
  // Push the initial hole onto the stack
797
  holes[n_holes++] = i_shell;
12,561,958✔
798

799
  while (n_holes > 0) {
98,644,272✔
800
    // Pop the next hole off the stack
801
    int i_hole = holes[--n_holes];
86,082,314✔
802
    const auto& shell {shells_[i_hole]};
86,082,314✔
803

804
    // If no transitions, assume fluorescent photon from captured free electron
805
    if (shell.transitions.empty()) {
86,082,314✔
806
      Direction u = isotropic_direction(p.current_seed());
48,420,300✔
807
      double E = shell.binding_energy;
48,420,300✔
808
      p.create_secondary(p.wgt(), u, E, ParticleType::photon);
48,420,300✔
809
      continue;
48,420,300✔
810
    }
48,420,300✔
811

812
    // Sample transition
813
    double c = -prn(p.current_seed());
37,662,014✔
814
    int i_trans;
815
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
977,746,453✔
816
      c += shell.transitions[i_trans].probability;
977,746,453✔
817
      if (c > 0)
977,746,453✔
818
        break;
37,662,014✔
819
    }
820
    const auto& transition = shell.transitions[i_trans];
37,662,014✔
821

822
    // Sample angle isotropically
823
    Direction u = isotropic_direction(p.current_seed());
37,662,014✔
824

825
    // Push the hole created by the electron transitioning to the photoelectron
826
    // hole onto the stack
827
    holes[n_holes++] = transition.primary_subshell;
37,662,014✔
828

829
    if (transition.secondary_subshell != -1) {
37,662,014✔
830
      // Non-radiative transition -- Auger/Coster-Kronig effect
831

832
      // Push the hole left by emitted auger electron onto the stack
833
      holes[n_holes++] = transition.secondary_subshell;
35,858,342✔
834

835
      // Create auger electron
836
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron);
35,858,342✔
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,803,672✔
842
    }
843
  }
844
}
845

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

850
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
8,091,585✔
851
{
852
  double alpha_out, mu;
853
  double beta = 1.0 + 2.0 * alpha;
8,091,585✔
854
  if (alpha < 3.0) {
8,091,585✔
855
    // Kahn's rejection method
856
    double t = beta / (beta + 8.0);
7,573,859✔
857
    double x;
858
    while (true) {
859
      if (prn(seed) < t) {
12,743,863✔
860
        // Left branch of flow chart
861
        double r = uniform_distribution(0.0, 2.0, seed);
2,244,234✔
862
        x = 1.0 + alpha * r;
2,244,234✔
863
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
2,244,234✔
864
          mu = 1 - r;
1,343,676✔
865
          break;
1,343,676✔
866
        }
867
      } else {
868
        // Right branch of flow chart
869
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
10,499,629✔
870
        mu = 1.0 + (1.0 - x) / alpha;
10,499,629✔
871
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
10,499,629✔
872
          break;
6,230,183✔
873
      }
874
    }
5,170,004✔
875
    alpha_out = alpha / x;
7,573,859✔
876

877
  } else {
878
    // Koblinger's direct method
879
    double gamma = 1.0 - std::pow(beta, -2);
517,726✔
880
    double s =
881
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
517,726✔
882
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
517,726✔
883
    if (s <= 2.0 / alpha) {
517,726✔
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,986✔
887
    } else if (s <= 4.0 / alpha) {
443,740✔
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;
74,899✔
891
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
368,841✔
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));
93,764✔
895
    } else {
896
      // For third term, x = beta^r
897
      // Therefore, a' = a/beta^r
898
      alpha_out = alpha / std::pow(beta, prn(seed));
275,077✔
899
    }
900

901
    // Calculate cosine of scattering angle based on basic relation
902
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
517,726✔
903
  }
904
  return {alpha_out, mu};
8,091,585✔
905
}
906

907
void free_memory_photon()
6,695✔
908
{
909
  data::elements.clear();
6,695✔
910
  data::compton_profile_pz.resize({0});
6,695✔
911
  data::ttb_e_grid.resize({0});
6,695✔
912
  data::ttb_k_grid.resize({0});
6,695✔
913
}
6,695✔
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

© 2025 Coveralls, Inc