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

openmc-dev / openmc / 23051473169

13 Mar 2026 12:46PM UTC coverage: 81.577% (+0.01%) from 81.563%
23051473169

Pull #3874

github

web-flow
Merge db77037d0 into 4bda85f17
Pull Request #3874: Fix a bug in compton scattering shell and angle selection

17577 of 25288 branches covered (69.51%)

Branch coverage included in aggregate %.

33 of 36 new or added lines in 1 file covered. (91.67%)

37 existing lines in 3 files now uncovered.

57990 of 67345 relevant lines covered (86.11%)

45235504.43 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)
693✔
44
{
45
  // Set index of element in global vector
46
  index_ = data::elements.size();
693✔
47

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

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

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

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

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

66
  if (object_exists(group, "anomalous_real")) {
693!
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")) {
693!
73
    dset = open_dataset(rgroup, "anomalous_imag");
×
74
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
75
    close_dataset(dset);
×
76
  }
77
  close_group(rgroup);
693✔
78

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

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

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

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

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

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

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

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

137
    int j = 1;
5,539✔
138
    for (const auto& subshell : SUBSHELLS) {
41,609!
139
      if (designator == subshell) {
41,609✔
140
        shell_map[j] = i;
5,539✔
141
        shells_[i].index_subshell = j;
5,539✔
142
        break;
5,539✔
143
      }
144
      ++j;
36,070✔
145
    }
146
  }
147
  shell_map[0] = -1;
693✔
148

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

153
    // TODO: Move to ElectronSubshell constructor
154

155
    hid_t tgroup = open_group(rgroup, designator.c_str());
5,539✔
156

157
    // Read binding energy if atomic relaxation data is present
158
    if (attribute_exists(tgroup, "binding_energy")) {
5,539!
159
      has_atomic_relaxation_ = true;
5,539✔
160
      read_attribute(tgroup, "binding_energy", shell.binding_energy);
5,539✔
161
    }
162

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

170
    auto cross_section =
5,539✔
171
      cross_sections_.slice(tensor::range(static_cast<size_t>(shell.threshold),
5,539✔
172
                              cross_sections_.shape(0)),
173
        i);
11,078!
174
    cross_section = tensor::where(xs > 0, tensor::log(xs), 0);
16,617✔
175

176
    if (settings::atomic_relaxation && object_exists(tgroup, "transitions")) {
5,539✔
177
      // Determine dimensions of transitions
178
      dset = open_dataset(tgroup, "transitions");
3,139✔
179
      auto dims = object_shape(dset);
3,139✔
180
      close_dataset(dset);
3,139✔
181

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

187
        // Transition probability normalization
188
        double norm =
3,139✔
189
          tensor::Tensor<double>(matrix.slice(tensor::all, 3)).sum();
6,278✔
190

191
        shell.transitions.resize(n_transition);
3,139✔
192
        for (int j = 0; j < n_transition; ++j) {
167,648✔
193
          auto& transition = shell.transitions[j];
164,509✔
194
          transition.primary_subshell = shell_map.at(matrix(j, 0));
164,509✔
195
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
164,509✔
196
          transition.energy = matrix(j, 2);
164,509✔
197
          transition.probability = matrix(j, 3) / norm;
164,509✔
198
        }
199
      }
3,139✔
200
    }
3,139✔
201
    close_group(tgroup);
5,539✔
202
  }
11,078✔
203
  close_group(rgroup);
693✔
204

205
  // Check the maximum size of the atomic relaxation stack
206
  auto max_size = this->calc_max_stack_size();
693✔
207
  if (max_size > MAX_STACK_SIZE && mpi::master) {
693!
208
    warning(fmt::format("The subshell vacancy stack in atomic relaxation can "
×
209
                        "grow up to {}, but 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");
693✔
215

216
  // Read electron shell PDF and binding energies
217
  tensor::Tensor<double> electron_pdf;
693✔
218
  read_dataset(rgroup, "num_electrons", electron_pdf);
693✔
219
  electron_pdf /= electron_pdf.sum();
693✔
220
  electron_cdf_ = tensor::Tensor<double>(electron_pdf.shape());
1,386✔
221
  electron_cdf_(0) = electron_pdf(0);
693✔
222
  for (int i = 1; i < electron_pdf.shape()[0]; ++i)
4,654✔
223
    electron_cdf_(i) = electron_cdf_(i - 1) + electron_pdf(i);
3,961✔
224
  read_dataset(rgroup, "binding_energy", binding_energy_);
693✔
225

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

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

235
  // Map Compton subshell data to atomic relaxation data by finding the
236
  // subshell with the equivalent binding energy
237
  if (settings::atomic_relaxation && has_atomic_relaxation_) {
693!
238
    auto is_close = [](double a, double b) {
9,652✔
239
      return std::abs(a - b) / a < FP_REL_PRECISION;
8,974✔
240
    };
241
    subshell_map_ = tensor::Tensor<int>(binding_energy_.shape(), -1);
1,356✔
242
    for (int i = 0; i < binding_energy_.size(); ++i) {
4,987✔
243
      double E_b = binding_energy_[i];
4,309!
244
      if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) {
4,309!
245
        subshell_map_[i] = i;
3,570✔
246
      } else {
247
        for (int j = 0; j < n_shell; ++j) {
4,665!
248
          if (is_close(E_b, shells_[j].binding_energy)) {
4,665✔
249
            subshell_map_[i] = j;
739✔
250
            break;
739✔
251
          }
252
        }
253
      }
254
    }
255
  }
256

257
  // Create Compton profile CDF
258
  auto n_profile = data::compton_profile_pz.size();
693!
259
  auto n_shell_compton = profile_pdf_.shape(0);
693!
260
  profile_cdf_ = tensor::Tensor<double>({n_shell_compton, n_profile});
693✔
261
  for (int i = 0; i < n_shell_compton; ++i) {
5,347✔
262
    double c = 0.0;
4,654✔
263
    profile_cdf_(i, 0) = 0.0;
4,654✔
264
    for (int j = 0; j < n_profile - 1; ++j) {
144,274✔
265
      c += 0.5 *
139,620✔
266
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
139,620✔
267
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
139,620✔
268
      profile_cdf_(i, j + 1) = c;
139,620✔
269
    }
270
  }
271

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

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

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

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

295
    // Truncate the bremsstrahlung data at the cutoff energy
296
    int photon = ParticleType::photon().transport_index();
557✔
297
    const auto& E {electron_energy};
557✔
298
    double cutoff = settings::energy_cutoff[photon];
557✔
299
    if (cutoff > E(0)) {
557✔
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
      tensor::Tensor<double> dcs({n_e - i_grid, n_k});
11✔
309
      for (int i = 0; i < n_k; ++i) {
341✔
310
        double y = std::exp(
990✔
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
        tensor::View<double> col_i = dcs.slice(tensor::all, i);
330✔
314
        col_i(0) = y;
330✔
315
        for (int j = i_grid + 1; j < n_e; ++j) {
61,230✔
316
          col_i(j - i_grid) = dcs_(j, i);
60,900✔
317
        }
318
      }
330✔
319
      dcs_ = dcs;
11✔
320

321
      tensor::Tensor<double> frst({static_cast<size_t>(1)});
11✔
322
      frst(0) = cutoff;
11✔
323
      tensor::Tensor<double> rest(electron_energy.slice(
11✔
324
        tensor::range(i_grid + 1, electron_energy.size())));
11✔
325
      electron_energy = tensor::concatenate(frst, rest);
22✔
326
    }
33✔
327

328
    // Set incident particle energy grid
329
    if (data::ttb_e_grid.size() == 0) {
557✔
330
      data::ttb_e_grid = electron_energy;
264✔
331
    }
332

333
    // Calculate the radiative stopping power
334
    stopping_power_radiative_ =
557✔
335
      tensor::Tensor<double>({data::ttb_e_grid.size()});
557✔
336
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
111,798✔
337
      // Integrate over reduced photon energy
338
      double c = 0.0;
339
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,337,230✔
340
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
3,225,989✔
341
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
3,225,989✔
342
      }
343
      double e = data::ttb_e_grid(i);
111,241✔
344

345
      // Square of the ratio of the speed of light to the velocity of the
346
      // charged particle
347
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
111,241✔
348
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
111,241✔
349

350
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
111,241✔
351
    }
352
  }
557✔
353

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

371
PhotonInteraction::~PhotonInteraction()
693✔
372
{
373
  data::element_map.erase(name_);
693✔
374
}
13,167✔
375

376
int PhotonInteraction::calc_max_stack_size() const
693✔
377
{
378
  // Table to store solutions to sub-problems
379
  std::unordered_map<int, int> visited;
693✔
380

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

391
int PhotonInteraction::calc_helper(
319,029✔
392
  std::unordered_map<int, int>& visited, int i_shell) const
393
{
394
  // No transitions for this subshell, so this is the only shell in the stack
395
  const auto& shell {shells_[i_shell]};
319,029✔
396
  if (shell.transitions.empty()) {
319,029✔
397
    return 1;
398
  }
399

400
  // Check the table to see if the maximum stack size has already been
401
  // calculated for this shell
402
  auto it = visited.find(i_shell);
187,636✔
403
  if (it != visited.end()) {
187,636✔
404
    return it->second;
184,497✔
405
  }
406

407
  int max_size = 0;
3,139✔
408
  for (const auto& transition : shell.transitions) {
167,648✔
409
    // If this is a non-radiative transition two vacancies are created and
410
    // the stack grows by one; if this is a radiative transition only one
411
    // vacancy is created and the stack size stays the same
412
    int size = 0;
164,509✔
413
    if (transition.secondary_subshell != -1) {
164,509✔
414
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
148,981✔
415
    }
416
    size =
329,018✔
417
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
164,509✔
418
    max_size = std::max(max_size, size);
169,479✔
419
  }
420
  visited[i_shell] = max_size;
3,139✔
421
  return max_size;
3,139✔
422
}
423

424
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
29,619,642✔
425
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
426
{
427
  double form_factor_xmax = 0.0;
29,619,642✔
428
  int last_shell = binding_energy_.shape()[0] - 1;
29,619,642✔
429
  double E_b = binding_energy_(last_shell);
29,619,642✔
430
  double E = alpha * MASS_ELECTRON_EV;
29,619,642✔
431
  double mu_max = 1 - E_b / (alpha * (E - E_b));
29,619,642✔
432

433
  while (true) {
30,368,102✔
434
    // Sample Klein-Nishina distribution for trial energy and angle
435
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
30,368,102!
436

437
    // If in every angle we cannot eject an electron
438
    // Exit with no shell
439
    if (mu_max < -1) {
30,368,102!
NEW
440
      *i_shell = -1;
×
NEW
441
      return;
×
442
    }
443

444
    if (doppler) {
30,368,102!
445
      // Reject angles that cannot eject the most loosely bound electron
446
      if (*mu > mu_max)
30,368,102✔
447
        continue;
41,804✔
448
    }
449

450
    // Note that the parameter used here does not correspond exactly to the
451
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
452
    // parameter as defined by Hubbell, where the actual data comes from
453
    double x =
30,326,298✔
454
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
30,326,298✔
455

456
    // Calculate S(x, Z) and S(x_max, Z)
457
    double form_factor_x = incoherent_form_factor_(x);
30,326,298✔
458
    if (form_factor_xmax == 0.0) {
30,326,298✔
459
      form_factor_xmax =
29,619,642✔
460
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
29,619,642✔
461
    }
462

463
    // Perform rejection on form factor
464
    if (prn(seed) < form_factor_x / form_factor_xmax) {
30,326,298✔
465
      if (doppler) {
29,619,642!
466
        double E_out;
29,619,642✔
467
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
29,619,642✔
468
        *alpha_out = E_out / MASS_ELECTRON_EV;
29,619,642✔
469
      } else {
470
        *i_shell = -1;
×
471
      }
472
      break;
473
    }
474
  }
475
}
476

477
void PhotonInteraction::compton_doppler(
29,619,642✔
478
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
479
{
480
  auto n = data::compton_profile_pz.size();
29,619,642✔
481
  double E = alpha * MASS_ELECTRON_EV;
29,619,642✔
482
  int j_shell = 0;
29,619,642✔
483
  for (double E_b : binding_energy_) {
31,170,269!
484
    if ((E_b - (E - E_b) * alpha * (1.0 - mu)) < 0.0)
31,170,269✔
485
      break;
486
    ++j_shell;
1,550,627✔
487
  }
488

489
  double offset = 0.0;
29,619,642✔
490
  if (j_shell > 0)
29,619,642✔
491
    offset = electron_cdf_(j_shell - 1);
1,267,544✔
492

493
  int shell; // index for shell
29,619,675✔
494
  while (true) {
29,619,675✔
495
    // Sample electron shell
496
    double rn = offset + prn(seed) * (1.0 - offset);
29,619,675✔
497
    double c;
29,619,675✔
498
    for (shell = j_shell; shell < electron_cdf_.size(); ++shell) {
73,167,679!
499
      c = electron_cdf_(shell);
73,167,679✔
500
      if (rn < c)
73,167,679✔
501
        break;
502
    }
503

504
    // Determine binding energy of shell
505
    double E_b = binding_energy_(shell);
29,619,675✔
506

507
    // Determine p_z,max
508
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
59,239,350✔
509
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
29,619,675✔
510
    // Determine profile cdf value corresponding to p_z,max
511
    double c_max;
29,619,675✔
512
    if (pz_max > data::compton_profile_pz(n - 1)) {
29,619,675✔
513
      c_max = profile_cdf_(shell, n - 1);
1,577,536✔
514
    } else {
515
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
28,042,139✔
516
        data::compton_profile_pz.cend(), pz_max);
28,042,139✔
517
      double pz_l = data::compton_profile_pz(i);
28,042,139!
518
      double pz_r = data::compton_profile_pz(i + 1);
28,042,139✔
519
      double p_l = profile_pdf_(shell, i);
28,042,139✔
520
      double p_r = profile_pdf_(shell, i + 1);
28,042,139✔
521
      double c_l = profile_cdf_(shell, i);
28,042,139✔
522
      if (pz_l == pz_r) {
28,042,139!
523
        c_max = c_l;
524
      } else if (p_l == p_r) {
28,042,139✔
525
        c_max = c_l + (pz_max - pz_l) * p_l;
45,242✔
526
      } else {
527
        double m = (p_l - p_r) / (pz_l - pz_r);
27,996,897✔
528
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
27,996,897✔
529
                        (2.0 * m);
27,996,897✔
530
      }
531
    }
532

533
    // Sample value on bounded cdf
534
    c = prn(seed) * c_max;
29,619,675✔
535

536
    // Determine pz corresponding to sampled cdf value
537
    tensor::View<const double> cdf_shell = profile_cdf_.slice(shell);
29,619,675✔
538
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
29,619,675✔
539
    double pz_l = data::compton_profile_pz(i);
29,619,675!
540
    double pz_r = data::compton_profile_pz(i + 1);
29,619,675✔
541
    double p_l = profile_pdf_(shell, i);
29,619,675✔
542
    double p_r = profile_pdf_(shell, i + 1);
29,619,675✔
543
    double c_l = profile_cdf_(shell, i);
29,619,675✔
544
    double pz;
29,619,675✔
545
    if (pz_l == pz_r) {
29,619,675!
546
      pz = pz_l;
547
    } else if (p_l == p_r) {
29,619,675✔
548
      pz = pz_l + (c - c_l) / p_l;
2,306,788✔
549
    } else {
550
      double m = (p_l - p_r) / (pz_l - pz_r);
27,312,887✔
551
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
27,312,887✔
552
    }
553

554
    // Determine outgoing photon energy corresponding to electron momentum
555
    // (solve Eq. 39 in LA-UR-04-0487 for E')
556
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
29,619,675✔
557
    double f = 1.0 + alpha * (1.0 - mu);
29,619,675✔
558
    double a = momentum_sq - f * f;
29,619,675✔
559
    double b = 2.0 * E * (f - momentum_sq * mu);
29,619,675✔
560
    c = E * E * (momentum_sq - 1.0);
29,619,675✔
561

562
    double quad = b * b - 4.0 * a * c;
29,619,675✔
563
    if (quad < 0)
29,619,675✔
564
      continue;
11✔
565
    quad = std::sqrt(quad);
29,619,664✔
566
    double E_out1 = -(b + quad) / (2.0 * a);
29,619,664✔
567
    double E_out2 = -(b - quad) / (2.0 * a);
29,619,664✔
568

569
    // If no positive solution -- resample
570
    if (std::max(E_out1, E_out2) < 0)
29,619,664!
NEW
571
      continue;
×
572

573
    // Determine solution to quadratic equation that is positive
574
    if ((E_out1 > 0.0) && (E_out2 > 0.0)) {
29,619,664!
575
      *E_out = prn(seed) < 0.5 ? E_out1 : E_out2;
44,437,813✔
576
    } else {
577
      *E_out = std::max(E_out1, E_out2);
22✔
578
    }
579
    if (*E_out < E - E_b)
29,619,664✔
580
      break;
581
  }
33✔
582

583
  *i_shell = shell;
29,619,642✔
584
}
29,619,642✔
585

586
void PhotonInteraction::calculate_xs(Particle& p) const
82,873,939✔
587
{
588
  // Perform binary search on the element energy grid in order to determine
589
  // which points to interpolate between
590
  int n_grid = energy_.size();
82,873,939✔
591
  double log_E = std::log(p.E());
82,873,939✔
592
  int i_grid;
82,873,939✔
593
  if (log_E <= energy_[0]) {
82,873,939!
594
    i_grid = 0;
595
  } else if (log_E > energy_(n_grid - 1)) {
82,873,939!
596
    i_grid = n_grid - 2;
×
597
  } else {
598
    // We use upper_bound_index here because sometimes photons are created with
599
    // energies that exactly match a grid point
600
    i_grid = upper_bound_index(energy_.cbegin(), energy_.cend(), log_E);
82,873,939✔
601
  }
602

603
  // check for case where two energy points are the same
604
  if (energy_(i_grid) == energy_(i_grid + 1))
82,873,939!
605
    ++i_grid;
×
606

607
  // calculate interpolation factor
608
  double f =
82,873,939✔
609
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
82,873,939✔
610

611
  auto& xs {p.photon_xs(index_)};
82,873,939✔
612
  xs.index_grid = i_grid;
82,873,939✔
613
  xs.interp_factor = f;
82,873,939✔
614

615
  // Calculate microscopic coherent cross section
616
  xs.coherent = std::exp(
165,747,878✔
617
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
82,873,939✔
618

619
  // Calculate microscopic incoherent cross section
620
  xs.incoherent = std::exp(
165,747,878✔
621
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
82,873,939✔
622

623
  // Calculate microscopic photoelectric cross section
624
  xs.photoelectric = 0.0;
82,873,939✔
625
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
82,873,939✔
626
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
82,873,939✔
627

628
  for (int i = 0; i < xs_upper.size(); ++i)
959,327,252✔
629
    if (xs_lower(i) != 0)
396,789,687✔
630
      xs.photoelectric +=
386,287,964✔
631
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
386,287,964✔
632

633
  // Calculate microscopic pair production cross section
634
  xs.pair_production = std::exp(
165,747,878✔
635
    pair_production_total_(i_grid) +
82,873,939✔
636
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
82,873,939✔
637

638
  // Calculate microscopic total cross section
639
  xs.total =
82,873,939✔
640
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
82,873,939✔
641
  xs.last_E = p.E();
82,873,939✔
642
}
165,747,878✔
643

644
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
2,289,167✔
645
{
646
  double mu;
2,610,431✔
647
  while (true) {
2,931,695✔
648
    // Determine maximum value of x^2
649
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
2,610,431✔
650

651
    // Determine F(x^2_max, Z)
652
    double F_max = coherent_int_form_factor_(x2_max);
2,610,431✔
653

654
    // Sample cumulative distribution
655
    double F = prn(seed) * F_max;
2,610,431✔
656

657
    // Determine x^2 corresponding to F
658
    const auto& x {coherent_int_form_factor_.x()};
2,610,431✔
659
    const auto& y {coherent_int_form_factor_.y()};
2,610,431✔
660
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
2,610,431✔
661
    double r = (F - y[i]) / (y[i + 1] - y[i]);
2,610,431✔
662
    double x2 = x[i] + r * (x[i + 1] - x[i]);
2,610,431✔
663

664
    // Calculate mu
665
    mu = 1.0 - 2.0 * x2 / x2_max;
2,610,431✔
666

667
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
2,610,431✔
668
      break;
669
  }
321,264✔
670
  return mu;
2,289,167✔
671
}
672

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

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

701
  // Compute the high-energy Coulomb correction
702
  double a = Z_ / FINE_STRUCTURE;
95,461✔
703
  double c =
95,461✔
704
    a * a *
95,461✔
705
    (1.0 / (1.0 + a * a) + 0.202059 +
95,461✔
706
      a * a *
95,461✔
707
        (-0.03693 +
95,461✔
708
          a * a *
95,461✔
709
            (0.00835 +
95,461✔
710
              a * a *
95,461✔
711
                (-0.00201 +
95,461✔
712
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
95,461✔
713

714
  // The analytical approximation of the DCS underestimates the cross section
715
  // at low energies. The correction factor f compensates for this.
716
  double q = std::sqrt(2.0 / alpha);
95,461✔
717
  double f = q * (-0.1774 - 12.10 * a + 11.18 * a * a) +
95,461✔
718
             q * q * (8.523 + 73.26 * a - 44.41 * a * a) +
95,461✔
719
             q * q * q * (-13.52 - 121.1 * a + 96.41 * a * a) +
95,461✔
720
             q * q * q * q * (8.946 + 62.05 * a - 63.41 * a * a);
95,461✔
721

722
  // Calculate phi_1(1/2) and phi_2(1/2). The unnormalized PDF for the reduced
723
  // energy is given by p = 2*(1/2 - e)^2*phi_1(e) + phi_2(e), where phi_1 and
724
  // phi_2 are non-negative and maximum at e = 1/2.
725
  double b = 2.0 * r[Z_] / alpha;
95,461✔
726
  double t1 = 2.0 * std::log(1.0 + b * b);
95,461✔
727
  double t2 = b * std::atan(1.0 / b);
95,461✔
728
  double t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
95,461✔
729
  double t4 = 4.0 * std::log(r[Z_]) - 4.0 * c + f;
95,461✔
730
  double phi1_max = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
95,461✔
731
  double phi2_max = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
95,461✔
732

733
  // To aid sampling, the unnormalized PDF can be expressed as
734
  // p = u_1*U_1(e)*pi_1(e) + u_2*U_2(e)*pi_2(e), where pi_1 and pi_2 are
735
  // normalized PDFs on the interval (e_min, e_max) from which values of e can
736
  // be sampled using the inverse transform method, and
737
  // U_1 = phi_1(e)/phi_1(1/2) and U_2 = phi_2(e)/phi_2(1/2) are valid
738
  // rejection functions. The reduced energy can now be sampled using a
739
  // combination of the composition and rejection methods.
740
  double u1 = 2.0 / 3.0 * std::pow(0.5 - 1.0 / alpha, 2) * phi1_max;
95,461✔
741
  double u2 = phi2_max;
95,461✔
742
  double e;
119,695✔
743
  while (true) {
119,695✔
744
    double rn = prn(seed);
119,695✔
745

746
    // Sample the index i in (1, 2) using the point probabilities
747
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
748
    int i;
119,695✔
749
    if (prn(seed) < u1 / (u1 + u2)) {
119,695✔
750
      i = 1;
10,435✔
751

752
      // Sample e from pi_1 using the inverse transform method
753
      e = rn >= 0.5
10,435✔
754
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
10,435✔
755
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
5,425✔
756
    } else {
757
      i = 2;
109,260✔
758

759
      // Sample e from pi_2 using the inverse transform method
760
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
109,260✔
761
    }
762

763
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
764
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
119,695✔
765
    t1 = 2.0 * std::log(1.0 + b * b);
119,695✔
766
    t2 = b * std::atan(1.0 / b);
119,695✔
767
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
119,695✔
768
    if (i == 1) {
119,695✔
769
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
10,435✔
770
      if (prn(seed) <= phi1 / phi1_max)
10,435✔
771
        break;
772
    } else {
773
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
109,260✔
774
      if (prn(seed) <= phi2 / phi2_max)
109,260✔
775
        break;
776
    }
777
  }
778

779
  // Compute the kinetic energy of the electron and the positron
780
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
95,461✔
781
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
95,461✔
782

783
  // Sample the scattering angle of the electron. The cosine of the polar
784
  // angle of the direction relative to the incident photon is sampled from
785
  // p(mu) = C/(1 - beta*mu)^2 using the inverse transform method.
786
  double beta =
95,461✔
787
    std::sqrt(*E_electron * (*E_electron + 2.0 * MASS_ELECTRON_EV)) /
95,461✔
788
    (*E_electron + MASS_ELECTRON_EV);
95,461✔
789
  double rn = uniform_distribution(-1., 1., seed);
95,461✔
790
  *mu_electron = (rn + beta) / (rn * beta + 1.0);
95,461✔
791

792
  // Sample the scattering angle of the positron
793
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
95,461✔
794
         (*E_positron + MASS_ELECTRON_EV);
95,461✔
795
  rn = uniform_distribution(-1., 1., seed);
95,461✔
796
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
95,461✔
797
}
95,461✔
798

799
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
36,581,802✔
800
{
801
  // Return if no atomic relaxation data is present or if the binding energy is
802
  // larger than the incident particle energy
803
  if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E())
36,581,802!
UNCOV
804
    return;
×
805

806
  // Stack for unprocessed holes left by transitioning electrons
807
  int n_holes = 0;
36,581,802✔
808
  array<int, MAX_STACK_SIZE> holes;
36,581,802✔
809

810
  // Push the initial hole onto the stack
811
  holes[n_holes++] = i_shell;
36,581,802✔
812

813
  while (n_holes > 0) {
161,483,096✔
814
    // Pop the next hole off the stack
815
    int i_hole = holes[--n_holes];
124,901,294✔
816
    const auto& shell {shells_[i_hole]};
124,901,294✔
817

818
    // If no transitions, assume fluorescent photon from captured free electron
819
    if (shell.transitions.empty()) {
124,901,294✔
820
      Direction u = isotropic_direction(p.current_seed());
79,808,077✔
821
      double E = shell.binding_energy;
79,808,077✔
822
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
79,808,077✔
823
      continue;
79,808,077✔
824
    }
79,808,077✔
825

826
    // Sample transition
827
    double c = -prn(p.current_seed());
45,093,217✔
828
    int i_trans;
45,093,217✔
829
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,026,896,856!
830
      c += shell.transitions[i_trans].probability;
1,026,896,856✔
831
      if (c > 0)
1,026,896,856✔
832
        break;
833
    }
834
    const auto& transition = shell.transitions[i_trans];
45,093,217✔
835

836
    // Sample angle isotropically
837
    Direction u = isotropic_direction(p.current_seed());
45,093,217✔
838

839
    // Push the hole created by the electron transitioning to the photoelectron
840
    // hole onto the stack
841
    holes[n_holes++] = transition.primary_subshell;
45,093,217✔
842

843
    if (transition.secondary_subshell != -1) {
45,093,217✔
844
      // Non-radiative transition -- Auger/Coster-Kronig effect
845

846
      // Push the hole left by emitted auger electron onto the stack
847
      holes[n_holes++] = transition.secondary_subshell;
43,226,275✔
848

849
      // Create auger electron
850
      p.create_secondary(
43,226,275✔
851
        p.wgt(), u, transition.energy, ParticleType::electron());
43,226,275✔
852
    } else {
853
      // Radiative transition -- get X-ray energy
854

855
      // Create fluorescent photon
856
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
1,866,942✔
857
    }
858
  }
859
}
860

861
//==============================================================================
862
// Non-member functions
863
//==============================================================================
864

865
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
30,368,102✔
866
{
867
  double alpha_out, mu;
30,368,102✔
868
  double beta = 1.0 + 2.0 * alpha;
30,368,102✔
869
  if (alpha < 3.0) {
30,368,102✔
870
    // Kahn's rejection method
871
    double t = beta / (beta + 8.0);
29,623,772✔
872
    double x;
50,178,999✔
873
    while (true) {
50,178,999✔
874
      if (prn(seed) < t) {
50,178,999✔
875
        // Left branch of flow chart
876
        double r = uniform_distribution(0.0, 2.0, seed);
8,074,154✔
877
        x = 1.0 + alpha * r;
8,074,154✔
878
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
8,074,154✔
879
          mu = 1 - r;
4,329,620✔
880
          break;
4,329,620✔
881
        }
882
      } else {
883
        // Right branch of flow chart
884
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
42,104,845✔
885
        mu = 1.0 + (1.0 - x) / alpha;
42,104,845✔
886
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
42,104,845✔
887
          break;
888
      }
889
    }
890
    alpha_out = alpha / x;
29,623,772✔
891

892
  } else {
893
    // Koblinger's direct method
894
    double gamma = 1.0 - std::pow(beta, -2);
744,330✔
895
    double s =
744,330✔
896
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
744,330✔
897
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
744,330✔
898
    if (s <= 2.0 / alpha) {
744,330✔
899
      // For first term, x = 1 + 2ar
900
      // Therefore, a' = a/(1 + 2ar)
901
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
111,360✔
902
    } else if (s <= 4.0 / alpha) {
632,970✔
903
      // For third term, x = beta/(1 + 2ar)
904
      // Therefore, a' = a(1 + 2ar)/beta
905
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
109,646✔
906
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
523,324✔
907
      // For fourth term, x = 1/sqrt(1 - gamma*r)
908
      // Therefore, a' = a*sqrt(1 - gamma*r)
909
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
135,698✔
910
    } else {
911
      // For third term, x = beta^r
912
      // Therefore, a' = a/beta^r
913
      alpha_out = alpha / std::pow(beta, prn(seed));
387,626✔
914
    }
915

916
    // Calculate cosine of scattering angle based on basic relation
917
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
744,330✔
918
  }
919
  return {alpha_out, mu};
30,368,102✔
920
}
921

922
void free_memory_photon()
8,287✔
923
{
924
  data::elements.clear();
8,287✔
925
  data::compton_profile_pz.resize({0});
8,287✔
926
  data::ttb_e_grid.resize({0});
8,287✔
927
  data::ttb_k_grid.resize({0});
8,287✔
928
}
8,287✔
929

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