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

openmc-dev / openmc / 30568119930

30 Jul 2026 05:55PM UTC coverage: 81.433% (-0.02%) from 81.452%
30568119930

Pull #4036

github

web-flow
Merge 76955d10f into 6285f579e
Pull Request #4036: Correct Compton shell selection and Doppler broadening

18458 of 26741 branches covered (69.03%)

Branch coverage included in aggregate %.

169 of 185 new or added lines in 1 file covered. (91.35%)

1 existing line in 1 file now uncovered.

60158 of 69800 relevant lines covered (86.19%)

49197188.9 hits per line

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

85.79
/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 <limits>
21
#include <stdexcept>
22
#include <tuple> // for tie
23

24
namespace openmc {
25

26
constexpr int PhotonInteraction::MAX_STACK_SIZE;
27

28
//==============================================================================
29
// Global variables
30
//==============================================================================
31

32
namespace data {
33

34
tensor::Tensor<double> compton_profile_pz;
35

36
std::unordered_map<std::string, int> element_map;
37
vector<unique_ptr<PhotonInteraction>> elements;
38

39
} // namespace data
40

41
//==============================================================================
42
// PhotonInteraction implementation
43
//==============================================================================
44

45
PhotonInteraction::PhotonInteraction(hid_t group)
1,579✔
46
{
47
  // Set index of element in global vector
48
  index_ = data::elements.size();
1,579✔
49

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

54
  // Get atomic number
55
  read_attribute(group, "Z", Z_);
1,579✔
56

57
  // Determine number of energies and read energy grid
58
  read_dataset(group, "energy", energy_);
1,579✔
59

60
  // Read coherent scattering
61
  hid_t rgroup = open_group(group, "coherent");
1,579✔
62
  read_dataset(rgroup, "xs", coherent_);
1,579✔
63

64
  hid_t dset = open_dataset(rgroup, "integrated_scattering_factor");
1,579✔
65
  coherent_int_form_factor_ = Tabulated1D {dset};
1,579✔
66
  close_dataset(dset);
1,579✔
67

68
  if (object_exists(group, "anomalous_real")) {
1,579!
69
    dset = open_dataset(rgroup, "anomalous_real");
×
70
    coherent_anomalous_real_ = Tabulated1D {dset};
×
71
    close_dataset(dset);
×
72
  }
73

74
  if (object_exists(group, "anomalous_imag")) {
1,579!
75
    dset = open_dataset(rgroup, "anomalous_imag");
×
76
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
77
    close_dataset(dset);
×
78
  }
79
  close_group(rgroup);
1,579✔
80

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

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

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

107
  // Read photoelectric
108
  rgroup = open_group(group, "photoelectric");
1,579✔
109
  read_dataset(rgroup, "xs", photoelectric_total_);
1,579✔
110
  close_group(rgroup);
1,579✔
111

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

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

131
  shells_.resize(n_shell);
1,579✔
132
  cross_sections_ = tensor::zeros<double>({energy_.size(), n_shell});
1,579✔
133

134
  // Create mapping from designator to index
135
  std::unordered_map<int, int> shell_map;
1,579✔
136
  for (int i = 0; i < n_shell; ++i) {
12,988✔
137
    const auto& designator {designators[i]};
11,409✔
138

139
    int j = 1;
11,409✔
140
    for (const auto& subshell : SUBSHELLS) {
72,974!
141
      if (designator == subshell) {
72,974✔
142
        shell_map[j] = i;
11,409✔
143
        shells_[i].index_subshell = j;
11,409✔
144
        break;
11,409✔
145
      }
146
      ++j;
61,565✔
147
    }
148
  }
149
  shell_map[0] = -1;
1,579✔
150

151
  for (int i = 0; i < n_shell; ++i) {
12,988✔
152
    const auto& designator {designators[i]};
11,409✔
153
    auto& shell {shells_[i]};
11,409✔
154

155
    // TODO: Move to ElectronSubshell constructor
156

157
    hid_t tgroup = open_group(rgroup, designator.c_str());
11,409✔
158

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

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

172
    auto cross_section =
11,409✔
173
      cross_sections_.slice(tensor::range(static_cast<size_t>(shell.threshold),
11,409✔
174
                              cross_sections_.shape(0)),
175
        i);
22,818!
176
    cross_section = tensor::where(xs > 0, tensor::log(xs), 0);
34,227✔
177

178
    if (settings::atomic_relaxation && object_exists(tgroup, "transitions")) {
11,409✔
179
      // Determine dimensions of transitions
180
      dset = open_dataset(tgroup, "transitions");
6,572✔
181
      auto dims = object_shape(dset);
6,572✔
182
      close_dataset(dset);
6,572✔
183

184
      int n_transition = dims[0];
6,572!
185
      if (n_transition > 0) {
6,572!
186
        tensor::Tensor<double> matrix;
6,572✔
187
        read_dataset(tgroup, "transitions", matrix);
6,572✔
188

189
        // Transition probability normalization
190
        double norm =
6,572✔
191
          tensor::Tensor<double>(matrix.slice(tensor::all, 3)).sum();
13,144✔
192

193
        shell.transitions.resize(n_transition);
6,572✔
194
        for (int j = 0; j < n_transition; ++j) {
277,072✔
195
          auto& transition = shell.transitions[j];
270,500✔
196
          transition.primary_subshell = shell_map.at(matrix(j, 0));
270,500✔
197
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
270,500✔
198
          transition.energy = matrix(j, 2);
270,500✔
199
          transition.probability = matrix(j, 3) / norm;
270,500✔
200
        }
201
      }
6,572✔
202
    }
6,572✔
203
    close_group(tgroup);
11,409✔
204
  }
22,818✔
205
  close_group(rgroup);
1,579✔
206

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

215
  // Determine number of electron shells
216
  rgroup = open_group(group, "compton_profiles");
1,579✔
217

218
  // Read electron shell PDF and binding energies
219
  read_dataset(rgroup, "num_electrons", electron_pdf_);
1,579✔
220
  electron_pdf_ /= electron_pdf_.sum();
1,579✔
221
  read_dataset(rgroup, "binding_energy", binding_energy_);
1,579✔
222

223
  // Read Compton profiles
224
  read_dataset(rgroup, "J", profile_pdf_);
1,579✔
225

226
  // Get Compton profile momentum grid
227
  if (data::compton_profile_pz.size() == 0) {
1,579✔
228
    read_dataset(rgroup, "pz", data::compton_profile_pz);
571✔
229
  }
230
  close_group(rgroup);
1,579✔
231

232
  // Map Compton subshell data to atomic relaxation data by finding the
233
  // subshell with the equivalent binding energy
234
  if (settings::atomic_relaxation && has_atomic_relaxation_) {
1,579!
235
    auto is_close = [](double a, double b) {
22,665✔
236
      return std::abs(a - b) / a < FP_REL_PRECISION;
21,116✔
237
    };
238
    subshell_map_ = tensor::Tensor<int>(binding_energy_.shape(), -1);
3,098✔
239
    for (int i = 0; i < binding_energy_.size(); ++i) {
10,593✔
240
      double E_b = binding_energy_[i];
9,044!
241
      if (i < n_shell && is_close(E_b, shells_[i].binding_energy)) {
9,044!
242
        subshell_map_[i] = i;
7,115✔
243
      } else {
244
        for (int j = 0; j < n_shell; ++j) {
12,072!
245
          if (is_close(E_b, shells_[j].binding_energy)) {
12,072✔
246
            subshell_map_[i] = j;
1,929✔
247
            break;
1,929✔
248
          }
249
        }
250
      }
251
    }
252
  }
253

254
  // Create Compton profile CDF
255
  auto n_profile = data::compton_profile_pz.size();
1,579!
256
  auto n_shell_compton = profile_pdf_.shape(0);
1,579!
257
  if (n_profile < 2) {
1,579!
NEW
258
    throw std::runtime_error {
×
NEW
259
      "At least two points are required in a Compton profile."};
×
260
  }
261
  profile_cdf_ = tensor::Tensor<double>({n_shell_compton, n_profile});
1,579✔
262
  profile_tail_slope_ = tensor::Tensor<double>({n_shell_compton});
1,579✔
263
  profile_negative_mass_ = tensor::Tensor<double>({n_shell_compton});
1,579✔
264
  if (n_shell_compton > SUBSHELLS.size()) {
1,579!
NEW
265
    throw std::runtime_error {"Photoatomic data for element " + name_ +
×
266
                              " has more Compton profiles than supported "
NEW
267
                              "electron subshells."};
×
268
  }
269
  for (int i = 0; i < n_shell_compton; ++i) {
10,983✔
270
    double c = 0.0;
9,404✔
271
    profile_cdf_(i, 0) = 0.0;
9,404✔
272
    for (int j = 0; j < n_profile - 1; ++j) {
291,524✔
273
      c += 0.5 *
282,120✔
274
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
282,120✔
275
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
282,120✔
276
      profile_cdf_(i, j + 1) = c;
282,120✔
277
    }
278

279
    // Extrapolate the profile beyond the tabulated grid linearly on a
280
    // log-linear scale. The normalization includes the extrapolated tail.
281
    double pz_last = data::compton_profile_pz(n_profile - 1);
9,404!
282
    double pz_prev = data::compton_profile_pz(n_profile - 2);
9,404✔
283
    double profile_last = profile_pdf_(i, n_profile - 1);
9,404✔
284
    double profile_prev = profile_pdf_(i, n_profile - 2);
9,404✔
285
    if (!(pz_last > pz_prev) || !(profile_last > 0.0) ||
9,404!
286
        !(profile_prev > 0.0)) {
NEW
287
      throw std::runtime_error {"The final two points of the Compton profile "
×
NEW
288
                                "for element " +
×
NEW
289
                                name_ + " are not valid for extrapolation."};
×
290
    }
291
    double slope = std::log(profile_last / profile_prev) / (pz_last - pz_prev);
9,404✔
292
    if (!std::isfinite(slope) || slope >= 0.0) {
9,404!
NEW
293
      throw std::runtime_error {"The final two values of the Compton profile "
×
NEW
294
                                "for element " +
×
NEW
295
                                name_ + " do not form a decreasing tail."};
×
296
    }
297
    profile_tail_slope_(i) = slope;
9,404!
298
    double norm = 2.0 * (c - profile_last / slope);
9,404✔
299
    if (!std::isfinite(norm) || norm <= 0.0) {
9,404!
NEW
300
      throw std::runtime_error {"The Compton profile for element " + name_ +
×
NEW
301
                                " has an invalid normalization."};
×
302
    }
303
    for (int j = 0; j < n_profile; ++j) {
300,928✔
304
      profile_pdf_(i, j) /= norm;
291,524✔
305
      profile_cdf_(i, j) /= norm;
291,524✔
306
    }
307
  }
308
  for (int i = 0; i < n_shell_compton; ++i) {
10,983✔
309
    profile_negative_mass_(i) = this->compton_profile_cdf(i, FINE_STRUCTURE);
9,404✔
310
  }
311

312
  // Calculate total pair production
313
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
1,579✔
314

315
  if (settings::electron_treatment == ElectronTreatment::TTB) {
1,579✔
316
    // Read bremsstrahlung scaled DCS
317
    rgroup = open_group(group, "bremsstrahlung");
1,362✔
318
    read_dataset(rgroup, "dcs", dcs_);
1,362✔
319
    auto n_e = dcs_.shape(0);
1,362!
320
    auto n_k = dcs_.shape(1);
1,362!
321

322
    // Get energy grids used for bremsstrahlung DCS and for stopping powers
323
    tensor::Tensor<double> electron_energy;
1,362✔
324
    read_dataset(rgroup, "electron_energy", electron_energy);
1,362✔
325
    if (data::ttb_k_grid.size() == 0) {
1,362✔
326
      read_dataset(rgroup, "photon_energy", data::ttb_k_grid);
486✔
327
    }
328

329
    // Get data used for density effect correction
330
    read_dataset(rgroup, "num_electrons", n_electrons_);
1,362✔
331
    read_dataset(rgroup, "ionization_energy", ionization_energy_);
1,362✔
332
    read_attribute(rgroup, "I", I_);
1,362✔
333
    close_group(rgroup);
1,362✔
334

335
    // Truncate the bremsstrahlung data at the cutoff energy
336
    int photon = ParticleType::photon().transport_index();
1,362✔
337
    const auto& E {electron_energy};
1,362✔
338
    double cutoff = settings::energy_cutoff[photon];
1,362✔
339
    if (cutoff > E(0)) {
1,362✔
340
      size_t i_grid = lower_bound_index(
11✔
341
        E.cbegin(), E.cend(), settings::energy_cutoff[photon]);
11✔
342

343
      // calculate interpolation factor
344
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
11✔
345
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
11✔
346

347
      // Interpolate bremsstrahlung DCS at the cutoff energy and truncate
348
      tensor::Tensor<double> dcs({n_e - i_grid, n_k});
11✔
349
      for (int i = 0; i < n_k; ++i) {
341✔
350
        double y = std::exp(
990✔
351
          std::log(dcs_(i_grid, i)) +
330✔
352
          f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i))));
330✔
353
        tensor::View<double> col_i = dcs.slice(tensor::all, i);
330✔
354
        col_i(0) = y;
330✔
355
        for (int j = i_grid + 1; j < n_e; ++j) {
61,200✔
356
          col_i(j - i_grid) = dcs_(j, i);
60,870✔
357
        }
358
      }
330✔
359
      dcs_ = dcs;
11✔
360

361
      tensor::Tensor<double> frst({static_cast<size_t>(1)});
11✔
362
      frst(0) = cutoff;
11✔
363
      tensor::Tensor<double> rest(electron_energy.slice(
11✔
364
        tensor::range(i_grid + 1, electron_energy.size())));
11✔
365
      electron_energy = tensor::concatenate(frst, rest);
22✔
366
    }
33✔
367

368
    // Set incident particle energy grid
369
    if (data::ttb_e_grid.size() == 0) {
1,362✔
370
      data::ttb_e_grid = electron_energy;
486✔
371
    }
372

373
    // Calculate the radiative stopping power
374
    stopping_power_radiative_ =
1,362✔
375
      tensor::Tensor<double>({data::ttb_e_grid.size()});
1,362✔
376
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
273,602✔
377
      // Integrate over reduced photon energy
378
      double c = 0.0;
379
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
8,167,200✔
380
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
7,894,960✔
381
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
7,894,960✔
382
      }
383
      double e = data::ttb_e_grid(i);
272,240✔
384

385
      // Square of the ratio of the speed of light to the velocity of the
386
      // charged particle
387
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
272,240✔
388
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
272,240✔
389

390
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
272,240✔
391
    }
392
  }
1,362✔
393

394
  // Take logarithm of energies and cross sections since they are log-log
395
  // interpolated. Note that cross section libraries converted from ACE files
396
  // represent zero as exp(-500) to avoid log-log interpolation errors. For
397
  // values below exp(-499) we store the log as -900, for which exp(-900)
398
  // evaluates to zero.
399
  double limit = std::exp(-499.0);
1,579✔
400
  energy_ = tensor::log(energy_);
1,579✔
401
  coherent_ = tensor::where(coherent_ > limit, tensor::log(coherent_), -900.0);
4,737✔
402
  incoherent_ =
1,579✔
403
    tensor::where(incoherent_ > limit, tensor::log(incoherent_), -900.0);
4,737✔
404
  photoelectric_total_ = tensor::where(
3,158✔
405
    photoelectric_total_ > limit, tensor::log(photoelectric_total_), -900.0);
4,737✔
406
  pair_production_total_ = tensor::where(pair_production_total_ > limit,
4,737✔
407
    tensor::log(pair_production_total_), -900.0);
3,158✔
408
  heating_ = tensor::where(heating_ > limit, tensor::log(heating_), -900.0);
6,316✔
409
}
1,579✔
410

411
PhotonInteraction::~PhotonInteraction()
1,579✔
412
{
413
  data::element_map.erase(name_);
1,579✔
414
}
33,159✔
415

416
int PhotonInteraction::calc_max_stack_size() const
1,579✔
417
{
418
  // Table to store solutions to sub-problems
419
  std::unordered_map<int, int> visited;
1,579✔
420

421
  // Find the maximum possible size of the stack used to store holes created
422
  // during atomic relaxation, checking over every subshell the initial hole
423
  // could be in
424
  int max_size = 0;
1,579✔
425
  for (int i_shell = 0; i_shell < shells_.size(); ++i_shell) {
12,988✔
426
    max_size = std::max(max_size, this->calc_helper(visited, i_shell));
12,988✔
427
  }
428
  return max_size;
1,579✔
429
}
1,579✔
430

431
int PhotonInteraction::calc_helper(
522,143✔
432
  std::unordered_map<int, int>& visited, int i_shell) const
433
{
434
  // No transitions for this subshell, so this is the only shell in the stack
435
  const auto& shell {shells_[i_shell]};
522,143✔
436
  if (shell.transitions.empty()) {
522,143✔
437
    return 1;
438
  }
439

440
  // Check the table to see if the maximum stack size has already been
441
  // calculated for this shell
442
  auto it = visited.find(i_shell);
312,510✔
443
  if (it != visited.end()) {
312,510✔
444
    return it->second;
305,938✔
445
  }
446

447
  int max_size = 0;
6,572✔
448
  for (const auto& transition : shell.transitions) {
277,072✔
449
    // If this is a non-radiative transition two vacancies are created and
450
    // the stack grows by one; if this is a radiative transition only one
451
    // vacancy is created and the stack size stays the same
452
    int size = 0;
270,500✔
453
    if (transition.secondary_subshell != -1) {
270,500✔
454
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
240,234✔
455
    }
456
    size =
541,000✔
457
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
270,500✔
458
    max_size = std::max(max_size, size);
280,981✔
459
  }
460
  visited[i_shell] = max_size;
6,572✔
461
  return max_size;
6,572✔
462
}
463

464
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
27,794,601✔
465
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
466
{
467
  double form_factor_xmax = 0.0;
27,794,601✔
468
  while (true) {
28,317,838✔
469
    // Sample Klein-Nishina distribution for trial energy and angle
470
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
28,317,838✔
471

472
    // Note that the parameter used here does not correspond exactly to the
473
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
474
    // parameter as defined by Hubbell, where the actual data comes from
475
    double x =
28,317,838✔
476
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
28,317,838✔
477

478
    // Calculate S(x, Z) and S(x_max, Z)
479
    double form_factor_x = incoherent_form_factor_(x);
28,317,838✔
480
    if (form_factor_xmax == 0.0) {
28,317,838✔
481
      form_factor_xmax =
27,794,601✔
482
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
27,794,601✔
483
    }
484

485
    // Perform rejection on form factor
486
    if (prn(seed) < form_factor_x / form_factor_xmax) {
28,317,838✔
487
      if (doppler) {
27,794,601!
488
        double E_out;
27,794,601✔
489
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
27,794,601✔
490
        *alpha_out = E_out / MASS_ELECTRON_EV;
27,794,601✔
491
      } else {
492
        *i_shell = -1;
×
493
      }
494
      break;
27,794,601✔
495
    }
496
  }
497
}
27,794,601✔
498

499
double PhotonInteraction::compton_profile_cdf(int i_shell, double pz) const
49,174,979✔
500
{
501
  if (pz <= 0.0)
49,174,979!
502
    return 0.0;
503

504
  auto n = data::compton_profile_pz.size();
49,174,979✔
505
  double pz_last = data::compton_profile_pz(n - 1);
49,174,979✔
506
  double c;
49,174,979✔
507
  if (pz >= pz_last) {
49,174,979✔
508
    c = profile_cdf_(i_shell, n - 1) + detail::compton_profile_tail_integral(pz,
15,757,726✔
509
                                         pz_last, profile_pdf_(i_shell, n - 1),
15,757,726✔
510
                                         profile_tail_slope_(i_shell));
15,757,726✔
511
  } else {
512
    int i = lower_bound_index(
33,417,253✔
513
      data::compton_profile_pz.cbegin(), data::compton_profile_pz.cend(), pz);
33,417,253✔
514
    double pz_l = data::compton_profile_pz(i);
33,417,253✔
515
    double pz_r = data::compton_profile_pz(i + 1);
33,417,253✔
516
    double p_l = profile_pdf_(i_shell, i);
33,417,253✔
517
    double p_r = profile_pdf_(i_shell, i + 1);
33,417,253✔
518
    double c_l = profile_cdf_(i_shell, i);
33,417,253✔
519
    double slope = (p_r - p_l) / (pz_r - pz_l);
33,417,253✔
520
    double delta = pz - pz_l;
33,417,253✔
521
    c = c_l + p_l * delta + 0.5 * slope * delta * delta;
33,417,253✔
522
  }
523
  return std::min(0.5, c);
94,922,754✔
524
}
525

526
double PhotonInteraction::invert_compton_profile_cdf(
44,812,922✔
527
  int i_shell, double c) const
528
{
529
  auto n = data::compton_profile_pz.size();
44,812,922✔
530
  double integral = c;
44,812,922✔
531
  double c_last = profile_cdf_(i_shell, n - 1);
44,812,922✔
532
  if (integral >= c_last) {
44,812,922✔
533
    // Invert the log-linear extrapolated tail using Kaltiaisenaho Eq. (3.123).
534
    return detail::invert_compton_profile_tail(integral - c_last,
3,289✔
535
      data::compton_profile_pz(n - 1), profile_pdf_(i_shell, n - 1),
3,289✔
536
      profile_tail_slope_(i_shell));
3,289✔
537
  }
538

539
  // Invert the piecewise-linear tabulated profile (Kaltiaisenaho Eq. 3.126).
540
  // The rationalized quadratic root used below is equivalent to that equation
541
  // but remains well-conditioned when the profile slope is small.
542
  tensor::View<const double> cdf_shell = profile_cdf_.slice(i_shell);
44,809,633✔
543
  int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), integral);
44,809,633✔
544
  double pz_l = data::compton_profile_pz(i);
44,809,633✔
545
  double pz_r = data::compton_profile_pz(i + 1);
44,809,633✔
546
  double p_l = profile_pdf_(i_shell, i);
44,809,633✔
547
  double p_r = profile_pdf_(i_shell, i + 1);
44,809,633✔
548
  double c_l = profile_cdf_(i_shell, i);
44,809,633✔
549
  if (p_l == p_r) {
44,809,633✔
550
    return pz_l + (integral - c_l) / p_l;
3,631,456✔
551
  }
552

553
  double slope = (p_r - p_l) / (pz_r - pz_l);
41,178,177✔
554
  double delta_c = integral - c_l;
41,178,177✔
555
  double discriminant = p_l * p_l + 2.0 * slope * delta_c;
41,178,177✔
556
  double denominator = p_l + std::sqrt(std::max(0.0, discriminant));
82,356,354!
557
  return pz_l + 2.0 * delta_c / denominator;
41,178,177✔
558
}
44,812,922✔
559

560
PhotonInteraction::ShellKinematics PhotonInteraction::compton_shell_kinematics(
49,172,736✔
561
  double alpha, double mu, double E, int i_shell) const
562
{
563
  ShellKinematics kinematics {};
49,172,736✔
564
  double E_b = binding_energy_(i_shell);
49,172,736✔
565
  if (E <= E_b)
49,172,736✔
566
    return kinematics;
567

568
  // Kaltiaisenaho Eq. (3.73): substitute E' = E - E_b in the RIA
569
  // kinematic relation to obtain the upper bound on the allowed pz interval.
570
  kinematics.pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
98,331,150✔
571
                      std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
49,165,575✔
572
  if (kinematics.pz_max <= -FINE_STRUCTURE)
49,165,575!
573
    return kinematics;
574

575
  // Eq. (3.118), using profile_negative_mass_ = K_i(1/alpha): the
576
  // kinematically accessible mass is the integral from -1/alpha to pz_max.
577
  double c_negative = profile_negative_mass_(i_shell);
49,165,575✔
578
  kinematics.c_limit =
98,331,150✔
579
    this->compton_profile_cdf(i_shell, std::abs(kinematics.pz_max));
49,165,575✔
580
  kinematics.profile_mass =
49,165,575✔
581
    c_negative + std::copysign(kinematics.c_limit, kinematics.pz_max);
49,165,575✔
582
  return kinematics;
49,165,575✔
583
}
584

585
bool PhotonInteraction::sample_compton_momentum(double alpha, double mu,
44,812,922✔
586
  double E, int i_shell, const ShellKinematics& kinematics, double* E_out,
587
  uint64_t* seed) const
588
{
589
  double c_negative = profile_negative_mass_(i_shell);
44,812,922✔
590
  double pz;
44,812,922✔
591
  // Inverse-transform sampling of the signed pz distribution, following
592
  // Kaltiaisenaho Eqs. (3.120)-(3.126). The tabulated profile is symmetric,
593
  // so its negative branch is obtained by reflecting the half-profile CDF.
594
  if (kinematics.pz_max < 0.0) {
44,812,922✔
595
    double c = kinematics.c_limit + prn(seed) * kinematics.profile_mass;
93,065✔
596
    pz = -this->invert_compton_profile_cdf(i_shell, c);
93,065✔
597
  } else {
598
    double c = prn(seed) * kinematics.profile_mass;
44,719,857✔
599
    if (c < c_negative) {
44,719,857✔
600
      pz = -this->invert_compton_profile_cdf(i_shell, c_negative - c);
22,523,709✔
601
    } else {
602
      pz = this->invert_compton_profile_cdf(i_shell, c - c_negative);
22,196,148✔
603
    }
604
  }
605

606
  double energy_ratio = detail::compton_energy_ratio(alpha, mu, pz);
44,812,922✔
607
  double max_energy_ratio = 1.0 - binding_energy_(i_shell) / E;
44,812,922!
608
  if (!std::isfinite(energy_ratio) || energy_ratio <= 0.0)
44,812,922!
609
    return false;
610

611
  double energy_tolerance = 16.0 * std::numeric_limits<double>::epsilon() *
44,812,922✔
612
                            std::max(1.0, max_energy_ratio);
44,812,922!
613
  if (energy_ratio > max_energy_ratio + energy_tolerance)
44,812,922!
614
    return false;
615

616
  energy_ratio = std::min(energy_ratio, max_energy_ratio);
44,812,922!
617
  *E_out = energy_ratio * E;
44,812,922✔
618

619
  // Kaltiaisenaho Eq. (3.127): account for the E'/E factor in the
620
  // approximate RIA DDCS after solving the scattered-photon energy.
621
  return prn(seed) <= energy_ratio;
44,812,922✔
622
}
623

624
bool PhotonInteraction::compton_doppler_conditional(double alpha, double mu,
2,806,894✔
625
  double E, double* E_out, int* i_shell, uint64_t* seed) const
626
{
627
  array<ShellKinematics, SUBSHELLS.size()> shell_data;
2,806,894✔
628
  array<double, SUBSHELLS.size()> shell_cdf;
2,806,894✔
629
  double shell_pmf_norm = 0.0;
2,806,894✔
630

631
  // Form the shell PMF in Eq. (3.116), f_i times the accessible profile mass.
632
  // This is algebraically equivalent to repeated shell rejection in Eq.
633
  // (3.119).
634
  for (int i = 0; i < electron_pdf_.size(); ++i) {
17,369,492✔
635
    shell_data[i] = this->compton_shell_kinematics(alpha, mu, E, i);
14,562,598✔
636
    shell_pmf_norm += electron_pdf_(i) * shell_data[i].profile_mass;
14,562,598✔
637
    shell_cdf[i] = shell_pmf_norm;
14,562,598✔
638
  }
639
  if (shell_pmf_norm == 0.0)
2,806,894!
640
    return false;
641

642
  // The conditional shell PMF avoids repeated rejection when the accessible
643
  // profile mass is small. Retain a bound to protect against degenerate
644
  // momentum/energy sampling and roundoff.
645
  constexpr int MAX_SAMPLES = 100000;
646
  for (int attempt = 0; attempt < MAX_SAMPLES; ++attempt) {
10,714,674!
647
    double rn = prn(seed) * shell_pmf_norm;
10,714,674✔
648
    int shell;
10,714,674✔
649
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
34,030,343!
650
      if (rn < shell_cdf[shell])
34,030,343✔
651
        break;
652
    }
653
    *i_shell = shell;
10,714,674✔
654

655
    if (this->sample_compton_momentum(
10,714,674✔
656
          alpha, mu, E, shell, shell_data[shell], E_out, seed))
10,714,674✔
657
      return true;
658
  }
659
  return false;
660
}
661

662
void PhotonInteraction::compton_doppler(
27,794,601✔
663
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
664
{
665
  // Implements the approximate RIA Doppler-broadening algorithm in Sec. 3.4.8
666
  // of T. Kaltiaisenaho, "Implementing a photon physics model in Serpent 2"
667
  // (2016), https://aaltodoc.aalto.fi/handle/123456789/21004.
668
  // First use Kaltiaisenaho's shell-rejection procedure (Eqs. 3.116-3.119),
669
  // which usually accepts quickly. If it does not, sample its equivalent
670
  // conditional shell PMF to bound work for near-forward scattering.
671
  constexpr int N_FAST_SAMPLES = 2;
27,794,601✔
672

673
  double E = alpha * MASS_ELECTRON_EV;
27,794,601✔
674
  int shell = 0;
27,794,601✔
675
  for (int attempt = 0; attempt < N_FAST_SAMPLES; ++attempt) {
37,417,032✔
676
    // Propose shell i according to occupancy f_i (first step of Eq. 3.119).
677
    double rn = prn(seed);
34,610,138✔
678
    double c = 0.0;
679
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
99,191,295!
680
      c += electron_pdf_(shell);
99,191,295✔
681
      if (rn < c)
99,191,295✔
682
        break;
683
    }
684

685
    auto kinematics = this->compton_shell_kinematics(alpha, mu, E, shell);
34,610,138✔
686
    if (kinematics.profile_mass <= 0.0)
34,610,138✔
687
      continue;
511,890✔
688

689
    // Accept with the accessible Compton-profile mass (Eq. 3.119).
690
    if (prn(seed) >= kinematics.profile_mass)
34,608,741✔
691
      continue;
510,493✔
692

693
    if (this->sample_compton_momentum(
34,098,248✔
694
          alpha, mu, E, shell, kinematics, E_out, seed)) {
695
      *i_shell = shell;
24,987,707✔
696
      return;
24,987,707✔
697
    }
698
  }
699

700
  *i_shell = shell;
2,806,894✔
701
  if (this->compton_doppler_conditional(alpha, mu, E, E_out, i_shell, seed))
2,806,894!
702
    return;
703

704
  // No shell/momentum sample was accepted within the iteration budget.
705
  // Fall back to the free-electron Compton energy for the last sampled
706
  // shell rather than looping indefinitely.
NEW
707
  *E_out = alpha / (1.0 + alpha * (1.0 - mu)) * MASS_ELECTRON_EV;
×
708
}
709

710
void PhotonInteraction::calculate_xs(Particle& p) const
193,307,146✔
711
{
712
  // Perform binary search on the element energy grid in order to determine
713
  // which points to interpolate between
714
  int n_grid = energy_.size();
193,307,146✔
715
  double log_E = std::log(p.E());
193,307,146✔
716
  int i_grid;
193,307,146✔
717
  if (log_E <= energy_[0]) {
193,307,146!
718
    i_grid = 0;
719
  } else if (log_E > energy_(n_grid - 1)) {
193,307,146!
720
    i_grid = n_grid - 2;
×
721
  } else {
722
    // We use upper_bound_index here because sometimes photons are created with
723
    // energies that exactly match a grid point
724
    i_grid = upper_bound_index(energy_.cbegin(), energy_.cend(), log_E);
193,307,146✔
725
  }
726

727
  // check for case where two energy points are the same
728
  if (energy_(i_grid) == energy_(i_grid + 1))
193,307,146!
729
    ++i_grid;
×
730

731
  // calculate interpolation factor
732
  double f =
193,307,146✔
733
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
193,307,146✔
734

735
  auto& xs {p.photon_xs(index_)};
193,307,146✔
736
  xs.index_grid = i_grid;
193,307,146✔
737
  xs.interp_factor = f;
193,307,146✔
738

739
  // Calculate microscopic coherent cross section
740
  xs.coherent = std::exp(
386,614,292✔
741
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
193,307,146✔
742

743
  // Calculate microscopic incoherent cross section
744
  xs.incoherent = std::exp(
386,614,292✔
745
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
193,307,146✔
746

747
  // Calculate microscopic photoelectric cross section
748
  xs.photoelectric = 0.0;
193,307,146✔
749
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
193,307,146✔
750
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
193,307,146✔
751

752
  for (int i = 0; i < xs_upper.size(); ++i)
2,147,483,647✔
753
    if (xs_lower(i) != 0)
1,215,783,318✔
754
      xs.photoelectric +=
1,203,896,778✔
755
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
1,203,896,778✔
756

757
  // Calculate microscopic pair production cross section
758
  xs.pair_production = std::exp(
386,614,292✔
759
    pair_production_total_(i_grid) +
193,307,146✔
760
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
193,307,146✔
761

762
  // Calculate microscopic total cross section
763
  xs.total =
193,307,146✔
764
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
193,307,146✔
765
  xs.last_E = p.E();
193,307,146✔
766
}
386,614,292✔
767

768
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
1,879,372✔
769
{
770
  double mu;
2,128,820✔
771
  while (true) {
2,378,268✔
772
    // Determine maximum value of x^2
773
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
2,128,820✔
774

775
    // Determine F(x^2_max, Z)
776
    double F_max = coherent_int_form_factor_(x2_max);
2,128,820✔
777

778
    // Sample cumulative distribution
779
    double F = prn(seed) * F_max;
2,128,820✔
780

781
    // Determine x^2 corresponding to F
782
    const auto& x {coherent_int_form_factor_.x()};
2,128,820✔
783
    const auto& y {coherent_int_form_factor_.y()};
2,128,820✔
784
    int i = lower_bound_index(y.cbegin(), y.cend(), F);
2,128,820✔
785
    double r = (F - y[i]) / (y[i + 1] - y[i]);
2,128,820✔
786
    double x2 = x[i] + r * (x[i + 1] - x[i]);
2,128,820✔
787

788
    // Calculate mu
789
    mu = 1.0 - 2.0 * x2 / x2_max;
2,128,820✔
790

791
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
2,128,820✔
792
      break;
793
  }
249,448✔
794
  return mu;
1,879,372✔
795
}
796

797
void PhotonInteraction::pair_production(double alpha, double* E_electron,
209,737✔
798
  double* E_positron, double* mu_electron, double* mu_positron,
799
  uint64_t* seed) const
800
{
801
  constexpr double r[] {122.81, 73.167, 69.228, 67.301, 64.696, 61.228, 57.524,
209,737✔
802
    54.033, 50.787, 47.851, 46.373, 45.401, 44.503, 43.815, 43.074, 42.321,
803
    41.586, 40.953, 40.524, 40.256, 39.756, 39.144, 38.462, 37.778, 37.174,
804
    36.663, 35.986, 35.317, 34.688, 34.197, 33.786, 33.422, 33.068, 32.740,
805
    32.438, 32.143, 31.884, 31.622, 31.438, 31.142, 30.950, 30.758, 30.561,
806
    30.285, 30.097, 29.832, 29.581, 29.411, 29.247, 29.085, 28.930, 28.721,
807
    28.580, 28.442, 28.312, 28.139, 27.973, 27.819, 27.675, 27.496, 27.285,
808
    27.093, 26.911, 26.705, 26.516, 26.304, 26.108, 25.929, 25.730, 25.577,
809
    25.403, 25.245, 25.100, 24.941, 24.790, 24.655, 24.506, 24.391, 24.262,
810
    24.145, 24.039, 23.922, 23.813, 23.712, 23.621, 23.523, 23.430, 23.331,
811
    23.238, 23.139, 23.048, 22.967, 22.833, 22.694, 22.624, 22.545, 22.446,
812
    22.358, 22.264};
813

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

825
  // Compute the high-energy Coulomb correction
826
  double a = Z_ / FINE_STRUCTURE;
209,737✔
827
  double c =
209,737✔
828
    a * a *
209,737✔
829
    (1.0 / (1.0 + a * a) + 0.202059 +
209,737✔
830
      a * a *
209,737✔
831
        (-0.03693 +
209,737✔
832
          a * a *
209,737✔
833
            (0.00835 +
209,737✔
834
              a * a *
209,737✔
835
                (-0.00201 +
209,737✔
836
                  a * a * (0.00049 + a * a * (-0.00012 + a * a * 0.00003))))));
209,737✔
837

838
  // The analytical approximation of the DCS underestimates the cross section
839
  // at low energies. The correction factor f compensates for this.
840
  double q = std::sqrt(2.0 / alpha);
209,737✔
841
  double f = q * (-0.1774 - 12.10 * a + 11.18 * a * a) +
209,737✔
842
             q * q * (8.523 + 73.26 * a - 44.41 * a * a) +
209,737✔
843
             q * q * q * (-13.52 - 121.1 * a + 96.41 * a * a) +
209,737✔
844
             q * q * q * q * (8.946 + 62.05 * a - 63.41 * a * a);
209,737✔
845

846
  // Calculate phi_1(1/2) and phi_2(1/2). The unnormalized PDF for the reduced
847
  // energy is given by p = 2*(1/2 - e)^2*phi_1(e) + phi_2(e), where phi_1 and
848
  // phi_2 are non-negative and maximum at e = 1/2.
849
  double b = 2.0 * r[Z_] / alpha;
209,737✔
850
  double t1 = 2.0 * std::log(1.0 + b * b);
209,737✔
851
  double t2 = b * std::atan(1.0 / b);
209,737✔
852
  double t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
209,737✔
853
  double t4 = 4.0 * std::log(r[Z_]) - 4.0 * c + f;
209,737✔
854
  double phi1_max = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
209,737✔
855
  double phi2_max = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
209,737✔
856

857
  // To aid sampling, the unnormalized PDF can be expressed as
858
  // p = u_1*U_1(e)*pi_1(e) + u_2*U_2(e)*pi_2(e), where pi_1 and pi_2 are
859
  // normalized PDFs on the interval (e_min, e_max) from which values of e can
860
  // be sampled using the inverse transform method, and
861
  // U_1 = phi_1(e)/phi_1(1/2) and U_2 = phi_2(e)/phi_2(1/2) are valid
862
  // rejection functions. The reduced energy can now be sampled using a
863
  // combination of the composition and rejection methods.
864
  double u1 = 2.0 / 3.0 * std::pow(0.5 - 1.0 / alpha, 2) * phi1_max;
209,737✔
865
  double u2 = phi2_max;
209,737✔
866
  double e;
263,934✔
867
  while (true) {
263,934✔
868
    double rn = prn(seed);
263,934✔
869

870
    // Sample the index i in (1, 2) using the point probabilities
871
    // p(1) = u_1/(u_1 + u_2) and p(2) = u_2/(u_1 + u_2)
872
    int i;
263,934✔
873
    if (prn(seed) < u1 / (u1 + u2)) {
263,934✔
874
      i = 1;
24,376✔
875

876
      // Sample e from pi_1 using the inverse transform method
877
      e = rn >= 0.5
24,376✔
878
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
24,376✔
879
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
11,781✔
880
    } else {
881
      i = 2;
239,558✔
882

883
      // Sample e from pi_2 using the inverse transform method
884
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
239,558✔
885
    }
886

887
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
888
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
263,934✔
889
    t1 = 2.0 * std::log(1.0 + b * b);
263,934✔
890
    t2 = b * std::atan(1.0 / b);
263,934✔
891
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
263,934✔
892
    if (i == 1) {
263,934✔
893
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
24,376✔
894
      if (prn(seed) <= phi1 / phi1_max)
24,376✔
895
        break;
896
    } else {
897
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
239,558✔
898
      if (prn(seed) <= phi2 / phi2_max)
239,558✔
899
        break;
900
    }
901
  }
902

903
  // Compute the kinetic energy of the electron and the positron
904
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
209,737✔
905
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
209,737✔
906

907
  // Sample the scattering angle of the electron. The cosine of the polar
908
  // angle of the direction relative to the incident photon is sampled from
909
  // p(mu) = C/(1 - beta*mu)^2 using the inverse transform method.
910
  double beta =
209,737✔
911
    std::sqrt(*E_electron * (*E_electron + 2.0 * MASS_ELECTRON_EV)) /
209,737✔
912
    (*E_electron + MASS_ELECTRON_EV);
209,737✔
913
  double rn = uniform_distribution(-1., 1., seed);
209,737✔
914
  *mu_electron = (rn + beta) / (rn * beta + 1.0);
209,737✔
915

916
  // Sample the scattering angle of the positron
917
  beta = std::sqrt(*E_positron * (*E_positron + 2.0 * MASS_ELECTRON_EV)) /
209,737✔
918
         (*E_positron + MASS_ELECTRON_EV);
209,737✔
919
  rn = uniform_distribution(-1., 1., seed);
209,737✔
920
  *mu_positron = (rn + beta) / (rn * beta + 1.0);
209,737✔
921
}
209,737✔
922

923
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
35,882,414✔
924
{
925
  // Return if no atomic relaxation data is present or if the binding energy is
926
  // larger than the incident particle energy
927
  if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E())
35,882,414!
UNCOV
928
    return;
×
929

930
  // Stack for unprocessed holes left by transitioning electrons
931
  int n_holes = 0;
35,882,414✔
932
  array<int, MAX_STACK_SIZE> holes;
35,882,414✔
933

934
  // Push the initial hole onto the stack
935
  holes[n_holes++] = i_shell;
35,882,414✔
936

937
  while (n_holes > 0) {
189,319,844✔
938
    // Pop the next hole off the stack
939
    int i_hole = holes[--n_holes];
153,437,430✔
940
    const auto& shell {shells_[i_hole]};
153,437,430✔
941

942
    // If no transitions, assume fluorescent photon from captured free electron
943
    if (shell.transitions.empty()) {
153,437,430✔
944
      Direction u = isotropic_direction(p.current_seed());
93,568,236✔
945
      double E = shell.binding_energy;
93,568,236✔
946
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
93,568,236✔
947
      continue;
93,568,236✔
948
    }
93,568,236✔
949

950
    // Sample transition
951
    double c = -prn(p.current_seed());
59,869,194✔
952
    int i_trans;
59,869,194✔
953
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,141,834,656!
954
      c += shell.transitions[i_trans].probability;
1,141,834,656✔
955
      if (c > 0)
1,141,834,656✔
956
        break;
957
    }
958
    const auto& transition = shell.transitions[i_trans];
59,869,194✔
959

960
    // Sample angle isotropically
961
    Direction u = isotropic_direction(p.current_seed());
59,869,194✔
962

963
    // Push the hole created by the electron transitioning to the photoelectron
964
    // hole onto the stack
965
    holes[n_holes++] = transition.primary_subshell;
59,869,194✔
966

967
    if (transition.secondary_subshell != -1) {
59,869,194✔
968
      // Non-radiative transition -- Auger/Coster-Kronig effect
969

970
      // Push the hole left by emitted auger electron onto the stack
971
      holes[n_holes++] = transition.secondary_subshell;
57,685,822✔
972

973
      // Create auger electron
974
      p.create_secondary(
57,685,822✔
975
        p.wgt(), u, transition.energy, ParticleType::electron());
57,685,822✔
976
    } else {
977
      // Radiative transition -- get X-ray energy
978

979
      // Create fluorescent photon
980
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
2,183,372✔
981
    }
982
  }
983
}
984

985
//==============================================================================
986
// Non-member functions
987
//==============================================================================
988

989
double detail::compton_profile_tail_integral(
15,757,759✔
990
  double pz, double pz_last, double profile_last, double slope)
991
{
992
  return profile_last * std::expm1(slope * (pz - pz_last)) / slope;
15,757,759✔
993
}
994

995
double detail::invert_compton_profile_tail(
3,300✔
996
  double integral, double pz_last, double profile_last, double slope)
997
{
998
  return pz_last + std::log1p(slope * integral / profile_last) / slope;
3,300✔
999
}
1000

1001
double detail::compton_energy_ratio(double alpha, double mu, double pz)
44,812,955✔
1002
{
1003
  if (pz == 0.0)
44,812,955✔
1004
    return 1.0 / (1.0 + alpha * (1.0 - mu));
11✔
1005

1006
  double momentum = pz / FINE_STRUCTURE;
44,812,944✔
1007
  double momentum_sq = momentum * momentum;
44,812,944✔
1008
  double f = 1.0 + alpha * (1.0 - mu);
44,812,944✔
1009
  double a = momentum_sq - f * f;
44,812,944✔
1010
  double b = 2.0 * (f - momentum_sq * mu);
44,812,944✔
1011
  double c = momentum_sq - 1.0;
44,812,944✔
1012
  double discriminant = b * b - 4.0 * a * c;
44,812,944✔
1013
  double discriminant_tolerance = 16.0 *
44,812,944✔
1014
                                  std::numeric_limits<double>::epsilon() *
1015
                                  (b * b + std::abs(4.0 * a * c));
44,812,944!
1016
  if (discriminant < -discriminant_tolerance)
44,812,944!
1017
    return std::numeric_limits<double>::quiet_NaN();
1018
  discriminant = std::max(0.0, discriminant);
44,812,944✔
1019

1020
  double root1;
44,812,944✔
1021
  double root2;
44,812,944✔
1022
  if (std::abs(a) < 1.0e-14 * (std::abs(b) + std::abs(c))) {
44,812,944!
NEW
1023
    if (b == 0.0)
×
1024
      return std::numeric_limits<double>::quiet_NaN();
NEW
1025
    root1 = -c / b;
×
NEW
1026
    root2 = root1;
×
1027
  } else {
1028
    double sqrt_discriminant = std::sqrt(discriminant);
44,812,944✔
1029
    double q = -0.5 * (b + std::copysign(sqrt_discriminant, b));
44,812,944✔
1030
    root1 = q / a;
44,812,944✔
1031
    root2 = q == 0.0 ? (-b + sqrt_discriminant) / (2.0 * a) : c / q;
44,812,944!
1032
  }
1033

1034
  double root_min = std::numeric_limits<double>::infinity();
44,812,944✔
1035
  double root_max = -std::numeric_limits<double>::infinity();
44,812,944✔
1036
  if (std::isfinite(root1) && root1 > 0.0) {
44,812,944!
1037
    root_min = root1;
44,812,944✔
1038
    root_max = root1;
44,812,944✔
1039
  }
1040
  if (std::isfinite(root2) && root2 > 0.0) {
44,812,944!
1041
    root_min = std::min(root_min, root2);
44,812,416✔
1042
    root_max = std::max(root_max, root2);
44,812,416!
1043
  }
1044
  if (!std::isfinite(root_min))
44,812,944!
1045
    return std::numeric_limits<double>::quiet_NaN();
1046

1047
  double energy_ratio = pz < 0.0 ? root_min : root_max;
44,812,944✔
1048
  double free_electron_ratio = 1.0 / f;
44,812,944✔
1049
  double tolerance = 16.0 * std::numeric_limits<double>::epsilon() *
44,812,944✔
1050
                     std::max(1.0, free_electron_ratio);
44,812,944!
1051
  if ((pz < 0.0 && energy_ratio > free_electron_ratio + tolerance) ||
44,812,944!
1052
      (pz > 0.0 && energy_ratio < free_electron_ratio - tolerance)) {
22,196,159!
1053
    return std::numeric_limits<double>::quiet_NaN();
1054
  }
1055
  return energy_ratio;
44,812,944✔
1056
}
1057

1058
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
28,317,838✔
1059
{
1060
  double alpha_out, mu;
28,317,838✔
1061
  double beta = 1.0 + 2.0 * alpha;
28,317,838✔
1062
  if (alpha < 3.0) {
28,317,838✔
1063
    // Kahn's rejection method
1064
    double t = beta / (beta + 8.0);
25,854,025✔
1065
    double x;
43,414,992✔
1066
    while (true) {
43,414,992✔
1067
      if (prn(seed) < t) {
43,414,992✔
1068
        // Left branch of flow chart
1069
        double r = uniform_distribution(0.0, 2.0, seed);
7,939,365✔
1070
        x = 1.0 + alpha * r;
7,939,365✔
1071
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
7,939,365✔
1072
          mu = 1 - r;
5,017,492✔
1073
          break;
5,017,492✔
1074
        }
1075
      } else {
1076
        // Right branch of flow chart
1077
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
35,475,627✔
1078
        mu = 1.0 + (1.0 - x) / alpha;
35,475,627✔
1079
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
35,475,627✔
1080
          break;
1081
      }
1082
    }
1083
    alpha_out = alpha / x;
25,854,025✔
1084

1085
  } else {
1086
    // Koblinger's direct method
1087
    double gamma = 1.0 - std::pow(beta, -2);
2,463,813✔
1088
    double s =
2,463,813✔
1089
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
2,463,813✔
1090
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
2,463,813✔
1091
    if (s <= 2.0 / alpha) {
2,463,813✔
1092
      // For first term, x = 1 + 2ar
1093
      // Therefore, a' = a/(1 + 2ar)
1094
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
380,754✔
1095
    } else if (s <= 4.0 / alpha) {
2,083,059✔
1096
      // For third term, x = beta/(1 + 2ar)
1097
      // Therefore, a' = a(1 + 2ar)/beta
1098
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
383,592✔
1099
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
1,699,467✔
1100
      // For fourth term, x = 1/sqrt(1 - gamma*r)
1101
      // Therefore, a' = a*sqrt(1 - gamma*r)
1102
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
463,386✔
1103
    } else {
1104
      // For third term, x = beta^r
1105
      // Therefore, a' = a/beta^r
1106
      alpha_out = alpha / std::pow(beta, prn(seed));
1,236,081✔
1107
    }
1108

1109
    // Calculate cosine of scattering angle based on basic relation
1110
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
2,463,813✔
1111
  }
1112
  return {alpha_out, mu};
28,317,838✔
1113
}
1114

1115
void free_memory_photon()
9,174✔
1116
{
1117
  data::elements.clear();
9,174✔
1118
  data::compton_profile_pz.resize({0});
9,174✔
1119
  data::ttb_e_grid.resize({0});
9,174✔
1120
  data::ttb_k_grid.resize({0});
9,174✔
1121
}
9,174✔
1122

1123
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc