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

openmc-dev / openmc / 22593927413

02 Mar 2026 08:16PM UTC coverage: 81.339% (-0.2%) from 81.508%
22593927413

Pull #3550

github

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

17498 of 25284 branches covered (69.21%)

Branch coverage included in aggregate %.

64 of 180 new or added lines in 12 files covered. (35.56%)

20 existing lines in 2 files now uncovered.

57713 of 67182 relevant lines covered (85.91%)

44906521.62 hits per line

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

70.75
/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
  const auto& factors = xs_.factors();
134✔
23
  auto n = factors.size();
134✔
24
  factors_diff_ = tensor::zeros<double>({n});
134✔
25
  factors_diff_.slice(0) = factors[0];
134✔
26
  for (int i = 1; i < n; ++i) {
19,878✔
27
    factors_diff_.slice(i) = factors[i] - factors[i - 1];
39,488✔
28
  }
29
}
134✔
30

31
void CoherentElasticAE::sample(
178,013✔
32
  double E_in, double& E_out, double& mu, uint64_t* seed) const
33
{
34
  // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1)
35
  E_out = E_in;
178,013✔
36
  const auto& energies {xs_.bragg_edges()};
178,013!
37
  assert(E_in >= energies.front());
178,013!
38

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

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

46
  const int k = std::lower_bound(factors.begin(), factors.begin() + i, prob) -
178,013✔
47
                factors.begin();
178,013✔
48

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

NEW
53
double CoherentElasticAE::sample_energy_and_pdf(
×
54
  double E_in, double mu, double& E_out, uint64_t* seed) const
55
{
56
  // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1)
57

NEW
58
  double pdf;
×
NEW
59
  E_out = E_in;
×
NEW
60
  const auto energies =
×
NEW
61
    tensor::Tensor<double>(xs_.bragg_edges().data(), xs_.bragg_edges().size());
×
NEW
62
  const auto& factors = xs_.factors();
×
63

NEW
64
  if (E_in < energies.front() || E_in > energies.back()) {
×
65
    return 0;
66
  }
67

NEW
68
  const int n = upper_bound_index(energies.begin(), energies.end(), E_in);
×
NEW
69
  double E = 0.5 * (1 - mu) * E_in;
×
NEW
70
  double C = 0.5 * E_in / factors[n];
×
71

NEW
72
  return C * get_pdf_discrete(
×
NEW
73
               energies.slice(0, n), factors_diff_.slice(0, n), E, 0, E_in);
×
NEW
74
}
×
75

76
//==============================================================================
77
// IncoherentElasticAE implementation
78
//==============================================================================
79

80
IncoherentElasticAE::IncoherentElasticAE(hid_t group)
×
81
{
82
  read_dataset(group, "debye_waller", debye_waller_);
×
83
}
×
84

85
void IncoherentElasticAE::sample(
×
86
  double E_in, double& E_out, double& mu, uint64_t* seed) const
87
{
NEW
88
  E_out = E_in;
×
89

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

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

105
//==============================================================================
106
// IncoherentElasticAEDiscrete implementation
107
//==============================================================================
108

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

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

124
  // Interpolate between two discrete cosines corresponding to neighboring
125
  // incoming energies.
126

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

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

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

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

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

152
  // Smear cosine
153
  mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5);
1,947✔
154

155
  // Energy doesn't change in elastic scattering
156
  E_out = E_in;
1,342✔
157
}
1,342✔
158

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

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

175
//==============================================================================
176
// IncoherentInelasticAEDiscrete implementation
177
//==============================================================================
178

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

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

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

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

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

232
  // Outgoing energy
233
  E_out = (1 - f) * E_ij + f * E_i1j;
123,683,823✔
234
}
123,683,823✔
235

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

244
  int j;
123,683,823✔
245
  sample_params(E_in, E_out, j, seed);
123,683,823✔
246

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

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

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

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

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

275
//==============================================================================
276
// IncoherentInelasticAE implementation
277
//==============================================================================
278

279
IncoherentInelasticAE::IncoherentInelasticAE(hid_t group)
44✔
280
{
281
  // Read correlated angle-energy distribution
282
  CorrelatedAngleEnergy dist {group};
44✔
283

284
  // Copy incident energies
285
  energy_ = dist.energy();
44✔
286

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

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

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

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

313
    distribution_.emplace_back(std::move(d));
132✔
314
  }
132✔
315
}
44✔
316

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
418
  int n_mu = distribution_[l].mu.shape()[1];
×
NEW
419
  const auto& mu_l = distribution_[l].mu;
×
420

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

427
//==============================================================================
428
// MixedElasticAE implementation
429
//==============================================================================
430

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

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

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

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

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

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