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

openmc-dev / openmc / 22590555373

02 Mar 2026 06:44PM UTC coverage: 81.331% (-0.2%) from 81.508%
22590555373

Pull #3550

github

web-flow
Merge d11f2e51a into 823b4c96c
Pull Request #3550: [Point Detector] Add distribution get_pdf functionality

17484 of 25271 branches covered (69.19%)

Branch coverage included in aggregate %.

53 of 164 new or added lines in 12 files covered. (32.32%)

33 existing lines in 3 files now uncovered.

57704 of 67176 relevant lines covered (85.9%)

44912746.91 hits per line

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

65.84
/src/secondary_thermal.cpp
1
#include "openmc/secondary_thermal.h"
2

3
#include "openmc/hdf5_interface.h"
4
#include "openmc/math_functions.h"
5
#include "openmc/random_lcg.h"
6
#include "openmc/search.h"
7
#include "openmc/vector.h"
8

9
#include "openmc/tensor.h"
10

11
#include <cassert>
12
#include <cmath> // for log, exp
13

14
namespace openmc {
15

16
//==============================================================================
17
// CoherentElasticAE implementation
18
//==============================================================================
19

20
CoherentElasticAE::CoherentElasticAE(const CoherentElasticXS& xs) : xs_ {xs} {}
134✔
21

22
void CoherentElasticAE::sample(
178,013✔
23
  double E_in, double& E_out, double& mu, uint64_t* seed) const
24
{
25
  // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1)
26
  E_out = E_in;
178,013✔
27

28
  const auto& energies {xs_.bragg_edges()};
178,013!
29

30
  assert(E_in >= energies.front());
178,013!
31

32
  const int i = lower_bound_index(energies.begin(), energies.end(), E_in);
178,013✔
33

34
  // Sample a Bragg edge between 1 and i
35
  // E[0] < E_in < E[i+1] -> can scatter in bragg edges 0..i
36
  const auto& factors = xs_.factors();
178,013✔
37
  const double prob = prn(seed) * factors[i];
178,013✔
38

39
  const int k = std::lower_bound(factors.begin(), factors.begin() + i, prob) -
178,013✔
40
                factors.begin();
178,013✔
41

42
  // Characteristic scattering cosine for this Bragg edge (ENDF-102, Eq. 7-2)
43
  mu = 1.0 - 2.0 * energies[k] / E_in;
178,013✔
44
}
178,013✔
45

UNCOV
46
double CoherentElasticAE::sample_energy_and_pdf(
×
47
  double E_in, double mu, double& E_out, uint64_t* seed) const
48
{
49
  // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1)
50

UNCOV
51
  double pdf;
×
UNCOV
52
  E_out = E_in;
×
UNCOV
53
  const auto& energies {xs_.bragg_edges()};
×
UNCOV
54
  const auto& factors = xs_.factors();
×
55

NEW
56
  if (E_in < energies.front() || E_in > energies.back()) {
×
57
    return 0;
58
  }
59

NEW
60
  const int n = upper_bound_index(energies.begin(), energies.end(), E_in);
×
61

NEW
62
  vector<double> mu_vector;
×
NEW
63
  mu_vector.reserve(n);
×
64

NEW
65
  std::transform(energies.rbegin() + n - 1, energies.rend(),
×
66
    std::back_inserter(mu_vector),
NEW
67
    [E_in](double Ei) { return 1 - 2 * Ei / E_in; });
×
68

NEW
69
  vector<double> weights;
×
NEW
70
  weights.reserve(n);
×
71

NEW
72
  weights.emplace_back(factors[0] / factors[n]);
×
NEW
73
  for (int i = 1; i <= n; ++i) {
×
NEW
74
    weights.emplace_back((factors[i] - factors[i - 1]) / factors[n]);
×
75
  }
NEW
76
  return get_pdf_discrete(mu_vector, weights, mu);
×
NEW
77
}
×
78

79
//==============================================================================
80
// IncoherentElasticAE implementation
81
//==============================================================================
82

UNCOV
83
IncoherentElasticAE::IncoherentElasticAE(hid_t group)
×
84
{
UNCOV
85
  read_dataset(group, "debye_waller", debye_waller_);
×
86
}
×
87

88
void IncoherentElasticAE::sample(
×
89
  double E_in, double& E_out, double& mu, uint64_t* seed) const
90
{
91
  E_out = E_in;
×
92

93
  // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4
NEW
94
  double c = 2 * E_in * debye_waller_;
×
NEW
95
  mu = std::log(1.0 + prn(seed) * (std::exp(2.0 * c) - 1)) / c - 1.0;
×
96
}
×
97
double IncoherentElasticAE::sample_energy_and_pdf(
×
98
  double E_in, double mu, double& E_out, uint64_t* seed) const
99
{
NEW
100
  E_out = E_in;
×
101

102
  // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4
103
  double c = 2 * E_in * debye_waller_;
×
NEW
104
  double A = c / (1 - std::exp(-2.0 * c)); // normalization factor
×
NEW
105
  return A * std::exp(-c * (1 - mu));
×
106
}
107

108
//==============================================================================
109
// IncoherentElasticAEDiscrete implementation
110
//==============================================================================
111

112
IncoherentElasticAEDiscrete::IncoherentElasticAEDiscrete(
44✔
113
  hid_t group, const vector<double>& energy)
44✔
114
  : energy_ {energy}
44✔
115
{
116
  read_dataset(group, "mu_out", mu_out_);
44✔
117
}
44✔
118

119
void IncoherentElasticAEDiscrete::sample(
1,342✔
120
  double E_in, double& E_out, double& mu, uint64_t* seed) const
121
{
122
  // Get index and interpolation factor for elastic grid
123
  int i;
1,342✔
124
  double f;
1,342✔
125
  get_energy_index(energy_, E_in, i, f);
1,342✔
126

127
  // Interpolate between two discrete cosines corresponding to neighboring
128
  // incoming energies.
129

130
  // Sample outgoing cosine bin
131
  int n_mu = mu_out_.shape(1);
1,342!
132
  int k = prn(seed) * n_mu;
1,342✔
133

134
  // Rather than use the sampled discrete mu directly, it is smeared over
135
  // a bin of width 0.5*min(mu[k] - mu[k-1], mu[k+1] - mu[k]) centered on the
136
  // discrete mu value itself.
137

138
  // Interpolate kth mu value between distributions at energies i and i+1
139
  mu = mu_out_(i, k) + f * (mu_out_(i + 1, k) - mu_out_(i, k));
1,342✔
140

141
  // Inteprolate (k-1)th mu value between distributions at energies i and i+1.
142
  // When k==0, pick a value that will smear the cosine out to a minimum of -1.
143
  double mu_left = (k == 0) ? -1.0 - (mu + 1.0)
1,342✔
144
                            : mu_out_(i, k - 1) +
990✔
145
                                f * (mu_out_(i + 1, k - 1) - mu_out_(i, k - 1));
990✔
146

147
  // Inteprolate (k+1)th mu value between distributions at energies i and i+1.
148
  // When k is the last discrete value, pick a value that will smear the cosine
149
  // out to a maximum of 1.
150
  double mu_right =
1,342✔
151
    (k == n_mu - 1)
1,342✔
152
      ? 1.0 + (1.0 - mu)
1,342✔
153
      : mu_out_(i, k + 1) + f * (mu_out_(i + 1, k + 1) - mu_out_(i, k + 1));
990✔
154

155
  // Smear cosine
156
  mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5);
1,947✔
157

158
  // Energy doesn't change in elastic scattering
159
  E_out = E_in;
1,342✔
160
}
1,342✔
161

UNCOV
162
double IncoherentElasticAEDiscrete::sample_energy_and_pdf(
×
163
  double E_in, double mu, double& E_out, uint64_t* seed) const
164
{
165
  // Get index and interpolation factor for elastic grid
NEW
166
  int i;
×
NEW
167
  double f;
×
NEW
168
  get_energy_index(energy_, E_in, i, f);
×
169
  // Energy doesn't change in elastic scattering
NEW
170
  E_out = E_in;
×
171

NEW
172
  double pdf = 0.0;
×
NEW
173
  pdf += f * get_pdf_discrete(mu_out_.slice(i + 1, tensor::all), mu);
×
NEW
174
  pdf += (1 - f) * get_pdf_discrete(mu_out_.slice(i, tensor::all), mu);
×
NEW
175
  return pdf;
×
176
}
177

178
//==============================================================================
179
// IncoherentInelasticAEDiscrete implementation
180
//==============================================================================
181

182
IncoherentInelasticAEDiscrete::IncoherentInelasticAEDiscrete(
1,240✔
183
  hid_t group, const vector<double>& energy)
1,240✔
184
  : energy_ {energy}
1,240✔
185
{
186
  read_dataset(group, "energy_out", energy_out_);
1,240✔
187
  read_dataset(group, "mu_out", mu_out_);
1,240✔
188
  read_dataset(group, "skewed", skewed_);
1,240✔
189
}
1,240✔
190

191
void IncoherentInelasticAEDiscrete::sample_params(
123,683,823✔
192
  double E_in, double& E_out, int& j, uint64_t* seed) const
193
{
194
  // Get index and interpolation factor for inelastic grid
195
  int i;
123,683,823✔
196
  double f;
123,683,823✔
197
  get_energy_index(energy_, E_in, i, f);
123,683,823✔
198

199
  // Now that we have an incoming energy bin, we need to determine the outgoing
200
  // energy bin. This will depend on whether the outgoing energy distribution is
201
  // skewed. If it is skewed, then the first two and last two bins have lower
202
  // probabilities than the other bins (0.1 for the first and last bins and 0.4
203
  // for the second and second to last bins, relative to a normal bin
204
  // probability of 1). Otherwise, each bin is equally probable.
205

206
  int n = energy_out_.shape(1);
123,683,823!
207
  if (!skewed_) {
123,683,823!
208
    // All bins equally likely
UNCOV
209
    j = prn(seed) * n;
×
210
  } else {
211
    // Distribution skewed away from edge points
212
    double r = prn(seed) * (n - 3);
123,683,823✔
213
    if (r > 1.0) {
123,683,823✔
214
      // equally likely N-4 middle bins
215
      j = r + 1;
121,650,936✔
216
    } else if (r > 0.6) {
2,032,887✔
217
      // second to last bin has relative probability of 0.4
218
      j = n - 2;
806,605✔
219
    } else if (r > 0.5) {
1,226,282✔
220
      // last bin has relative probability of 0.1
221
      j = n - 1;
203,402✔
222
    } else if (r > 0.1) {
1,022,880✔
223
      // second bin has relative probability of 0.4
224
      j = 1;
819,261✔
225
    } else {
226
      // first bin has relative probability of 0.1
227
      j = 0;
203,619✔
228
    }
229
  }
230

231
  // Determine outgoing energy corresponding to E_in[i] and E_in[i+1]
232
  double E_ij = energy_out_(i, j);
123,683,823✔
233
  double E_i1j = energy_out_(i + 1, j);
123,683,823✔
234

235
  // Outgoing energy
236
  E_out = (1 - f) * E_ij + f * E_i1j;
123,683,823✔
237
}
123,683,823✔
238

239
void IncoherentInelasticAEDiscrete::sample(
123,683,823✔
240
  double E_in, double& E_out, double& mu, uint64_t* seed) const
241
{
242
  // Get index and interpolation factor for inelastic grid
243
  int i;
123,683,823✔
244
  double f;
123,683,823✔
245
  get_energy_index(energy_, E_in, i, f);
123,683,823✔
246

247
  int j;
123,683,823✔
248
  sample_params(E_in, E_out, j, seed);
123,683,823✔
249

250
  // Sample outgoing cosine bin
251
  int m = mu_out_.shape(2);
123,683,823!
252
  int k = prn(seed) * m;
123,683,823✔
253

254
  // Determine outgoing cosine corresponding to E_in[i] and E_in[i+1]
255
  double mu_ijk = mu_out_(i, j, k);
123,683,823✔
256
  double mu_i1jk = mu_out_(i + 1, j, k);
123,683,823✔
257

258
  // Cosine of angle between incoming and outgoing neutron
259
  mu = (1 - f) * mu_ijk + f * mu_i1jk;
123,683,823✔
260
}
123,683,823✔
261

UNCOV
262
double IncoherentInelasticAEDiscrete::sample_energy_and_pdf(
×
263
  double E_in, double mu, double& E_out, uint64_t* seed) const
264
{
265
  // Get index and interpolation factor for inelastic grid
NEW
266
  int i;
×
NEW
267
  double f;
×
NEW
268
  get_energy_index(energy_, E_in, i, f);
×
NEW
269
  int j;
×
NEW
270
  sample_params(E_in, E_out, j, seed);
×
271

NEW
272
  double pdf = 0.0;
×
NEW
273
  pdf += f * get_pdf_discrete(mu_out_.slice(i + 1, j, tensor::all), mu);
×
NEW
274
  pdf += (1 - f) * get_pdf_discrete(mu_out_.slice(i, j, tensor::all), mu);
×
NEW
275
  return pdf;
×
276
}
277

278
//==============================================================================
279
// IncoherentInelasticAE implementation
280
//==============================================================================
281

282
IncoherentInelasticAE::IncoherentInelasticAE(hid_t group)
44✔
283
{
284
  // Read correlated angle-energy distribution
285
  CorrelatedAngleEnergy dist {group};
44✔
286

287
  // Copy incident energies
288
  energy_ = dist.energy();
44✔
289

290
  // Convert to S(a,b) native format
291
  for (const auto& edist : dist.distribution()) {
176✔
292
    // Create temporary distribution
293
    DistEnergySab d;
132✔
294

295
    // Copy outgoing energy distribution
296
    d.n_e_out = edist.e_out.size();
132✔
297
    d.e_out = edist.e_out;
132✔
298
    d.e_out_pdf = edist.p;
132✔
299
    d.e_out_cdf = edist.c;
132✔
300

301
    for (int j = 0; j < d.n_e_out; ++j) {
660✔
302
      auto adist = dynamic_cast<Tabular*>(edist.angle[j].get());
528!
303
      if (adist) {
528!
304
        // On first pass, allocate space for angles
305
        if (j == 0) {
528✔
306
          auto n_mu = adist->x().size();
132✔
307
          d.mu = tensor::Tensor<double>({d.n_e_out, n_mu});
264✔
308
        }
309

310
        // Copy outgoing angles
311
        tensor::View<double> mu_j = d.mu.slice(j);
528✔
312
        std::copy(adist->x().begin(), adist->x().end(), mu_j.begin());
528✔
313
      }
528✔
314
    }
315

316
    distribution_.emplace_back(std::move(d));
132✔
317
  }
132✔
318
}
44✔
319

320
void IncoherentInelasticAE::sample_params(
3,614,897✔
321
  double E_in, double& E_out, double& f, int& l, int& j, uint64_t* seed) const
322
{
323
  // Get index and interpolation factor for inelastic grid
324
  int i;
3,614,897✔
325
  double f0;
3,614,897✔
326
  get_energy_index(energy_, E_in, i, f0);
3,614,897✔
327

328
  // Pick closer energy based on interpolation factor
329
  l = f0 > 0.5 ? i + 1 : i;
3,614,897✔
330

331
  // Determine outgoing energy bin
332
  // (First reset n_energy_out to the right value)
333
  int n = distribution_[l].n_e_out;
3,614,897✔
334
  double r1 = prn(seed);
3,614,897✔
335
  double c_j = distribution_[l].e_out_cdf[0];
3,614,897✔
336
  double c_j1;
3,614,897✔
337
  for (j = 0; j < n - 1; ++j) {
8,721,537!
338
    c_j1 = distribution_[l].e_out_cdf[j + 1];
8,721,537✔
339
    if (r1 < c_j1)
8,721,537✔
340
      break;
341
    c_j = c_j1;
5,106,640✔
342
  }
343

344
  // check to make sure j is <= n_energy_out - 2
345
  j = std::min(j, n - 2);
3,614,897!
346

347
  // Get the data to interpolate between
348
  double E_l_j = distribution_[l].e_out[j];
3,614,897!
349
  double p_l_j = distribution_[l].e_out_pdf[j];
3,614,897✔
350

351
  // Next part assumes linear-linear interpolation in standard
352
  double E_l_j1 = distribution_[l].e_out[j + 1];
3,614,897✔
353
  double p_l_j1 = distribution_[l].e_out_pdf[j + 1];
3,614,897✔
354

355
  // Find secondary energy (variable E)
356
  double frac = (p_l_j1 - p_l_j) / (E_l_j1 - E_l_j);
3,614,897✔
357
  if (frac == 0.0) {
3,614,897!
358
    E_out = E_l_j + (r1 - c_j) / p_l_j;
3,614,897✔
359
  } else {
UNCOV
360
    E_out = E_l_j +
×
UNCOV
361
            (std::sqrt(std::max(0.0, p_l_j * p_l_j + 2.0 * frac * (r1 - c_j))) -
×
UNCOV
362
              p_l_j) /
×
363
              frac;
364
  }
365

366
  // Adjustment of outgoing energy
367
  double E_l = energy_[l];
3,614,897✔
368
  if (E_out < 0.5 * E_l) {
3,614,897✔
369
    E_out *= 2.0 * E_in / E_l - 1.0;
344,476✔
370
  } else {
371
    E_out += E_in - E_l;
3,270,421✔
372
  }
373

374
  f = (r1 - c_j) / (c_j1 - c_j);
3,614,897✔
375
}
3,614,897✔
376
void IncoherentInelasticAE::sample(
3,614,897✔
377
  double E_in, double& E_out, double& mu, uint64_t* seed) const
378
{
379
  double f;
3,614,897✔
380
  int l, j;
3,614,897✔
381
  sample_params(E_in, E_out, f, l, j, seed);
3,614,897✔
382

383
  // Sample outgoing cosine bin
384
  int n_mu = distribution_[l].mu.shape(1);
3,614,897!
385
  std::size_t k = prn(seed) * n_mu;
3,614,897✔
386

387
  // Rather than use the sampled discrete mu directly, it is smeared over
388
  // a bin of width 0.5*min(mu[k] - mu[k-1], mu[k+1] - mu[k]) centered on the
389
  // discrete mu value itself.
390
  const auto& mu_l = distribution_[l].mu;
3,614,897✔
391

392
  // Interpolate kth mu value between distributions at energies j and j+1
393
  mu = mu_l(j, k) + f * (mu_l(j + 1, k) - mu_l(j, k));
3,614,897✔
394

395
  // Inteprolate (k-1)th mu value between distributions at energies j and j+1.
396
  // When k==0, pick a value that will smear the cosine out to a minimum of -1.
397
  double mu_left =
3,614,897✔
398
    (k == 0)
399
      ? mu_left = -1.0 - (mu + 1.0)
3,614,897✔
400
      : mu_left = mu_l(j, k - 1) + f * (mu_l(j + 1, k - 1) - mu_l(j, k - 1));
3,159,464✔
401

402
  // Inteprolate (k+1)th mu value between distributions at energies j and j+1.
403
  // When k is the last discrete value, pick a value that will smear the cosine
404
  // out to a maximum of 1.
405
  double mu_right =
3,614,897✔
406
    (k == n_mu - 1)
3,614,897✔
407
      ? mu_right = 1.0 + (1.0 - mu)
3,614,897✔
408
      : mu_right = mu_l(j, k + 1) + f * (mu_l(j + 1, k + 1) - mu_l(j, k + 1));
3,160,762✔
409

410
  // Smear cosine
411
  mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5);
5,427,510✔
412
}
3,614,897✔
413

UNCOV
414
double IncoherentInelasticAE::sample_energy_and_pdf(
×
415
  double E_in, double mu, double& E_out, uint64_t* seed) const
416
{
NEW
417
  double f;
×
NEW
418
  int l, j;
×
NEW
419
  sample_params(E_in, E_out, f, l, j, seed);
×
420

NEW
421
  int n_mu = distribution_[l].mu.shape()[1];
×
NEW
422
  const auto& mu_l = distribution_[l].mu;
×
423

NEW
424
  double pdf = 0.0;
×
NEW
425
  pdf += f * get_pdf_discrete(mu_l.slice(j + 1, tensor::all), mu);
×
NEW
426
  pdf += (1 - f) * get_pdf_discrete(mu_l.slice(j, tensor::all), mu);
×
NEW
427
  return pdf;
×
428
}
429

430
//==============================================================================
431
// MixedElasticAE implementation
432
//==============================================================================
433

434
MixedElasticAE::MixedElasticAE(
44✔
435
  hid_t group, const CoherentElasticXS& coh_xs, const Function1D& incoh_xs)
44✔
436
  : coherent_dist_(coh_xs), coherent_xs_(coh_xs), incoherent_xs_(incoh_xs)
44✔
437
{
438
  // Read incoherent elastic distribution
439
  hid_t incoherent_group = open_group(group, "incoherent");
44✔
440
  std::string temp;
44✔
441
  read_attribute(incoherent_group, "type", temp);
44✔
442
  if (temp == "incoherent_elastic") {
44!
UNCOV
443
    incoherent_dist_ = make_unique<IncoherentElasticAE>(incoherent_group);
×
444
  } else if (temp == "incoherent_elastic_discrete") {
44!
445
    auto xs = dynamic_cast<const Tabulated1D*>(&incoh_xs);
44✔
446
    incoherent_dist_ =
44✔
447
      make_unique<IncoherentElasticAEDiscrete>(incoherent_group, xs->x());
44✔
448
  }
449
  close_group(incoherent_group);
44✔
450
}
44✔
451

452
const AngleEnergy& MixedElasticAE::sample_dist(
46,486✔
453
  double E_in, uint64_t* seed) const
454
{
455
  // Evaluate coherent and incoherent elastic cross sections
456
  double xs_coh = coherent_xs_(E_in);
46,486✔
457
  double xs_incoh = incoherent_xs_(E_in);
46,486✔
458

459
  if (prn(seed) * (xs_coh + xs_incoh) < xs_coh) {
46,486✔
460
    return coherent_dist_;
45,144✔
461
  } else {
462
    return *incoherent_dist_;
1,342✔
463
  }
464
}
465

466
void MixedElasticAE::sample(
46,486✔
467
  double E_in, double& E_out, double& mu, uint64_t* seed) const
468
{
469
  sample_dist(E_in, seed).sample(E_in, E_out, mu, seed);
46,486✔
470
}
46,486✔
471

NEW
472
double MixedElasticAE::sample_energy_and_pdf(
×
473
  double E_in, double mu, double& E_out, uint64_t* seed) const
474
{
NEW
475
  return sample_dist(E_in, seed).sample_energy_and_pdf(E_in, mu, E_out, seed);
×
476
}
477

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

© 2026 Coveralls, Inc