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

openmc-dev / openmc / 23865126087

01 Apr 2026 06:47PM UTC coverage: 81.002% (-0.3%) from 81.326%
23865126087

Pull #3874

github

web-flow
Merge 0310503c1 into 8223099ed
Pull Request #3874: Fix a bug in compton scattering shell selection

17058 of 24534 branches covered (69.53%)

Branch coverage included in aggregate %.

22 of 23 new or added lines in 1 file covered. (95.65%)

400 existing lines in 29 files now uncovered.

57113 of 67033 relevant lines covered (85.2%)

37678292.29 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

110
  // Read heating
111
  if (object_exists(group, "heating")) {
484!
112
    rgroup = open_group(group, "heating");
484✔
113
    read_dataset(rgroup, "xs", heating_);
484✔
114
    close_group(rgroup);
484✔
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");
484✔
121
  vector<std::string> designators;
484✔
122
  read_attribute(rgroup, "designators", designators);
484✔
123
  auto n_shell = designators.size();
484!
124
  if (n_shell == 0) {
484!
125
    throw std::runtime_error {
×
126
      "Photoatomic data for " + name_ + " does not have subshell data."};
×
127
  }
128

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

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

137
    int j = 1;
3,832✔
138
    for (const auto& subshell : SUBSHELLS) {
28,432!
139
      if (designator == subshell) {
28,432✔
140
        shell_map[j] = i;
3,832✔
141
        shells_[i].index_subshell = j;
3,832✔
142
        break;
3,832✔
143
      }
144
      ++j;
24,600✔
145
    }
146
  }
147
  shell_map[0] = -1;
484✔
148

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

153
    // TODO: Move to ElectronSubshell constructor
154

155
    hid_t tgroup = open_group(rgroup, designator.c_str());
3,832✔
156

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

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

170
    auto cross_section =
3,832✔
171
      cross_sections_.slice(tensor::range(static_cast<size_t>(shell.threshold),
3,832✔
172
                              cross_sections_.shape(0)),
173
        i);
7,664!
174
    cross_section = tensor::where(xs > 0, tensor::log(xs), 0);
11,496✔
175

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

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

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

191
        shell.transitions.resize(n_transition);
2,172✔
192
        for (int j = 0; j < n_transition; ++j) {
113,874✔
193
          auto& transition = shell.transitions[j];
111,702✔
194
          transition.primary_subshell = shell_map.at(matrix(j, 0));
111,702✔
195
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
111,702✔
196
          transition.energy = matrix(j, 2);
111,702✔
197
          transition.probability = matrix(j, 3) / norm;
111,702✔
198
        }
199
      }
2,172✔
200
    }
2,172✔
201
    close_group(tgroup);
3,832✔
202
  }
7,664✔
203
  close_group(rgroup);
484✔
204

205
  // Check the maximum size of the atomic relaxation stack
206
  auto max_size = this->calc_max_stack_size();
484✔
207
  if (max_size > MAX_STACK_SIZE && mpi::master) {
484!
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");
484✔
215

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

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

224
  // Get Compton profile momentum grid
225
  if (data::compton_profile_pz.size() == 0) {
484✔
226
    read_dataset(rgroup, "pz", data::compton_profile_pz);
216✔
227
  }
228
  close_group(rgroup);
484✔
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_) {
484!
233
    auto is_close = [](double a, double b) {
6,756✔
234
      return std::abs(a - b) / a < FP_REL_PRECISION;
6,282✔
235
    };
236
    subshell_map_ = tensor::Tensor<int>(binding_energy_.shape(), -1);
948✔
237
    for (int i = 0; i < binding_energy_.size(); ++i) {
3,456✔
238
      double E_b = binding_energy_[i];
2,982!
239
      if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) {
2,982!
240
        subshell_map_[i] = i;
2,460✔
241
      } else {
242
        for (int j = 0; j < n_shell; ++j) {
3,300!
243
          if (is_close(E_b, shells_[j].binding_energy)) {
3,300✔
244
            subshell_map_[i] = j;
522✔
245
            break;
522✔
246
          }
247
        }
248
      }
249
    }
250
  }
251

252
  // Create Compton profile CDF
253
  auto n_profile = data::compton_profile_pz.size();
484!
254
  auto n_shell_compton = profile_pdf_.shape(0);
484!
255
  profile_cdf_ = tensor::Tensor<double>({n_shell_compton, n_profile});
484✔
256
  for (int i = 0; i < n_shell_compton; ++i) {
3,696✔
257
    double c = 0.0;
3,212✔
258
    profile_cdf_(i, 0) = 0.0;
3,212✔
259
    for (int j = 0; j < n_profile - 1; ++j) {
99,572✔
260
      c += 0.5 *
96,360✔
261
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
96,360✔
262
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
96,360✔
263
      profile_cdf_(i, j + 1) = c;
96,360✔
264
    }
265
  }
266

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

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

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

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

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

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

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

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

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

328
    // Calculate the radiative stopping power
329
    stopping_power_radiative_ =
386✔
330
      tensor::Tensor<double>({data::ttb_e_grid.size()});
386✔
331
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
77,489✔
332
      // Integrate over reduced photon energy
333
      double c = 0.0;
334
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
2,313,090✔
335
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
2,235,987✔
336
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
2,235,987✔
337
      }
338
      double e = data::ttb_e_grid(i);
77,103✔
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) /
77,103✔
343
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
77,103✔
344

345
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
77,103✔
346
    }
347
  }
386✔
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);
484✔
355
  energy_ = tensor::log(energy_);
484✔
356
  coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0);
1,452✔
357
  incoherent_ =
484✔
358
    tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0);
1,452✔
359
  photoelectric_total_ = tensor::where(
968✔
360
    photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0);
1,452✔
361
  pair_production_total_ = tensor::where(pair_production_total_ > limit,
1,452✔
362
    tensor::log(pair_production_total_), -900.0);
968✔
363
  heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0);
1,936✔
364
}
484✔
365

366
PhotonInteraction::~PhotonInteraction()
484✔
367
{
368
  data::element_map.erase(name_);
484✔
369
}
9,196✔
370

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

386
int PhotonInteraction::calc_helper(
216,562✔
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]};
216,562✔
391
  if (shell.transitions.empty()) {
216,562✔
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);
127,338✔
398
  if (it != visited.end()) {
127,338✔
399
    return it->second;
125,166✔
400
  }
401

402
  int max_size = 0;
2,172✔
403
  for (const auto& transition : shell.transitions) {
113,874✔
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;
111,702✔
408
    if (transition.secondary_subshell != -1) {
111,702✔
409
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
101,028✔
410
    }
411
    size =
223,404✔
412
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
111,702✔
413
    max_size = std::max(max_size, size);
115,142✔
414
  }
415
  visited[i_shell] = max_size;
2,172✔
416
  return max_size;
2,172✔
417
}
418

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

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

433
    // Calculate S(x, Z) and S(x_max, Z)
434
    double form_factor_x = incoherent_form_factor_(x);
18,011,858✔
435
    if (form_factor_xmax == 0.0) {
18,011,858✔
436
      form_factor_xmax =
17,564,056✔
437
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
17,564,056✔
438
    }
439

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

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

459
  int shell; // index for shell
24,379,496✔
460
  while (true) {
24,379,496✔
461
    // Sample electron shell
462
    double rn = prn(seed);
24,379,496✔
463
    double c = 0.0;
24,379,496✔
464
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
68,805,438!
465
      c += electron_pdf_(shell);
68,805,438✔
466
      if (rn < c)
68,805,438✔
467
        break;
468
    }
469
    double E = alpha * MASS_ELECTRON_EV;
24,379,496✔
470

471
    // Determine binding energy of shell
472
    double E_b = binding_energy_(shell);
24,379,496✔
473

474
    // Resample if photon energy insufficient
475
    if (E < E_b)
24,379,496✔
476
      continue;
1,024✔
477

478
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
24,378,472✔
479
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
24,378,472✔
480

481
    // Determine profile cdf value corresponding to p_z,max
482
    double c_max;
24,378,472✔
483
    if (std::abs(pz_max) > data::compton_profile_pz(n - 1)) {
24,378,472✔
484
      c_max = profile_cdf_(shell, n - 1);
4,411,624✔
485
    } else {
486
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
19,966,848✔
487
        data::compton_profile_pz.cend(), std::abs(pz_max));
19,966,848✔
488
      double pz_l = data::compton_profile_pz(i);
19,966,848!
489
      double pz_r = data::compton_profile_pz(i + 1);
19,966,848✔
490
      double p_l = profile_pdf_(shell, i);
19,966,848✔
491
      double p_r = profile_pdf_(shell, i + 1);
19,966,848✔
492
      double c_l = profile_cdf_(shell, i);
19,966,848✔
493
      if (pz_l == pz_r) {
19,966,848!
494
        c_max = c_l;
495
      } else if (p_l == p_r) {
19,966,848✔
496
        c_max = c_l + (std::abs(pz_max) - pz_l) * p_l;
54,106✔
497
      } else {
498
        double m = (p_l - p_r) / (pz_l - pz_r);
19,912,742✔
499
        c_max = c_l + (std::pow((m * (std::abs(pz_max) - pz_l) + p_l), 2) -
19,912,742✔
500
                        p_l * p_l) /
19,912,742✔
501
                        (2.0 * m);
19,912,742✔
502
      }
503
    }
504

505
    // Sample value on bounded cdf
506
    if (pz_max > 0.0) {
24,378,472✔
507
      c = prn(seed) * c_max;
24,153,026✔
508
    } else {
509
      c = 1.0 + (c_max - 1.0) * prn(seed);
225,446✔
510
    }
511

512
    // Determine pz corresponding to sampled cdf value
513
    tensor::View<const double> cdf_shell = profile_cdf_.slice(shell);
24,378,472✔
514
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
24,378,472✔
515
    double pz_l = data::compton_profile_pz(i);
24,378,472!
516
    double pz_r = data::compton_profile_pz(i + 1);
24,378,472✔
517
    double p_l = profile_pdf_(shell, i);
24,378,472✔
518
    double p_r = profile_pdf_(shell, i + 1);
24,378,472✔
519
    double c_l = profile_cdf_(shell, i);
24,378,472✔
520
    double pz;
24,378,472✔
521
    if (pz_l == pz_r) {
24,378,472!
522
      pz = pz_l;
523
    } else if (p_l == p_r) {
24,378,472✔
524
      pz = pz_l + (c - c_l) / p_l;
1,912,063✔
525
    } else {
526
      double m = (p_l - p_r) / (pz_l - pz_r);
22,466,409✔
527
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
22,466,409✔
528
    }
529

530
    // Determine outgoing photon energy corresponding to electron momentum
531
    // (solve Eq. 39 in LA-UR-04-0487 for E')
532
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
24,378,472✔
533
    double f = 1.0 + alpha * (1.0 - mu);
24,378,472✔
534
    double a = momentum_sq - f * f;
24,378,472✔
535
    double b = 2.0 * (f - momentum_sq * mu);
24,378,472✔
536
    c = (momentum_sq - 1.0);
24,378,472✔
537
    double quad = b * b - 4.0 * a * c;
24,378,472✔
538
    if (quad < 0)
24,378,472✔
539
      continue;
4,158✔
540
    quad = std::sqrt(quad);
24,374,314✔
541
    double E_out1 = -(b + quad) / (2.0 * a) * E;
24,374,314✔
542
    double E_out2 = -(b - quad) / (2.0 * a) * E;
24,374,314✔
543
    // Determine solution to quadratic equation that is positive and minimal
544
    if ((E_out1 < 0.0) && (E_out2 < 0.0))
24,374,314!
NEW
545
      continue;
×
546
    if ((E_out1 > 0.0) && (E_out2 > 0.0))
24,374,314!
547
      *E_out = std::min(E_out1, E_out2);
48,452,209✔
548
    else
549
      *E_out = std::max(E_out1, E_out2);
148,188!
550

551
    if (prn(seed) * E <= *E_out)
24,374,314✔
552
      break;
553
  }
6,815,440✔
554

555
  *i_shell = shell;
17,564,056✔
556
}
17,564,056✔
557

558
void PhotonInteraction::calculate_xs(Particle& p) const
52,115,196✔
559
{
560
  // Perform binary search on the element energy grid in order to determine
561
  // which points to interpolate between
562
  int n_grid = energy_.size();
52,115,196✔
563
  double log_E = std::log(p.E());
52,115,196✔
564
  int i_grid;
52,115,196✔
565
  if (log_E <= energy_[0]) {
52,115,196!
566
    i_grid = 0;
567
  } else if (log_E > energy_(n_grid - 1)) {
52,115,196!
568
    i_grid = n_grid - 2;
×
569
  } else {
570
    // We use upper_bound_index here because sometimes photons are created with
571
    // energies that exactly match a grid point
572
    i_grid = upper_bound_index(energy_.cbegin(), energy_.cend(), log_E);
52,115,196✔
573
  }
574

575
  // check for case where two energy points are the same
576
  if (energy_(i_grid) == energy_(i_grid + 1))
52,115,196!
577
    ++i_grid;
×
578

579
  // calculate interpolation factor
580
  double f =
52,115,196✔
581
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
52,115,196✔
582

583
  auto& xs {p.photon_xs(index_)};
52,115,196✔
584
  xs.index_grid = i_grid;
52,115,196✔
585
  xs.interp_factor = f;
52,115,196✔
586

587
  // Calculate microscopic coherent cross section
588
  xs.coherent = std::exp(
104,230,392✔
589
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
52,115,196✔
590

591
  // Calculate microscopic incoherent cross section
592
  xs.incoherent = std::exp(
104,230,392✔
593
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
52,115,196✔
594

595
  // Calculate microscopic photoelectric cross section
596
  xs.photoelectric = 0.0;
52,115,196✔
597
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
52,115,196✔
598
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
52,115,196✔
599

600
  for (int i = 0; i < xs_upper.size(); ++i)
641,885,064✔
601
    if (xs_lower(i) != 0)
268,827,336✔
602
      xs.photoelectric +=
261,225,772✔
603
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
261,225,772✔
604

605
  // Calculate microscopic pair production cross section
606
  xs.pair_production = std::exp(
104,230,392✔
607
    pair_production_total_(i_grid) +
52,115,196✔
608
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
52,115,196✔
609

610
  // Calculate microscopic total cross section
611
  xs.total =
52,115,196✔
612
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
52,115,196✔
613
  xs.last_E = p.E();
52,115,196✔
614
}
104,230,392✔
615

616
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
1,416,947✔
617
{
618
  double mu;
1,622,085✔
619
  while (true) {
1,827,223✔
620
    // Determine maximum value of x^2
621
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
1,622,085✔
622

623
    // Determine F(x^2_max, Z)
624
    double F_max = coherent_int_form_factor_(x2_max);
1,622,085✔
625

626
    // Sample cumulative distribution
627
    double F = prn(seed) * F_max;
1,622,085✔
628

629
    // Determine x^2 corresponding to F
630
    const auto& x {coherent_int_form_factor_.x()};
1,622,085✔
631
    const auto& y {coherent_int_form_factor_.y()};
1,622,085✔
632
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
1,622,085✔
633
    double r = (F - y[i]) / (y[i + 1] - y[i]);
1,622,085✔
634
    double x2 = x[i] + r * (x[i + 1] - x[i]);
1,622,085✔
635

636
    // Calculate mu
637
    mu = 1.0 - 2.0 * x2 / x2_max;
1,622,085✔
638

639
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
1,622,085✔
640
      break;
641
  }
205,138✔
642
  return mu;
1,416,947✔
643
}
644

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

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

673
  // Compute the high-energy Coulomb correction
674
  double a = Z_ / FINE_STRUCTURE;
69,692✔
675
  double c =
69,692✔
676
    a * a *
69,692✔
677
    (1.0 / (1.0 + a * a) + 0.202059 +
69,692✔
678
      a * a *
69,692✔
679
        (-0.03693 +
69,692✔
680
          a * a *
69,692✔
681
            (0.00835 +
69,692✔
682
              a * a *
69,692✔
683
                (-0.00201 +
69,692✔
684
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
69,692✔
685

686
  // The analytical approximation of the DCS underestimates the cross section
687
  // at low energies. The correction factor f compensates for this.
688
  double q = std::sqrt(2.0 / alpha);
69,692✔
689
  double f = q * (-0.1774 - 12.10 * a + 11.18 * a * a) +
69,692✔
690
             q * q * (8.523 + 73.26 * a - 44.41 * a * a) +
69,692✔
691
             q * q * q * (-13.52 - 121.1 * a + 96.41 * a * a) +
69,692✔
692
             q * q * q * q * (8.946 + 62.05 * a - 63.41 * a * a);
69,692✔
693

694
  // Calculate phi_1(1/2) and phi_2(1/2). The unnormalized PDF for the reduced
695
  // energy is given by p = 2*(1/2 - e)^2*phi_1(e) + phi_2(e), where phi_1 and
696
  // phi_2 are non-negative and maximum at e = 1/2.
697
  double b = 2.0 * r[Z_] / alpha;
69,692✔
698
  double t1 = 2.0 * std::log(1.0 + b * b);
69,692✔
699
  double t2 = b * std::atan(1.0 / b);
69,692✔
700
  double t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
69,692✔
701
  double t4 = 4.0 * std::log(r[Z_]) - 4.0 * c + f;
69,692✔
702
  double phi1_max = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
69,692✔
703
  double phi2_max = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
69,692✔
704

705
  // To aid sampling, the unnormalized PDF can be expressed as
706
  // p = u_1*U_1(e)*pi_1(e) + u_2*U_2(e)*pi_2(e), where pi_1 and pi_2 are
707
  // normalized PDFs on the interval (e_min, e_max) from which values of e can
708
  // be sampled using the inverse transform method, and
709
  // U_1 = phi_1(e)/phi_1(1/2) and U_2 = phi_2(e)/phi_2(1/2) are valid
710
  // rejection functions. The reduced energy can now be sampled using a
711
  // combination of the composition and rejection methods.
712
  double u1 = 2.0 / 3.0 * std::pow(0.5 - 1.0 / alpha, 2) * phi1_max;
69,692✔
713
  double u2 = phi2_max;
69,692✔
714
  double e;
87,092✔
715
  while (true) {
87,092✔
716
    double rn = prn(seed);
87,092✔
717

718
    // Sample the index i in (1, 2) using the point probabilities
719
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
720
    int i;
87,092✔
721
    if (prn(seed) < u1 / (u1 + u2)) {
87,092✔
722
      i = 1;
7,839✔
723

724
      // Sample e from pi_1 using the inverse transform method
725
      e = rn >= 0.5
7,839✔
726
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
7,839✔
727
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
3,951✔
728
    } else {
729
      i = 2;
79,253✔
730

731
      // Sample e from pi_2 using the inverse transform method
732
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
79,253✔
733
    }
734

735
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
736
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
87,092✔
737
    t1 = 2.0 * std::log(1.0 + b * b);
87,092✔
738
    t2 = b * std::atan(1.0 / b);
87,092✔
739
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
87,092✔
740
    if (i == 1) {
87,092✔
741
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
7,839✔
742
      if (prn(seed) <= phi1 / phi1_max)
7,839✔
743
        break;
744
    } else {
745
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
79,253✔
746
      if (prn(seed) <= phi2 / phi2_max)
79,253✔
747
        break;
748
    }
749
  }
750

751
  // Compute the kinetic energy of the electron and the positron
752
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
69,692✔
753
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
69,692✔
754

755
  // Sample the scattering angle of the electron. The cosine of the polar
756
  // angle of the direction relative to the incident photon is sampled from
757
  // p(mu) = C/(1 - beta*mu)^2 using the inverse transform method.
758
  double beta =
69,692✔
759
    std::sqrt(*E_electron * (*E_electron + 2.0 * MASS_ELECTRON_EV)) /
69,692✔
760
    (*E_electron + MASS_ELECTRON_EV);
69,692✔
761
  double rn = uniform_distribution(-1., 1., seed);
69,692✔
762
  *mu_electron = (rn + beta) / (rn * beta + 1.0);
69,692✔
763

764
  // Sample the scattering angle of the positron
765
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
69,692✔
766
         (*E_positron + MASS_ELECTRON_EV);
69,692✔
767
  rn = uniform_distribution(-1., 1., seed);
69,692✔
768
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
69,692✔
769
}
69,692✔
770

771
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
22,343,155✔
772
{
773
  // Return if no atomic relaxation data is present or if the binding energy is
774
  // larger than the incident particle energy
775
  if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E())
22,343,155!
UNCOV
776
    return;
×
777

778
  // Stack for unprocessed holes left by transitioning electrons
779
  int n_holes = 0;
22,343,155✔
780
  array<int, MAX_STACK_SIZE> holes;
22,343,155✔
781

782
  // Push the initial hole onto the stack
783
  holes[n_holes++] = i_shell;
22,343,155✔
784

785
  while (n_holes > 0) {
106,435,546✔
786
    // Pop the next hole off the stack
787
    int i_hole = holes[--n_holes];
84,092,391✔
788
    const auto& shell {shells_[i_hole]};
84,092,391✔
789

790
    // If no transitions, assume fluorescent photon from captured free electron
791
    if (shell.transitions.empty()) {
84,092,391✔
792
      Direction u = isotropic_direction(p.current_seed());
52,546,615✔
793
      double E = shell.binding_energy;
52,546,615✔
794
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
52,546,615✔
795
      continue;
52,546,615✔
796
    }
52,546,615✔
797

798
    // Sample transition
799
    double c = -prn(p.current_seed());
31,545,776✔
800
    int i_trans;
31,545,776✔
801
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
736,907,734!
802
      c += shell.transitions[i_trans].probability;
736,907,734✔
803
      if (c > 0)
736,907,734✔
804
        break;
805
    }
806
    const auto& transition = shell.transitions[i_trans];
31,545,776✔
807

808
    // Sample angle isotropically
809
    Direction u = isotropic_direction(p.current_seed());
31,545,776✔
810

811
    // Push the hole created by the electron transitioning to the photoelectron
812
    // hole onto the stack
813
    holes[n_holes++] = transition.primary_subshell;
31,545,776✔
814

815
    if (transition.secondary_subshell != -1) {
31,545,776✔
816
      // Non-radiative transition -- Auger/Coster-Kronig effect
817

818
      // Push the hole left by emitted auger electron onto the stack
819
      holes[n_holes++] = transition.secondary_subshell;
30,203,460✔
820

821
      // Create auger electron
822
      p.create_secondary(
30,203,460✔
823
        p.wgt(), u, transition.energy, ParticleType::electron());
30,203,460✔
824
    } else {
825
      // Radiative transition -- get X-ray energy
826

827
      // Create fluorescent photon
828
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
1,342,316✔
829
    }
830
  }
831
}
832

833
//==============================================================================
834
// Non-member functions
835
//==============================================================================
836

837
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
18,011,858✔
838
{
839
  double alpha_out, mu;
18,011,858✔
840
  double beta = 1.0 + 2.0 * alpha;
18,011,858✔
841
  if (alpha < 3.0) {
18,011,858✔
842
    // Kahn's rejection method
843
    double t = beta / (beta + 8.0);
17,476,101✔
844
    double x;
29,547,454✔
845
    while (true) {
29,547,454✔
846
      if (prn(seed) < t) {
29,547,454✔
847
        // Left branch of flow chart
848
        double r = uniform_distribution(0.0, 2.0, seed);
4,805,911✔
849
        x = 1.0 + alpha * r;
4,805,911✔
850
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
4,805,911✔
851
          mu = 1 - r;
2,608,038✔
852
          break;
2,608,038✔
853
        }
854
      } else {
855
        // Right branch of flow chart
856
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
24,741,543✔
857
        mu = 1.0 + (1.0 - x) / alpha;
24,741,543✔
858
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
24,741,543✔
859
          break;
860
      }
861
    }
862
    alpha_out = alpha / x;
17,476,101✔
863

864
  } else {
865
    // Koblinger's direct method
866
    double gamma = 1.0 - std::pow(beta, -2);
535,757✔
867
    double s =
535,757✔
868
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
535,757✔
869
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
535,757✔
870
    if (s <= 2.0 / alpha) {
535,757✔
871
      // For first term, x = 1 + 2ar
872
      // Therefore, a' = a/(1 + 2ar)
873
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
80,017✔
874
    } else if (s <= 4.0 / alpha) {
455,740✔
875
      // For third term, x = beta/(1 + 2ar)
876
      // Therefore, a' = a(1 + 2ar)/beta
877
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
79,018✔
878
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
376,722✔
879
      // For fourth term, x = 1/sqrt(1 - gamma*r)
880
      // Therefore, a' = a*sqrt(1 - gamma*r)
881
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
96,841✔
882
    } else {
883
      // For third term, x = beta^r
884
      // Therefore, a' = a/beta^r
885
      alpha_out = alpha / std::pow(beta, prn(seed));
279,881✔
886
    }
887

888
    // Calculate cosine of scattering angle based on basic relation
889
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
535,757✔
890
  }
891
  return {alpha_out, mu};
18,011,858✔
892
}
893

894
void free_memory_photon()
5,785✔
895
{
896
  data::elements.clear();
5,785✔
897
  data::compton_profile_pz.resize({0});
5,785✔
898
  data::ttb_e_grid.resize({0});
5,785✔
899
  data::ttb_k_grid.resize({0});
5,785✔
900
}
5,785✔
901

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