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

openmc-dev / openmc / 30559508182

30 Jul 2026 04:01PM UTC coverage: 81.437% (-0.02%) from 81.452%
30559508182

Pull #4036

github

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

18468 of 26750 branches covered (69.04%)

Branch coverage included in aggregate %.

170 of 186 new or added lines in 1 file covered. (91.4%)

1 existing line in 1 file now uncovered.

60160 of 69801 relevant lines covered (86.19%)

49195983.63 hits per line

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

85.91
/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_norm_ = tensor::Tensor<double>({n_shell_compton});
1,579✔
264
  profile_negative_mass_ = tensor::Tensor<double>({n_shell_compton});
1,579✔
265
  if (n_shell_compton > SUBSHELLS.size()) {
1,579!
NEW
266
    throw std::runtime_error {"Photoatomic data for element " + name_ +
×
267
                              " has more Compton profiles than supported "
NEW
268
                              "electron subshells."};
×
269
  }
270
  for (int i = 0; i < n_shell_compton; ++i) {
10,983✔
271
    double c = 0.0;
9,404✔
272
    profile_cdf_(i, 0) = 0.0;
9,404✔
273
    for (int j = 0; j < n_profile - 1; ++j) {
291,524✔
274
      c += 0.5 *
282,120✔
275
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
282,120✔
276
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
282,120✔
277
      profile_cdf_(i, j + 1) = c;
282,120✔
278
    }
279

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

309
  // Calculate total pair production
310
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
1,579✔
311

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

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

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

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

340
      // calculate interpolation factor
341
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
11✔
342
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
11✔
343

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

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

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

370
    // Calculate the radiative stopping power
371
    stopping_power_radiative_ =
1,362✔
372
      tensor::Tensor<double>({data::ttb_e_grid.size()});
1,362✔
373
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
273,637✔
374
      // Integrate over reduced photon energy
375
      double c = 0.0;
376
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
8,168,250✔
377
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
7,895,975✔
378
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
7,895,975✔
379
      }
380
      double e = data::ttb_e_grid(i);
272,275✔
381

382
      // Square of the ratio of the speed of light to the velocity of the
383
      // charged particle
384
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
272,275✔
385
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
272,275✔
386

387
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
272,275✔
388
    }
389
  }
1,362✔
390

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

408
PhotonInteraction::~PhotonInteraction()
1,579✔
409
{
410
  data::element_map.erase(name_);
1,579✔
411
}
34,738✔
412

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

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

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

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

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

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

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

475
    // Calculate S(x, Z) and S(x_max, Z)
476
    double form_factor_x = incoherent_form_factor_(x);
28,317,648✔
477
    if (form_factor_xmax == 0.0) {
28,317,648✔
478
      form_factor_xmax =
27,794,425✔
479
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
27,794,425✔
480
    }
481

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

496
double PhotonInteraction::compton_profile_cdf(int i_shell, double pz) const
49,174,837✔
497
{
498
  if (pz <= 0.0)
49,174,837!
499
    return 0.0;
500

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

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

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

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

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

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

576
  // Eq. (3.118), using profile_negative_mass_ = K_i(1/alpha): the
577
  // kinematically accessible mass is the integral from -1/alpha to pz_max.
578
  double c_negative = profile_negative_mass_(i_shell);
49,165,433✔
579
  if (kinematics.pz_max < 0.0) {
49,165,433✔
580
    kinematics.c_limit = this->compton_profile_cdf(i_shell, -kinematics.pz_max);
402,678✔
581
    kinematics.profile_mass = c_negative - kinematics.c_limit;
402,678✔
582
  } else {
583
    kinematics.c_limit = this->compton_profile_cdf(i_shell, kinematics.pz_max);
48,762,755✔
584
    kinematics.profile_mass = c_negative + kinematics.c_limit;
48,762,755✔
585
  }
586
  return kinematics;
587
}
588

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

610
  double energy_ratio = detail::compton_energy_ratio(alpha, mu, pz);
44,812,723✔
611
  double max_energy_ratio = 1.0 - binding_energy_(i_shell) / E;
44,812,723!
612
  if (!std::isfinite(energy_ratio) || energy_ratio <= 0.0)
44,812,723!
613
    return false;
614

615
  double energy_tolerance = 16.0 * std::numeric_limits<double>::epsilon() *
44,812,723✔
616
                            std::max(1.0, max_energy_ratio);
44,812,723!
617
  if (energy_ratio > max_energy_ratio + energy_tolerance)
44,812,723!
618
    return false;
619

620
  energy_ratio = std::min(energy_ratio, max_energy_ratio);
44,812,723!
621
  *E_out = energy_ratio * E;
44,812,723✔
622

623
  // Kaltiaisenaho Eq. (3.127): account for the E'/E factor in the
624
  // approximate RIA DDCS after solving the scattered-photon energy.
625
  return prn(seed) <= energy_ratio;
44,812,723✔
626
}
627

628
bool PhotonInteraction::compton_doppler_conditional(double alpha, double mu,
2,806,899✔
629
  double E, double* E_out, int* i_shell, uint64_t* seed) const
630
{
631
  array<ShellKinematics, SUBSHELLS.size()> shell_data;
2,806,899✔
632
  array<double, SUBSHELLS.size()> shell_cdf;
2,806,899✔
633
  double shell_pmf_norm = 0.0;
2,806,899✔
634

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

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

659
    if (this->sample_compton_momentum(
10,714,678✔
660
          alpha, mu, E, shell, shell_data[shell], E_out, seed))
10,714,678✔
661
      return true;
662
  }
663
  return false;
664
}
665

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

677
  double E = alpha * MASS_ELECTRON_EV;
27,794,425✔
678
  int shell = 0;
27,794,425✔
679
  for (int attempt = 0; attempt < N_FAST_SAMPLES; ++attempt) {
37,416,827✔
680
    // Propose shell i according to occupancy f_i (first step of Eq. 3.119).
681
    double rn = prn(seed);
34,609,928✔
682
    double c = 0.0;
683
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
99,189,571!
684
      c += electron_pdf_(shell);
99,189,571✔
685
      if (rn < c)
99,189,571✔
686
        break;
687
    }
688

689
    auto kinematics = this->compton_shell_kinematics(alpha, mu, E, shell);
34,609,928✔
690
    if (kinematics.profile_mass <= 0.0)
34,609,928✔
691
      continue;
511,883✔
692

693
    // Accept with the accessible Compton-profile mass (Eq. 3.119).
694
    if (prn(seed) >= kinematics.profile_mass)
34,608,531✔
695
      continue;
510,486✔
696

697
    if (this->sample_compton_momentum(
34,098,045✔
698
          alpha, mu, E, shell, kinematics, E_out, seed)) {
699
      *i_shell = shell;
24,987,526✔
700
      return;
24,987,526✔
701
    }
702
  }
703

704
  *i_shell = shell;
2,806,899✔
705
  if (this->compton_doppler_conditional(alpha, mu, E, E_out, i_shell, seed))
2,806,899!
706
    return;
707

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

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

731
  // check for case where two energy points are the same
732
  if (energy_(i_grid) == energy_(i_grid + 1))
193,305,748!
733
    ++i_grid;
×
734

735
  // calculate interpolation factor
736
  double f =
193,305,748✔
737
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
193,305,748✔
738

739
  auto& xs {p.photon_xs(index_)};
193,305,748✔
740
  xs.index_grid = i_grid;
193,305,748✔
741
  xs.interp_factor = f;
193,305,748✔
742

743
  // Calculate microscopic coherent cross section
744
  xs.coherent = std::exp(
386,611,496✔
745
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
193,305,748✔
746

747
  // Calculate microscopic incoherent cross section
748
  xs.incoherent = std::exp(
386,611,496✔
749
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
193,305,748✔
750

751
  // Calculate microscopic photoelectric cross section
752
  xs.photoelectric = 0.0;
193,305,748✔
753
  tensor::View<const double> xs_lower = cross_sections_.slice(i_grid);
193,305,748✔
754
  tensor::View<const double> xs_upper = cross_sections_.slice(i_grid + 1);
193,305,748✔
755

756
  for (int i = 0; i < xs_upper.size(); ++i)
2,147,483,647✔
757
    if (xs_lower(i) != 0)
1,215,762,348✔
758
      xs.photoelectric +=
1,203,876,012✔
759
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
1,203,876,012✔
760

761
  // Calculate microscopic pair production cross section
762
  xs.pair_production = std::exp(
386,611,496✔
763
    pair_production_total_(i_grid) +
193,305,748✔
764
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
193,305,748✔
765

766
  // Calculate microscopic total cross section
767
  xs.total =
193,305,748✔
768
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
193,305,748✔
769
  xs.last_E = p.E();
193,305,748✔
770
}
386,611,496✔
771

772
double PhotonInteraction::rayleigh_scatter(double alpha, uint64_t* seed) const
1,879,282✔
773
{
774
  double mu;
2,128,720✔
775
  while (true) {
2,378,158✔
776
    // Determine maximum value of x^2
777
    double x2_max = std::pow(MASS_ELECTRON_EV / PLANCK_C * alpha, 2);
2,128,720✔
778

779
    // Determine F(x^2_max, Z)
780
    double F_max = coherent_int_form_factor_(x2_max);
2,128,720✔
781

782
    // Sample cumulative distribution
783
    double F = prn(seed) * F_max;
2,128,720✔
784

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

792
    // Calculate mu
793
    mu = 1.0 - 2.0 * x2 / x2_max;
2,128,720✔
794

795
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
2,128,720✔
796
      break;
797
  }
249,438✔
798
  return mu;
1,879,282✔
799
}
800

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

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

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

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

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

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

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

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

887
      // Sample e from pi_2 using the inverse transform method
888
      e = 1.0 / alpha + (0.5 - 1.0 / alpha) * 2.0 * rn;
239,558✔
889
    }
890

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

907
  // Compute the kinetic energy of the electron and the positron
908
  *E_electron = (alpha * e - 1.0) * MASS_ELECTRON_EV;
209,737✔
909
  *E_positron = (alpha * (1.0 - e) - 1.0) * MASS_ELECTRON_EV;
209,737✔
910

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

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

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

934
  // Stack for unprocessed holes left by transitioning electrons
935
  int n_holes = 0;
35,881,670✔
936
  array<int, MAX_STACK_SIZE> holes;
35,881,670✔
937

938
  // Push the initial hole onto the stack
939
  holes[n_holes++] = i_shell;
35,881,670✔
940

941
  while (n_holes > 0) {
189,301,776✔
942
    // Pop the next hole off the stack
943
    int i_hole = holes[--n_holes];
153,420,106✔
944
    const auto& shell {shells_[i_hole]};
153,420,106✔
945

946
    // If no transitions, assume fluorescent photon from captured free electron
947
    if (shell.transitions.empty()) {
153,420,106✔
948
      Direction u = isotropic_direction(p.current_seed());
93,559,547✔
949
      double E = shell.binding_energy;
93,559,547✔
950
      p.create_secondary(p.wgt(), u, E, ParticleType::photon());
93,559,547✔
951
      continue;
93,559,547✔
952
    }
93,559,547✔
953

954
    // Sample transition
955
    double c = -prn(p.current_seed());
59,860,559✔
956
    int i_trans;
59,860,559✔
957
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,141,756,700!
958
      c += shell.transitions[i_trans].probability;
1,141,756,700✔
959
      if (c > 0)
1,141,756,700✔
960
        break;
961
    }
962
    const auto& transition = shell.transitions[i_trans];
59,860,559✔
963

964
    // Sample angle isotropically
965
    Direction u = isotropic_direction(p.current_seed());
59,860,559✔
966

967
    // Push the hole created by the electron transitioning to the photoelectron
968
    // hole onto the stack
969
    holes[n_holes++] = transition.primary_subshell;
59,860,559✔
970

971
    if (transition.secondary_subshell != -1) {
59,860,559✔
972
      // Non-radiative transition -- Auger/Coster-Kronig effect
973

974
      // Push the hole left by emitted auger electron onto the stack
975
      holes[n_holes++] = transition.secondary_subshell;
57,677,877✔
976

977
      // Create auger electron
978
      p.create_secondary(
57,677,877✔
979
        p.wgt(), u, transition.energy, ParticleType::electron());
57,677,877✔
980
    } else {
981
      // Radiative transition -- get X-ray energy
982

983
      // Create fluorescent photon
984
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon());
2,182,682✔
985
    }
986
  }
987
}
988

989
//==============================================================================
990
// Non-member functions
991
//==============================================================================
992

993
double detail::compton_profile_tail_integral(
15,757,759✔
994
  double pz, double pz_last, double profile_last, double slope)
995
{
996
  return profile_last * std::expm1(slope * (pz - pz_last)) / slope;
15,757,759✔
997
}
998

999
double detail::invert_compton_profile_tail(
3,300✔
1000
  double integral, double pz_last, double profile_last, double slope)
1001
{
1002
  return pz_last + std::log1p(slope * integral / profile_last) / slope;
3,300✔
1003
}
1004

1005
double detail::compton_energy_ratio(double alpha, double mu, double pz)
44,812,756✔
1006
{
1007
  if (pz == 0.0)
44,812,756✔
1008
    return 1.0 / (1.0 + alpha * (1.0 - mu));
11✔
1009

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

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

1038
  double root_min = std::numeric_limits<double>::infinity();
44,812,745✔
1039
  double root_max = -std::numeric_limits<double>::infinity();
44,812,745✔
1040
  if (std::isfinite(root1) && root1 > 0.0) {
44,812,745!
1041
    root_min = root1;
44,812,745✔
1042
    root_max = root1;
44,812,745✔
1043
  }
1044
  if (std::isfinite(root2) && root2 > 0.0) {
44,812,745!
1045
    root_min = std::min(root_min, root2);
44,812,217✔
1046
    root_max = std::max(root_max, root2);
44,812,217!
1047
  }
1048
  if (!std::isfinite(root_min))
44,812,745!
1049
    return std::numeric_limits<double>::quiet_NaN();
1050

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

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

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

1113
    // Calculate cosine of scattering angle based on basic relation
1114
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
2,463,813✔
1115
  }
1116
  return {alpha_out, mu};
28,317,648✔
1117
}
1118

1119
void free_memory_photon()
9,174✔
1120
{
1121
  data::elements.clear();
9,174✔
1122
  data::compton_profile_pz.resize({0});
9,174✔
1123
  data::ttb_e_grid.resize({0});
9,174✔
1124
  data::ttb_k_grid.resize({0});
9,174✔
1125
}
9,174✔
1126

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