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

openmc-dev / openmc / 22109060455

17 Feb 2026 05:38PM UTC coverage: 81.693% (-0.03%) from 81.721%
22109060455

Pull #3813

github

web-flow
Merge d2d368c60 into 977ade79a
Pull Request #3813: Check for positive radii

16733 of 23304 branches covered (71.8%)

Branch coverage included in aggregate %.

4 of 11 new or added lines in 1 file covered. (36.36%)

1361 existing lines in 48 files now uncovered.

56648 of 66521 relevant lines covered (85.16%)

43301606.31 hits per line

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

89.53
/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,
7,542✔
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();
7,542✔
26

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

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

38
    // Make sure the multiplicity does not have 0s
39
    for (int go = 0; go < mult[gin].size(); go++) {
144,372✔
40
      if (mult[gin][go] == 0.) {
117,606✔
41
        mult[gin][go] = 1.;
10,032✔
42
      }
43
    }
44

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

48
    if (norm != 0.) {
26,766✔
49
      for (auto& n : energy[gin])
144,012✔
50
        n /= norm;
117,426✔
51
    }
52

53
    // Initialize the distribution data
54
    dist[gin].resize(in_gmax[gin] - in_gmin[gin] + 1);
26,766✔
55
    for (auto& v : dist[gin]) {
144,372✔
56
      v.resize(order);
117,606✔
57
    }
58
  }
59
}
7,542✔
60

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

63
void ScattData::base_combine(size_t max_order, size_t order_dim,
2,334✔
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,334✔
69

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

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

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

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

89
    // Do the same with the P0 matrices
90
    for (int gin = 0; gin < groups; gin++) {
12,180✔
91
      for (int go = 0; go < groups; go++) {
216,264✔
92
        this_nuscatt_P0(gin, go) +=
413,700✔
93
          scalars[i] * that->get_xs(MgxsType::NU_SCATTER, gin, &go, nullptr);
206,850✔
94
        this_scatt_P0(gin, go) +=
206,850✔
95
          scalars[i] * that->get_xs(MgxsType::SCATTER, gin, &go, nullptr);
206,850✔
96
      }
97
    }
98
  }
2,766✔
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);
2,334✔
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++) {
10,920✔
107
    // Find the minimum and maximum group boundaries
108
    int gmin_;
109
    for (gmin_ = 0; gmin_ < groups; gmin_++) {
103,638✔
110
      bool non_zero = false;
103,578✔
111
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
300,978✔
112
        if (this_nuscatt_matrix(gin, gmin_, l) != 0.) {
205,926✔
113
          non_zero = true;
8,526✔
114
          break;
8,526✔
115
        }
116
      }
117
      if (non_zero)
103,578✔
118
        break;
8,526✔
119
    }
120
    int gmax_;
121
    for (gmax_ = groups - 1; gmax_ >= 0; gmax_--) {
83,514✔
122
      bool non_zero = false;
83,454✔
123
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
235,578✔
124
        if (this_nuscatt_matrix(gin, gmax_, l) != 0.) {
160,650✔
125
          non_zero = true;
8,526✔
126
          break;
8,526✔
127
        }
128
      }
129
      if (non_zero)
83,454✔
130
        break;
8,526✔
131
    }
132

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

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

143
    // Store the data in the compressed format
144
    sparse_scatter[gin].resize(gmax_ - gmin_ + 1);
8,586✔
145
    sparse_mult[gin].resize(gmax_ - gmin_ + 1);
8,586✔
146
    int i_gout = 0;
8,586✔
147
    for (int gout = gmin_; gout <= gmax_; gout++) {
47,280✔
148
      sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape(2));
38,694✔
149
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
136,674✔
150
        sparse_scatter[gin][i_gout][l] = this_nuscatt_matrix(gin, gout, l);
97,980✔
151
      }
152
      sparse_mult[gin][i_gout] = this_mult(gin, gout);
38,694✔
153
      i_gout++;
38,694✔
154
    }
155
  }
156
}
2,334✔
157

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

160
void ScattData::sample_energy(int gin, int& gout, int& i_gout, uint64_t* seed)
1,365,885,576✔
161
{
162
  // Sample the outgoing group
163
  double xi = prn(seed);
1,365,885,576✔
164
  double prob = 0.;
1,365,885,576✔
165
  i_gout = 0;
1,365,885,576✔
166
  for (gout = gmin[gin]; gout < gmax[gin]; ++gout) {
1,665,615,456✔
167
    prob += energy[gin][i_gout];
1,314,110,214✔
168
    if (xi < prob)
1,314,110,214✔
169
      break;
1,014,380,334✔
170
    ++i_gout;
299,729,880✔
171
  }
172
}
1,365,885,576✔
173

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

176
double ScattData::get_xs(
15,712,926✔
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;
15,712,926✔
181
  if (gout != nullptr) {
15,712,926!
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?
15,712,926✔
185
      return 0.;
498,804✔
186
    }
187
    i_gout = *gout - gmin[gin];
15,214,122✔
188
  }
189

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

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

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

239
  // make a copy of coeffs that we can use to both extract data and normalize
240
  double_3dvec matrix = coeffs;
2,862✔
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});
2,862✔
245
  for (int gin = 0; gin < groups; gin++) {
12,468✔
246
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
9,606✔
247
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
49,836✔
248
      scattxs[gin] += matrix[gin][i_gout][0];
40,230✔
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;
2,862✔
256
  in_energy.resize(groups);
2,862✔
257
  for (int gin = 0; gin < groups; gin++) {
12,468✔
258
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
9,606✔
259
    in_energy[gin].resize(num_groups);
9,606✔
260
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
49,836✔
261
      double norm = matrix[gin][i_gout][0];
40,230✔
262
      in_energy[gin][i_gout] = norm;
40,230✔
263
      if (norm != 0.) {
40,230✔
264
        for (auto& n : matrix[gin][i_gout])
72,732✔
265
          n /= norm;
37,482✔
266
      }
267
    }
268
  }
269

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

273
  // Set the distribution (sdata.dist) values and initialize max_val
274
  max_val.resize(groups);
2,862✔
275
  for (int gin = 0; gin < groups; gin++) {
12,468✔
276
    int num_groups = gmax[gin] - gmin[gin] + 1;
9,606✔
277
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
49,836✔
278
      dist[gin][i_gout] = matrix[gin][i_gout];
40,230✔
279
    }
280
    max_val[gin].resize(num_groups);
9,606✔
281
    for (auto& n : max_val[gin])
49,836✔
282
      n = 0.;
40,230✔
283
  }
284

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

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

291
void ScattDataLegendre::update_max_val()
2,862✔
292
{
293
  size_t groups = max_val.size();
2,862✔
294
  // Step through the polynomial with fixed number of points to identify the
295
  // maximal value
296
  int Nmu = 1001;
2,862✔
297
  double dmu = 2. / (Nmu - 1);
2,862✔
298
  for (int gin = 0; gin < groups; gin++) {
12,468✔
299
    int num_groups = gmax[gin] - gmin[gin] + 1;
9,606✔
300
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
49,836✔
301
      for (int imu = 0; imu < Nmu; imu++) {
40,310,460✔
302
        double mu;
303
        if (imu == 0) {
40,270,230✔
304
          mu = -1.;
40,230✔
305
        } else if (imu == (Nmu - 1)) {
40,230,000✔
306
          mu = 1.;
40,230✔
307
        } else {
308
          mu = -1. + (imu - 1) * dmu;
40,189,770✔
309
        }
310

311
        // Calculate probability
312
        double f = evaluate_legendre(
80,540,460✔
313
          dist[gin][i_gout].size() - 1, dist[gin][i_gout].data(), mu);
40,270,230✔
314

315
        // if this is a new maximum, store it
316
        if (f > max_val[gin][i_gout])
40,270,230✔
317
          max_val[gin][i_gout] = f;
1,050,042✔
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;
40,230✔
322
    }
323
  }
324
}
2,862✔
325

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

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

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

343
void ScattDataLegendre::sample(
12,799,881✔
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;
348
  sample_energy(gin, gout, i_gout, seed);
12,799,881✔
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];
12,799,881✔
353
  int samples;
354
  for (samples = 0; samples < MAX_SAMPLE; ++samples) {
22,669,227!
355
    mu = 2. * prn(seed) - 1.;
22,669,227✔
356
    double f = calc_f(gin, gout, mu);
22,669,227✔
357
    if (f > 0.) {
22,669,227!
358
      double u = prn(seed) * M;
22,669,227✔
359
      if (u <= f)
22,669,227✔
360
        break;
12,799,881✔
361
    }
362
  }
363
  if (samples == MAX_SAMPLE) {
12,799,881!
UNCOV
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];
12,799,881✔
369
}
12,799,881✔
370

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

373
void ScattDataLegendre::combine(
204✔
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;
204✔
378
  for (int i = 0; i < those_scatts.size(); i++) {
420✔
379
    // Lets also make sure these items are combineable
380
    ScattDataLegendre* that = dynamic_cast<ScattDataLegendre*>(those_scatts[i]);
216!
381
    if (!that) {
216!
UNCOV
382
      fatal_error("Cannot combine the ScattData objects!");
×
383
    }
384
    size_t that_order = that->get_order();
216✔
385
    if (that_order > max_order)
216✔
386
      max_order = that_order;
204✔
387
  }
388

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

391
  tensor::Tensor<int> in_gmin({groups}, 0);
204✔
392
  tensor::Tensor<int> in_gmax({groups}, 0);
204✔
393
  double_3dvec sparse_scatter(groups);
204✔
394
  double_2dvec sparse_mult(groups);
204✔
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;
204✔
400
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
204✔
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);
204✔
405
}
204✔
406

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

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

417
  for (int gin = 0; gin < groups; gin++) {
648✔
418
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
1,080✔
419
      int gout = i_gout + gmin[gin];
648✔
420
      for (int l = 0; l < order_dim; l++) {
1,944✔
421
        matrix(gin, gout, l) =
1,296✔
422
          scattxs[gin] * energy[gin][i_gout] * dist[gin][i_gout][l];
1,296✔
423
      }
424
    }
425
  }
426
  return matrix;
216✔
427
}
428

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

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

440
  // make a copy of coeffs that we can use to both extract data and normalize
441
  double_3dvec matrix = coeffs;
96✔
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});
96✔
446
  for (int gin = 0; gin < groups; gin++) {
288✔
447
    for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) {
480✔
448
      scattxs[gin] += std::accumulate(
576✔
449
        matrix[gin][i_gout].begin(), matrix[gin][i_gout].end(), 0.);
576✔
450
    }
451
  }
452

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

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

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

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

487
      // Integrate the histogram
488
      dist[gin][i_gout][0] = dmu * matrix[gin][i_gout][0];
288✔
489
      for (int imu = 1; imu < order; imu++) {
8,136✔
490
        dist[gin][i_gout][imu] =
7,848✔
491
          dmu * matrix[gin][i_gout][imu] + dist[gin][i_gout][imu - 1];
7,848✔
492
      }
493

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

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

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

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

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

531
void ScattDataHistogram::sample(
25,992✔
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;
536
  sample_energy(gin, gout, i_gout, seed);
25,992✔
537

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

541
  int imu;
542
  if (xi < dist[gin][i_gout][0]) {
25,992✔
543
    imu = 0;
1,251✔
544
  } else {
545
    imu =
24,741✔
546
      std::upper_bound(dist[gin][i_gout].begin(), dist[gin][i_gout].end(), xi) -
24,741✔
547
      dist[gin][i_gout].begin();
49,482✔
548
  }
549

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

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

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

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

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

573
  for (int gin = 0; gin < groups; gin++) {
144✔
574
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
240✔
575
      int gout = i_gout + gmin[gin];
144✔
576
      for (int l = 0; l < order_dim; l++) {
4,212✔
577
        matrix(gin, gout, l) =
4,068✔
578
          scattxs[gin] * energy[gin][i_gout] * fmu[gin][i_gout][l];
4,068✔
579
      }
580
    }
581
  }
582
  return matrix;
48✔
583
}
584

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

587
void ScattDataHistogram::combine(
48✔
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();
48✔
592
  for (int i = 0; i < those_scatts.size(); i++) {
96✔
593
    // Lets also make sure these items are combineable
594
    ScattDataHistogram* that =
595
      dynamic_cast<ScattDataHistogram*>(those_scatts[i]);
48!
596
    if (!that) {
48!
597
      fatal_error("Cannot combine the ScattData objects!");
×
598
    }
599
    if (max_order != that->get_order()) {
48!
UNCOV
600
      fatal_error("Cannot combine the ScattData objects!");
×
601
    }
602
  }
603

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

606
  tensor::Tensor<int> in_gmin({groups}, 0);
48✔
607
  tensor::Tensor<int> in_gmax({groups}, 0);
48✔
608
  double_3dvec sparse_scatter(groups);
48✔
609
  double_2dvec sparse_mult(groups);
48✔
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;
48✔
615
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
48✔
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);
48✔
620
}
48✔
621

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

626
void ScattDataTabular::init(const tensor::Tensor<int>& in_gmin,
2,130✔
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,130✔
631
  size_t order = coeffs[0][0].size();
2,130✔
632

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

636
  // Build the angular distribution mu values
637
  mu = tensor::linspace(-1., 1., order);
2,130✔
638
  dmu = 2. / (order - 1);
2,130✔
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,130✔
643
  for (int gin = 0; gin < groups; gin++) {
10,308✔
644
    for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) {
46,260✔
645
      for (int imu = 1; imu < order; imu++) {
95,280✔
646
        scattxs[gin] +=
57,198✔
647
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
57,198✔
648
      }
649
    }
650
  }
651

652
  // Build the energy transfer matrix from data in the variable matrix
653
  double_2dvec in_energy(groups);
2,130✔
654
  for (int gin = 0; gin < groups; gin++) {
10,308✔
655
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
8,178✔
656
    in_energy[gin].resize(num_groups);
8,178✔
657
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
46,260✔
658
      double norm = 0.;
38,082✔
659
      for (int imu = 1; imu < order; imu++) {
95,280✔
660
        norm +=
57,198✔
661
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
57,198✔
662
      }
663
      in_energy[gin][i_gout] = norm;
38,082✔
664
    }
665
  }
666

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

670
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
671
  fmu.resize(groups);
2,130✔
672
  for (int gin = 0; gin < groups; gin++) {
10,308✔
673
    int num_groups = gmax[gin] - gmin[gin] + 1;
8,178✔
674
    fmu[gin].resize(num_groups);
8,178✔
675
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
46,260✔
676
      fmu[gin][i_gout].resize(order);
38,082✔
677
      // The variable matrix contains f(mu); so directly assign it
678
      fmu[gin][i_gout] = matrix[gin][i_gout];
38,082✔
679

680
      // Ensure positivity
681
      for (auto& val : fmu[gin][i_gout]) {
133,362✔
682
        if (val < 0.)
95,280✔
683
          val = 0.;
1,320✔
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.;
38,082✔
689
      for (int imu = 1; imu < order; imu++) {
95,280✔
690
        norm += 0.5 * dmu * (fmu[gin][i_gout][imu - 1] + fmu[gin][i_gout][imu]);
57,198✔
691
        // incorporate to the CDF
692
        dist[gin][i_gout][imu] = norm;
57,198✔
693
      }
694

695
      // now do the normalization
696
      if (norm > 0.) {
38,082✔
697
        for (int imu = 0; imu < order; imu++) {
116,070✔
698
          fmu[gin][i_gout][imu] /= norm;
83,628✔
699
          dist[gin][i_gout][imu] /= norm;
83,628✔
700
        }
701
      }
702
    }
703
  }
704
}
2,130✔
705

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

708
double ScattDataTabular::calc_f(int gin, int gout, double mu)
×
709
{
710
  double f;
UNCOV
711
  if ((gout < gmin[gin]) || (gout > gmax[gin])) {
×
712
    f = 0.;
×
713
  } else {
714
    // Find mu bin
UNCOV
715
    int i_gout = gout - gmin[gin];
×
716
    int imu;
UNCOV
717
    if (mu == 1.) {
×
718
      // use size -2 to have the index one before the end
UNCOV
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]);
×
UNCOV
725
    f = (1. - r) * fmu[gin][i_gout][imu] + r * fmu[gin][i_gout][imu + 1];
×
726
  }
UNCOV
727
  return f;
×
728
}
729

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

732
void ScattDataTabular::sample(
1,353,059,703✔
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;
737
  sample_energy(gin, gout, i_gout, seed);
1,353,059,703✔
738

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

743
  double c_k = dist[gin][i_gout][0];
1,353,059,703✔
744
  int k;
745
  for (k = 0; k < NP - 1; k++) {
1,364,291,793!
746
    double c_k1 = dist[gin][i_gout][k + 1];
1,364,291,793✔
747
    if (xi < c_k1)
1,364,291,793✔
748
      break;
1,353,059,703✔
749
    c_k = c_k1;
11,232,090✔
750
  }
751

752
  // Check to make sure k is <= NP - 1
753
  k = std::min(k, NP - 2);
1,353,059,703✔
754

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

761
  if (p0 == p1) {
1,353,059,703✔
762
    mu = mu0 + (xi - c_k) / p0;
1,352,481,273✔
763
  } else {
764
    double frac = (p1 - p0) / (mu1 - mu0);
578,430✔
765
    mu =
578,430✔
766
      mu0 +
578,430✔
767
      (std::sqrt(std::max(0., p0 * p0 + 2. * frac * (xi - c_k))) - p0) / frac;
578,430✔
768
  }
769

770
  if (mu < -1.) {
1,353,059,703!
UNCOV
771
    mu = -1.;
×
772
  } else if (mu > 1.) {
1,353,059,703!
UNCOV
773
    mu = 1.;
×
774
  }
775

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

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

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

791
  for (int gin = 0; gin < groups; gin++) {
11,388✔
792
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
48,036✔
793
      int gout = i_gout + gmin[gin];
39,150✔
794
      for (int l = 0; l < order_dim; l++) {
134,262✔
795
        matrix(gin, gout, l) =
95,112✔
796
          scattxs[gin] * energy[gin][i_gout] * fmu[gin][i_gout][l];
95,112✔
797
      }
798
    }
799
  }
800
  return matrix;
2,502✔
801
}
802

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

805
void ScattDataTabular::combine(
2,082✔
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,082✔
810
  for (int i = 0; i < those_scatts.size(); i++) {
4,584✔
811
    // Lets also make sure these items are combineable
812
    ScattDataTabular* that = dynamic_cast<ScattDataTabular*>(those_scatts[i]);
2,502!
813
    if (!that) {
2,502!
UNCOV
814
      fatal_error("Cannot combine the ScattData objects!");
×
815
    }
816
    if (max_order != that->get_order()) {
2,502!
UNCOV
817
      fatal_error("Cannot combine the ScattData objects!");
×
818
    }
819
  }
820

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

823
  tensor::Tensor<int> in_gmin({groups}, 0);
2,082✔
824
  tensor::Tensor<int> in_gmax({groups}, 0);
2,082✔
825
  double_3dvec sparse_scatter(groups);
2,082✔
826
  double_2dvec sparse_mult(groups);
2,082✔
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,082✔
832
  ScattData::base_combine(max_order, order_dim, those_scatts, scalars, in_gmin,
2,082✔
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,082✔
837
}
2,082✔
838

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

843
void convert_legendre_to_tabular(ScattDataLegendre& leg, ScattDataTabular& tab)
2,454✔
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;
2,454✔
847
  if (n_mu == C_NONE) {
2,454!
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) {
2,454✔
851
      n_mu = 2;
2,286✔
852
    } else {
853
      n_mu = DEFAULT_NMU;
168✔
854
    }
855
  }
856

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

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

864
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
865
  size_t groups = tab.energy.size();
2,454✔
866
  tab.fmu.resize(groups);
2,454✔
867
  for (int gin = 0; gin < groups; gin++) {
11,244✔
868
    int num_groups = tab.gmax[gin] - tab.gmin[gin] + 1;
8,790✔
869
    tab.fmu[gin].resize(num_groups);
8,790✔
870
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
47,796✔
871
      tab.fmu[gin][i_gout].resize(n_mu);
39,006✔
872
      for (int imu = 0; imu < n_mu; imu++) {
131,526✔
873
        tab.fmu[gin][i_gout][imu] =
92,520✔
874
          evaluate_legendre(leg.dist[gin][i_gout].size() - 1,
92,520✔
875
            leg.dist[gin][i_gout].data(), tab.mu[imu]);
92,520✔
876
      }
877

878
      // Ensure positivity
879
      for (auto& val : tab.fmu[gin][i_gout]) {
131,526✔
880
        if (val < 0.)
92,520✔
881
          val = 0.;
432✔
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.;
39,006✔
887
      tab.dist[gin][i_gout][0] = 0.;
39,006✔
888
      for (int imu = 1; imu < n_mu; imu++) {
92,520✔
889
        norm += 0.5 * tab.dmu *
107,028✔
890
                (tab.fmu[gin][i_gout][imu - 1] + tab.fmu[gin][i_gout][imu]);
53,514✔
891
        // incorporate to the CDF
892
        tab.dist[gin][i_gout][imu] = norm;
53,514✔
893
      }
894

895
      // now do the normalization
896
      if (norm > 0.) {
39,006✔
897
        for (int imu = 0; imu < n_mu; imu++) {
116,214✔
898
          tab.fmu[gin][i_gout][imu] /= norm;
82,188✔
899
          tab.dist[gin][i_gout][imu] /= norm;
82,188✔
900
        }
901
      }
902
    }
903
  }
904
}
2,454✔
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