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

openmc-dev / openmc / 12691153770

09 Jan 2025 01:45PM UTC coverage: 84.93% (+0.06%) from 84.869%
12691153770

Pull #3252

github

web-flow
Merge 177e8dc8b into 1eca46f53
Pull Request #3252: Adding vtkhdf option to write vtk data

70 of 94 new or added lines in 1 file covered. (74.47%)

189 existing lines in 3 files now uncovered.

50159 of 59059 relevant lines covered (84.93%)

34197787.76 hits per line

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

94.37
/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 "xtensor/xbuilder.hpp"
17
#include "xtensor/xmath.hpp"
18
#include "xtensor/xoperation.hpp"
19
#include "xtensor/xslice.hpp"
20
#include "xtensor/xview.hpp"
21

22
#include <cmath>
23
#include <fmt/core.h>
24
#include <tuple> // for tie
25

26
namespace openmc {
27

28
constexpr int PhotonInteraction::MAX_STACK_SIZE;
29

30
//==============================================================================
31
// Global variables
32
//==============================================================================
33

34
namespace data {
35

36
xt::xtensor<double, 1> compton_profile_pz;
37

38
std::unordered_map<std::string, int> element_map;
39
vector<unique_ptr<PhotonInteraction>> elements;
40

41
} // namespace data
42

43
//==============================================================================
44
// PhotonInteraction implementation
45
//==============================================================================
46

47
PhotonInteraction::PhotonInteraction(hid_t group)
623✔
48
{
49
  using namespace xt::placeholders;
50

51
  // Set index of element in global vector
52
  index_ = data::elements.size();
623✔
53

54
  // Get name of nuclide from group, removing leading '/'
55
  name_ = object_name(group).substr(1);
623✔
56
  data::element_map[name_] = index_;
623✔
57

58
  // Get atomic number
59
  read_attribute(group, "Z", Z_);
623✔
60

61
  // Determine number of energies and read energy grid
62
  read_dataset(group, "energy", energy_);
623✔
63

64
  // Read coherent scattering
65
  hid_t rgroup = open_group(group, "coherent");
623✔
66
  read_dataset(rgroup, "xs", coherent_);
623✔
67

68
  hid_t dset = open_dataset(rgroup, "integrated_scattering_factor");
623✔
69
  coherent_int_form_factor_ = Tabulated1D {dset};
623✔
70
  close_dataset(dset);
623✔
71

72
  if (object_exists(group, "anomalous_real")) {
623✔
73
    dset = open_dataset(rgroup, "anomalous_real");
×
74
    coherent_anomalous_real_ = Tabulated1D {dset};
×
75
    close_dataset(dset);
×
76
  }
77

78
  if (object_exists(group, "anomalous_imag")) {
623✔
79
    dset = open_dataset(rgroup, "anomalous_imag");
×
80
    coherent_anomalous_imag_ = Tabulated1D {dset};
×
81
    close_dataset(dset);
×
82
  }
83
  close_group(rgroup);
623✔
84

85
  // Read incoherent scattering
86
  rgroup = open_group(group, "incoherent");
623✔
87
  read_dataset(rgroup, "xs", incoherent_);
623✔
88
  dset = open_dataset(rgroup, "scattering_factor");
623✔
89
  incoherent_form_factor_ = Tabulated1D {dset};
623✔
90
  close_dataset(dset);
623✔
91
  close_group(rgroup);
623✔
92

93
  // Read pair production
94
  if (object_exists(group, "pair_production_electron")) {
623✔
95
    rgroup = open_group(group, "pair_production_electron");
623✔
96
    read_dataset(rgroup, "xs", pair_production_electron_);
623✔
97
    close_group(rgroup);
623✔
98
  } else {
99
    pair_production_electron_ = xt::zeros_like(energy_);
×
100
  }
101

102
  // Read pair production
103
  if (object_exists(group, "pair_production_nuclear")) {
623✔
104
    rgroup = open_group(group, "pair_production_nuclear");
623✔
105
    read_dataset(rgroup, "xs", pair_production_nuclear_);
623✔
106
    close_group(rgroup);
623✔
107
  } else {
108
    pair_production_nuclear_ = xt::zeros_like(energy_);
×
109
  }
110

111
  // Read photoelectric
112
  rgroup = open_group(group, "photoelectric");
623✔
113
  read_dataset(rgroup, "xs", photoelectric_total_);
623✔
114
  close_group(rgroup);
623✔
115

116
  // Read heating
117
  if (object_exists(group, "heating")) {
623✔
118
    rgroup = open_group(group, "heating");
623✔
119
    read_dataset(rgroup, "xs", heating_);
623✔
120
    close_group(rgroup);
623✔
121
  } else {
122
    heating_ = xt::zeros_like(energy_);
×
123
  }
124

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

135
  shells_.resize(n_shell);
623✔
136
  cross_sections_ = xt::zeros<double>({energy_.size(), n_shell});
623✔
137

138
  // Create mapping from designator to index
139
  std::unordered_map<int, int> shell_map;
623✔
140
  for (int i = 0; i < n_shell; ++i) {
5,902✔
141
    const auto& designator {designators[i]};
5,279✔
142

143
    int j = 1;
5,279✔
144
    for (const auto& subshell : SUBSHELLS) {
39,525✔
145
      if (designator == subshell) {
39,525✔
146
        shell_map[j] = i;
5,279✔
147
        shells_[i].index_subshell = j;
5,279✔
148
        break;
5,279✔
149
      }
150
      ++j;
34,246✔
151
    }
152
  }
153
  shell_map[0] = -1;
623✔
154

155
  for (int i = 0; i < n_shell; ++i) {
5,902✔
156
    const auto& designator {designators[i]};
5,279✔
157
    auto& shell {shells_[i]};
5,279✔
158

159
    // TODO: Move to ElectronSubshell constructor
160

161
    hid_t tgroup = open_group(rgroup, designator.c_str());
5,279✔
162

163
    // Read binding energy energy and number of electrons if atomic relaxation
164
    // data is present
165
    if (attribute_exists(tgroup, "binding_energy")) {
5,279✔
166
      has_atomic_relaxation_ = true;
5,279✔
167
      read_attribute(tgroup, "binding_energy", shell.binding_energy);
5,279✔
168
      read_attribute(tgroup, "num_electrons", shell.n_electrons);
5,279✔
169
    }
170

171
    // Read subshell cross section
172
    xt::xtensor<double, 1> xs;
5,279✔
173
    dset = open_dataset(tgroup, "xs");
5,279✔
174
    read_attribute(dset, "threshold_idx", shell.threshold);
5,279✔
175
    close_dataset(dset);
5,279✔
176
    read_dataset(tgroup, "xs", xs);
5,279✔
177

178
    auto cross_section =
179
      xt::view(cross_sections_, xt::range(shell.threshold, _), i);
5,279✔
180
    cross_section = xt::where(xs > 0, xt::log(xs), 0);
5,279✔
181

182
    if (object_exists(tgroup, "transitions")) {
5,279✔
183
      // Determine dimensions of transitions
184
      dset = open_dataset(tgroup, "transitions");
3,293✔
185
      auto dims = object_shape(dset);
3,293✔
186
      close_dataset(dset);
3,293✔
187

188
      int n_transition = dims[0];
3,293✔
189
      if (n_transition > 0) {
3,293✔
190
        xt::xtensor<double, 2> matrix;
3,293✔
191
        read_dataset(tgroup, "transitions", matrix);
3,293✔
192

193
        // Transition probability normalization
194
        double norm = xt::sum(xt::col(matrix, 3))();
3,293✔
195

196
        shell.transitions.resize(n_transition);
3,293✔
197
        for (int j = 0; j < n_transition; ++j) {
184,004✔
198
          auto& transition = shell.transitions[j];
180,711✔
199
          transition.primary_subshell = shell_map.at(matrix(j, 0));
180,711✔
200
          transition.secondary_subshell = shell_map.at(matrix(j, 1));
180,711✔
201
          transition.energy = matrix(j, 2);
180,711✔
202
          transition.probability = matrix(j, 3) / norm;
180,711✔
203
        }
204
      }
3,293✔
205
    }
3,293✔
206
    close_group(tgroup);
5,279✔
207
  }
5,279✔
208
  close_group(rgroup);
623✔
209

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

219
  // Determine number of electron shells
220
  rgroup = open_group(group, "compton_profiles");
623✔
221

222
  // Read electron shell PDF and binding energies
223
  read_dataset(rgroup, "num_electrons", electron_pdf_);
623✔
224
  electron_pdf_ /= xt::sum(electron_pdf_);
623✔
225
  read_dataset(rgroup, "binding_energy", binding_energy_);
623✔
226

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

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

236
  // Create Compton profile CDF
237
  auto n_profile = data::compton_profile_pz.size();
623✔
238
  auto n_shell_compton = profile_pdf_.shape(0);
623✔
239
  profile_cdf_ = xt::empty<double>({n_shell_compton, n_profile});
623✔
240
  for (int i = 0; i < n_shell_compton; ++i) {
5,050✔
241
    double c = 0.0;
4,427✔
242
    profile_cdf_(i, 0) = 0.0;
4,427✔
243
    for (int j = 0; j < n_profile - 1; ++j) {
137,237✔
244
      c += 0.5 *
265,620✔
245
           (data::compton_profile_pz(j + 1) - data::compton_profile_pz(j)) *
132,810✔
246
           (profile_pdf_(i, j) + profile_pdf_(i, j + 1));
132,810✔
247
      profile_cdf_(i, j + 1) = c;
132,810✔
248
    }
249
  }
250

251
  // Calculate total pair production
252
  pair_production_total_ = pair_production_nuclear_ + pair_production_electron_;
623✔
253

254
  if (settings::electron_treatment == ElectronTreatment::TTB) {
623✔
255
    // Read bremsstrahlung scaled DCS
256
    rgroup = open_group(group, "bremsstrahlung");
539✔
257
    read_dataset(rgroup, "dcs", dcs_);
539✔
258
    auto n_e = dcs_.shape()[0];
539✔
259
    auto n_k = dcs_.shape()[1];
539✔
260

261
    // Get energy grids used for bremsstrahlung DCS and for stopping powers
262
    xt::xtensor<double, 1> electron_energy;
539✔
263
    read_dataset(rgroup, "electron_energy", electron_energy);
539✔
264
    if (data::ttb_k_grid.size() == 0) {
539✔
265
      read_dataset(rgroup, "photon_energy", data::ttb_k_grid);
242✔
266
    }
267

268
    // Get data used for density effect correction
269
    read_dataset(rgroup, "num_electrons", n_electrons_);
539✔
270
    read_dataset(rgroup, "ionization_energy", ionization_energy_);
539✔
271
    read_attribute(rgroup, "I", I_);
539✔
272
    close_group(rgroup);
539✔
273

274
    // Truncate the bremsstrahlung data at the cutoff energy
275
    int photon = static_cast<int>(ParticleType::photon);
539✔
276
    const auto& E {electron_energy};
539✔
277
    double cutoff = settings::energy_cutoff[photon];
539✔
278
    if (cutoff > E(0)) {
539✔
279
      size_t i_grid = lower_bound_index(
12✔
280
        E.cbegin(), E.cend(), settings::energy_cutoff[photon]);
12✔
281

282
      // calculate interpolation factor
283
      double f = (std::log(cutoff) - std::log(E(i_grid))) /
12✔
284
                 (std::log(E(i_grid + 1)) - std::log(E(i_grid)));
12✔
285

286
      // Interpolate bremsstrahlung DCS at the cutoff energy and truncate
287
      xt::xtensor<double, 2> dcs({n_e - i_grid, n_k});
12✔
288
      for (int i = 0; i < n_k; ++i) {
372✔
289
        double y = std::exp(
360✔
290
          std::log(dcs_(i_grid, i)) +
360✔
291
          f * (std::log(dcs_(i_grid + 1, i)) - std::log(dcs_(i_grid, i))));
360✔
292
        auto col_i = xt::view(dcs, xt::all(), i);
360✔
293
        col_i(0) = y;
360✔
294
        for (int j = i_grid + 1; j < n_e; ++j) {
67,320✔
295
          col_i(j - i_grid) = dcs_(j, i);
66,960✔
296
        }
297
      }
360✔
298
      dcs_ = dcs;
12✔
299

300
      xt::xtensor<double, 1> frst {cutoff};
12✔
301
      electron_energy = xt::concatenate(xt::xtuple(
24✔
302
        frst, xt::view(electron_energy, xt::range(i_grid + 1, n_e))));
36✔
303
    }
12✔
304

305
    // Set incident particle energy grid
306
    if (data::ttb_e_grid.size() == 0) {
539✔
307
      data::ttb_e_grid = electron_energy;
242✔
308
    }
309

310
    // Calculate the radiative stopping power
311
    stopping_power_radiative_ = xt::empty<double>({data::ttb_e_grid.size()});
539✔
312
    for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
108,183✔
313
      // Integrate over reduced photon energy
314
      double c = 0.0;
107,644✔
315
      for (int j = 0; j < data::ttb_k_grid.size() - 1; ++j) {
3,229,320✔
316
        c += 0.5 * (dcs_(i, j + 1) + dcs_(i, j)) *
3,121,676✔
317
             (data::ttb_k_grid(j + 1) - data::ttb_k_grid(j));
3,121,676✔
318
      }
319
      double e = data::ttb_e_grid(i);
107,644✔
320

321
      // Square of the ratio of the speed of light to the velocity of the
322
      // charged particle
323
      double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
107,644✔
324
                       ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
107,644✔
325

326
      stopping_power_radiative_(i) = Z_ * Z_ / beta_sq * e * c;
107,644✔
327
    }
328
  }
539✔
329

330
  // Take logarithm of energies and cross sections since they are log-log
331
  // interpolated. Note that cross section libraries converted from ACE files
332
  // represent zero as exp(-500) to avoid log-log interpolation errors. For
333
  // values below exp(-499) we store the log as -900, for which exp(-900)
334
  // evaluates to zero.
335
  double limit = std::exp(-499.0);
623✔
336
  energy_ = xt::log(energy_);
623✔
337
  coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0);
623✔
338
  incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0);
623✔
339
  photoelectric_total_ = xt::where(
1,246✔
340
    photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0);
1,869✔
341
  pair_production_total_ = xt::where(
1,246✔
342
    pair_production_total_ > limit, xt::log(pair_production_total_), -900.0);
1,869✔
343
  heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0);
623✔
344
}
623✔
345

346
PhotonInteraction::~PhotonInteraction()
623✔
347
{
348
  data::element_map.erase(name_);
623✔
349
}
623✔
350

351
int PhotonInteraction::calc_max_stack_size() const
623✔
352
{
353
  // Table to store solutions to sub-problems
354
  std::unordered_map<int, int> visited;
623✔
355

356
  // Find the maximum possible size of the stack used to store holes created
357
  // during atomic relaxation, checking over every subshell the initial hole
358
  // could be in
359
  int max_size = 0;
623✔
360
  for (int i_shell = 0; i_shell < shells_.size(); ++i_shell) {
5,902✔
361
    max_size = std::max(max_size, this->calc_helper(visited, i_shell));
5,279✔
362
  }
363
  return max_size;
623✔
364
}
623✔
365

366
int PhotonInteraction::calc_helper(
350,130✔
367
  std::unordered_map<int, int>& visited, int i_shell) const
368
{
369
  // No transitions for this subshell, so this is the only shell in the stack
370
  const auto& shell {shells_[i_shell]};
350,130✔
371
  if (shell.transitions.empty()) {
350,130✔
372
    return 1;
143,146✔
373
  }
374

375
  // Check the table to see if the maximum stack size has already been
376
  // calculated for this shell
377
  auto it = visited.find(i_shell);
206,984✔
378
  if (it != visited.end()) {
206,984✔
379
    return it->second;
203,691✔
380
  }
381

382
  int max_size = 0;
3,293✔
383
  for (const auto& transition : shell.transitions) {
184,004✔
384
    // If this is a non-radiative transition two vacancies are created and
385
    // the stack grows by one; if this is a radiative transition only one
386
    // vacancy is created and the stack size stays the same
387
    int size = 0;
180,711✔
388
    if (transition.secondary_subshell != -1) {
180,711✔
389
      size = this->calc_helper(visited, transition.secondary_subshell) + 1;
164,140✔
390
    }
391
    size =
180,711✔
392
      std::max(size, this->calc_helper(visited, transition.primary_subshell));
180,711✔
393
    max_size = std::max(max_size, size);
180,711✔
394
  }
395
  visited[i_shell] = max_size;
3,293✔
396
  return max_size;
3,293✔
397
}
398

399
void PhotonInteraction::compton_scatter(double alpha, bool doppler,
5,764,452✔
400
  double* alpha_out, double* mu, int* i_shell, uint64_t* seed) const
401
{
402
  double form_factor_xmax = 0.0;
5,764,452✔
403
  while (true) {
404
    // Sample Klein-Nishina distribution for trial energy and angle
405
    std::tie(*alpha_out, *mu) = klein_nishina(alpha, seed);
5,880,324✔
406

407
    // Note that the parameter used here does not correspond exactly to the
408
    // momentum transfer q in ENDF-102 Eq. (27.2). Rather, this is the
409
    // parameter as defined by Hubbell, where the actual data comes from
410
    double x =
411
      MASS_ELECTRON_EV / PLANCK_C * alpha * std::sqrt(0.5 * (1.0 - *mu));
5,880,324✔
412

413
    // Calculate S(x, Z) and S(x_max, Z)
414
    double form_factor_x = incoherent_form_factor_(x);
5,880,324✔
415
    if (form_factor_xmax == 0.0) {
5,880,324✔
416
      form_factor_xmax =
417
        incoherent_form_factor_(MASS_ELECTRON_EV / PLANCK_C * alpha);
5,764,452✔
418
    }
419

420
    // Perform rejection on form factor
421
    if (prn(seed) < form_factor_x / form_factor_xmax) {
5,880,324✔
422
      if (doppler) {
5,764,452✔
423
        double E_out;
424
        this->compton_doppler(alpha, *mu, &E_out, i_shell, seed);
5,764,452✔
425
        *alpha_out = E_out / MASS_ELECTRON_EV;
5,764,452✔
426

427
        // It's possible for the Compton profile data to have more shells than
428
        // there are in the ENDF data. Make sure the shell index doesn't end up
429
        // out of bounds.
430
        if (*i_shell >= shells_.size()) {
5,764,452✔
UNCOV
431
          *i_shell = -1;
×
432
        }
433
      } else {
UNCOV
434
        *i_shell = -1;
×
435
      }
436
      break;
5,764,452✔
437
    }
438
  }
115,872✔
439
}
5,764,452✔
440

441
void PhotonInteraction::compton_doppler(
5,764,452✔
442
  double alpha, double mu, double* E_out, int* i_shell, uint64_t* seed) const
443
{
444
  auto n = data::compton_profile_pz.size();
5,764,452✔
445

446
  int shell; // index for shell
447
  while (true) {
448
    // Sample electron shell
449
    double rn = prn(seed);
5,764,452✔
450
    double c = 0.0;
5,764,452✔
451
    for (shell = 0; shell < electron_pdf_.size(); ++shell) {
21,876,876✔
452
      c += electron_pdf_(shell);
21,876,876✔
453
      if (rn < c)
21,876,876✔
454
        break;
5,764,452✔
455
    }
456

457
    // Determine binding energy of shell
458
    double E_b = binding_energy_(shell);
5,764,452✔
459

460
    // Determine p_z,max
461
    double E = alpha * MASS_ELECTRON_EV;
5,764,452✔
462
    if (E < E_b) {
5,764,452✔
463
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
1,200✔
464
      break;
1,200✔
465
    }
466

467
    double pz_max = -FINE_STRUCTURE * (E_b - (E - E_b) * alpha * (1.0 - mu)) /
5,763,252✔
468
                    std::sqrt(2.0 * E * (E - E_b) * (1.0 - mu) + E_b * E_b);
5,763,252✔
469
    if (pz_max < 0.0) {
5,763,252✔
470
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
48,624✔
471
      break;
48,624✔
472
    }
473

474
    // Determine profile cdf value corresponding to p_z,max
475
    double c_max;
476
    if (pz_max > data::compton_profile_pz(n - 1)) {
5,714,628✔
477
      c_max = profile_cdf_(shell, n - 1);
946,116✔
478
    } else {
479
      int i = lower_bound_index(data::compton_profile_pz.cbegin(),
4,768,512✔
480
        data::compton_profile_pz.cend(), pz_max);
4,768,512✔
481
      double pz_l = data::compton_profile_pz(i);
4,768,512✔
482
      double pz_r = data::compton_profile_pz(i + 1);
4,768,512✔
483
      double p_l = profile_pdf_(shell, i);
4,768,512✔
484
      double p_r = profile_pdf_(shell, i + 1);
4,768,512✔
485
      double c_l = profile_cdf_(shell, i);
4,768,512✔
486
      if (pz_l == pz_r) {
4,768,512✔
UNCOV
487
        c_max = c_l;
×
488
      } else if (p_l == p_r) {
4,768,512✔
489
        c_max = c_l + (pz_max - pz_l) * p_l;
8,628✔
490
      } else {
491
        double m = (p_l - p_r) / (pz_l - pz_r);
4,759,884✔
492
        c_max = c_l + (std::pow((m * (pz_max - pz_l) + p_l), 2) - p_l * p_l) /
4,759,884✔
493
                        (2.0 * m);
4,759,884✔
494
      }
495
    }
496

497
    // Sample value on bounded cdf
498
    c = prn(seed) * c_max;
5,714,628✔
499

500
    // Determine pz corresponding to sampled cdf value
501
    auto cdf_shell = xt::view(profile_cdf_, shell, xt::all());
5,714,628✔
502
    int i = lower_bound_index(cdf_shell.cbegin(), cdf_shell.cend(), c);
5,714,628✔
503
    double pz_l = data::compton_profile_pz(i);
5,714,628✔
504
    double pz_r = data::compton_profile_pz(i + 1);
5,714,628✔
505
    double p_l = profile_pdf_(shell, i);
5,714,628✔
506
    double p_r = profile_pdf_(shell, i + 1);
5,714,628✔
507
    double c_l = profile_cdf_(shell, i);
5,714,628✔
508
    double pz;
509
    if (pz_l == pz_r) {
5,714,628✔
UNCOV
510
      pz = pz_l;
×
511
    } else if (p_l == p_r) {
5,714,628✔
512
      pz = pz_l + (c - c_l) / p_l;
525,444✔
513
    } else {
514
      double m = (p_l - p_r) / (pz_l - pz_r);
5,189,184✔
515
      pz = pz_l + (std::sqrt(p_l * p_l + 2.0 * m * (c - c_l)) - p_l) / m;
5,189,184✔
516
    }
517

518
    // Determine outgoing photon energy corresponding to electron momentum
519
    // (solve Eq. 39 in LA-UR-04-0487 for E')
520
    double momentum_sq = std::pow((pz / FINE_STRUCTURE), 2);
5,714,628✔
521
    double f = 1.0 + alpha * (1.0 - mu);
5,714,628✔
522
    double a = momentum_sq - f * f;
5,714,628✔
523
    double b = 2.0 * E * (f - momentum_sq * mu);
5,714,628✔
524
    c = E * E * (momentum_sq - 1.0);
5,714,628✔
525

526
    double quad = b * b - 4.0 * a * c;
5,714,628✔
527
    if (quad < 0) {
5,714,628✔
UNCOV
528
      *E_out = alpha / (1 + alpha * (1 - mu)) * MASS_ELECTRON_EV;
×
UNCOV
529
      break;
×
530
    }
531
    quad = std::sqrt(quad);
5,714,628✔
532
    double E_out1 = -(b + quad) / (2.0 * a);
5,714,628✔
533
    double E_out2 = -(b - quad) / (2.0 * a);
5,714,628✔
534

535
    // Determine solution to quadratic equation that is positive
536
    if (E_out1 > 0.0) {
5,714,628✔
537
      if (E_out2 > 0.0) {
5,714,628✔
538
        // If both are positive, pick one at random
539
        *E_out = prn(seed) < 0.5 ? E_out1 : E_out2;
5,714,628✔
540
      } else {
541
        *E_out = E_out1;
×
542
      }
543
    } else {
544
      if (E_out2 > 0.0) {
×
UNCOV
545
        *E_out = E_out2;
×
546
      } else {
547
        // No positive solution -- resample
UNCOV
548
        continue;
×
549
      }
550
    }
551
    if (*E_out < E - E_b)
5,714,628✔
552
      break;
5,714,628✔
553
  }
5,714,628✔
554

555
  *i_shell = shell;
5,764,452✔
556
}
5,764,452✔
557

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

575
  // check for case where two energy points are the same
576
  if (energy_(i_grid) == energy_(i_grid + 1))
31,751,192✔
UNCOV
577
    ++i_grid;
×
578

579
  // calculate interpolation factor
580
  double f =
581
    (log_E - energy_(i_grid)) / (energy_(i_grid + 1) - energy_(i_grid));
31,751,192✔
582

583
  auto& xs {p.photon_xs(index_)};
31,751,192✔
584
  xs.index_grid = i_grid;
31,751,192✔
585
  xs.interp_factor = f;
31,751,192✔
586

587
  // Calculate microscopic coherent cross section
588
  xs.coherent = std::exp(
31,751,192✔
589
    coherent_(i_grid) + f * (coherent_(i_grid + 1) - coherent_(i_grid)));
31,751,192✔
590

591
  // Calculate microscopic incoherent cross section
592
  xs.incoherent = std::exp(
31,751,192✔
593
    incoherent_(i_grid) + f * (incoherent_(i_grid + 1) - incoherent_(i_grid)));
31,751,192✔
594

595
  // Calculate microscopic photoelectric cross section
596
  xs.photoelectric = 0.0;
31,751,192✔
597
  const auto& xs_lower = xt::row(cross_sections_, i_grid);
31,751,192✔
598
  const auto& xs_upper = xt::row(cross_sections_, i_grid + 1);
31,751,192✔
599

600
  for (int i = 0; i < xs_upper.size(); ++i)
307,215,468✔
601
    if (xs_lower(i) != 0)
275,464,276✔
602
      xs.photoelectric +=
264,132,748✔
603
        std::exp(xs_lower(i) + f * (xs_upper(i) - xs_lower(i)));
264,132,748✔
604

605
  // Calculate microscopic pair production cross section
606
  xs.pair_production = std::exp(
31,751,192✔
607
    pair_production_total_(i_grid) +
31,751,192✔
608
    f * (pair_production_total_(i_grid + 1) - pair_production_total_(i_grid)));
31,751,192✔
609

610
  // Calculate microscopic total cross section
611
  xs.total =
31,751,192✔
612
    xs.coherent + xs.incoherent + xs.photoelectric + xs.pair_production;
31,751,192✔
613
  xs.last_E = p.E();
31,751,192✔
614
}
31,751,192✔
615

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

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

626
    // Sample cumulative distribution
627
    double F = prn(seed) * F_max;
644,100✔
628

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

636
    // Calculate mu
637
    mu = 1.0 - 2.0 * x2 / x2_max;
644,100✔
638

639
    if (prn(seed) < 0.5 * (1.0 + mu * mu))
644,100✔
640
      break;
559,692✔
641
  }
84,408✔
642
  return mu;
559,692✔
643
}
644

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

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

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

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

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

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

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

724
      // Sample e from pi_1 using the inverse transform method
725
      e = rn >= 0.5
10,560✔
726
            ? 0.5 + (0.5 - 1.0 / alpha) * std::pow(2.0 * rn - 1.0, 1.0 / 3.0)
10,560✔
727
            : 0.5 - (0.5 - 1.0 / alpha) * std::pow(1.0 - 2.0 * rn, 1.0 / 3.0);
5,268✔
728
    } else {
729
      i = 2;
105,060✔
730

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

735
    // Calculate phi_i(e) and deliver e if rn <= U_i(e)
736
    b = r[Z_] / (2.0 * alpha * e * (1.0 - e));
115,620✔
737
    t1 = 2.0 * std::log(1.0 + b * b);
115,620✔
738
    t2 = b * std::atan(1.0 / b);
115,620✔
739
    t3 = b * b * (4.0 - 4.0 * t2 - 3.0 * std::log(1.0 + 1.0 / (b * b)));
115,620✔
740
    if (i == 1) {
115,620✔
741
      double phi1 = 7.0 / 3.0 - t1 - 6.0 * t2 - t3 + t4;
10,560✔
742
      if (prn(seed) <= phi1 / phi1_max)
10,560✔
743
        break;
6,780✔
744
    } else {
745
      double phi2 = 11.0 / 6.0 - t1 - 3.0 * t2 + 0.5 * t3 + t4;
105,060✔
746
      if (prn(seed) <= phi2 / phi2_max)
105,060✔
747
        break;
85,512✔
748
    }
749
  }
23,328✔
750

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

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

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

771
void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const
10,469,472✔
772
{
773
  // Return if no atomic relaxation data is present
774
  if (!has_atomic_relaxation_)
10,469,472✔
UNCOV
775
    return;
×
776

777
  // Stack for unprocessed holes left by transitioning electrons
778
  int n_holes = 0;
10,469,472✔
779
  array<int, MAX_STACK_SIZE> holes;
780

781
  // Push the initial hole onto the stack
782
  holes[n_holes++] = i_shell;
10,469,472✔
783

784
  while (n_holes > 0) {
99,318,804✔
785
    // Pop the next hole off the stack
786
    int i_hole = holes[--n_holes];
88,849,332✔
787
    const auto& shell {shells_[i_hole]};
88,849,332✔
788

789
    // If no transitions, assume fluorescent photon from captured free electron
790
    if (shell.transitions.empty()) {
88,849,332✔
791
      Direction u = isotropic_direction(p.current_seed());
48,678,084✔
792
      double E = shell.binding_energy;
48,678,084✔
793
      p.create_secondary(p.wgt(), u, E, ParticleType::photon);
48,678,084✔
794
      continue;
48,678,084✔
795
    }
48,678,084✔
796

797
    // Sample transition
798
    double c = -prn(p.current_seed());
40,171,248✔
799
    int i_trans;
800
    for (i_trans = 0; i_trans < shell.transitions.size(); ++i_trans) {
1,061,793,564✔
801
      c += shell.transitions[i_trans].probability;
1,061,793,564✔
802
      if (c > 0)
1,061,793,564✔
803
        break;
40,171,248✔
804
    }
805
    const auto& transition = shell.transitions[i_trans];
40,171,248✔
806

807
    // Sample angle isotropically
808
    Direction u = isotropic_direction(p.current_seed());
40,171,248✔
809

810
    // Push the hole created by the electron transitioning to the photoelectron
811
    // hole onto the stack
812
    holes[n_holes++] = transition.primary_subshell;
40,171,248✔
813

814
    if (transition.secondary_subshell != -1) {
40,171,248✔
815
      // Non-radiative transition -- Auger/Coster-Kronig effect
816

817
      // Push the hole left by emitted auger electron onto the stack
818
      holes[n_holes++] = transition.secondary_subshell;
38,208,612✔
819

820
      // Create auger electron
821
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron);
38,208,612✔
822
    } else {
823
      // Radiative transition -- get X-ray energy
824

825
      // Create fluorescent photon
826
      p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon);
1,962,636✔
827
    }
828
  }
829
}
830

831
//==============================================================================
832
// Non-member functions
833
//==============================================================================
834

835
std::pair<double, double> klein_nishina(double alpha, uint64_t* seed)
5,880,324✔
836
{
837
  double alpha_out, mu;
838
  double beta = 1.0 + 2.0 * alpha;
5,880,324✔
839
  if (alpha < 3.0) {
5,880,324✔
840
    // Kahn's rejection method
841
    double t = beta / (beta + 8.0);
5,325,252✔
842
    double x;
843
    while (true) {
844
      if (prn(seed) < t) {
8,909,856✔
845
        // Left branch of flow chart
846
        double r = uniform_distribution(0.0, 2.0, seed);
1,682,280✔
847
        x = 1.0 + alpha * r;
1,682,280✔
848
        if (prn(seed) < 4.0 / x * (1.0 - 1.0 / x)) {
1,682,280✔
849
          mu = 1 - r;
1,081,896✔
850
          break;
1,081,896✔
851
        }
852
      } else {
853
        // Right branch of flow chart
854
        x = beta / (1.0 + 2.0 * alpha * prn(seed));
7,227,576✔
855
        mu = 1.0 + (1.0 - x) / alpha;
7,227,576✔
856
        if (prn(seed) < 0.5 * (mu * mu + 1.0 / x))
7,227,576✔
857
          break;
4,243,356✔
858
      }
859
    }
3,584,604✔
860
    alpha_out = alpha / x;
5,325,252✔
861

862
  } else {
863
    // Koblinger's direct method
864
    double gamma = 1.0 - std::pow(beta, -2);
555,072✔
865
    double s =
866
      prn(seed) * (4.0 / alpha + 0.5 * gamma +
555,072✔
867
                    (1.0 - (1.0 + beta) / (alpha * alpha)) * std::log(beta));
555,072✔
868
    if (s <= 2.0 / alpha) {
555,072✔
869
      // For first term, x = 1 + 2ar
870
      // Therefore, a' = a/(1 + 2ar)
871
      alpha_out = alpha / (1.0 + 2.0 * alpha * prn(seed));
79,128✔
872
    } else if (s <= 4.0 / alpha) {
475,944✔
873
      // For third term, x = beta/(1 + 2ar)
874
      // Therefore, a' = a(1 + 2ar)/beta
875
      alpha_out = alpha * (1.0 + 2.0 * alpha * prn(seed)) / beta;
80,256✔
876
    } else if (s <= 4.0 / alpha + 0.5 * gamma) {
395,688✔
877
      // For fourth term, x = 1/sqrt(1 - gamma*r)
878
      // Therefore, a' = a*sqrt(1 - gamma*r)
879
      alpha_out = alpha * std::sqrt(1.0 - gamma * prn(seed));
100,992✔
880
    } else {
881
      // For third term, x = beta^r
882
      // Therefore, a' = a/beta^r
883
      alpha_out = alpha / std::pow(beta, prn(seed));
294,696✔
884
    }
885

886
    // Calculate cosine of scattering angle based on basic relation
887
    mu = 1.0 + 1.0 / alpha - 1.0 / alpha_out;
555,072✔
888
  }
889
  return {alpha_out, mu};
5,880,324✔
890
}
891

892
void free_memory_photon()
6,931✔
893
{
894
  data::elements.clear();
6,931✔
895
  data::compton_profile_pz.resize({0});
6,931✔
896
  data::ttb_e_grid.resize({0});
6,931✔
897
  data::ttb_k_grid.resize({0});
6,931✔
898
}
6,931✔
899

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

© 2025 Coveralls, Inc