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

openmc-dev / openmc / 17271733860

27 Aug 2025 03:46PM UTC coverage: 84.486% (-0.7%) from 85.204%
17271733860

Pull #3550

github

web-flow
Merge 06ae298be into d1df80a21
Pull Request #3550: [Point Detector] Add distribution get_pdf functionality

4 of 551 new or added lines in 11 files covered. (0.73%)

4 existing lines in 2 files now uncovered.

52940 of 62661 relevant lines covered (84.49%)

37443356.37 hits per line

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

73.54
/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)
12,152✔
25
{
26
  auto params = get_node_array<double>(node, "parameters");
12,152✔
27
  std::size_t n = params.size() / 2;
12,152✔
28

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

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

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

41
  this->init_alias();
65,251✔
42
}
65,251✔
43

44
void DiscreteIndex::init_alias()
65,251✔
45
{
46
  normalize();
65,251✔
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;
65,251✔
52
  vector<size_t> small;
65,251✔
53

54
  size_t n = prob_.size();
65,251✔
55

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

59
  // Fill large and small vectors based on 1/n
60
  for (size_t i = 0; i < n; i++) {
709,789✔
61
    prob_[i] *= n;
644,538✔
62
    if (prob_[i] > 1.0) {
644,538✔
63
      large.push_back(i);
92,414✔
64
    } else {
65
      small.push_back(i);
552,124✔
66
    }
67
  }
68

69
  while (!large.empty() && !small.empty()) {
570,681✔
70
    int j = small.back();
505,430✔
71
    int k = large.back();
505,430✔
72

73
    // Remove last element of small
74
    small.pop_back();
505,430✔
75

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

80
    // Move large index to small vector, if it is no longer large
81
    if (prob_[k] < 1.0) {
505,430✔
82
      small.push_back(k);
89,179✔
83
      large.pop_back();
89,179✔
84
    }
85
  }
86
}
65,251✔
87

88
size_t DiscreteIndex::sample(uint64_t* seed) const
58,572,742✔
89
{
90
  // Alias sampling of discrete distribution
91
  size_t n = prob_.size();
58,572,742✔
92
  if (n > 1) {
58,572,742✔
93
    size_t u = prn(seed) * n;
13,973,994✔
94
    if (prn(seed) < prob_[u]) {
13,973,994✔
95
      return u;
8,571,294✔
96
    } else {
97
      return alias_[u];
5,402,700✔
98
    }
99
  } else {
100
    return 0;
44,598,748✔
101
  }
102
}
103

104
void DiscreteIndex::normalize()
65,251✔
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);
65,251✔
110
  for (auto& p_i : prob_) {
709,789✔
111
    p_i /= integral_;
644,538✔
112
  }
113
}
65,251✔
114

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

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

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

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

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

131
  x_.assign(x, x + n);
45,926✔
132
}
45,926✔
133

134
double Discrete::sample(uint64_t* seed) const
55,763,482✔
135
{
136
  return x_[di_.sample(seed)];
55,763,482✔
137
}
138
// Not implemented
NEW
139
double Discrete::get_pdf(double x) const
×
140
{
141

NEW
142
  return -1;
×
143
}
144

145
//==============================================================================
146
// Uniform implementation
147
//==============================================================================
148

149
Uniform::Uniform(pugi::xml_node node)
256✔
150
{
151
  auto params = get_node_array<double>(node, "parameters");
256✔
152
  if (params.size() != 2) {
256✔
153
    fatal_error("Uniform distribution must have two "
×
154
                "parameters specified.");
155
  }
156

157
  a_ = params.at(0);
256✔
158
  b_ = params.at(1);
256✔
159
}
256✔
160

161
double Uniform::sample(uint64_t* seed) const
285,116✔
162
{
163
  return a_ + prn(seed) * (b_ - a_);
285,116✔
164
}
NEW
165
double Uniform::get_pdf(double x) const
×
166
{
NEW
167
  if (x <= b_ && x >= a_)
×
NEW
168
    return 1 / (b_ - a_);
×
169
  else
NEW
170
    return 0;
×
171
}
172

173
//==============================================================================
174
// PowerLaw implementation
175
//==============================================================================
176

177
PowerLaw::PowerLaw(pugi::xml_node node)
32✔
178
{
179
  auto params = get_node_array<double>(node, "parameters");
32✔
180
  if (params.size() != 3) {
32✔
181
    fatal_error("PowerLaw distribution must have three "
×
182
                "parameters specified.");
183
  }
184

185
  const double a = params.at(0);
32✔
186
  const double b = params.at(1);
32✔
187
  const double n = params.at(2);
32✔
188

189
  offset_ = std::pow(a, n + 1);
32✔
190
  span_ = std::pow(b, n + 1) - offset_;
32✔
191
  ninv_ = 1 / (n + 1);
32✔
192
}
32✔
193

194
double PowerLaw::sample(uint64_t* seed) const
2,222✔
195
{
196
  return std::pow(offset_ + prn(seed) * span_, ninv_);
2,222✔
197
}
NEW
198
double PowerLaw::get_pdf(double x) const
×
199
{
NEW
200
  return x / span_ / ninv_;
×
201
}
202

203
//==============================================================================
204
// Maxwell implementation
205
//==============================================================================
206

207
Maxwell::Maxwell(pugi::xml_node node)
64✔
208
{
209
  theta_ = std::stod(get_node_value(node, "parameters"));
64✔
210
}
64✔
211

212
double Maxwell::sample(uint64_t* seed) const
3,883✔
213
{
214
  return maxwell_spectrum(theta_, seed);
3,883✔
215
}
216
// Not implemented
NEW
217
double Maxwell::get_pdf(double x) const
×
218
{
NEW
219
  return -1;
×
220
}
221

222
//==============================================================================
223
// Watt implementation
224
//==============================================================================
225

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

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

237
double Watt::sample(uint64_t* seed) const
10,754,354✔
238
{
239
  return watt_spectrum(a_, b_, seed);
10,754,354✔
240
}
241
// Not implemented
NEW
242
double Watt::get_pdf(double x) const
×
243
{
NEW
244
  return -1;
×
245
}
246

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

258
  mean_value_ = params.at(0);
×
259
  std_dev_ = params.at(1);
×
260
}
261

262
double Normal::sample(uint64_t* seed) const
×
263
{
264
  return normal_variate(mean_value_, std_dev_, seed);
×
265
}
NEW
266
double Normal::get_pdf(double x) const
×
267
{
NEW
268
  double exponent = -0.5 * std::pow((x - mean_value_) / std_dev_, 2);
×
NEW
269
  double coefficient = 1 / (std_dev_ * std::sqrt(2 * PI));
×
NEW
270
  return coefficient * std::exp(exponent);
×
271
}
272

273
//==============================================================================
274
// Tabular implementation
275
//==============================================================================
276

277
Tabular::Tabular(pugi::xml_node node)
2,772✔
278
{
279
  if (check_for_node(node, "interpolation")) {
2,772✔
280
    std::string temp = get_node_value(node, "interpolation");
2,772✔
281
    if (temp == "histogram") {
2,772✔
282
      interp_ = Interpolation::histogram;
2,708✔
283
    } else if (temp == "linear-linear") {
64✔
284
      interp_ = Interpolation::lin_lin;
64✔
285
    } else {
286
      openmc::fatal_error(
×
287
        "Unsupported interpolation type for distribution: " + temp);
×
288
    }
289
  } else {
2,772✔
290
    interp_ = Interpolation::histogram;
×
291
  }
292

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

305
Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp,
43,073,646✔
306
  const double* c)
43,073,646✔
307
  : interp_ {interp}
43,073,646✔
308
{
309
  init(x, p, n, c);
43,073,646✔
310
}
43,073,646✔
311

312
void Tabular::init(
43,076,418✔
313
  const double* x, const double* p, std::size_t n, const double* c)
314
{
315
  // Copy x/p arrays into vectors
316
  std::copy(x, x + n, std::back_inserter(x_));
43,076,418✔
317
  std::copy(p, p + n, std::back_inserter(p_));
43,076,418✔
318

319
  // Check interpolation parameter
320
  if (interp_ != Interpolation::histogram &&
43,076,418✔
321
      interp_ != Interpolation::lin_lin) {
36,021,353✔
322
    openmc::fatal_error("Only histogram and linear-linear interpolation "
×
323
                        "for tabular distribution is supported.");
324
  }
325

326
  // Calculate cumulative distribution function
327
  if (c) {
43,076,418✔
328
    std::copy(c, c + n, std::back_inserter(c_));
43,073,646✔
329
  } else {
330
    c_.resize(n);
2,772✔
331
    c_[0] = 0.0;
2,772✔
332
    for (int i = 1; i < n; ++i) {
36,336✔
333
      if (interp_ == Interpolation::histogram) {
33,564✔
334
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]);
33,436✔
335
      } else if (interp_ == Interpolation::lin_lin) {
128✔
336
        c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]);
128✔
337
      }
338
    }
339
  }
340

341
  // Normalize density and distribution functions. Note that we save the
342
  // integral of the distribution so that if it is used as part of another
343
  // distribution (e.g., Mixture), we know its relative strength.
344
  integral_ = c_[n - 1];
43,076,418✔
345
  for (int i = 0; i < n; ++i) {
613,375,991✔
346
    p_[i] = p_[i] / integral_;
570,299,573✔
347
    c_[i] = c_[i] / integral_;
570,299,573✔
348
  }
349
}
43,076,418✔
350

351
double Tabular::sample(uint64_t* seed) const
569,744,642✔
352
{
353
  // Sample value of CDF
354
  double c = prn(seed);
569,744,642✔
355

356
  // Find first CDF bin which is above the sampled value
357
  double c_i = c_[0];
569,744,642✔
358
  int i;
359
  std::size_t n = c_.size();
569,744,642✔
360
  for (i = 0; i < n - 1; ++i) {
2,006,090,681✔
361
    if (c <= c_[i + 1])
2,006,090,681✔
362
      break;
569,744,642✔
363
    c_i = c_[i + 1];
1,436,346,039✔
364
  }
365

366
  // Determine bounding PDF values
367
  double x_i = x_[i];
569,744,642✔
368
  double p_i = p_[i];
569,744,642✔
369

370
  if (interp_ == Interpolation::histogram) {
569,744,642✔
371
    // Histogram interpolation
372
    if (p_i > 0.0) {
3,407,965✔
373
      return x_i + (c - c_i) / p_i;
3,407,965✔
374
    } else {
375
      return x_i;
×
376
    }
377
  } else {
378
    // Linear-linear interpolation
379
    double x_i1 = x_[i + 1];
566,336,677✔
380
    double p_i1 = p_[i + 1];
566,336,677✔
381

382
    double m = (p_i1 - p_i) / (x_i1 - x_i);
566,336,677✔
383
    if (m == 0.0) {
566,336,677✔
384
      return x_i + (c - c_i) / p_i;
307,622,181✔
385
    } else {
386
      return x_i +
387
             (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) /
258,714,496✔
388
               m;
258,714,496✔
389
    }
390
  }
391
}
392

NEW
393
double Tabular::get_pdf(double x) const
×
394
{
395
  // get PDF value at x
396

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

407
  // Determine bounding PDF values
NEW
408
  double x_i = x_[i];
×
NEW
409
  double p_i = p_[i];
×
410

NEW
411
  if (interp_ == Interpolation::histogram) {
×
412
    // Histogram interpolation
NEW
413
    return p_i;
×
414
  } else {
415
    // Linear-linear interpolation
NEW
416
    double x_i1 = x_[i + 1];
×
NEW
417
    double p_i1 = p_[i + 1];
×
418

NEW
419
    double m = (p_i1 - p_i) / (x_i1 - x_i);
×
NEW
420
    if (m == 0.0) {
×
NEW
421
      return p_i;
×
422
    } else {
NEW
423
      return p_i + (x - x_i) * m;
×
424
    }
425
  }
426
}
427

428
//==============================================================================
429
// Equiprobable implementation
430
//==============================================================================
431

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

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

439
  double xl = x_[i];
×
440
  double xr = x_[i + i];
×
441
  return xl + ((n - 1) * r - i) * (xr - xl);
×
442
}
NEW
443
double Equiprobable::get_pdf(double x) const
×
444
{
NEW
445
  return -1;
×
446
}
447

448
//==============================================================================
449
// Mixture implementation
450
//==============================================================================
451

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

462
    // cummulative sum of probabilities
463
    double p = std::stod(pair.attribute("probability").value());
144✔
464

465
    // Save cummulative probability and distribution
466
    auto dist = distribution_from_xml(pair.child("dist"));
144✔
467
    cumsum += p * dist->integral();
144✔
468

469
    distribution_.push_back(std::make_pair(cumsum, std::move(dist)));
144✔
470
  }
144✔
471

472
  // Save integral of distribution
473
  integral_ = cumsum;
48✔
474

475
  // Normalize cummulative probabilities to 1
476
  for (auto& pair : distribution_) {
192✔
477
    pair.first /= cumsum;
144✔
478
  }
479
}
48✔
480

481
double Mixture::sample(uint64_t* seed) const
3,564✔
482
{
483
  // Sample value of CDF
484
  const double p = prn(seed);
3,564✔
485

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

490
  // This should not happen. Catch it
491
  assert(it != distribution_.cend());
2,916✔
492

493
  // Sample the chosen distribution
494
  return it->second->sample(seed);
7,128✔
495
}
NEW
496
double Mixture::get_pdf(double x) const
×
497
{
NEW
498
  return -1;
×
499
}
500

501
//==============================================================================
502
// Helper function
503
//==============================================================================
504

505
UPtrDist distribution_from_xml(pugi::xml_node node)
15,409✔
506
{
507
  if (!check_for_node(node, "type"))
15,409✔
508
    openmc::fatal_error("Distribution type must be specified.");
×
509

510
  // Determine type of distribution
511
  std::string type = get_node_value(node, "type", true, true);
15,409✔
512

513
  // Allocate extension of Distribution
514
  UPtrDist dist;
15,409✔
515
  if (type == "uniform") {
15,409✔
516
    dist = UPtrDist {new Uniform(node)};
256✔
517
  } else if (type == "powerlaw") {
15,153✔
518
    dist = UPtrDist {new PowerLaw(node)};
32✔
519
  } else if (type == "maxwell") {
15,121✔
520
    dist = UPtrDist {new Maxwell(node)};
64✔
521
  } else if (type == "watt") {
15,057✔
522
    dist = UPtrDist {new Watt(node)};
96✔
523
  } else if (type == "normal") {
14,961✔
524
    dist = UPtrDist {new Normal(node)};
×
525
  } else if (type == "discrete") {
14,961✔
526
    dist = UPtrDist {new Discrete(node)};
12,141✔
527
  } else if (type == "tabular") {
2,820✔
528
    dist = UPtrDist {new Tabular(node)};
2,772✔
529
  } else if (type == "mixture") {
48✔
530
    dist = UPtrDist {new Mixture(node)};
48✔
531
  } else if (type == "muir") {
×
532
    openmc::fatal_error(
×
533
      "'muir' distributions are now specified using the openmc.stats.muir() "
534
      "function in Python. Please regenerate your XML files.");
535
  } else {
536
    openmc::fatal_error("Invalid distribution type: " + type);
×
537
  }
538
  return dist;
30,818✔
539
}
15,409✔
540

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