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

openmc-dev / openmc / 22500302709

27 Feb 2026 07:16PM UTC coverage: 81.512% (-0.3%) from 81.826%
22500302709

Pull #3830

github

web-flow
Merge 25fbb4266 into b3788f11e
Pull Request #3830: Parallelize sampling external sources and threadsafe rejection counters

17488 of 25193 branches covered (69.42%)

Branch coverage included in aggregate %.

59 of 66 new or added lines in 6 files covered. (89.39%)

841 existing lines in 44 files now uncovered.

57726 of 67081 relevant lines covered (86.05%)

44920080.48 hits per line

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

88.2
/src/scattdata.cpp
1
#include "openmc/scattdata.h"
2

3
#include <algorithm>
4
#include <cmath>
5
#include <numeric>
6

7
#include "openmc/tensor.h"
8

9
#include "openmc/constants.h"
10
#include "openmc/error.h"
11
#include "openmc/math_functions.h"
12
#include "openmc/random_lcg.h"
13
#include "openmc/settings.h"
14

15
namespace openmc {
16

17
//==============================================================================
18
// ScattData base-class methods
19
//==============================================================================
20

21
void ScattData::base_init(int order, const tensor::Tensor<int>& in_gmin,
9,468✔
22
  const tensor::Tensor<int>& in_gmax, const double_2dvec& in_energy,
23
  const double_2dvec& in_mult)
24
{
25
  size_t groups = in_energy.size();
9,468✔
26

27
  gmin = in_gmin;
9,468✔
28
  gmax = in_gmax;
9,468✔
29
  energy.resize(groups);
9,468✔
30
  mult.resize(groups);
9,468✔
31
  dist.resize(groups);
9,468✔
32

33
  for (int gin = 0; gin < groups; gin++) {
42,960✔
34
    // Store the inputted data
35
    energy[gin] = in_energy[gin];
33,492✔
36
    mult[gin] = in_mult[gin];
33,492✔
37

38
    // Make sure the multiplicity does not have 0s
39
    for (int go = 0; go < mult[gin].size(); go++) {
180,516✔
40
      if (mult[gin][go] == 0.) {
147,024✔
41
        mult[gin][go] = 1.;
12,555✔
42
      }
43
    }
44

45
    // Make sure the energy is normalized
46
    double norm = std::accumulate(energy[gin].begin(), energy[gin].end(), 0.);
33,492✔
47

48
    if (norm != 0.) {
33,492✔
49
      for (auto& n : energy[gin])
179,976✔
50
        n /= norm;
146,754✔
51
    }
52

53
    // Initialize the distribution data
54
    dist[gin].resize(in_gmax[gin] - in_gmin[gin] + 1);
33,492✔
55
    for (auto& v : dist[gin]) {
180,516✔
56
      v.resize(order);
147,024✔
57
    }
58
  }
59
}
9,468✔
60

61
//==============================================================================
62

63
void ScattData::base_combine(size_t max_order, size_t order_dim,
2,931✔
64
  const vector<ScattData*>& those_scatts, const vector<double>& scalars,
65
  tensor::Tensor<int>& in_gmin, tensor::Tensor<int>& in_gmax,
66
  double_2dvec& sparse_mult, double_3dvec& sparse_scatter)
67
{
68
  size_t groups = those_scatts[0]->energy.size();
2,931✔
69

70
  // Now allocate and zero our storage spaces
71
  tensor::Tensor<double> this_nuscatt_matrix =
2,931✔
72
    tensor::zeros<double>({groups, groups, order_dim});
2,931✔
73
  tensor::Tensor<double> this_nuscatt_P0 =
2,931✔
74
    tensor::zeros<double>({groups, groups});
2,931✔
75
  tensor::Tensor<double> this_scatt_P0 =
2,931✔
76
    tensor::zeros<double>({groups, groups});
2,931✔
77
  tensor::Tensor<double> this_mult = tensor::ones<double>({groups, groups});
2,931✔
78

79
  // Build the dense scattering and multiplicity matrices
80
  for (int i = 0; i < those_scatts.size(); i++) {
6,402✔
81
    ScattData* that = those_scatts[i];
3,471✔
82

83
    // Build the dense matrix for that object
84
    tensor::Tensor<double> that_matrix = that->get_matrix(max_order);
3,471✔
85

86
    // Now add that to this for the nu-scatter matrix
87
    this_nuscatt_matrix += scalars[i] * that_matrix;
3,471✔
88

89
    // Do the same with the P0 matrices
90
    for (int gin = 0; gin < groups; gin++) {
15,250✔
91
      for (int go = 0; go < groups; go++) {
270,334✔
92
        this_nuscatt_P0(gin, go) +=
517,110✔
93
          scalars[i] * that->get_xs(MgxsType::NU_SCATTER, gin, &go, nullptr);
258,555✔
94
        this_scatt_P0(gin, go) +=
517,110✔
95
          scalars[i] * that->get_xs(MgxsType::SCATTER, gin, &go, nullptr);
258,555✔
96
      }
97
    }
98
  }
3,471✔
99

100
  // Now we have the dense nuscatt and scatt, we can easily compute the
101
  // multiplicity matrix by dividing the two and fixing any nans
102
  this_mult = tensor::nan_to_num(this_nuscatt_P0 / this_scatt_P0);
5,862✔
103

104
  // We have the data, now we need to convert to a jagged array and then use
105
  // the initialize function to store it on the object.
106
  for (int gin = 0; gin < groups; gin++) {
13,675✔
107
    // Find the minimum and maximum group boundaries
108
    int gmin_;
109
    for (gmin_ = 0; gmin_ < groups; gmin_++) {
129,565✔
110
      bool non_zero = false;
376,237✔
111
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
752,474!
112
        if (this_nuscatt_matrix(gin, gmin_, l) != 0.) {
257,416✔
113
          non_zero = true;
114
          break;
115
        }
116
      }
117
      if (non_zero)
129,475✔
118
        break;
119
    }
120
    int gmax_;
10,744✔
121
    for (gmax_ = groups - 1; gmax_ >= 0; gmax_--) {
104,415✔
122
      bool non_zero = false;
294,502✔
123
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
589,004!
124
        if (this_nuscatt_matrix(gin, gmax_, l) != 0.) {
200,831✔
125
          non_zero = true;
126
          break;
127
        }
128
      }
129
      if (non_zero)
104,325✔
130
        break;
131
    }
132

133
    // treat the case of all values being 0
134
    if (gmin_ > gmax_) {
10,744✔
135
      gmin_ = gin;
90✔
136
      gmax_ = gin;
90✔
137
    }
138

139
    // Store the group bounds
140
    in_gmin[gin] = gmin_;
10,744✔
141
    in_gmax[gin] = gmax_;
10,744✔
142

143
    // Store the data in the compressed format
144
    sparse_scatter[gin].resize(gmax_ - gmin_ + 1);
10,744✔
145
    sparse_mult[gin].resize(gmax_ - gmin_ + 1);
10,744✔
146
    int i_gout = 0;
147
    for (int gout = gmin_; gout <= gmax_; gout++) {
59,117✔
148
      sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape(2));
96,746!
149
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
219,232!
150
        sparse_scatter[gin][i_gout][l] = this_nuscatt_matrix(gin, gout, l);
122,486✔
151
      }
152
      sparse_mult[gin][i_gout] = this_mult(gin, gout);
48,373✔
153
      i_gout++;
48,373✔
154
    }
155
  }
156
}
11,724✔
157

158
//==============================================================================
159

160
void ScattData::sample_energy(int gin, int& gout, int& i_gout, uint64_t* seed)
1,669,415,704✔
161
{
162
  // Sample the outgoing group
163
  double xi = prn(seed);
1,669,415,704✔
164
  double prob = 0.;
1,669,415,704✔
165
  i_gout = 0;
1,669,415,704✔
166
  for (gout = gmin[gin]; gout < gmax[gin]; ++gout) {
2,035,752,224✔
167
    prob += energy[gin][i_gout];
1,606,134,706✔
168
    if (xi < prob)
1,606,134,706✔
169
      break;
170
    ++i_gout;
366,336,520✔
171
  }
172
}
1,669,415,704✔
173

174
//==============================================================================
175

176
double ScattData::get_xs(
19,221,741✔
177
  MgxsType xstype, int gin, const int* gout, const double* mu)
178
{
179
  // Set the outgoing group offset index as needed
180
  int i_gout = 0;
19,221,741✔
181
  if (gout != nullptr) {
19,221,741!
182
    // short circuit the function if gout is from a zero portion of the
183
    // scattering matrix
184
    if ((*gout < gmin[gin]) || (*gout > gmax[gin])) { // > gmax?
19,221,741✔
185
      return 0.;
186
    }
187
    i_gout = *gout - gmin[gin];
18,598,275✔
188
  }
189

190
  double val = scattxs[gin];
18,598,275!
191
  switch (xstype) {
18,598,275!
192
  case MgxsType::NU_SCATTER:
94,918✔
193
    if (gout != nullptr)
94,918!
194
      val *= energy[gin][i_gout];
94,918✔
195
    break;
196
  case MgxsType::SCATTER:
49,933✔
197
    if (gout != nullptr) {
49,933!
198
      val *= energy[gin][i_gout] / mult[gin][i_gout];
49,933✔
199
    } else {
200
      val /= std::inner_product(
×
201
        mult[gin].begin(), mult[gin].end(), energy[gin].begin(), 0.0);
×
202
    }
203
    break;
204
  case MgxsType::NU_SCATTER_FMU:
9,226,712✔
205
    if ((gout != nullptr) && (mu != nullptr)) {
9,226,712!
206
      val *= energy[gin][i_gout] * calc_f(gin, *gout, *mu);
9,226,712✔
207
    } else {
208
      // This is not an expected path (asking for f_mu without asking for a
209
      // group or mu is not useful
210
      fatal_error("Invalid call to get_xs");
×
211
    }
212
    break;
9,226,712✔
213
  case MgxsType::SCATTER_FMU:
9,226,712✔
214
    if ((gout != nullptr) && (mu != nullptr)) {
9,226,712!
215
      val *= energy[gin][i_gout] * calc_f(gin, *gout, *mu) / mult[gin][i_gout];
9,226,712✔
216
    } else {
217
      // This is not an expected path (asking for f_mu without asking for a
218
      // group or mu is not useful
219
      fatal_error("Invalid call to get_xs");
×
220
    }
221
    break;
9,226,712✔
222
  default:
223
    break;
224
  }
225
  return val;
226
}
227

228
//==============================================================================
229
// ScattDataLegendre methods
230
//==============================================================================
231

232
void ScattDataLegendre::init(const tensor::Tensor<int>& in_gmin,
3,591✔
233
  const tensor::Tensor<int>& in_gmax, const double_2dvec& in_mult,
234
  const double_3dvec& coeffs)
235
{
236
  size_t groups = coeffs.size();
3,591✔
237
  size_t order = coeffs[0][0].size();
3,591✔
238

239
  // make a copy of coeffs that we can use to both extract data and normalize
240
  double_3dvec matrix = coeffs;
3,591✔
241

242
  // Get the scattering cross section value by summing the un-normalized P0
243
  // coefficient in the variable matrix over all outgoing groups.
244
  scattxs = tensor::zeros<double>({groups});
3,591✔
245
  for (int gin = 0; gin < groups; gin++) {
15,610✔
246
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
12,019✔
247
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
62,312✔
248
      scattxs[gin] += matrix[gin][i_gout][0];
50,293✔
249
    }
250
  }
251

252
  // Build the energy transfer matrix from data in the variable matrix while
253
  // also normalizing the variable matrix itself
254
  // (forcing the CDF of f(mu=1) == 1)
255
  double_2dvec in_energy;
3,591✔
256
  in_energy.resize(groups);
3,591✔
257
  for (int gin = 0; gin < groups; gin++) {
15,610✔
258
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
12,019✔
259
    in_energy[gin].resize(num_groups);
12,019✔
260
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
62,312✔
261
      double norm = matrix[gin][i_gout][0];
50,293✔
262
      in_energy[gin][i_gout] = norm;
50,293✔
263
      if (norm != 0.) {
50,293✔
264
        for (auto& n : matrix[gin][i_gout])
90,896✔
265
          n /= norm;
46,843✔
266
      }
267
    }
268
  }
269

270
  // Initialize the base class attributes
271
  ScattData::base_init(order, in_gmin, in_gmax, in_energy, in_mult);
3,591✔
272

273
  // Set the distribution (sdata.dist) values and initialize max_val
274
  max_val.resize(groups);
3,591✔
275
  for (int gin = 0; gin < groups; gin++) {
15,610✔
276
    int num_groups = gmax[gin] - gmin[gin] + 1;
12,019✔
277
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
62,312✔
278
      dist[gin][i_gout] = matrix[gin][i_gout];
50,293✔
279
    }
280
    max_val[gin].resize(num_groups);
12,019✔
281
    for (auto& n : max_val[gin])
62,312✔
282
      n = 0.;
50,293✔
283
  }
284

285
  // Now update the maximum value
286
  update_max_val();
3,591✔
287
}
3,591✔
288

289
//==============================================================================
290

291
void ScattDataLegendre::update_max_val()
3,591✔
292
{
293
  size_t groups = max_val.size();
3,591✔
294
  // Step through the polynomial with fixed number of points to identify the
295
  // maximal value
296
  int Nmu = 1001;
3,591✔
297
  double dmu = 2. / (Nmu - 1);
3,591✔
298
  for (int gin = 0; gin < groups; gin++) {
15,610✔
299
    int num_groups = gmax[gin] - gmin[gin] + 1;
12,019✔
300
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
62,312✔
301
      for (int imu = 0; imu < Nmu; imu++) {
50,393,586✔
302
        double mu;
50,343,293✔
303
        if (imu == 0) {
50,343,293✔
304
          mu = -1.;
305
        } else if (imu == (Nmu - 1)) {
50,293,000✔
306
          mu = 1.;
307
        } else {
308
          mu = -1. + (imu - 1) * dmu;
50,242,707✔
309
        }
310

311
        // Calculate probability
312
        double f = evaluate_legendre(
100,686,586✔
313
          dist[gin][i_gout].size() - 1, dist[gin][i_gout].data(), mu);
50,343,293✔
314

315
        // if this is a new maximum, store it
316
        if (f > max_val[gin][i_gout])
50,343,293✔
317
          max_val[gin][i_gout] = f;
1,312,543✔
318
      } // end imu loop
319

320
      // Since we may not have caught the true max, add 10% margin
321
      max_val[gin][i_gout] *= 1.1;
50,293✔
322
    }
323
  }
324
}
3,591✔
325

326
//==============================================================================
327

328
double ScattDataLegendre::calc_f(int gin, int gout, double mu)
46,160,257✔
329
{
330
  double f;
46,160,257✔
331
  if ((gout < gmin[gin]) || (gout > gmax[gin])) {
46,160,257!
332
    f = 0.;
333
  } else {
334
    int i_gout = gout - gmin[gin];
46,160,257✔
335
    f = evaluate_legendre(
46,160,257✔
336
      dist[gin][i_gout].size() - 1, dist[gin][i_gout].data(), mu);
46,160,257✔
337
  }
338
  return f;
46,160,257✔
339
}
340

341
//==============================================================================
342

343
void ScattDataLegendre::sample(
15,644,299✔
344
  int gin, int& gout, double& mu, double& wgt, uint64_t* seed)
345
{
346
  // Sample the outgoing energy using the base-class method
347
  int i_gout;
15,644,299✔
348
  sample_energy(gin, gout, i_gout, seed);
15,644,299✔
349

350
  // Now we can sample mu using the scattering kernel using rejection
351
  // sampling from a rectangular bounding box
352
  double M = max_val[gin][i_gout];
15,644,299✔
353
  int samples;
15,644,299✔
354
  for (samples = 0; samples < MAX_SAMPLE; ++samples) {
27,706,833!
355
    mu = 2. * prn(seed) - 1.;
27,706,833✔
356
    double f = calc_f(gin, gout, mu);
27,706,833✔
357
    if (f > 0.) {
27,706,833!
358
      double u = prn(seed) * M;
27,706,833✔
359
      if (u <= f)
27,706,833✔
360
        break;
361
    }
362
  }
363
  if (samples == MAX_SAMPLE) {
15,644,299!
364
    fatal_error("Maximum number of Legendre expansion samples reached!");
×
365
  }
366

367
  // Update the weight to reflect neutron multiplicity
368
  wgt *= mult[gin][i_gout];
15,644,299✔
369
}
15,644,299✔
370

371
//==============================================================================
372

373
void ScattDataLegendre::combine(
255✔
374
  const vector<ScattData*>& those_scatts, const vector<double>& scalars)
375
{
376
  // Find the max order in the data set and make sure we can combine the sets
377
  size_t max_order = 0;
255✔
378
  for (int i = 0; i < those_scatts.size(); i++) {
525✔
379
    // Lets also make sure these items are combineable
380
    ScattDataLegendre* that = dynamic_cast<ScattDataLegendre*>(those_scatts[i]);
270!
381
    if (!that) {
270!
382
      fatal_error("Cannot combine the ScattData objects!");
×
383
    }
384
    size_t that_order = that->get_order();
270✔
385
    if (that_order > max_order)
270✔
386
      max_order = that_order;
387
  }
388

389
  size_t groups = those_scatts[0]->energy.size();
255✔
390

391
  tensor::Tensor<int> in_gmin({groups}, 0);
255✔
392
  tensor::Tensor<int> in_gmax({groups}, 0);
255✔
393
  double_3dvec sparse_scatter(groups);
255✔
394
  double_2dvec sparse_mult(groups);
255✔
395

396
  // The rest of the steps do not depend on the type of angular representation
397
  // so we use a base class method to sum up xs and create new energy and mult
398
  // matrices
399
  size_t order_dim = max_order + 1;
255✔
400
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
255✔
401
    in_gmax, sparse_mult, sparse_scatter);
402

403
  // Got everything we need, store it.
404
  init(in_gmin, in_gmax, sparse_mult, sparse_scatter);
255✔
405
}
765✔
406

407
//==============================================================================
408

409
tensor::Tensor<double> ScattDataLegendre::get_matrix(size_t max_order)
270✔
410
{
411
  // Get the sizes and initialize the data to 0
412
  size_t groups = energy.size();
270✔
413
  size_t order_dim = max_order + 1;
270✔
414
  tensor::Tensor<double> matrix =
270✔
415
    tensor::zeros<double>({groups, groups, order_dim});
270✔
416

417
  for (int gin = 0; gin < groups; gin++) {
810✔
418
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
1,350✔
419
      int gout = i_gout + gmin[gin];
810✔
420
      for (int l = 0; l < order_dim; l++) {
2,430✔
421
        matrix(gin, gout, l) =
1,620✔
422
          scattxs[gin] * energy[gin][i_gout] * dist[gin][i_gout][l];
1,620✔
423
      }
424
    }
425
  }
426
  return matrix;
270✔
427
}
428

429
//==============================================================================
430
// ScattDataHistogram methods
431
//==============================================================================
432

433
void ScattDataHistogram::init(const tensor::Tensor<int>& in_gmin,
120✔
434
  const tensor::Tensor<int>& in_gmax, const double_2dvec& in_mult,
435
  const double_3dvec& coeffs)
436
{
437
  size_t groups = coeffs.size();
120✔
438
  size_t order = coeffs[0][0].size();
120✔
439

440
  // make a copy of coeffs that we can use to both extract data and normalize
441
  double_3dvec matrix = coeffs;
120✔
442

443
  // Get the scattering cross section value by summing the distribution
444
  // over all the histogram bins in angle and outgoing energy groups
445
  scattxs = tensor::zeros<double>({groups});
120✔
446
  for (int gin = 0; gin < groups; gin++) {
360✔
447
    for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) {
600✔
448
      scattxs[gin] += std::accumulate(
360✔
449
        matrix[gin][i_gout].begin(), matrix[gin][i_gout].end(), 0.);
360✔
450
    }
451
  }
452

453
  // Build the energy transfer matrix from data in the variable matrix
454
  double_2dvec in_energy;
120✔
455
  in_energy.resize(groups);
120✔
456
  for (int gin = 0; gin < groups; gin++) {
360✔
457
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
240✔
458
    in_energy[gin].resize(num_groups);
240✔
459
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
600✔
460
      double norm = std::accumulate(
720✔
461
        matrix[gin][i_gout].begin(), matrix[gin][i_gout].end(), 0.);
360✔
462
      in_energy[gin][i_gout] = norm;
360!
463
      if (norm != 0.) {
360!
464
        for (auto& n : matrix[gin][i_gout])
10,530✔
465
          n /= norm;
10,170✔
466
      }
467
    }
468
  }
469

470
  // Initialize the base class attributes
471
  ScattData::base_init(order, in_gmin, in_gmax, in_energy, in_mult);
120✔
472

473
  // Build the angular distribution mu values
474
  mu = tensor::linspace(-1., 1., order + 1);
120✔
475
  dmu = 2. / order;
120✔
476

477
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
478
  fmu.resize(groups);
120✔
479
  for (int gin = 0; gin < groups; gin++) {
360✔
480
    int num_groups = gmax[gin] - gmin[gin] + 1;
240✔
481
    fmu[gin].resize(num_groups);
240✔
482
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
600✔
483
      fmu[gin][i_gout].resize(order);
360✔
484
      // The variable matrix contains f(mu); so directly assign it
485
      fmu[gin][i_gout] = matrix[gin][i_gout];
360✔
486

487
      // Integrate the histogram
488
      dist[gin][i_gout][0] = dmu * matrix[gin][i_gout][0];
360✔
489
      for (int imu = 1; imu < order; imu++) {
10,170✔
490
        dist[gin][i_gout][imu] =
9,810✔
491
          dmu * matrix[gin][i_gout][imu] + dist[gin][i_gout][imu - 1];
9,810✔
492
      }
493

494
      // Now re-normalize for integral to unity
495
      double norm = dist[gin][i_gout][order - 1];
360!
496
      if (norm > 0.) {
360!
497
        for (int imu = 0; imu < order; imu++) {
10,530✔
498
          fmu[gin][i_gout][imu] /= norm;
10,170✔
499
          dist[gin][i_gout][imu] /= norm;
10,170✔
500
        }
501
      }
502
    }
503
  }
504
}
120✔
505

506
//==============================================================================
507

508
double ScattDataHistogram::calc_f(int gin, int gout, double mu)
×
509
{
UNCOV
510
  double f;
×
511
  if ((gout < gmin[gin]) || (gout > gmax[gin])) {
×
512
    f = 0.;
513
  } else {
514
    // Find mu bin
515
    int i_gout = gout - gmin[gin];
×
UNCOV
516
    int imu;
×
517
    if (mu == 1.) {
×
518
      // use size -2 to have the index one before the end
519
      imu = this->mu.shape(0) - 2;
×
520
    } else {
521
      imu = std::floor((mu + 1.) / dmu + 1.) - 1;
×
522
    }
523

524
    f = fmu[gin][i_gout][imu];
×
525
  }
526
  return f;
×
527
}
528

529
//==============================================================================
530

531
void ScattDataHistogram::sample(
31,768✔
532
  int gin, int& gout, double& mu, double& wgt, uint64_t* seed)
533
{
534
  // Sample the outgoing energy using the base-class method
535
  int i_gout;
31,768✔
536
  sample_energy(gin, gout, i_gout, seed);
31,768✔
537

538
  // Determine the outgoing cosine bin
539
  double xi = prn(seed);
31,768✔
540

541
  int imu;
31,768✔
542
  if (xi < dist[gin][i_gout][0]) {
31,768✔
543
    imu = 0;
544
  } else {
545
    imu =
30,239✔
546
      std::upper_bound(dist[gin][i_gout].begin(), dist[gin][i_gout].end(), xi) -
30,239✔
547
      dist[gin][i_gout].begin();
30,239✔
548
  }
549

550
  // Randomly select mu within the imu bin
551
  mu = prn(seed) * dmu + this->mu[imu];
31,768!
552

553
  if (mu < -1.) {
31,768!
554
    mu = -1.;
×
555
  } else if (mu > 1.) {
31,768!
556
    mu = 1.;
×
557
  }
558

559
  // Update the weight to reflect neutron multiplicity
560
  wgt *= mult[gin][i_gout];
31,768✔
561
}
31,768✔
562

563
//==============================================================================
564

565
tensor::Tensor<double> ScattDataHistogram::get_matrix(size_t max_order)
60✔
566
{
567
  // Get the sizes and initialize the data to 0
568
  size_t groups = energy.size();
60✔
569
  // We ignore the requested order for Histogram and Tabular representations
570
  size_t order_dim = get_order();
60✔
571
  tensor::Tensor<double> matrix({groups, groups, order_dim}, 0);
60✔
572

573
  for (int gin = 0; gin < groups; gin++) {
180✔
574
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
300✔
575
      int gout = i_gout + gmin[gin];
180✔
576
      for (int l = 0; l < order_dim; l++) {
5,265✔
577
        matrix(gin, gout, l) =
5,085✔
578
          scattxs[gin] * energy[gin][i_gout] * fmu[gin][i_gout][l];
5,085✔
579
      }
580
    }
581
  }
582
  return matrix;
60✔
583
}
584

585
//==============================================================================
586

587
void ScattDataHistogram::combine(
60✔
588
  const vector<ScattData*>& those_scatts, const vector<double>& scalars)
589
{
590
  // Find the max order in the data set and make sure we can combine the sets
591
  size_t max_order = those_scatts[0]->get_order();
60✔
592
  for (int i = 0; i < those_scatts.size(); i++) {
120✔
593
    // Lets also make sure these items are combineable
594
    ScattDataHistogram* that =
60✔
595
      dynamic_cast<ScattDataHistogram*>(those_scatts[i]);
60!
596
    if (!that) {
60!
597
      fatal_error("Cannot combine the ScattData objects!");
×
598
    }
599
    if (max_order != that->get_order()) {
60!
600
      fatal_error("Cannot combine the ScattData objects!");
×
601
    }
602
  }
603

604
  size_t groups = those_scatts[0]->energy.size();
60✔
605

606
  tensor::Tensor<int> in_gmin({groups}, 0);
60✔
607
  tensor::Tensor<int> in_gmax({groups}, 0);
60✔
608
  double_3dvec sparse_scatter(groups);
60✔
609
  double_2dvec sparse_mult(groups);
60✔
610

611
  // The rest of the steps do not depend on the type of angular representation
612
  // so we use a base class method to sum up xs and create new energy and mult
613
  // matrices
614
  size_t order_dim = max_order;
60✔
615
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
60✔
616
    in_gmax, sparse_mult, sparse_scatter);
617

618
  // Got everything we need, store it.
619
  init(in_gmin, in_gmax, sparse_mult, sparse_scatter);
60✔
620
}
180✔
621

622
//==============================================================================
623
// ScattDataTabular methods
624
//==============================================================================
625

626
void ScattDataTabular::init(const tensor::Tensor<int>& in_gmin,
2,676✔
627
  const tensor::Tensor<int>& in_gmax, const double_2dvec& in_mult,
628
  const double_3dvec& coeffs)
629
{
630
  size_t groups = coeffs.size();
2,676✔
631
  size_t order = coeffs[0][0].size();
2,676✔
632

633
  // make a copy of coeffs that we can use to both extract data and normalize
634
  double_3dvec matrix = coeffs;
2,676✔
635

636
  // Build the angular distribution mu values
637
  mu = tensor::linspace(-1., 1., order);
2,676✔
638
  dmu = 2. / (order - 1);
2,676✔
639

640
  // Get the scattering cross section value by integrating the distribution
641
  // over all mu points and then combining over all outgoing groups
642
  scattxs = tensor::zeros<double>({groups});
2,676✔
643
  for (int gin = 0; gin < groups; gin++) {
12,910✔
644
    for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) {
57,842✔
645
      for (int imu = 1; imu < order; imu++) {
119,111✔
646
        scattxs[gin] +=
71,503✔
647
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
71,503✔
648
      }
649
    }
650
  }
651

652
  // Build the energy transfer matrix from data in the variable matrix
653
  double_2dvec in_energy(groups);
2,676✔
654
  for (int gin = 0; gin < groups; gin++) {
12,910✔
655
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
10,234✔
656
    in_energy[gin].resize(num_groups);
10,234✔
657
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
57,842✔
658
      double norm = 0.;
659
      for (int imu = 1; imu < order; imu++) {
119,111✔
660
        norm +=
71,503✔
661
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
71,503✔
662
      }
663
      in_energy[gin][i_gout] = norm;
47,608✔
664
    }
665
  }
666

667
  // Initialize the base class attributes
668
  ScattData::base_init(order, in_gmin, in_gmax, in_energy, in_mult);
2,676✔
669

670
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
671
  fmu.resize(groups);
2,676✔
672
  for (int gin = 0; gin < groups; gin++) {
12,910✔
673
    int num_groups = gmax[gin] - gmin[gin] + 1;
10,234✔
674
    fmu[gin].resize(num_groups);
10,234✔
675
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
57,842✔
676
      fmu[gin][i_gout].resize(order);
47,608✔
677
      // The variable matrix contains f(mu); so directly assign it
678
      fmu[gin][i_gout] = matrix[gin][i_gout];
47,608✔
679

680
      // Ensure positivity
681
      for (auto& val : fmu[gin][i_gout]) {
166,719✔
682
        if (val < 0.)
119,111✔
683
          val = 0.;
1,650✔
684
      }
685

686
      // Now re-normalize for numerical integration issues and to take care of
687
      // the above negative fix-up.  Also accrue the CDF
688
      double norm = 0.;
689
      for (int imu = 1; imu < order; imu++) {
119,111✔
690
        norm += 0.5 * dmu * (fmu[gin][i_gout][imu - 1] + fmu[gin][i_gout][imu]);
71,503✔
691
        // incorporate to the CDF
692
        dist[gin][i_gout][imu] = norm;
71,503✔
693
      }
694

695
      // now do the normalization
696
      if (norm > 0.) {
47,608✔
697
        for (int imu = 0; imu < order; imu++) {
145,059✔
698
          fmu[gin][i_gout][imu] /= norm;
104,516✔
699
          dist[gin][i_gout][imu] /= norm;
104,516✔
700
        }
701
      }
702
    }
703
  }
704
}
2,676✔
705

706
//==============================================================================
707

708
double ScattDataTabular::calc_f(int gin, int gout, double mu)
×
709
{
UNCOV
710
  double f;
×
711
  if ((gout < gmin[gin]) || (gout > gmax[gin])) {
×
712
    f = 0.;
713
  } else {
714
    // Find mu bin
715
    int i_gout = gout - gmin[gin];
×
UNCOV
716
    int imu;
×
717
    if (mu == 1.) {
×
718
      // use size -2 to have the index one before the end
719
      imu = this->mu.shape(0) - 2;
×
720
    } else {
721
      imu = std::floor((mu + 1.) / dmu + 1.) - 1;
×
722
    }
723

724
    double r = (mu - this->mu[imu]) / (this->mu[imu + 1] - this->mu[imu]);
×
725
    f = (1. - r) * fmu[gin][i_gout][imu] + r * fmu[gin][i_gout][imu + 1];
×
726
  }
727
  return f;
×
728
}
729

730
//==============================================================================
731

732
void ScattDataTabular::sample(
1,653,739,637✔
733
  int gin, int& gout, double& mu, double& wgt, uint64_t* seed)
734
{
735
  // Sample the outgoing energy using the base-class method
736
  int i_gout;
1,653,739,637✔
737
  sample_energy(gin, gout, i_gout, seed);
1,653,739,637✔
738

739
  // Determine the outgoing cosine bin
740
  int NP = this->mu.shape(0);
1,653,739,637!
741
  double xi = prn(seed);
1,653,739,637✔
742

743
  double c_k = dist[gin][i_gout][0];
1,653,739,637✔
744
  int k;
1,653,739,637✔
745
  for (k = 0; k < NP - 1; k++) {
1,667,467,747!
746
    double c_k1 = dist[gin][i_gout][k + 1];
1,667,467,747✔
747
    if (xi < c_k1)
1,667,467,747✔
748
      break;
749
    c_k = c_k1;
13,728,110✔
750
  }
751

752
  // Check to make sure k is <= NP - 1
753
  k = std::min(k, NP - 2);
1,653,739,637!
754

755
  // Find the pdf values we want
756
  double p0 = fmu[gin][i_gout][k];
1,653,739,637✔
757
  double mu0 = this->mu[k];
1,653,739,637✔
758
  double p1 = fmu[gin][i_gout][k + 1];
1,653,739,637✔
759
  double mu1 = this->mu[k + 1];
1,653,739,637✔
760

761
  if (p0 == p1) {
1,653,739,637✔
762
    mu = mu0 + (xi - c_k) / p0;
1,653,032,667✔
763
  } else {
764
    double frac = (p1 - p0) / (mu1 - mu0);
706,970✔
765
    mu =
1,413,940✔
766
      mu0 +
706,970✔
767
      (std::sqrt(std::max(0., p0 * p0 + 2. * frac * (xi - c_k))) - p0) / frac;
1,413,940!
768
  }
769

770
  if (mu < -1.) {
1,653,739,637!
771
    mu = -1.;
×
772
  } else if (mu > 1.) {
1,653,739,637!
773
    mu = 1.;
×
774
  }
775

776
  // Update the weight to reflect neutron multiplicity
777
  wgt *= mult[gin][i_gout];
1,653,739,637✔
778
}
1,653,739,637✔
779

780
//==============================================================================
781

782
tensor::Tensor<double> ScattDataTabular::get_matrix(size_t max_order)
3,141✔
783
{
784
  // Get the sizes and initialize the data to 0
785
  size_t groups = energy.size();
3,141✔
786
  // We ignore the requested order for Histogram and Tabular representations
787
  size_t order_dim = get_order();
3,141✔
788
  tensor::Tensor<double> matrix =
3,141✔
789
    tensor::zeros<double>({groups, groups, order_dim});
3,141✔
790

791
  for (int gin = 0; gin < groups; gin++) {
14,260✔
792
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
60,062✔
793
      int gout = i_gout + gmin[gin];
48,943✔
794
      for (int l = 0; l < order_dim; l++) {
167,844✔
795
        matrix(gin, gout, l) =
118,901✔
796
          scattxs[gin] * energy[gin][i_gout] * fmu[gin][i_gout][l];
118,901✔
797
      }
798
    }
799
  }
800
  return matrix;
3,141✔
801
}
802

803
//==============================================================================
804

805
void ScattDataTabular::combine(
2,616✔
806
  const vector<ScattData*>& those_scatts, const vector<double>& scalars)
807
{
808
  // Find the max order in the data set and make sure we can combine the sets
809
  size_t max_order = those_scatts[0]->get_order();
2,616✔
810
  for (int i = 0; i < those_scatts.size(); i++) {
5,757✔
811
    // Lets also make sure these items are combineable
812
    ScattDataTabular* that = dynamic_cast<ScattDataTabular*>(those_scatts[i]);
3,141!
813
    if (!that) {
3,141!
814
      fatal_error("Cannot combine the ScattData objects!");
×
815
    }
816
    if (max_order != that->get_order()) {
3,141!
817
      fatal_error("Cannot combine the ScattData objects!");
×
818
    }
819
  }
820

821
  size_t groups = those_scatts[0]->energy.size();
2,616✔
822

823
  tensor::Tensor<int> in_gmin({groups}, 0);
2,616✔
824
  tensor::Tensor<int> in_gmax({groups}, 0);
2,616✔
825
  double_3dvec sparse_scatter(groups);
2,616✔
826
  double_2dvec sparse_mult(groups);
2,616✔
827

828
  // The rest of the steps do not depend on the type of angular representation
829
  // so we use a base class method to sum up xs and create new energy and mult
830
  // matrices
831
  size_t order_dim = max_order;
2,616✔
832
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
2,616✔
833
    in_gmax, sparse_mult, sparse_scatter);
834

835
  // Got everything we need, store it.
836
  init(in_gmin, in_gmax, sparse_mult, sparse_scatter);
2,616✔
837
}
7,848✔
838

839
//==============================================================================
840
// module-level methods
841
//==============================================================================
842

843
void convert_legendre_to_tabular(ScattDataLegendre& leg, ScattDataTabular& tab)
3,081✔
844
{
845
  // See if the user wants us to figure out how many points to use
846
  int n_mu = settings::legendre_to_tabular_points;
3,081✔
847
  if (n_mu == C_NONE) {
3,081!
848
    // then we will use 2 pts if its P0, or the default if a higher order
849
    // TODO use an error minimization algorithm that also picks n_mu
850
    if (leg.get_order() == 0) {
3,081✔
851
      n_mu = 2;
852
    } else {
853
      n_mu = DEFAULT_NMU;
210✔
854
    }
855
  }
856

857
  tab.base_init(n_mu, leg.gmin, leg.gmax, leg.energy, leg.mult);
3,081✔
858
  tab.scattxs = leg.scattxs;
3,081✔
859

860
  // Build mu and dmu
861
  tab.mu = tensor::linspace(-1., 1., n_mu);
3,081✔
862
  tab.dmu = 2. / (n_mu - 1);
3,081✔
863

864
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
865
  size_t groups = tab.energy.size();
3,081✔
866
  tab.fmu.resize(groups);
3,081✔
867
  for (int gin = 0; gin < groups; gin++) {
14,080✔
868
    int num_groups = tab.gmax[gin] - tab.gmin[gin] + 1;
10,999✔
869
    tab.fmu[gin].resize(num_groups);
10,999✔
870
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
59,762✔
871
      tab.fmu[gin][i_gout].resize(n_mu);
48,763✔
872
      for (int imu = 0; imu < n_mu; imu++) {
164,424✔
873
        tab.fmu[gin][i_gout][imu] =
115,661✔
874
          evaluate_legendre(leg.dist[gin][i_gout].size() - 1,
115,661✔
875
            leg.dist[gin][i_gout].data(), tab.mu[imu]);
115,661✔
876
      }
877

878
      // Ensure positivity
879
      for (auto& val : tab.fmu[gin][i_gout]) {
164,424✔
880
        if (val < 0.)
115,661✔
881
          val = 0.;
540✔
882
      }
883

884
      // Now re-normalize for numerical integration issues and to take care of
885
      // the above negative fix-up.  Also accrue the CDF
886
      double norm = 0.;
48,763✔
887
      tab.dist[gin][i_gout][0] = 0.;
48,763✔
888
      for (int imu = 1; imu < n_mu; imu++) {
115,661✔
889
        norm += 0.5 * tab.dmu *
66,898✔
890
                (tab.fmu[gin][i_gout][imu - 1] + tab.fmu[gin][i_gout][imu]);
66,898✔
891
        // incorporate to the CDF
892
        tab.dist[gin][i_gout][imu] = norm;
66,898✔
893
      }
894

895
      // now do the normalization
896
      if (norm > 0.) {
48,763✔
897
        for (int imu = 0; imu < n_mu; imu++) {
145,239✔
898
          tab.fmu[gin][i_gout][imu] /= norm;
102,716✔
899
          tab.dist[gin][i_gout][imu] /= norm;
102,716✔
900
        }
901
      }
902
    }
903
  }
904
}
3,081✔
905

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