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

openmc-dev / openmc / 20470828925

23 Dec 2025 08:24PM UTC coverage: 81.898% (-0.04%) from 81.94%
20470828925

Pull #3550

github

web-flow
Merge b067e7f24 into 3f06a42ab
Pull Request #3550: [Point Detector] Add distribution get_pdf functionality

17078 of 23776 branches covered (71.83%)

Branch coverage included in aggregate %.

48 of 212 new or added lines in 13 files covered. (22.64%)

4 existing lines in 2 files now uncovered.

55208 of 64487 relevant lines covered (85.61%)

43370386.44 hits per line

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

72.91
/src/distribution.cpp
1
#include "openmc/distribution.h"
2

3
#include <algorithm> // for copy
4
#include <array>
5
#include <cmath>     // for sqrt, floor, max
6
#include <iterator>  // for back_inserter
7
#include <numeric>   // for accumulate
8
#include <stdexcept> // for runtime_error
9
#include <string>    // for string, stod
10

11
#include "openmc/error.h"
12
#include "openmc/math_functions.h"
13
#include "openmc/random_dist.h"
14
#include "openmc/random_lcg.h"
15
#include "openmc/search.h"
16
#include "openmc/xml_interface.h"
17

18
namespace openmc {
19

20
//==============================================================================
21
// DiscreteIndex implementation
22
//==============================================================================
23

24
DiscreteIndex::DiscreteIndex(pugi::xml_node node)
26,038✔
25
{
26
  auto params = get_node_array<double>(node, "parameters");
26,038✔
27
  std::size_t n = params.size() / 2;
26,038✔
28

29
  assign({params.data() + n, n});
26,038✔
30
}
26,038✔
31

32
DiscreteIndex::DiscreteIndex(span<const double> p)
46,867✔
33
{
34
  assign(p);
46,867✔
35
}
46,867✔
36

37
void DiscreteIndex::assign(span<const double> p)
80,806✔
38
{
39
  prob_.assign(p.begin(), p.end());
80,806✔
40

41
  this->init_alias();
80,806✔
42
}
80,806✔
43

44
void DiscreteIndex::init_alias()
80,806✔
45
{
46
  normalize();
80,806✔
47

48
  // The initialization and sampling method is based on Vose
49
  // (DOI: 10.1109/32.92917)
50
  // Vectors for large and small probabilities based on 1/n
51
  vector<size_t> large;
80,806✔
52
  vector<size_t> small;
80,806✔
53

54
  size_t n = prob_.size();
80,806✔
55

56
  // Set and allocate memory
57
  alias_.assign(n, 0);
80,806✔
58

59
  // Fill large and small vectors based on 1/n
60
  for (size_t i = 0; i < n; i++) {
1,551,214✔
61
    prob_[i] *= n;
1,470,408✔
62
    if (prob_[i] > 1.0) {
1,470,408✔
63
      large.push_back(i);
219,832✔
64
    } else {
65
      small.push_back(i);
1,250,576✔
66
    }
67
  }
68

69
  while (!large.empty() && !small.empty()) {
1,395,913✔
70
    int j = small.back();
1,315,107✔
71
    int k = large.back();
1,315,107✔
72

73
    // Remove last element of small
74
    small.pop_back();
1,315,107✔
75

76
    // Update probability and alias based on Vose's algorithm
77
    prob_[k] += prob_[j] - 1.0;
1,315,107✔
78
    alias_[j] = k;
1,315,107✔
79

80
    // Move large index to small vector, if it is no longer large
81
    if (prob_[k] < 1.0) {
1,315,107✔
82
      small.push_back(k);
211,915✔
83
      large.pop_back();
211,915✔
84
    }
85
  }
86
}
80,806✔
87

88
size_t DiscreteIndex::sample(uint64_t* seed) const
64,157,980✔
89
{
90
  // Alias sampling of discrete distribution
91
  size_t n = prob_.size();
64,157,980✔
92
  if (n > 1) {
64,157,980✔
93
    size_t u = prn(seed) * n;
15,738,116✔
94
    if (prn(seed) < prob_[u]) {
15,738,116✔
95
      return u;
9,304,482✔
96
    } else {
97
      return alias_[u];
6,433,634✔
98
    }
99
  } else {
100
    return 0;
48,419,864✔
101
  }
102
}
103

104
void DiscreteIndex::normalize()
80,806✔
105
{
106
  // Renormalize density function so that it sums to unity. Note that we save
107
  // the integral of the distribution so that if it is used as part of another
108
  // distribution (e.g., Mixture), we know its relative strength.
109
  integral_ = std::accumulate(prob_.begin(), prob_.end(), 0.0);
80,806✔
110
  for (auto& p_i : prob_) {
1,551,214✔
111
    p_i /= integral_;
1,470,408✔
112
  }
113
}
80,806✔
114

115
//==============================================================================
116
// Discrete implementation
117
//==============================================================================
118

119
Discrete::Discrete(pugi::xml_node node) : di_(node)
26,038✔
120
{
121
  auto params = get_node_array<double>(node, "parameters");
26,038✔
122

123
  std::size_t n = params.size() / 2;
26,038✔
124

125
  x_.assign(params.begin(), params.begin() + n);
26,038✔
126
}
26,038✔
127

128
Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n})
46,867✔
129
{
130

131
  x_.assign(x, x + n);
46,867✔
132
}
46,867✔
133

134
double Discrete::sample(uint64_t* seed) const
59,387,347✔
135
{
136
  return x_[di_.sample(seed)];
59,387,347✔
137
}
138

139
//==============================================================================
140
// Uniform implementation
141
//==============================================================================
142

143
Uniform::Uniform(pugi::xml_node node)
256✔
144
{
145
  auto params = get_node_array<double>(node, "parameters");
256✔
146
  if (params.size() != 2) {
256!
147
    fatal_error("Uniform distribution must have two "
×
148
                "parameters specified.");
149
  }
150

151
  a_ = params.at(0);
256✔
152
  b_ = params.at(1);
256✔
153
}
256✔
154

155
double Uniform::sample(uint64_t* seed) const
283,488✔
156
{
157
  return a_ + prn(seed) * (b_ - a_);
283,488✔
158
}
159

NEW
160
double Uniform::evaluate(double x) const
×
161
{
NEW
162
  if (x <= b_ && x >= a_)
×
NEW
163
    return 1 / (b_ - a_);
×
164
  else
NEW
165
    return 0;
×
166
}
167

168
//==============================================================================
169
// PowerLaw implementation
170
//==============================================================================
171

172
PowerLaw::PowerLaw(pugi::xml_node node)
32✔
173
{
174
  auto params = get_node_array<double>(node, "parameters");
32✔
175
  if (params.size() != 3) {
32!
176
    fatal_error("PowerLaw distribution must have three "
×
177
                "parameters specified.");
178
  }
179

180
  const double a = params.at(0);
32✔
181
  const double b = params.at(1);
32✔
182
  const double n = params.at(2);
32✔
183

184
  offset_ = std::pow(a, n + 1);
32✔
185
  span_ = std::pow(b, n + 1) - offset_;
32✔
186
  ninv_ = 1 / (n + 1);
32✔
187
}
32✔
188

189
double PowerLaw::sample(uint64_t* seed) const
2,222✔
190
{
191
  return std::pow(offset_ + prn(seed) * span_, ninv_);
2,222✔
192
}
193

NEW
194
double PowerLaw::evaluate(double x) const
×
195
{
196
  // Use accessors
NEW
197
  double a_val = this->a();
×
NEW
198
  double b_val = this->b();
×
NEW
199
  double n_val = this->n();
×
200

NEW
201
  if (x < a_val || x > b_val) {
×
NEW
202
    return 0.0; // outside support
×
203
  }
204

NEW
205
  return (n_val + 1.0) * std::pow(x, n_val) /
×
NEW
206
         (std::pow(b_val, n_val + 1.0) - std::pow(a_val, n_val + 1.0));
×
207
}
208

209
//==============================================================================
210
// Maxwell implementation
211
//==============================================================================
212

213
Maxwell::Maxwell(pugi::xml_node node)
64✔
214
{
215
  theta_ = std::stod(get_node_value(node, "parameters"));
64✔
216
}
64✔
217

218
double Maxwell::sample(uint64_t* seed) const
3,883✔
219
{
220
  return maxwell_spectrum(theta_, seed);
3,883✔
221
}
222

223
//==============================================================================
224
// Watt implementation
225
//==============================================================================
226

227
Watt::Watt(pugi::xml_node node)
96✔
228
{
229
  auto params = get_node_array<double>(node, "parameters");
96✔
230
  if (params.size() != 2)
96!
231
    openmc::fatal_error("Watt energy distribution must have two "
×
232
                        "parameters specified.");
233

234
  a_ = params.at(0);
96✔
235
  b_ = params.at(1);
96✔
236
}
96✔
237

238
double Watt::sample(uint64_t* seed) const
12,145,507✔
239
{
240
  return watt_spectrum(a_, b_, seed);
12,145,507✔
241
}
242

243
//==============================================================================
244
// Normal implementation
245
//==============================================================================
246
Normal::Normal(pugi::xml_node node)
×
247
{
248
  auto params = get_node_array<double>(node, "parameters");
×
249
  if (params.size() != 2) {
×
250
    openmc::fatal_error("Normal energy distribution must have two "
×
251
                        "parameters specified.");
252
  }
253

254
  mean_value_ = params.at(0);
×
255
  std_dev_ = params.at(1);
×
256
}
×
257

258
double Normal::sample(uint64_t* seed) const
×
259
{
260
  return normal_variate(mean_value_, std_dev_, seed);
×
261
}
262

NEW
263
double Normal::evaluate(double x) const
×
264
{
NEW
265
  double exponent = -0.5 * std::pow((x - mean_value_) / std_dev_, 2);
×
NEW
266
  double coefficient = 1 / (std_dev_ * std::sqrt(2 * PI));
×
NEW
267
  return coefficient * std::exp(exponent);
×
268
}
269

270
//==============================================================================
271
// Tabular implementation
272
//==============================================================================
273

274
Tabular::Tabular(pugi::xml_node node)
7,320✔
275
{
276
  if (check_for_node(node, "interpolation")) {
7,320!
277
    std::string temp = get_node_value(node, "interpolation");
7,320✔
278
    if (temp == "histogram") {
7,320✔
279
      interp_ = Interpolation::histogram;
7,256✔
280
    } else if (temp == "linear-linear") {
64!
281
      interp_ = Interpolation::lin_lin;
64✔
282
    } else {
283
      openmc::fatal_error(
×
284
        "Unsupported interpolation type for distribution: " + temp);
×
285
    }
286
  } else {
7,320✔
287
    interp_ = Interpolation::histogram;
×
288
  }
289

290
  // Read and initialize tabular distribution. If number of parameters is odd,
291
  // add an extra zero for the 'p' array.
292
  auto params = get_node_array<double>(node, "parameters");
7,320✔
293
  if (params.size() % 2 != 0) {
7,320!
294
    params.push_back(0.0);
×
295
  }
296
  std::size_t n = params.size() / 2;
7,320✔
297
  const double* x = params.data();
7,320✔
298
  const double* p = x + n;
7,320✔
299
  init(x, p, n);
7,320✔
300
}
7,320✔
301

302
Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp,
46,274,153✔
303
  const double* c)
46,274,153✔
304
  : interp_ {interp}
46,274,153✔
305
{
306
  init(x, p, n, c);
46,274,153✔
307
}
46,274,153✔
308

309
void Tabular::init(
46,281,473✔
310
  const double* x, const double* p, std::size_t n, const double* c)
311
{
312
  // Copy x/p arrays into vectors
313
  std::copy(x, x + n, std::back_inserter(x_));
46,281,473✔
314
  std::copy(p, p + n, std::back_inserter(p_));
46,281,473✔
315

316
  // Check interpolation parameter
317
  if (interp_ != Interpolation::histogram &&
46,281,473✔
318
      interp_ != Interpolation::lin_lin) {
38,616,513!
319
    openmc::fatal_error("Only histogram and linear-linear interpolation "
×
320
                        "for tabular distribution is supported.");
321
  }
322

323
  // Calculate cumulative distribution function
324
  if (c) {
46,281,473✔
325
    std::copy(c, c + n, std::back_inserter(c_));
46,274,153✔
326
  } else {
327
    c_.resize(n);
7,320✔
328
    c_[0] = 0.0;
7,320✔
329
    for (int i = 1; i < n; ++i) {
90,912✔
330
      if (interp_ == Interpolation::histogram) {
83,592✔
331
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]);
83,464✔
332
      } else if (interp_ == Interpolation::lin_lin) {
128!
333
        c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]);
128✔
334
      }
335
    }
336
  }
337

338
  // Normalize density and distribution functions. Note that we save the
339
  // integral of the distribution so that if it is used as part of another
340
  // distribution (e.g., Mixture), we know its relative strength.
341
  integral_ = c_[n - 1];
46,281,473✔
342
  for (int i = 0; i < n; ++i) {
665,571,341✔
343
    p_[i] = p_[i] / integral_;
619,289,868✔
344
    c_[i] = c_[i] / integral_;
619,289,868✔
345
  }
346
}
46,281,473✔
347

348
double Tabular::sample(uint64_t* seed) const
692,006,949✔
349
{
350
  // Sample value of CDF
351
  double c = prn(seed);
692,006,949✔
352

353
  // Find first CDF bin which is above the sampled value
354
  double c_i = c_[0];
692,006,949✔
355
  int i;
356
  std::size_t n = c_.size();
692,006,949✔
357
  for (i = 0; i < n - 1; ++i) {
2,147,483,647!
358
    if (c <= c_[i + 1])
2,147,483,647✔
359
      break;
692,006,949✔
360
    c_i = c_[i + 1];
1,912,775,725✔
361
  }
362

363
  // Determine bounding PDF values
364
  double x_i = x_[i];
692,006,949✔
365
  double p_i = p_[i];
692,006,949✔
366

367
  if (interp_ == Interpolation::histogram) {
692,006,949✔
368
    // Histogram interpolation
369
    if (p_i > 0.0) {
3,421,748!
370
      return x_i + (c - c_i) / p_i;
3,421,748✔
371
    } else {
372
      return x_i;
×
373
    }
374
  } else {
375
    // Linear-linear interpolation
376
    double x_i1 = x_[i + 1];
688,585,201✔
377
    double p_i1 = p_[i + 1];
688,585,201✔
378

379
    double m = (p_i1 - p_i) / (x_i1 - x_i);
688,585,201✔
380
    if (m == 0.0) {
688,585,201✔
381
      return x_i + (c - c_i) / p_i;
321,971,350✔
382
    } else {
383
      return x_i +
384
             (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) /
366,613,851✔
385
               m;
366,613,851✔
386
    }
387
  }
388
}
389

NEW
390
double Tabular::evaluate(double x) const
×
391
{
392
  // get PDF value at x
393

394
  int i;
NEW
395
  std::size_t n = x_.size();
×
NEW
396
  if (x < x_[0]) {
×
NEW
397
    return 0;
×
NEW
398
  } else if (x > x_[n - 1]) {
×
NEW
399
    return 0;
×
400
  } else {
NEW
401
    i = lower_bound_index(x_.begin(), x_.end(), x);
×
402
  }
403

404
  // Determine bounding PDF values
NEW
405
  double x_i = x_[i];
×
NEW
406
  double p_i = p_[i];
×
407

NEW
408
  switch (interp_) {
×
NEW
409
  case Interpolation::histogram:
×
410
    // Histogram interpolation
NEW
411
    return p_i;
×
412

NEW
413
  case Interpolation::lin_lin: {
×
414
    // Linear-linear interpolation
NEW
415
    double x_i1 = x_[i + 1];
×
NEW
416
    double p_i1 = p_[i + 1];
×
417

NEW
418
    double m = (p_i1 - p_i) / (x_i1 - x_i);
×
NEW
419
    return (m == 0.0) ? p_i : p_i + (x - x_i) * m;
×
420
  }
421

NEW
422
  default:
×
NEW
423
    fatal_error("Unsupported interpolation type in PDF evaluation.");
×
424
  }
425
}
426

427
//==============================================================================
428
// Equiprobable implementation
429
//==============================================================================
430

431
double Equiprobable::sample(uint64_t* seed) const
×
432
{
433
  std::size_t n = x_.size();
×
434

435
  double r = prn(seed);
×
436
  int i = std::floor((n - 1) * r);
×
437

438
  double xl = x_[i];
×
439
  double xr = x_[i + i];
×
440
  return xl + ((n - 1) * r - i) * (xr - xl);
×
441
}
442

443
//==============================================================================
444
// Mixture implementation
445
//==============================================================================
446

447
Mixture::Mixture(pugi::xml_node node)
48✔
448
{
449
  double cumsum = 0.0;
48✔
450
  for (pugi::xml_node pair : node.children("pair")) {
192✔
451
    // Check that required data exists
452
    if (!pair.attribute("probability"))
144!
453
      fatal_error("Mixture pair element does not have probability.");
×
454
    if (!pair.child("dist"))
144!
455
      fatal_error("Mixture pair element does not have a distribution.");
×
456

457
    // cummulative sum of probabilities
458
    double p = std::stod(pair.attribute("probability").value());
144✔
459

460
    // Save cummulative probability and distribution
461
    auto dist = distribution_from_xml(pair.child("dist"));
144✔
462
    cumsum += p * dist->integral();
144✔
463

464
    distribution_.push_back(std::make_pair(cumsum, std::move(dist)));
144✔
465
  }
144✔
466

467
  // Save integral of distribution
468
  integral_ = cumsum;
48✔
469

470
  // Normalize cummulative probabilities to 1
471
  for (auto& pair : distribution_) {
192✔
472
    pair.first /= cumsum;
144✔
473
  }
474
}
48✔
475

476
double Mixture::sample(uint64_t* seed) const
3,564✔
477
{
478
  // Sample value of CDF
479
  const double p = prn(seed);
3,564✔
480

481
  // find matching distribution
482
  const auto it = std::lower_bound(distribution_.cbegin(), distribution_.cend(),
3,564✔
483
    p, [](const DistPair& pair, double p) { return pair.first < p; });
7,128✔
484

485
  // This should not happen. Catch it
486
  assert(it != distribution_.cend());
2,916!
487

488
  // Sample the chosen distribution
489
  return it->second->sample(seed);
7,128✔
490
}
491

492
//==============================================================================
493
// Helper function
494
//==============================================================================
495

496
UPtrDist distribution_from_xml(pugi::xml_node node)
33,843✔
497
{
498
  if (!check_for_node(node, "type"))
33,843!
499
    openmc::fatal_error("Distribution type must be specified.");
×
500

501
  // Determine type of distribution
502
  std::string type = get_node_value(node, "type", true, true);
33,843✔
503

504
  // Allocate extension of Distribution
505
  UPtrDist dist;
33,843✔
506
  if (type == "uniform") {
33,843✔
507
    dist = UPtrDist {new Uniform(node)};
256✔
508
  } else if (type == "powerlaw") {
33,587✔
509
    dist = UPtrDist {new PowerLaw(node)};
32✔
510
  } else if (type == "maxwell") {
33,555✔
511
    dist = UPtrDist {new Maxwell(node)};
64✔
512
  } else if (type == "watt") {
33,491✔
513
    dist = UPtrDist {new Watt(node)};
96✔
514
  } else if (type == "normal") {
33,395!
515
    dist = UPtrDist {new Normal(node)};
×
516
  } else if (type == "discrete") {
33,395✔
517
    dist = UPtrDist {new Discrete(node)};
26,027✔
518
  } else if (type == "tabular") {
7,368✔
519
    dist = UPtrDist {new Tabular(node)};
7,320✔
520
  } else if (type == "mixture") {
48!
521
    dist = UPtrDist {new Mixture(node)};
48✔
522
  } else if (type == "muir") {
×
523
    openmc::fatal_error(
×
524
      "'muir' distributions are now specified using the openmc.stats.muir() "
525
      "function in Python. Please regenerate your XML files.");
526
  } else {
527
    openmc::fatal_error("Invalid distribution type: " + type);
×
528
  }
529
  return dist;
67,686✔
530
}
33,843✔
531

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