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

openmc-dev / openmc / 29589626194

17 Jul 2026 02:50PM UTC coverage: 81.339% (+0.006%) from 81.333%
29589626194

Pull #3950

github

web-flow
Merge b5db7288b into 796ae384b
Pull Request #3950: Automatic C++ doc generation

18353 of 26615 branches covered (68.96%)

Branch coverage included in aggregate %.

59883 of 69570 relevant lines covered (86.08%)

47961782.35 hits per line

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

71.33
/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
#include <unordered_set>
11

12
#include "openmc/chain.h"
13
#include "openmc/constants.h"
14
#include "openmc/error.h"
15
#include "openmc/math_functions.h"
16
#include "openmc/random_dist.h"
17
#include "openmc/random_lcg.h"
18
#include "openmc/xml_interface.h"
19

20
namespace {
21
std::unordered_set<std::string> decay_spectrum_missing_chain_nuclides;
22
}
23

24
namespace openmc {
25

26
//==============================================================================
27
// Helper function for computing importance weights from biased sampling
28
//==============================================================================
29

30
vector<double> compute_importance_weights(
11 ✔
31
  const vector<double>& p, const vector<double>& b)
32
{
33
  std::size_t n = p.size();
11 ✔
34

35
  // Normalize original probabilities
36
  double sum_p = std::accumulate(p.begin(), p.end(), 0.0);
11 ✔
37
  vector<double> p_norm(n);
11 ✔
38
  for (std::size_t i = 0; i < n; ++i) {
44 ✔
39
    p_norm[i] = p[i] / sum_p;
33 ✔
40
  }
41

42
  // Normalize bias probabilities
43
  double sum_b = std::accumulate(b.begin(), b.end(), 0.0);
11 ✔
44
  vector<double> b_norm(n);
11 ✔
45
  for (std::size_t i = 0; i < n; ++i) {
44 ✔
46
    b_norm[i] = b[i] / sum_b;
33 ✔
47
  }
48

49
  // Compute importance weights
50
  vector<double> weights(n);
11 ✔
51
  for (std::size_t i = 0; i < n; ++i) {
44 ✔
52
    weights[i] = (b_norm[i] == 0.0) ? INFTY : p_norm[i] / b_norm[i];
33 !
53
  }
54
  return weights;
11 ✔
55
}
11 ✔
56

57
std::pair<double, double> Distribution::sample(uint64_t* seed) const
1,205,294,271 ✔
58
{
59
  if (bias_) {
1,205,294,271 ✔
60
    // Sample from the bias distribution and compute importance weight
61
    double val = bias_->sample_unbiased(seed);
715,011 ✔
62
    double wgt = this->evaluate(val) / bias_->evaluate(val);
715,011 ✔
63
    return {val, wgt};
715,011 ✔
64
  } else {
65
    // Unbiased sampling: return sampled value with weight 1.0
66
    double val = sample_unbiased(seed);
1,204,579,260 ✔
67
    return {val, 1.0};
1,204,579,260 ✔
68
  }
69
}
70

71
// PDF evaluation not supported for all distribution types
72
double Distribution::evaluate(double x) const
×
73
{
74
  throw std::runtime_error(
×
75
    "PDF evaluation not implemented for this distribution type.");
×
76
}
77

78
void Distribution::read_bias_from_xml(pugi::xml_node node)
9,019 ✔
79
{
80
  if (check_for_node(node, "bias")) {
9,019 ✔
81
    pugi::xml_node bias_node = node.child("bias");
77 ✔
82

83
    if (check_for_node(bias_node, "bias")) {
77 !
84
      openmc::fatal_error(
×
85
        "Distribution has a bias distribution with its own bias distribution. "
86
        "Please ensure bias distributions do not have their own bias.");
87
    }
88

89
    UPtrDist bias = distribution_from_xml(bias_node);
77 ✔
90
    this->set_bias(std::move(bias));
77 ✔
91
  }
77 ✔
92
}
9,019 ✔
93

94
//==============================================================================
95
// DiscreteIndex implementation
96
//==============================================================================
97

98
DiscreteIndex::DiscreteIndex(pugi::xml_node node)
×
99
{
100
  auto params = get_node_array<double>(node, "parameters");
×
101
  std::size_t n = params.size() / 2;
×
102

103
  assign({params.data() + n, n});
×
104
}
×
105

106
DiscreteIndex::DiscreteIndex(span<const double> p)
48,446 ✔
107
{
108
  assign(p);
48,446 ✔
109
}
48,446 ✔
110

111
void DiscreteIndex::assign(span<const double> p)
88,708 ✔
112
{
113
  prob_.assign(p.begin(), p.end());
88,708 ✔
114
  this->init_alias();
88,708 ✔
115
}
88,708 ✔
116

117
void DiscreteIndex::init_alias()
88,708 ✔
118
{
119
  normalize();
88,708 ✔
120

121
  // The initialization and sampling method is based on Vose
122
  // (DOI: 10.1109/32.92917)
123
  // Vectors for large and small probabilities based on 1/n
124
  vector<size_t> large;
88,708 ✔
125
  vector<size_t> small;
88,708 ✔
126

127
  size_t n = prob_.size();
88,708 ✔
128

129
  // Set and allocate memory
130
  alias_.assign(n, 0);
88,708 ✔
131

132
  // Fill large and small vectors based on 1/n
133
  for (size_t i = 0; i < n; i++) {
1,762,591 ✔
134
    prob_[i] *= n;
1,673,883 ✔
135
    if (prob_[i] > 1.0) {
1,673,883 ✔
136
      large.push_back(i);
250,997 ✔
137
    } else {
138
      small.push_back(i);
1,422,886 ✔
139
    }
140
  }
141

142
  while (!large.empty() && !small.empty()) {
1,608,948 ✔
143
    int j = small.back();
1,510,583 ✔
144
    int k = large.back();
1,510,583 ✔
145

146
    // Remove last element of small
147
    small.pop_back();
1,510,583 ✔
148

149
    // Update probability and alias based on Vose's algorithm
150
    prob_[k] += prob_[j] - 1.0;
1,510,583 ✔
151
    alias_[j] = k;
1,510,583 ✔
152

153
    // Move large index to small vector, if it is no longer large
154
    if (prob_[k] < 1.0) {
1,510,583 ✔
155
      small.push_back(k);
241,160 ✔
156
      large.pop_back();
1,840,451 ✔
157
    }
158
  }
159
}
88,708 ✔
160

161
size_t DiscreteIndex::sample(uint64_t* seed) const
74,066,497 ✔
162
{
163
  // Alias sampling of discrete distribution
164
  size_t n = prob_.size();
74,066,497 ✔
165
  if (n > 1) {
74,066,497 ✔
166
    size_t u = prn(seed) * n;
17,684,115 ✔
167
    if (prn(seed) < prob_[u]) {
17,684,115 ✔
168
      return u;
169
    } else {
170
      return alias_[u];
7,118,776 ✔
171
    }
172
  } else {
173
    return 0;
174
  }
175
}
176

177
void DiscreteIndex::normalize()
88,708 ✔
178
{
179
  // Renormalize density function so that it sums to unity. Note that we save
180
  // the integral of the distribution so that if it is used as part of another
181
  // distribution (e.g., Mixture), we know its relative strength.
182
  integral_ = std::accumulate(prob_.begin(), prob_.end(), 0.0);
88,708 ✔
183
  for (auto& p_i : prob_) {
1,762,591 ✔
184
    p_i /= integral_;
1,673,883 ✔
185
  }
186
}
88,708 ✔
187

188
//==============================================================================
189
// Discrete implementation
190
//==============================================================================
191

192
Discrete::Discrete(pugi::xml_node node)
30,925 ✔
193
{
194
  auto params = get_node_array<double>(node, "parameters");
30,925 ✔
195
  std::size_t n = params.size() / 2;
30,925 ✔
196

197
  // First half is x values, second half is probabilities
198
  x_.assign(params.begin(), params.begin() + n);
30,925 ✔
199
  const double* p = params.data() + n;
30,925 ✔
200

201
  // Check for bias
202
  if (check_for_node(node, "bias")) {
30,925 ✔
203
    // Get bias probabilities
204
    auto bias_params = get_node_array<double>(node, "bias");
11 ✔
205
    if (bias_params.size() != n) {
11 !
206
      openmc::fatal_error(
×
207
        "Size mismatch: Attempted to bias Discrete distribution with " +
×
208
        std::to_string(n) + " probability entries using a bias with " +
×
209
        std::to_string(bias_params.size()) +
×
210
        " entries. Please ensure distributions have the same size.");
211
    }
212

213
    // Compute importance weights
214
    vector<double> p_vec(p, p + n);
11 ✔
215
    weight_ = compute_importance_weights(p_vec, bias_params);
11 ✔
216

217
    // Initialize DiscreteIndex with bias probabilities for sampling
218
    di_.assign(bias_params);
11 ✔
219
  } else {
11 ✔
220
    // Unbiased case: weight_ stays empty
221
    di_.assign({p, n});
30,914 ✔
222
  }
223
}
30,925 ✔
224

225
Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n})
48,446 ✔
226
{
227
  x_.assign(x, x + n);
48,446 ✔
228
}
48,446 ✔
229

230
std::pair<double, double> Discrete::sample(uint64_t* seed) const
67,693,923 ✔
231
{
232
  size_t idx = di_.sample(seed);
67,693,923 ✔
233
  double wgt = weight_.empty() ? 1.0 : weight_[idx];
67,693,923 ✔
234
  return {x_[idx], wgt};
67,693,923 ✔
235
}
236

237
double Discrete::sample_unbiased(uint64_t* seed) const
×
238
{
239
  size_t idx = di_.sample(seed);
×
240
  return x_[idx];
×
241
}
242

243
//==============================================================================
244
// Uniform implementation
245
//==============================================================================
246

247
Uniform::Uniform(pugi::xml_node node)
392 ✔
248
{
249
  auto params = get_node_array<double>(node, "parameters");
392 ✔
250
  if (params.size() != 2) {
392 !
251
    fatal_error("Uniform distribution must have two "
×
252
                "parameters specified.");
253
  }
254

255
  a_ = params.at(0);
392 ✔
256
  b_ = params.at(1);
392 ✔
257

258
  read_bias_from_xml(node);
392 ✔
259
}
392 ✔
260

261
double Uniform::sample_unbiased(uint64_t* seed) const
791,864 ✔
262
{
263
  return a_ + prn(seed) * (b_ - a_);
791,864 ✔
264
}
265

266
double Uniform::evaluate(double x) const
385,011 ✔
267
{
268
  if (x <= a()) {
385,011 !
269
    return 0.0;
270
  } else if (x >= b()) {
385,011 !
271
    return 0.0;
272
  } else {
273
    return 1 / (b() - a());
385,011 ✔
274
  }
275
}
276

277
//==============================================================================
278
// PowerLaw implementation
279
//==============================================================================
280

281
PowerLaw::PowerLaw(pugi::xml_node node)
74 ✔
282
{
283
  auto params = get_node_array<double>(node, "parameters");
74 ✔
284
  if (params.size() != 3) {
74 !
285
    fatal_error("PowerLaw distribution must have three "
×
286
                "parameters specified.");
287
  }
288

289
  const double a = params.at(0);
74 ✔
290
  const double b = params.at(1);
74 ✔
291
  const double n = params.at(2);
74 ✔
292

293
  offset_ = std::pow(a, n + 1);
74 ✔
294
  span_ = std::pow(b, n + 1) - offset_;
74 ✔
295
  ninv_ = 1 / (n + 1);
74 ✔
296

297
  read_bias_from_xml(node);
74 ✔
298
}
74 ✔
299

300
double PowerLaw::evaluate(double x) const
275,011 ✔
301
{
302
  if (x <= a()) {
275,011 !
303
    return 0.0;
304
  } else if (x >= b()) {
275,011 !
305
    return 0.0;
306
  } else {
307
    int pwr = n() + 1;
275,011 ✔
308
    double norm = pwr / span_;
275,011 ✔
309
    return norm * std::pow(std::fabs(x), n());
275,011 ✔
310
  }
311
}
312

313
double PowerLaw::sample_unbiased(uint64_t* seed) const
277,233 ✔
314
{
315
  return std::pow(offset_ + prn(seed) * span_, ninv_);
277,233 ✔
316
}
317

318
//==============================================================================
319
// Maxwell implementation
320
//==============================================================================
321

322
Maxwell::Maxwell(pugi::xml_node node)
93 ✔
323
{
324
  theta_ = std::stod(get_node_value(node, "parameters"));
186 ✔
325

326
  read_bias_from_xml(node);
93 ✔
327
}
93 ✔
328

329
double Maxwell::sample_unbiased(uint64_t* seed) const
223,872 ✔
330
{
331
  return maxwell_spectrum(theta_, seed);
223,872 ✔
332
}
333

334
double Maxwell::evaluate(double x) const
220,000 ✔
335
{
336
  double c = (2.0 / SQRT_PI) * std::pow(theta_, -1.5);
220,000 ✔
337
  return c * std::sqrt(x) * std::exp(-x / theta_);
220,000 ✔
338
}
339

340
//==============================================================================
341
// Watt implementation
342
//==============================================================================
343

344
Watt::Watt(pugi::xml_node node)
123 ✔
345
{
346
  auto params = get_node_array<double>(node, "parameters");
123 ✔
347
  if (params.size() != 2)
123 !
348
    openmc::fatal_error("Watt energy distribution must have two "
×
349
                        "parameters specified.");
350

351
  a_ = params.at(0);
123 ✔
352
  b_ = params.at(1);
123 ✔
353

354
  read_bias_from_xml(node);
123 ✔
355
}
123 ✔
356

357
double Watt::sample_unbiased(uint64_t* seed) const
14,297,983 ✔
358
{
359
  return watt_spectrum(a_, b_, seed);
14,297,983 ✔
360
}
361

362
double Watt::evaluate(double x) const
220,000 ✔
363
{
364
  double c =
220,000 ✔
365
    2.0 / (std::sqrt(PI * b_) * std::pow(a_, 1.5) * std::exp(a_ * b_ / 4.0));
220,000 ✔
366
  return c * std::exp(-x / a_) * std::sinh(std::sqrt(b_ * x));
220,000 ✔
367
}
368

369
//==============================================================================
370
// Normal implementation
371
//==============================================================================
372

373
Normal::Normal(double mean_value, double std_dev, double lower, double upper)
66 ✔
374
  : mean_value_ {mean_value}, std_dev_ {std_dev}, lower_ {lower}, upper_ {upper}
66 ✔
375
{
376
  compute_normalization();
66 ✔
377
}
66 ✔
378

379
Normal::Normal(pugi::xml_node node)
44 ✔
380
{
381
  auto params = get_node_array<double>(node, "parameters");
44 ✔
382
  if (params.size() != 2 && params.size() != 4) {
44 !
383
    openmc::fatal_error("Normal energy distribution must have two "
×
384
                        "parameters (mean, std_dev) or four parameters "
385
                        "(mean, std_dev, lower, upper) specified.");
386
  }
387

388
  mean_value_ = params.at(0);
44 ✔
389
  std_dev_ = params.at(1);
44 ✔
390

391
  // Optional truncation bounds
392
  if (params.size() == 4) {
44 ✔
393
    lower_ = params.at(2);
11 ✔
394
    upper_ = params.at(3);
11 ✔
395
  } else {
396
    lower_ = -INFTY;
33 ✔
397
    upper_ = INFTY;
33 ✔
398
  }
399

400
  compute_normalization();
44 ✔
401
  read_bias_from_xml(node);
44 ✔
402
}
44 ✔
403

404
void Normal::compute_normalization()
110 ✔
405
{
406
  // Validate bounds
407
  if (lower_ >= upper_) {
110 !
408
    openmc::fatal_error(
×
409
      "Normal distribution lower bound must be less than upper bound.");
410
  }
411

412
  // Check if truncation bounds are finite
413
  is_truncated_ = (lower_ > -INFTY || upper_ < INFTY);
110 ✔
414

415
  if (is_truncated_) {
110 ✔
416
    double alpha = (lower_ - mean_value_) / std_dev_;
55 ✔
417
    double beta = (upper_ - mean_value_) / std_dev_;
55 ✔
418
    double cdf_diff = standard_normal_cdf(beta) - standard_normal_cdf(alpha);
55 ✔
419

420
    if (cdf_diff <= 0.0) {
55 !
421
      openmc::fatal_error(
×
422
        "Normal distribution truncation bounds exclude entire distribution.");
423
    }
424
    norm_factor_ = 1.0 / cdf_diff;
55 ✔
425
  } else {
426
    norm_factor_ = 1.0;
55 ✔
427
  }
428
}
110 ✔
429

430
double Normal::sample_unbiased(uint64_t* seed) const
330,000 ✔
431
{
432
  if (!is_truncated_) {
330,000 ✔
433
    return normal_variate(mean_value_, std_dev_, seed);
220,000 ✔
434
  }
435

436
  // Rejection sampling for truncated normal
437
  double x;
161,249 ✔
438
  do {
161,249 ✔
439
    x = normal_variate(mean_value_, std_dev_, seed);
161,249 ✔
440
  } while (x < lower_ || x > upper_);
161,249 ✔
441
  return x;
442
}
443

444
double Normal::evaluate(double x) const
220,121 ✔
445
{
446
  // Return 0 outside truncation bounds
447
  if (x < lower_ || x > upper_) {
220,121 ✔
448
    return 0.0;
449
  }
450

451
  // Standard normal PDF value
452
  double pdf = (1.0 / (std::sqrt(2.0 * PI) * std_dev_)) *
220,077 ✔
453
               std::exp(-std::pow((x - mean_value_), 2.0) /
220,077 ✔
454
                        (2.0 * std::pow(std_dev_, 2.0)));
220,077 ✔
455

456
  // Apply normalization for truncation
457
  return pdf * norm_factor_;
220,077 ✔
458
}
459

460
//==============================================================================
461
// Tabular implementation
462
//==============================================================================
463

464
Tabular::Tabular(pugi::xml_node node)
8,293 ✔
465
{
466
  if (check_for_node(node, "interpolation")) {
8,293 !
467
    std::string temp = get_node_value(node, "interpolation");
8,293 ✔
468
    if (temp == "histogram") {
8,293 ✔
469
      interp_ = Interpolation::histogram;
8,211 ✔
470
    } else if (temp == "linear-linear") {
82 !
471
      interp_ = Interpolation::lin_lin;
82 ✔
472
    } else if (temp == "log-linear") {
×
473
      interp_ = Interpolation::log_lin;
×
474
    } else if (temp == "log-log") {
×
475
      interp_ = Interpolation::log_log;
×
476
    } else {
477
      openmc::fatal_error(
×
478
        "Unsupported interpolation type for distribution: " + temp);
×
479
    }
480
  } else {
8,293 ✔
481
    interp_ = Interpolation::histogram;
×
482
  }
483

484
  // Read and initialize tabular distribution. If number of parameters is odd,
485
  // add an extra zero for the 'p' array.
486
  auto params = get_node_array<double>(node, "parameters");
8,293 ✔
487
  if (params.size() % 2 != 0) {
8,293 !
488
    params.push_back(0.0);
×
489
  }
490
  std::size_t n = params.size() / 2;
8,293 ✔
491
  const double* x = params.data();
8,293 ✔
492
  const double* p = x + n;
8,293 ✔
493
  init(x, p, n);
8,293 ✔
494

495
  read_bias_from_xml(node);
8,293 ✔
496
}
8,293 ✔
497

498
Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp,
53,787,682 ✔
499
  const double* c)
53,787,682 ✔
500
  : interp_ {interp}
53,787,682 ✔
501
{
502
  init(x, p, n, c);
53,787,682 ✔
503
}
53,787,682 ✔
504

505
void Tabular::init(
53,795,975 ✔
506
  const double* x, const double* p, std::size_t n, const double* c)
507
{
508
  // Copy x/p arrays into vectors
509
  std::copy(x, x + n, std::back_inserter(x_));
53,795,975 ✔
510
  std::copy(p, p + n, std::back_inserter(p_));
53,795,975 ✔
511

512
  // Calculate cumulative distribution function
513
  if (c) {
53,795,975 ✔
514
    std::copy(c, c + n, std::back_inserter(c_));
53,787,363 ✔
515
  } else {
516
    c_.resize(n);
8,612 ✔
517
    c_[0] = 0.0;
8,612 ✔
518
    for (int i = 1; i < n; ++i) {
289,671 ✔
519
      if (interp_ == Interpolation::histogram) {
281,059 ✔
520
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]);
93,741 ✔
521
      } else if (interp_ == Interpolation::lin_lin) {
187,318 !
522
        c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]);
187,318 ✔
523
      } else if (interp_ == Interpolation::log_lin) {
×
524
        double m = std::log(p_[i] / p_[i - 1]) / (x_[i] - x_[i - 1]);
×
525
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) *
×
526
                              exprel(m * (x_[i] - x_[i - 1]));
×
527
      } else if (interp_ == Interpolation::log_log) {
×
528
        double m = std::log((x_[i] * p_[i]) / (x_[i - 1] * p_[i - 1])) /
×
529
                   std::log(x_[i] / x_[i - 1]);
×
530
        c_[i] = c_[i - 1] + x_[i - 1] * p_[i - 1] *
×
531
                              std::log(x_[i] / x_[i - 1]) *
×
532
                              exprel(m * std::log(x_[i] / x_[i - 1]));
×
533
      } else {
534
        UNREACHABLE();
×
535
      }
536
    }
537
  }
538

539
  // Normalize density and distribution functions. Note that we save the
540
  // integral of the distribution so that if it is used as part of another
541
  // distribution (e.g., Mixture), we know its relative strength.
542
  integral_ = c_[n - 1];
53,795,975 ✔
543
  for (int i = 0; i < n; ++i) {
771,189,158 ✔
544
    p_[i] = p_[i] / integral_;
717,393,183 ✔
545
    c_[i] = c_[i] / integral_;
717,393,183 ✔
546
  }
547
}
53,795,975 ✔
548

549
double Tabular::sample_unbiased(uint64_t* seed) const
1,189,373,319 ✔
550
{
551
  // Sample value of CDF
552
  double c = prn(seed);
1,189,373,319 ✔
553

554
  // Find first CDF bin which is above the sampled value
555
  auto c_iter = std::lower_bound(c_.begin() + 1, c_.end(), c);
1,189,373,319 ✔
556
  int i = std::distance(c_.begin(), c_iter) - 1;
1,189,373,319 ✔
557
  double c_i = c_[i];
1,189,373,319 ✔
558

559
  // Determine bounding PDF values
560
  double x_i = x_[i];
1,189,373,319 ✔
561
  double p_i = p_[i];
1,189,373,319 ✔
562

563
  if (interp_ == Interpolation::histogram) {
1,189,373,319 ✔
564
    // Histogram interpolation
565
    if (p_i > 0.0) {
5,009,081 !
566
      return x_i + (c - c_i) / p_i;
5,009,081 ✔
567
    } else {
568
      return x_i;
569
    }
570
  } else if (interp_ == Interpolation::lin_lin) {
1,184,364,238 !
571
    // Linear-linear interpolation
572
    double x_i1 = x_[i + 1];
1,184,364,238 ✔
573
    double p_i1 = p_[i + 1];
1,184,364,238 ✔
574

575
    double m = (p_i1 - p_i) / (x_i1 - x_i);
1,184,364,238 ✔
576
    if (m == 0.0) {
1,184,364,238 ✔
577
      return x_i + (c - c_i) / p_i;
544,266,883 ✔
578
    } else {
579
      return x_i +
640,097,355 ✔
580
             (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) /
1,280,194,710 !
581
               m;
640,097,355 ✔
582
    }
583
  } else if (interp_ == Interpolation::log_lin) {
×
584
    // Log-linear interpolation
585
    double x_i1 = x_[i + 1];
×
586
    double p_i1 = p_[i + 1];
×
587

588
    double m = std::log(p_i1 / p_i) / (x_i1 - x_i);
×
589
    double f = (c - c_i) / p_i;
×
590
    return x_i + f * log1prel(m * f);
×
591
  } else if (interp_ == Interpolation::log_log) {
×
592
    // Log-Log interpolation
593
    double x_i1 = x_[i + 1];
×
594
    double p_i1 = p_[i + 1];
×
595

596
    double m = std::log((x_i1 * p_i1) / (x_i * p_i)) / std::log(x_i1 / x_i);
×
597
    double f = (c - c_i) / (p_i * x_i);
×
598
    return x_i * std::exp(f * log1prel(m * f));
×
599
  } else {
600
    UNREACHABLE();
×
601
  }
602
}
603

604
double Tabular::evaluate(double x) const
110,000 ✔
605
{
606
  int i;
110,000 ✔
607

608
  if (interp_ == Interpolation::histogram) {
110,000 !
609
    i = std::upper_bound(x_.begin(), x_.end(), x) - x_.begin() - 1;
×
610
    if (i < 0 || i >= static_cast<int>(p_.size())) {
×
611
      return 0.0;
612
    } else {
613
      return p_[i];
×
614
    }
615
  } else {
616
    i = std::lower_bound(x_.begin(), x_.end(), x) - x_.begin() - 1;
110,000 !
617

618
    if (i < 0 || i >= static_cast<int>(p_.size()) - 1) {
110,000 !
619
      return 0.0;
620
    } else {
621
      double x0 = x_[i];
110,000 ✔
622
      double x1 = x_[i + 1];
110,000 ✔
623
      double p0 = p_[i];
110,000 ✔
624
      double p1 = p_[i + 1];
110,000 ✔
625

626
      double t = (x - x0) / (x1 - x0);
110,000 ✔
627
      return (1 - t) * p0 + t * p1;
110,000 ✔
628
    }
629
  }
630
}
631

632
//==============================================================================
633
// Equiprobable implementation
634
//==============================================================================
635

636
double Equiprobable::sample_unbiased(uint64_t* seed) const
×
637
{
638
  std::size_t n = x_.size();
×
639

640
  double r = prn(seed);
×
641
  int i = std::floor((n - 1) * r);
×
642

643
  double xl = x_[i];
×
644
  double xr = x_[i + i];
×
645
  return xl + ((n - 1) * r - i) * (xr - xl);
×
646
}
647

648
double Equiprobable::evaluate(double x) const
×
649
{
650
  double x_min = *std::min_element(x_.begin(), x_.end());
×
651
  double x_max = *std::max_element(x_.begin(), x_.end());
×
652

653
  if (x < x_min || x > x_max) {
×
654
    return 0.0;
655
  } else {
656
    return 1.0 / (x_max - x_min);
×
657
  }
658
}
659

660
//==============================================================================
661
// Mixture implementation
662
//==============================================================================
663

664
Mixture::Mixture(pugi::xml_node node)
67 ✔
665
{
666
  vector<double> probabilities;
67 ✔
667

668
  // First pass: collect distributions and their probabilities
669
  for (pugi::xml_node pair : node.children("pair")) {
425 ✔
670
    // Check that required data exists
671
    if (!pair.attribute("probability"))
179 !
672
      fatal_error("Mixture pair element does not have probability.");
×
673
    if (!pair.child("dist"))
179 !
674
      fatal_error("Mixture pair element does not have a distribution.");
×
675

676
    // Get probability and distribution
677
    double p = std::stod(pair.attribute("probability").value());
358 ✔
678
    auto dist = distribution_from_xml(pair.child("dist"));
179 ✔
679

680
    // Weight probability by the distribution's integral
681
    double weighted_prob = p * dist->integral();
179 ✔
682
    probabilities.push_back(weighted_prob);
179 ✔
683
    distribution_.push_back(std::move(dist));
179 ✔
684
  }
179 ✔
685

686
  // Save sum of weighted probabilities
687
  integral_ = std::accumulate(probabilities.begin(), probabilities.end(), 0.0);
67 ✔
688

689
  std::size_t n = probabilities.size();
67 ✔
690

691
  // Check for bias
692
  if (check_for_node(node, "bias")) {
67 !
693
    // Get bias probabilities
694
    auto bias_params = get_node_array<double>(node, "bias");
×
695
    if (bias_params.size() != n) {
×
696
      openmc::fatal_error(
×
697
        "Size mismatch: Attempted to bias Mixture distribution with " +
×
698
        std::to_string(n) + " components using a bias with " +
×
699
        std::to_string(bias_params.size()) +
×
700
        " entries. Please ensure distributions have the same size.");
701
    }
702

703
    // Compute importance weights
704
    weight_ = compute_importance_weights(probabilities, bias_params);
×
705

706
    // Initialize DiscreteIndex with bias probabilities for sampling
707
    di_.assign(bias_params);
×
708
  } else {
×
709
    // Unbiased case: weight_ stays empty
710
    di_.assign(probabilities);
67 ✔
711
  }
712
}
67 ✔
713

714
std::pair<double, double> Mixture::sample(uint64_t* seed) const
223,564 ✔
715
{
716
  size_t idx = di_.sample(seed);
223,564 ✔
717

718
  // Sample the chosen distribution
719
  auto [val, sub_wgt] = distribution_[idx]->sample(seed);
223,564 ✔
720

721
  // Multiply by component selection weight
722
  double mix_wgt = weight_.empty() ? 1.0 : weight_[idx];
223,564 !
723
  return {val, mix_wgt * sub_wgt};
223,564 ✔
724
}
725

726
double Mixture::sample_unbiased(uint64_t* seed) const
×
727
{
728
  size_t idx = di_.sample(seed);
×
729
  return distribution_[idx]->sample(seed).first;
×
730
}
731

732
//==============================================================================
733
// Helper function
734
//==============================================================================
735

736
UPtrDist distribution_from_xml(pugi::xml_node node)
40,044 ✔
737
{
738
  if (!check_for_node(node, "type"))
40,044 !
739
    openmc::fatal_error("Distribution type must be specified.");
×
740

741
  // Determine type of distribution
742
  std::string type = get_node_value(node, "type", true, true);
40,044 ✔
743

744
  // Allocate extension of Distribution
745
  UPtrDist dist;
40,044 ✔
746
  if (type == "uniform") {
40,044 ✔
747
    dist = UPtrDist {new Uniform(node)};
392 ✔
748
  } else if (type == "powerlaw") {
39,652 ✔
749
    dist = UPtrDist {new PowerLaw(node)};
74 ✔
750
  } else if (type == "maxwell") {
39,578 ✔
751
    dist = UPtrDist {new Maxwell(node)};
93 ✔
752
  } else if (type == "watt") {
39,485 ✔
753
    dist = UPtrDist {new Watt(node)};
123 ✔
754
  } else if (type == "normal") {
39,362 ✔
755
    dist = UPtrDist {new Normal(node)};
33 ✔
756
  } else if (type == "discrete") {
39,329 ✔
757
    dist = UPtrDist {new Discrete(node)};
30,914 ✔
758
  } else if (type == "tabular") {
8,415 ✔
759
    dist = UPtrDist {new Tabular(node)};
8,293 ✔
760
  } else if (type == "mixture") {
122 ✔
761
    dist = UPtrDist {new Mixture(node)};
67 ✔
762
  } else if (type == "decay_spectrum") {
55 !
763
    dist = UPtrDist {new DecaySpectrum(node)};
55 ✔
764
  } else if (type == "muir") {
×
765
    openmc::fatal_error(
×
766
      "'muir' distributions are now specified using the openmc.stats.muir() "
767
      "function in Python. Please regenerate your XML files.");
768
  } else {
769
    openmc::fatal_error("Invalid distribution type: " + type);
×
770
  }
771
  return dist;
40,044 ✔
772
}
40,044 ✔
773

774
//==============================================================================
775
// DecaySpectrum implementation
776
//==============================================================================
777

778
DecaySpectrum::DecaySpectrum(pugi::xml_node node)
55 ✔
779
{
780
  // Read the region volume [cm^3] needed for absolute emission rate
781
  if (!check_for_node(node, "volume"))
55 !
782
    fatal_error("DecaySpectrum: 'volume' attribute is required.");
×
783
  double volume = std::stod(get_node_value(node, "volume"));
110 ✔
784

785
  // Read nuclide names and atom densities from XML
786
  vector<int> nuclide_indices;
55 ✔
787
  vector<double> atoms;
55 ✔
788
  auto names = get_node_array<std::string>(node, "nuclides");
55 ✔
789
  auto densities = get_node_array<double>(node, "parameters");
55 ✔
790
  if (names.size() != densities.size()) {
55 !
791
    fatal_error("DecaySpectrum nuclides and parameters must have the same "
×
792
                "length.");
793
  }
794

795
  for (size_t i = 0; i < names.size(); ++i) {
121 ✔
796
    const auto& name = names[i];
66 ✔
797
    double density = densities[i];
66 ✔
798

799
    // Look up nuclide in the depletion chain
800
    auto it = data::chain_nuclide_map.find(name);
66 ✔
801
    if (it == data::chain_nuclide_map.end()) {
66 !
802
      if (decay_spectrum_missing_chain_nuclides.insert(name).second) {
×
803
        warning("Nuclide '" + name +
×
804
                "' appears in a DecaySpectrum source but is not present in "
805
                "the depletion chain; it will be ignored.");
806
      }
807
      continue;
×
808
    }
809

810
    int nuclide_index = it->second;
66 !
811
    const auto& chain_nuc = data::chain_nuclides[nuclide_index];
66 !
812
    const Distribution* photon_dist = chain_nuc->photon_energy();
66 !
813
    if (!photon_dist)
66 !
814
      continue;
×
815

816
    // Skip non-positive densities and warn if negative
817
    if (density <= 0.0) {
66 !
818
      if (density < 0.0) {
×
819
        warning("Nuclide '" + name +
×
820
                "' has a negative density in a DecaySpectrum source; it will "
821
                "be ignored.");
822
      }
823
      continue;
×
824
    }
825

826
    // atoms = density [atom/b-cm] * 1e24 [b/cm^2] * volume [cm^3]
827
    double atoms_i = density * 1.0e24 * volume;
66 ✔
828

829
    nuclide_indices.push_back(nuclide_index);
66 ✔
830
    atoms.push_back(atoms_i);
66 ✔
831
  }
832

833
  init(std::move(nuclide_indices), atoms);
110 ✔
834
}
55 ✔
835

836
void DecaySpectrum::init(
55 ✔
837
  vector<int> nuclide_indices, const vector<double>& atoms)
838
{
839
  if (nuclide_indices.size() != atoms.size()) {
55 !
840
    fatal_error("DecaySpectrum nuclide index and atoms arrays must have "
×
841
                "the same length.");
842
  }
843

844
  vector<double> probs;
55 ✔
845
  probs.reserve(nuclide_indices.size());
55 ✔
846
  for (size_t i = 0; i < nuclide_indices.size(); ++i) {
121 ✔
847
    // Distribution integral is in [photons/s/atom]; multiplying by atoms gives
848
    // the total emission rate [photons/s] for this nuclide.
849
    const auto* dist =
66 ✔
850
      data::chain_nuclides[nuclide_indices[i]]->photon_energy();
66 ✔
851
    probs.push_back(atoms[i] * dist->integral());
66 ✔
852
  }
853

854
  nuclide_indices_ = std::move(nuclide_indices);
55 ✔
855
  integral_ = std::accumulate(probs.begin(), probs.end(), 0.0);
55 ✔
856
  if (nuclide_indices_.empty() || integral_ <= 0.0) {
55 !
857
    fatal_error("DecaySpectrum source did not resolve any nuclides with decay "
×
858
                "photon spectra and positive atom densities. Ensure "
859
                "OPENMC_CHAIN_FILE is set and matches the nuclides in the "
860
                "source definition.");
861
  }
862
  di_.assign(probs);
55 ✔
863
}
55 ✔
864

865
DecaySpectrum::Sample DecaySpectrum::sample_with_parent(uint64_t* seed) const
71,500 ✔
866
{
867
  size_t idx = di_.sample(seed);
71,500 ✔
868
  int parent_nuclide = nuclide_indices_[idx];
71,500 ✔
869
  const auto* dist = data::chain_nuclides[parent_nuclide]->photon_energy();
71,500 ✔
870
  auto [energy, weight] = dist->sample(seed);
71,500 ✔
871
  return {energy, weight, parent_nuclide};
71,500 ✔
872
}
873

874
std::pair<double, double> DecaySpectrum::sample(uint64_t* seed) const
×
875
{
876
  auto sample = sample_with_parent(seed);
×
877
  return {sample.energy, sample.weight};
×
878
}
879

880
double DecaySpectrum::integral() const
55 ✔
881
{
882
  return integral_;
55 ✔
883
}
884

885
double DecaySpectrum::sample_unbiased(uint64_t* seed) const
×
886
{
887
  return sample_with_parent(seed).energy;
×
888
}
889

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

© 2026 Coveralls, Inc