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

openmc-dev / openmc / 23024185923

12 Mar 2026 09:12PM UTC coverage: 81.566% (+0.003%) from 81.563%
23024185923

Pull #3874

github

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

17561 of 25274 branches covered (69.48%)

Branch coverage included in aggregate %.

16 of 20 new or added lines in 1 file covered. (80.0%)

37 existing lines in 3 files now uncovered.

57965 of 67321 relevant lines covered (86.1%)

45206682.22 hits per line

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

89.55
/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
  read_dataset(rgroup, "num_electrons", electron_pdf_);
693✔
218
  electron_pdf_ /= electron_pdf_.sum();
693✔
219
  read_dataset(rgroup, "binding_energy", binding_energy_);
693✔
220

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

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

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

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

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

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

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

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

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

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

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

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

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

328
    // Calculate the radiative stopping power
329
    stopping_power_radiative_ =
557✔
330
      tensor::Tensor<double>({data::ttb_e_grid.size()});
557✔
331
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
111,799✔
332
      // Integrate over reduced photon energy
333
      double c = 0.0;
334
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,337,260✔
335
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
3,226,018✔
336
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
3,226,018✔
337
      }
338
      double e = data::ttb_e_grid(i);
111,242✔
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) /
111,242✔
343
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
111,242✔
344

345
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
111,242✔
346
    }
347
  }
557✔
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);
693✔
355
  energy_ = tensor::log(energy_);
693✔
356
  coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0);
2,079✔
357
  incoherent_ =
693✔
358
    tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0);
2,079✔
359
  photoelectric_total_ = tensor::where(
1,386✔
360
    photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0);
2,079✔
361
  pair_production_total_ = tensor::where(pair_production_total_ > limit,
2,079✔
362
    tensor::log(pair_production_total_), -900.0);
1,386✔
363
  heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0);
2,772✔
364
}
693✔
365

366
PhotonInteraction::~PhotonInteraction()
693✔
367
{
368
  data::element_map.erase(name_);
693✔
369
}
13,167✔
370

371
int PhotonInteraction::calc_max_stack_size() const
693✔
372
{
373
  // Table to store solutions to sub-problems
374
  std::unordered_map<int, int> visited;
693✔
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;
693✔
380
  for (int i_shell = 0; i_shell < shells_.size(); ++i_shell) {
6,232✔
381
    max_size = std::max(max_size, this->calc_helper(visited, i_shell));
6,232✔
382
  }
383
  return max_size;
693✔
384
}
693✔
385

386
int PhotonInteraction::calc_helper(
319,029✔
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]};
319,029✔
391
  if (shell.transitions.empty()) {
319,029✔
392
    return 1;
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);
187,636✔
398
  if (it != visited.end()) {
187,636✔
399
    return it->second;
184,497✔
400
  }
401

402
  int max_size = 0;
3,139✔
403
  for (const auto& transition : shell.transitions) {
167,648✔
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;
164,509✔
408
    if (transition.secondary_subshell != -1) {
164,509✔
409
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
148,981✔
410
    }
411
    size =
329,018✔
412
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
164,509✔
413
    max_size = std::max(max_size, size);
169,479✔
414
  }
415
  visited[i_shell] = max_size;
3,139✔
416
  return max_size;
3,139✔
417
}
418

419
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
30,491,699✔
420
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
421
{
422
  double form_factor_xmax = 0.0;
30,491,699✔
423
  int last_shell = binding_energy_.shape()[0] - 1;
30,491,699✔
424
  double E_b = binding_energy_(last_shell);
30,491,699✔
425
  double E = alpha * MASS_ELECTRON_EV;
30,491,699✔
426
  double mu_max = 1 - E_b / (alpha * (E - E_b));
30,491,699✔
427

428
  while (true) {
31,263,565✔
429
    // Sample Klein-Nishina distribution for trial energy and angle
430
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
31,263,565!
431

432
    // If in every angle we cannot eject an electron
433
    // Exit with no shell
434
    if (mu_max < -1) {
31,263,565!
NEW
435
      *i_shell = -1;
×
NEW
436
      return;
×
437
    }
438

439
    if (doppler) {
31,263,565!
440
      // Reject angles that cannot eject the most loosely bound electron
441
      if (*mu > mu_max)
31,263,565✔
442
        continue;
43,494✔
443
    }
444

445
    // Note that the parameter used here does not correspond exactly to the
446
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
447
    // parameter as defined by Hubbell, where the actual data comes from
448
    double x =
31,220,071✔
449
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
31,220,071✔
450

451
    // Calculate S(x, Z) and S(x_max, Z)
452
    double form_factor_x = incoherent_form_factor_(x);
31,220,071✔
453
    if (form_factor_xmax == 0.0) {
31,220,071✔
454
      form_factor_xmax =
30,491,699✔
455
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
30,491,699✔
456
    }
457

458
    // Perform rejection on form factor
459
    if (prn(seed) < form_factor_x / form_factor_xmax) {
31,220,071✔
460
      if (doppler) {
30,491,699!
461
        double E_out;
30,491,699✔
462
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
30,491,699✔
463
        *alpha_out = E_out / MASS_ELECTRON_EV;
30,491,699✔
464
      } else {
465
        *i_shell = -1;
×
466
      }
467
      break;
468
    }
469
  }
470
}
471

472
void PhotonInteraction::compton_doppler(
30,491,699✔
473
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
474
{
475
  auto n = data::compton_profile_pz.size();
30,491,699✔
476

477
  int shell; // index for shell
30,890,883✔
478
  while (true) {
30,890,883✔
479
    // Sample electron shell
480
    double rn = prn(seed);
30,890,883✔
481
    double c = 0.0;
30,890,883✔
482
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
77,127,889!
483
      c += electron_pdf_(shell);
77,127,889✔
484
      if (rn < c)
77,127,889✔
485
        break;
486
    }
487

488
    // Determine binding energy of shell
489
    double E_b = binding_energy_(shell);
30,890,883✔
490

491
    // Determine p_z,max
492
    double E = alpha * MASS_ELECTRON_EV;
30,890,883✔
493
    if (E < E_b) {
30,890,883✔
494
      continue;
1,496✔
495
    }
496

497
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
61,778,774✔
498
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
30,889,387✔
499
    if (pz_max < 0.0)
30,889,387✔
500
      continue;
397,666✔
501
    // Determine profile cdf value corresponding to p_z,max
502
    double c_max;
30,491,721✔
503
    if (pz_max > data::compton_profile_pz(n - 1)) {
30,491,721✔
504
      c_max = profile_cdf_(shell, n - 1);
1,606,088✔
505
    } else {
506
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
28,885,633✔
507
        data::compton_profile_pz.cend(), pz_max);
28,885,633✔
508
      double pz_l = data::compton_profile_pz(i);
28,885,633!
509
      double pz_r = data::compton_profile_pz(i + 1);
28,885,633✔
510
      double p_l = profile_pdf_(shell, i);
28,885,633✔
511
      double p_r = profile_pdf_(shell, i + 1);
28,885,633✔
512
      double c_l = profile_cdf_(shell, i);
28,885,633✔
513
      if (pz_l == pz_r) {
28,885,633!
514
        c_max = c_l;
515
      } else if (p_l == p_r) {
28,885,633✔
516
        c_max = c_l + (pz_max - pz_l) * p_l;
46,823✔
517
      } else {
518
        double m = (p_l - p_r) / (pz_l - pz_r);
28,838,810✔
519
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
28,838,810✔
520
                        (2.0 * m);
28,838,810✔
521
      }
522
    }
523

524
    // Sample value on bounded cdf
525
    c = prn(seed) * c_max;
30,491,721✔
526

527
    // Determine pz corresponding to sampled cdf value
528
    tensor::View<const double> cdf_shell = profile_cdf_.slice(shell);
30,491,721✔
529
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
30,491,721✔
530
    double pz_l = data::compton_profile_pz(i);
30,491,721!
531
    double pz_r = data::compton_profile_pz(i + 1);
30,491,721✔
532
    double p_l = profile_pdf_(shell, i);
30,491,721✔
533
    double p_r = profile_pdf_(shell, i + 1);
30,491,721✔
534
    double c_l = profile_cdf_(shell, i);
30,491,721✔
535
    double pz;
30,491,721✔
536
    if (pz_l == pz_r) {
30,491,721!
537
      pz = pz_l;
538
    } else if (p_l == p_r) {
30,491,721✔
539
      pz = pz_l + (c - c_l) / p_l;
2,382,014✔
540
    } else {
541
      double m = (p_l - p_r) / (pz_l - pz_r);
28,109,707✔
542
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
28,109,707✔
543
    }
544

545
    // Determine outgoing photon energy corresponding to electron momentum
546
    // (solve Eq. 39 in LA-UR-04-0487 for E')
547
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
30,491,721✔
548
    double f = 1.0 + alpha * (1.0 - mu);
30,491,721✔
549
    double a = momentum_sq - f * f;
30,491,721✔
550
    double b = 2.0 * E * (f - momentum_sq * mu);
30,491,721✔
551
    c = E * E * (momentum_sq - 1.0);
30,491,721✔
552

553
    double quad = b * b - 4.0 * a * c;
30,491,721✔
554
    if (quad < 0)
30,491,721✔
555
      continue;
11✔
556
    quad = std::sqrt(quad);
30,491,710✔
557
    double E_out1 = -(b + quad) / (2.0 * a);
30,491,710✔
558
    double E_out2 = -(b - quad) / (2.0 * a);
30,491,710✔
559

560
    // If no positive solution -- resample
561
    if (std::max(E_out1, E_out2) < 0)
30,491,710!
NEW
562
      continue;
×
563

564
    // Determine solution to quadratic equation that is positive
565
    if ((E_out1 > 0.0) && (E_out2 > 0.0)) {
30,491,710!
566
      *E_out = prn(seed) < 0.5 ? E_out1 : E_out2;
45,746,969✔
567
    } else {
NEW
568
      *E_out = std::max(E_out1, E_out2);
×
569
    }
570
    if (*E_out < E - E_b)
30,491,710✔
571
      break;
572
  }
399,184✔
573

574
  *i_shell = shell;
30,491,699✔
575
}
30,491,699✔
576

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

594
  // check for case where two energy points are the same
595
  if (energy_(i_grid) == energy_(i_grid + 1))
85,211,241!
596
    ++i_grid;
×
597

598
  // calculate interpolation factor
599
  double f =
85,211,241✔
600
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
85,211,241✔
601

602
  auto& xs {p.photon_xs(index_)};
85,211,241✔
603
  xs.index_grid = i_grid;
85,211,241✔
604
  xs.interp_factor = f;
85,211,241✔
605

606
  // Calculate microscopic coherent cross section
607
  xs.coherent = std::exp(
170,422,482✔
608
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
85,211,241✔
609

610
  // Calculate microscopic incoherent cross section
611
  xs.incoherent = std::exp(
170,422,482✔
612
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
85,211,241✔
613

614
  // Calculate microscopic photoelectric cross section
615
  xs.photoelectric = 0.0;
85,211,241✔
616
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
85,211,241✔
617
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
85,211,241✔
618

619
  for (int i = 0; i < xs_upper.size(); ++i)
979,276,140✔
620
    if (xs_lower(i) != 0)
404,426,829✔
621
      xs.photoelectric +=
393,913,497✔
622
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
393,913,497✔
623

624
  // Calculate microscopic pair production cross section
625
  xs.pair_production = std::exp(
170,422,482✔
626
    pair_production_total_(i_grid) +
85,211,241✔
627
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
85,211,241✔
628

629
  // Calculate microscopic total cross section
630
  xs.total =
85,211,241✔
631
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
85,211,241✔
632
  xs.last_E = p.E();
85,211,241✔
633
}
170,422,482✔
634

635
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
2,382,787✔
636
{
637
  double mu;
2,720,398✔
638
  while (true) {
3,058,009✔
639
    // Determine maximum value of x^2
640
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
2,720,398✔
641

642
    // Determine F(x^2_max, Z)
643
    double F_max = coherent_int_form_factor_(x2_max);
2,720,398✔
644

645
    // Sample cumulative distribution
646
    double F = prn(seed) * F_max;
2,720,398✔
647

648
    // Determine x^2 corresponding to F
649
    const auto& x {coherent_int_form_factor_.x()};
2,720,398✔
650
    const auto& y {coherent_int_form_factor_.y()};
2,720,398✔
651
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
2,720,398✔
652
    double r = (F - y[i]) / (y[i + 1] - y[i]);
2,720,398✔
653
    double x2 = x[i] + r * (x[i + 1] - x[i]);
2,720,398✔
654

655
    // Calculate mu
656
    mu = 1.0 - 2.0 * x2 / x2_max;
2,720,398✔
657

658
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
2,720,398✔
659
      break;
660
  }
337,611✔
661
  return mu;
2,382,787✔
662
}
663

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

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

692
  // Compute the high-energy Coulomb correction
693
  double a = Z_ / FINE_STRUCTURE;
96,169✔
694
  double c =
96,169✔
695
    a * a *
96,169✔
696
    (1.0 / (1.0 + a * a) + 0.202059 +
96,169✔
697
      a * a *
96,169✔
698
        (-0.03693 +
96,169✔
699
          a * a *
96,169✔
700
            (0.00835 +
96,169✔
701
              a * a *
96,169✔
702
                (-0.00201 +
96,169✔
703
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
96,169✔
704

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

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

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

737
    // Sample the index i in (1, 2) using the point probabilities
738
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
739
    int i;
120,565✔
740
    if (prn(seed) < u1 / (u1 + u2)) {
120,565✔
741
      i = 1;
10,898✔
742

743
      // Sample e from pi_1 using the inverse transform method
744
      e = rn >= 0.5
10,898✔
745
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
10,898✔
746
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
5,530✔
747
    } else {
748
      i = 2;
109,667✔
749

750
      // Sample e from pi_2 using the inverse transform method
751
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
109,667✔
752
    }
753

754
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
755
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
120,565✔
756
    t1 = 2.0 * std::log(1.0 + b * b);
120,565✔
757
    t2 = b * std::atan(1.0 / b);
120,565✔
758
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
120,565✔
759
    if (i == 1) {
120,565✔
760
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
10,898✔
761
      if (prn(seed) <= phi1 / phi1_max)
10,898✔
762
        break;
763
    } else {
764
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
109,667✔
765
      if (prn(seed) <= phi2 / phi2_max)
109,667✔
766
        break;
767
    }
768
  }
769

770
  // Compute the kinetic energy of the electron and the positron
771
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
96,169✔
772
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
96,169✔
773

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

783
  // Sample the scattering angle of the positron
784
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
96,169✔
785
         (*E_positron + MASS_ELECTRON_EV);
96,169✔
786
  rn = uniform_distribution(-1., 1., seed);
96,169✔
787
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
96,169✔
788
}
96,169✔
789

790
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
37,617,366✔
791
{
792
  // Return if no atomic relaxation data is present or if the binding energy is
793
  // larger than the incident particle energy
794
  if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E())
37,617,366!
UNCOV
795
    return;
×
796

797
  // Stack for unprocessed holes left by transitioning electrons
798
  int n_holes = 0;
37,617,366✔
799
  array<int, MAX_STACK_SIZE> holes;
37,617,366✔
800

801
  // Push the initial hole onto the stack
802
  holes[n_holes++] = i_shell;
37,617,366✔
803

804
  while (n_holes > 0) {
164,400,473✔
805
    // Pop the next hole off the stack
806
    int i_hole = holes[--n_holes];
126,783,107✔
807
    const auto& shell {shells_[i_hole]};
126,783,107✔
808

809
    // If no transitions, assume fluorescent photon from captured free electron
810
    if (shell.transitions.empty()) {
126,783,107✔
811
      Direction u = isotropic_direction(p.current_seed());
81,265,798✔
812
      double E = shell.binding_energy;
81,265,798✔
813
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
81,265,798✔
814
      continue;
81,265,798✔
815
    }
81,265,798✔
816

817
    // Sample transition
818
    double c = -prn(p.current_seed());
45,517,309✔
819
    int i_trans;
45,517,309✔
820
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,030,668,104!
821
      c += shell.transitions[i_trans].probability;
1,030,668,104✔
822
      if (c > 0)
1,030,668,104✔
823
        break;
824
    }
825
    const auto& transition = shell.transitions[i_trans];
45,517,309✔
826

827
    // Sample angle isotropically
828
    Direction u = isotropic_direction(p.current_seed());
45,517,309✔
829

830
    // Push the hole created by the electron transitioning to the photoelectron
831
    // hole onto the stack
832
    holes[n_holes++] = transition.primary_subshell;
45,517,309✔
833

834
    if (transition.secondary_subshell != -1) {
45,517,309✔
835
      // Non-radiative transition -- Auger/Coster-Kronig effect
836

837
      // Push the hole left by emitted auger electron onto the stack
838
      holes[n_holes++] = transition.secondary_subshell;
43,648,432✔
839

840
      // Create auger electron
841
      p.create_secondary(
43,648,432✔
842
        p.wgt(), u, transition.energy, ParticleType::electron());
43,648,432✔
843
    } else {
844
      // Radiative transition -- get X-ray energy
845

846
      // Create fluorescent photon
847
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
1,868,877✔
848
    }
849
  }
850
}
851

852
//==============================================================================
853
// Non-member functions
854
//==============================================================================
855

856
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
31,263,565✔
857
{
858
  double alpha_out, mu;
31,263,565✔
859
  double beta = 1.0 + 2.0 * alpha;
31,263,565✔
860
  if (alpha < 3.0) {
31,263,565✔
861
    // Kahn's rejection method
862
    double t = beta / (beta + 8.0);
30,511,461✔
863
    double x;
51,640,003✔
864
    while (true) {
51,640,003✔
865
      if (prn(seed) < t) {
51,640,003✔
866
        // Left branch of flow chart
867
        double r = uniform_distribution(0.0, 2.0, seed);
8,283,757✔
868
        x = 1.0 + alpha * r;
8,283,757✔
869
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
8,283,757✔
870
          mu = 1 - r;
4,425,404✔
871
          break;
4,425,404✔
872
        }
873
      } else {
874
        // Right branch of flow chart
875
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
43,356,246✔
876
        mu = 1.0 + (1.0 - x) / alpha;
43,356,246✔
877
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
43,356,246✔
878
          break;
879
      }
880
    }
881
    alpha_out = alpha / x;
30,511,461✔
882

883
  } else {
884
    // Koblinger's direct method
885
    double gamma = 1.0 - std::pow(beta, -2);
752,104✔
886
    double s =
752,104✔
887
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
752,104✔
888
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
752,104✔
889
    if (s <= 2.0 / alpha) {
752,104✔
890
      // For first term, x = 1 + 2ar
891
      // Therefore, a' = a/(1 + 2ar)
892
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
113,103✔
893
    } else if (s <= 4.0 / alpha) {
639,001✔
894
      // For third term, x = beta/(1 + 2ar)
895
      // Therefore, a' = a(1 + 2ar)/beta
896
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
111,157✔
897
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
527,844✔
898
      // For fourth term, x = 1/sqrt(1 - gamma*r)
899
      // Therefore, a' = a*sqrt(1 - gamma*r)
900
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
137,433✔
901
    } else {
902
      // For third term, x = beta^r
903
      // Therefore, a' = a/beta^r
904
      alpha_out = alpha / std::pow(beta, prn(seed));
390,411✔
905
    }
906

907
    // Calculate cosine of scattering angle based on basic relation
908
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
752,104✔
909
  }
910
  return {alpha_out, mu};
31,263,565✔
911
}
912

913
void free_memory_photon()
8,287✔
914
{
915
  data::elements.clear();
8,287✔
916
  data::compton_profile_pz.resize({0});
8,287✔
917
  data::ttb_e_grid.resize({0});
8,287✔
918
  data::ttb_k_grid.resize({0});
8,287✔
919
}
8,287✔
920

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