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

openmc-dev / openmc / 25117850655

29 Apr 2026 03:25PM UTC coverage: 81.286% (-0.09%) from 81.374%
25117850655

Pull #3930

github

web-flow
Merge 4d085c26a into 368ea069c
Pull Request #3930: Implement DecaySpectrum distribution type and utilize in R2S

17748 of 25642 branches covered (69.21%)

Branch coverage included in aggregate %.

235 of 278 new or added lines in 7 files covered. (84.53%)

131 existing lines in 4 files now uncovered.

58787 of 68513 relevant lines covered (85.8%)

47157362.79 hits per line

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

71.8
/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/chain.h"
12
#include "openmc/constants.h"
13
#include "openmc/error.h"
14
#include "openmc/math_functions.h"
15
#include "openmc/random_dist.h"
16
#include "openmc/random_lcg.h"
17
#include "openmc/xml_interface.h"
18

19
namespace openmc {
20

21
//==============================================================================
22
// Helper function for computing importance weights from biased sampling
23
//==============================================================================
24

25
vector<double> compute_importance_weights(
11✔
26
  const vector<double>& p, const vector<double>& b)
27
{
28
  std::size_t n = p.size();
11✔
29

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

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

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

52
std::pair<double, double> Distribution::sample(uint64_t* seed) const
1,049,699,144✔
53
{
54
  if (bias_) {
1,049,699,144✔
55
    // Sample from the bias distribution and compute importance weight
56
    double val = bias_->sample_unbiased(seed);
715,043✔
57
    double wgt = this->evaluate(val) / bias_->evaluate(val);
715,043✔
58
    return {val, wgt};
715,043✔
59
  } else {
60
    // Unbiased sampling: return sampled value with weight 1.0
61
    double val = sample_unbiased(seed);
1,048,984,101✔
62
    return {val, 1.0};
1,048,984,101✔
63
  }
64
}
65

66
// PDF evaluation not supported for all distribution types
67
double Distribution::evaluate(double x) const
×
68
{
69
  throw std::runtime_error(
×
70
    "PDF evaluation not implemented for this distribution type.");
×
71
}
72

73
void Distribution::read_bias_from_xml(pugi::xml_node node)
8,684✔
74
{
75
  if (check_for_node(node, "bias")) {
8,684✔
76
    pugi::xml_node bias_node = node.child("bias");
77✔
77

78
    if (check_for_node(bias_node, "bias")) {
77!
79
      openmc::fatal_error(
×
80
        "Distribution has a bias distribution with its own bias distribution. "
81
        "Please ensure bias distributions do not have their own bias.");
82
    }
83

84
    UPtrDist bias = distribution_from_xml(bias_node);
77✔
85
    this->set_bias(std::move(bias));
77✔
86
  }
77✔
87
}
8,684✔
88

89
//==============================================================================
90
// DiscreteIndex implementation
91
//==============================================================================
92

93
DiscreteIndex::DiscreteIndex(pugi::xml_node node)
×
94
{
95
  auto params = get_node_array<double>(node, "parameters");
×
96
  std::size_t n = params.size() / 2;
×
97

98
  assign({params.data() + n, n});
×
99
}
×
100

101
DiscreteIndex::DiscreteIndex(span<const double> p)
47,858✔
102
{
103
  assign(p);
47,858✔
104
}
47,858✔
105

106
void DiscreteIndex::assign(span<const double> p)
86,313✔
107
{
108
  prob_.assign(p.begin(), p.end());
86,313✔
109
  this->init_alias();
86,313✔
110
}
86,313✔
111

112
void DiscreteIndex::init_alias()
86,313✔
113
{
114
  normalize();
86,313✔
115

116
  // The initialization and sampling method is based on Vose
117
  // (DOI: 10.1109/32.92917)
118
  // Vectors for large and small probabilities based on 1/n
119
  vector<size_t> large;
86,313✔
120
  vector<size_t> small;
86,313✔
121

122
  size_t n = prob_.size();
86,313✔
123

124
  // Set and allocate memory
125
  alias_.assign(n, 0);
86,313✔
126

127
  // Fill large and small vectors based on 1/n
128
  for (size_t i = 0; i < n; i++) {
1,703,951✔
129
    prob_[i] *= n;
1,617,638✔
130
    if (prob_[i] > 1.0) {
1,617,638✔
131
      large.push_back(i);
242,555✔
132
    } else {
133
      small.push_back(i);
1,375,083✔
134
    }
135
  }
136

137
  while (!large.empty() && !small.empty()) {
1,552,412✔
138
    int j = small.back();
1,456,733✔
139
    int k = large.back();
1,456,733✔
140

141
    // Remove last element of small
142
    small.pop_back();
1,456,733✔
143

144
    // Update probability and alias based on Vose's algorithm
145
    prob_[k] += prob_[j] - 1.0;
1,456,733✔
146
    alias_[j] = k;
1,456,733✔
147

148
    // Move large index to small vector, if it is no longer large
149
    if (prob_[k] < 1.0) {
1,456,733✔
150
      small.push_back(k);
233,009✔
151
      large.pop_back();
1,776,055✔
152
    }
153
  }
154
}
86,313✔
155

156
size_t DiscreteIndex::sample(uint64_t* seed) const
71,259,509✔
157
{
158
  // Alias sampling of discrete distribution
159
  size_t n = prob_.size();
71,259,509✔
160
  if (n > 1) {
71,259,509✔
161
    size_t u = prn(seed) * n;
18,471,572✔
162
    if (prn(seed) < prob_[u]) {
18,471,572✔
163
      return u;
164
    } else {
165
      return alias_[u];
7,632,811✔
166
    }
167
  } else {
168
    return 0;
169
  }
170
}
171

172
void DiscreteIndex::normalize()
86,313✔
173
{
174
  // Renormalize density function so that it sums to unity. Note that we save
175
  // the integral of the distribution so that if it is used as part of another
176
  // distribution (e.g., Mixture), we know its relative strength.
177
  integral_ = std::accumulate(prob_.begin(), prob_.end(), 0.0);
86,313✔
178
  for (auto& p_i : prob_) {
1,703,951✔
179
    p_i /= integral_;
1,617,638✔
180
  }
181
}
86,313✔
182

183
//==============================================================================
184
// Discrete implementation
185
//==============================================================================
186

187
Discrete::Discrete(pugi::xml_node node)
29,717✔
188
{
189
  auto params = get_node_array<double>(node, "parameters");
29,717✔
190
  std::size_t n = params.size() / 2;
29,717✔
191

192
  // First half is x values, second half is probabilities
193
  x_.assign(params.begin(), params.begin() + n);
29,717✔
194
  const double* p = params.data() + n;
29,717✔
195

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

208
    // Compute importance weights
209
    vector<double> p_vec(p, p + n);
11✔
210
    weight_ = compute_importance_weights(p_vec, bias_params);
11✔
211

212
    // Initialize DiscreteIndex with bias probabilities for sampling
213
    di_.assign(bias_params);
11✔
214
  } else {
11✔
215
    // Unbiased case: weight_ stays empty
216
    di_.assign({p, n});
29,706✔
217
  }
218
}
29,717✔
219

220
Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n})
47,858✔
221
{
222
  x_.assign(x, x + n);
47,858✔
223
}
47,858✔
224

225
std::pair<double, double> Discrete::sample(uint64_t* seed) const
64,429,478✔
226
{
227
  size_t idx = di_.sample(seed);
64,429,478✔
228
  double wgt = weight_.empty() ? 1.0 : weight_[idx];
64,429,478✔
229
  return {x_[idx], wgt};
64,429,478✔
230
}
231

232
double Discrete::sample_unbiased(uint64_t* seed) const
×
233
{
234
  size_t idx = di_.sample(seed);
×
235
  return x_[idx];
×
236
}
237

238
//==============================================================================
239
// Uniform implementation
240
//==============================================================================
241

242
Uniform::Uniform(pugi::xml_node node)
362✔
243
{
244
  auto params = get_node_array<double>(node, "parameters");
362✔
245
  if (params.size() != 2) {
362!
246
    fatal_error("Uniform distribution must have two "
×
247
                "parameters specified.");
248
  }
249

250
  a_ = params.at(0);
362✔
251
  b_ = params.at(1);
362✔
252

253
  read_bias_from_xml(node);
362✔
254
}
362✔
255

256
double Uniform::sample_unbiased(uint64_t* seed) const
786,332✔
257
{
258
  return a_ + prn(seed) * (b_ - a_);
786,332✔
259
}
260

261
double Uniform::evaluate(double x) const
385,043✔
262
{
263
  if (x <= a()) {
385,043!
264
    return 0.0;
265
  } else if (x >= b()) {
385,043!
266
    return 0.0;
267
  } else {
268
    return 1 / (b() - a());
385,043✔
269
  }
270
}
271

272
//==============================================================================
273
// PowerLaw implementation
274
//==============================================================================
275

276
PowerLaw::PowerLaw(pugi::xml_node node)
74✔
277
{
278
  auto params = get_node_array<double>(node, "parameters");
74✔
279
  if (params.size() != 3) {
74!
280
    fatal_error("PowerLaw distribution must have three "
×
281
                "parameters specified.");
282
  }
283

284
  const double a = params.at(0);
74✔
285
  const double b = params.at(1);
74✔
286
  const double n = params.at(2);
74✔
287

288
  offset_ = std::pow(a, n + 1);
74✔
289
  span_ = std::pow(b, n + 1) - offset_;
74✔
290
  ninv_ = 1 / (n + 1);
74✔
291

292
  read_bias_from_xml(node);
74✔
293
}
74✔
294

295
double PowerLaw::evaluate(double x) const
275,043✔
296
{
297
  if (x <= a()) {
275,043!
298
    return 0.0;
299
  } else if (x >= b()) {
275,043!
300
    return 0.0;
301
  } else {
302
    int pwr = n() + 1;
275,043✔
303
    double norm = pwr / span_;
275,043✔
304
    return norm * std::pow(std::fabs(x), n());
275,043✔
305
  }
306
}
307

308
double PowerLaw::sample_unbiased(uint64_t* seed) const
277,265✔
309
{
310
  return std::pow(offset_ + prn(seed) * span_, ninv_);
277,265✔
311
}
312

313
//==============================================================================
314
// Maxwell implementation
315
//==============================================================================
316

317
Maxwell::Maxwell(pugi::xml_node node)
93✔
318
{
319
  theta_ = std::stod(get_node_value(node, "parameters"));
186✔
320

321
  read_bias_from_xml(node);
93✔
322
}
93✔
323

324
double Maxwell::sample_unbiased(uint64_t* seed) const
223,872✔
325
{
326
  return maxwell_spectrum(theta_, seed);
223,872✔
327
}
328

329
double Maxwell::evaluate(double x) const
220,000✔
330
{
331
  double c = (2.0 / SQRT_PI) * std::pow(theta_, -1.5);
220,000✔
332
  return c * std::sqrt(x) * std::exp(-x / theta_);
220,000✔
333
}
334

335
//==============================================================================
336
// Watt implementation
337
//==============================================================================
338

339
Watt::Watt(pugi::xml_node node)
123✔
340
{
341
  auto params = get_node_array<double>(node, "parameters");
123✔
342
  if (params.size() != 2)
123!
343
    openmc::fatal_error("Watt energy distribution must have two "
×
344
                        "parameters specified.");
345

346
  a_ = params.at(0);
123✔
347
  b_ = params.at(1);
123✔
348

349
  read_bias_from_xml(node);
123✔
350
}
123✔
351

352
double Watt::sample_unbiased(uint64_t* seed) const
14,383,830✔
353
{
354
  return watt_spectrum(a_, b_, seed);
14,383,830✔
355
}
356

357
double Watt::evaluate(double x) const
220,000✔
358
{
359
  double c =
220,000✔
360
    2.0 / (std::sqrt(PI * b_) * std::pow(a_, 1.5) * std::exp(a_ * b_ / 4.0));
220,000✔
361
  return c * std::exp(-x / a_) * std::sinh(std::sqrt(b_ * x));
220,000✔
362
}
363

364
//==============================================================================
365
// Normal implementation
366
//==============================================================================
367

368
Normal::Normal(double mean_value, double std_dev, double lower, double upper)
66✔
369
  : mean_value_ {mean_value}, std_dev_ {std_dev}, lower_ {lower}, upper_ {upper}
66✔
370
{
371
  compute_normalization();
66✔
372
}
66✔
373

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

383
  mean_value_ = params.at(0);
44✔
384
  std_dev_ = params.at(1);
44✔
385

386
  // Optional truncation bounds
387
  if (params.size() == 4) {
44✔
388
    lower_ = params.at(2);
11✔
389
    upper_ = params.at(3);
11✔
390
  } else {
391
    lower_ = -INFTY;
33✔
392
    upper_ = INFTY;
33✔
393
  }
394

395
  compute_normalization();
44✔
396
  read_bias_from_xml(node);
44✔
397
}
44✔
398

399
void Normal::compute_normalization()
110✔
400
{
401
  // Validate bounds
402
  if (lower_ >= upper_) {
110!
403
    openmc::fatal_error(
×
404
      "Normal distribution lower bound must be less than upper bound.");
405
  }
406

407
  // Check if truncation bounds are finite
408
  is_truncated_ = (lower_ > -INFTY || upper_ < INFTY);
110✔
409

410
  if (is_truncated_) {
110✔
411
    double alpha = (lower_ - mean_value_) / std_dev_;
55✔
412
    double beta = (upper_ - mean_value_) / std_dev_;
55✔
413
    double cdf_diff = standard_normal_cdf(beta) - standard_normal_cdf(alpha);
55✔
414

415
    if (cdf_diff <= 0.0) {
55!
416
      openmc::fatal_error(
×
417
        "Normal distribution truncation bounds exclude entire distribution.");
418
    }
419
    norm_factor_ = 1.0 / cdf_diff;
55✔
420
  } else {
421
    norm_factor_ = 1.0;
55✔
422
  }
423
}
110✔
424

425
double Normal::sample_unbiased(uint64_t* seed) const
330,000✔
426
{
427
  if (!is_truncated_) {
330,000✔
428
    return normal_variate(mean_value_, std_dev_, seed);
220,000✔
429
  }
430

431
  // Rejection sampling for truncated normal
432
  double x;
161,249✔
433
  do {
161,249✔
434
    x = normal_variate(mean_value_, std_dev_, seed);
161,249✔
435
  } while (x < lower_ || x > upper_);
161,249✔
436
  return x;
437
}
438

439
double Normal::evaluate(double x) const
220,121✔
440
{
441
  // Return 0 outside truncation bounds
442
  if (x < lower_ || x > upper_) {
220,121✔
443
    return 0.0;
444
  }
445

446
  // Standard normal PDF value
447
  double pdf = (1.0 / (std::sqrt(2.0 * PI) * std_dev_)) *
220,077✔
448
               std::exp(-std::pow((x - mean_value_), 2.0) /
220,077✔
449
                        (2.0 * std::pow(std_dev_, 2.0)));
220,077✔
450

451
  // Apply normalization for truncation
452
  return pdf * norm_factor_;
220,077✔
453
}
454

455
//==============================================================================
456
// Tabular implementation
457
//==============================================================================
458

459
Tabular::Tabular(pugi::xml_node node)
7,988✔
460
{
461
  if (check_for_node(node, "interpolation")) {
7,988!
462
    std::string temp = get_node_value(node, "interpolation");
7,988✔
463
    if (temp == "histogram") {
7,988✔
464
      interp_ = Interpolation::histogram;
7,906✔
465
    } else if (temp == "linear-linear") {
82!
466
      interp_ = Interpolation::lin_lin;
82✔
467
    } else if (temp == "log-linear") {
×
468
      interp_ = Interpolation::log_lin;
×
469
    } else if (temp == "log-log") {
×
470
      interp_ = Interpolation::log_log;
×
471
    } else {
472
      openmc::fatal_error(
×
473
        "Unsupported interpolation type for distribution: " + temp);
×
474
    }
475
  } else {
7,988✔
476
    interp_ = Interpolation::histogram;
×
477
  }
478

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

490
  read_bias_from_xml(node);
7,988✔
491
}
7,988✔
492

493
Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp,
50,858,915✔
494
  const double* c)
50,858,915✔
495
  : interp_ {interp}
50,858,915✔
496
{
497
  init(x, p, n, c);
50,858,915✔
498
}
50,858,915✔
499

500
void Tabular::init(
50,866,903✔
501
  const double* x, const double* p, std::size_t n, const double* c)
502
{
503
  // Copy x/p arrays into vectors
504
  std::copy(x, x + n, std::back_inserter(x_));
50,866,903✔
505
  std::copy(p, p + n, std::back_inserter(p_));
50,866,903✔
506

507
  // Calculate cumulative distribution function
508
  if (c) {
50,866,903✔
509
    std::copy(c, c + n, std::back_inserter(c_));
50,858,915✔
510
  } else {
511
    c_.resize(n);
7,988✔
512
    c_[0] = 0.0;
7,988✔
513
    for (int i = 1; i < n; ++i) {
98,560✔
514
      if (interp_ == Interpolation::histogram) {
90,572✔
515
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]);
90,386✔
516
      } else if (interp_ == Interpolation::lin_lin) {
186!
517
        c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]);
186✔
518
      } else if (interp_ == Interpolation::log_lin) {
×
519
        double m = std::log(p_[i] / p_[i - 1]) / (x_[i] - x_[i - 1]);
×
520
        c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) *
×
521
                              exprel(m * (x_[i] - x_[i - 1]));
×
522
      } else if (interp_ == Interpolation::log_log) {
×
523
        double m = std::log((x_[i] * p_[i]) / (x_[i - 1] * p_[i - 1])) /
×
524
                   std::log(x_[i] / x_[i - 1]);
×
525
        c_[i] = c_[i - 1] + x_[i - 1] * p_[i - 1] *
×
526
                              std::log(x_[i] / x_[i - 1]) *
×
527
                              exprel(m * std::log(x_[i] / x_[i - 1]));
×
528
      } else {
529
        UNREACHABLE();
×
530
      }
531
    }
532
  }
533

534
  // Normalize density and distribution functions. Note that we save the
535
  // integral of the distribution so that if it is used as part of another
536
  // distribution (e.g., Mixture), we know its relative strength.
537
  integral_ = c_[n - 1];
50,866,903✔
538
  for (int i = 0; i < n; ++i) {
730,026,048✔
539
    p_[i] = p_[i] / integral_;
679,159,145✔
540
    c_[i] = c_[i] / integral_;
679,159,145✔
541
  }
542
}
50,866,903✔
543

544
double Tabular::sample_unbiased(uint64_t* seed) const
1,033,697,845✔
545
{
546
  // Sample value of CDF
547
  double c = prn(seed);
1,033,697,845✔
548

549
  // Find first CDF bin which is above the sampled value
550
  double c_i = c_[0];
1,033,697,845✔
551
  int i;
1,033,697,845✔
552
  std::size_t n = c_.size();
1,033,697,845✔
553
  for (i = 0; i < n - 1; ++i) {
2,147,483,647!
554
    if (c <= c_[i + 1])
2,147,483,647✔
555
      break;
556
    c_i = c_[i + 1];
557
  }
558

559
  // Determine bounding PDF values
560
  double x_i = x_[i];
1,033,697,845✔
561
  double p_i = p_[i];
1,033,697,845✔
562

563
  if (interp_ == Interpolation::histogram) {
1,033,697,845✔
564
    // Histogram interpolation
565
    if (p_i > 0.0) {
3,410,891!
566
      return x_i + (c - c_i) / p_i;
3,410,891✔
567
    } else {
568
      return x_i;
569
    }
570
  } else if (interp_ == Interpolation::lin_lin) {
1,030,286,954!
571
    // Linear-linear interpolation
572
    double x_i1 = x_[i + 1];
1,030,286,954✔
573
    double p_i1 = p_[i + 1];
1,030,286,954✔
574

575
    double m = (p_i1 - p_i) / (x_i1 - x_i);
1,030,286,954✔
576
    if (m == 0.0) {
1,030,286,954✔
577
      return x_i + (c - c_i) / p_i;
410,353,858✔
578
    } else {
579
      return x_i +
619,933,096✔
580
             (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) /
1,239,866,192!
581
               m;
619,933,096✔
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)
38,501✔
737
{
738
  if (!check_for_node(node, "type"))
38,501!
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);
38,501✔
743

744
  // Allocate extension of Distribution
745
  UPtrDist dist;
38,501✔
746
  if (type == "uniform") {
38,501✔
747
    dist = UPtrDist {new Uniform(node)};
362✔
748
  } else if (type == "powerlaw") {
38,139✔
749
    dist = UPtrDist {new PowerLaw(node)};
74✔
750
  } else if (type == "maxwell") {
38,065✔
751
    dist = UPtrDist {new Maxwell(node)};
93✔
752
  } else if (type == "watt") {
37,972✔
753
    dist = UPtrDist {new Watt(node)};
123✔
754
  } else if (type == "normal") {
37,849✔
755
    dist = UPtrDist {new Normal(node)};
33✔
756
  } else if (type == "discrete") {
37,816✔
757
    dist = UPtrDist {new Discrete(node)};
29,706✔
758
  } else if (type == "tabular") {
8,110✔
759
    dist = UPtrDist {new Tabular(node)};
7,988✔
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;
38,501✔
772
}
38,501✔
773

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

778
// Helper function to compute weighted probabilities for decay spectrum sampling
779
vector<double> decay_spectrum_probabilities(
55✔
780
  const vector<int>& nuclide_indices, const vector<double>& atoms)
781
{
782
  if (nuclide_indices.size() != atoms.size()) {
55!
NEW
783
    fatal_error("DecaySpectrum nuclide index and atoms arrays must have "
×
784
                "the same length.");
785
  }
786

787
  vector<double> probs;
55✔
788
  probs.reserve(nuclide_indices.size());
55✔
789
  for (size_t i = 0; i < nuclide_indices.size(); ++i) {
561✔
790
    // Distribution integral is in [photons/s/atom]; multiplying by atoms gives
791
    // the total emission rate [photons/s] for this nuclide.
792
    const auto* dist =
506✔
793
      data::chain_nuclides[nuclide_indices[i]]->photon_energy();
506✔
794
    probs.push_back(atoms[i] * dist->integral());
506✔
795
  }
796
  return probs;
55✔
NEW
797
}
×
798

NEW
799
DecaySpectrum::DecaySpectrum(vector<int> nuclide_indices, vector<double> atoms)
×
NEW
800
  : nuclide_indices_(std::move(nuclide_indices))
×
801
{
NEW
802
  vector<double> probs = decay_spectrum_probabilities(nuclide_indices_, atoms);
×
NEW
803
  integral_ = std::accumulate(probs.begin(), probs.end(), 0.0);
×
NEW
804
  if (nuclide_indices_.empty() || integral_ <= 0.0) {
×
NEW
805
    fatal_error("DecaySpectrum must contain at least one decay photon emitter "
×
806
                "with a positive emission rate.");
807
  }
NEW
808
  di_.assign(probs);
×
NEW
809
}
×
810

811
DecaySpectrum::DecaySpectrum(pugi::xml_node node)
55✔
812
{
813
  // Read the region volume [cm^3] needed for absolute emission rate
814
  if (!check_for_node(node, "volume"))
55!
NEW
815
    fatal_error("DecaySpectrum: 'volume' attribute is required.");
×
816
  double volume = std::stod(get_node_value(node, "volume"));
110✔
817

818
  // Read nuclide names and atom densities from XML
819
  vector<int> nuclide_indices;
55✔
820
  vector<double> atoms;
55✔
821

822
  for (auto nuclide_node : node.children("nuclide")) {
1,067✔
823
    std::string name = get_node_value(nuclide_node, "name");
506✔
824
    double density = std::stod(get_node_value(nuclide_node, "density"));
1,012✔
825

826
    // Look up nuclide in the depletion chain
827
    auto it = data::chain_nuclide_map.find(name);
506✔
828
    if (it == data::chain_nuclide_map.end())
506!
NEW
829
      continue;
×
830

831
    int nuclide_index = it->second;
506!
832
    const auto& chain_nuc = data::chain_nuclides[nuclide_index];
506!
833
    const Distribution* photon_dist = chain_nuc->photon_energy();
506!
834
    if (!photon_dist)
506!
NEW
835
      continue;
×
836

837
    // atoms = density [atom/b-cm] * 1e24 [b/cm^2] * volume [cm^3]
838
    double atoms_i = density * 1.0e24 * volume;
506✔
839
    if (atoms_i <= 0.0)
506!
NEW
840
      continue;
×
841

842
    nuclide_indices.push_back(nuclide_index);
506✔
843
    atoms.push_back(atoms_i);
506✔
844
  }
506✔
845

846
  nuclide_indices_ = std::move(nuclide_indices);
55✔
847

848
  // Build alias table from weighted probabilities
849
  vector<double> probs = decay_spectrum_probabilities(nuclide_indices_, atoms);
55✔
850
  integral_ = std::accumulate(probs.begin(), probs.end(), 0.0);
55✔
851
  if (nuclide_indices_.empty() || integral_ <= 0.0) {
55!
NEW
852
    fatal_error("DecaySpectrum source did not resolve any nuclides with decay "
×
853
                "photon spectra and positive atom densities. Ensure "
854
                "OPENMC_CHAIN_FILE is set and matches the nuclides in the "
855
                "source definition.");
856
  }
857
  di_.assign(probs);
55✔
858
}
55✔
859

860
DecaySpectrum::Sample DecaySpectrum::sample_with_parent(uint64_t* seed) const
385,000✔
861
{
862
  size_t idx = di_.sample(seed);
385,000✔
863
  int parent_nuclide = nuclide_indices_[idx];
385,000✔
864
  const auto* dist = data::chain_nuclides[parent_nuclide]->photon_energy();
385,000✔
865
  auto [energy, weight] = dist->sample(seed);
385,000✔
866
  return {energy, weight, parent_nuclide};
385,000✔
867
}
868

NEW
869
std::pair<double, double> DecaySpectrum::sample(uint64_t* seed) const
×
870
{
NEW
871
  auto sample = sample_with_parent(seed);
×
NEW
872
  return {sample.energy, sample.weight};
×
873
}
874

875
double DecaySpectrum::integral() const
55✔
876
{
877
  return integral_;
55✔
878
}
879

NEW
880
double DecaySpectrum::sample_unbiased(uint64_t* seed) const
×
881
{
NEW
882
  return sample_with_parent(seed).energy;
×
883
}
884

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