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

openmc-dev / openmc / 22231282333

20 Feb 2026 04:04PM UTC coverage: 81.861% (-0.2%) from 82.058%
22231282333

Pull #2693

github

web-flow
Merge e5188d202 into 53ce1910f
Pull Request #2693: Add reactivity control to coupled transport-depletion analyses

17278 of 24307 branches covered (71.08%)

Branch coverage included in aggregate %.

75 of 80 new or added lines in 4 files covered. (93.75%)

3462 existing lines in 80 files now uncovered.

57630 of 67199 relevant lines covered (85.76%)

49119197.82 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,
8,829✔
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();
8,829✔
26

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

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

38
    // Make sure the multiplicity does not have 0s
39
    for (int go = 0; go < mult[gin].size(); go++) {
168,422✔
40
      if (mult[gin][go] == 0.) {
137,180✔
41
        mult[gin][go] = 1.;
11,718✔
42
      }
43
    }
44

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

48
    if (norm != 0.) {
31,242✔
49
      for (auto& n : energy[gin])
167,918✔
50
        n /= norm;
136,928✔
51
    }
52

53
    // Initialize the distribution data
54
    dist[gin].resize(in_gmax[gin] - in_gmin[gin] + 1);
31,242✔
55
    for (auto& v : dist[gin]) {
168,422✔
56
      v.resize(order);
137,180✔
57
    }
58
  }
59
}
8,829✔
60

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

63
void ScattData::base_combine(size_t max_order, size_t order_dim,
2,733✔
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,733✔
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,733✔
73
  tensor::Tensor<double> this_nuscatt_P0 =
74
    tensor::zeros<double>({groups, groups});
2,733✔
75
  tensor::Tensor<double> this_scatt_P0 =
76
    tensor::zeros<double>({groups, groups});
2,733✔
77
  tensor::Tensor<double> this_mult = tensor::ones<double>({groups, groups});
2,733✔
78

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

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

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

89
    // Do the same with the P0 matrices
90
    for (int gin = 0; gin < groups; gin++) {
14,225✔
91
      for (int go = 0; go < groups; go++) {
252,278✔
92
        this_nuscatt_P0(gin, go) +=
482,580✔
93
          scalars[i] * that->get_xs(MgxsType::NU_SCATTER, gin, &go, nullptr);
241,290✔
94
        this_scatt_P0(gin, go) +=
241,290✔
95
          scalars[i] * that->get_xs(MgxsType::SCATTER, gin, &go, nullptr);
241,290✔
96
      }
97
    }
98
  }
3,237✔
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,733✔
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++) {
12,755✔
107
    // Find the minimum and maximum group boundaries
108
    int gmin_;
109
    for (gmin_ = 0; gmin_ < groups; gmin_++) {
120,912✔
110
      bool non_zero = false;
120,828✔
111
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
351,120✔
112
        if (this_nuscatt_matrix(gin, gmin_, l) != 0.) {
240,230✔
113
          non_zero = true;
9,938✔
114
          break;
9,938✔
115
        }
116
      }
117
      if (non_zero)
120,828✔
118
        break;
9,938✔
119
    }
120
    int gmax_;
121
    for (gmax_ = groups - 1; gmax_ >= 0; gmax_--) {
97,444✔
122
      bool non_zero = false;
97,360✔
123
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
274,850✔
124
        if (this_nuscatt_matrix(gin, gmax_, l) != 0.) {
187,428✔
125
          non_zero = true;
9,938✔
126
          break;
9,938✔
127
        }
128
      }
129
      if (non_zero)
97,360✔
130
        break;
9,938✔
131
    }
132

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

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

143
    // Store the data in the compressed format
144
    sparse_scatter[gin].resize(gmax_ - gmin_ + 1);
10,022✔
145
    sparse_mult[gin].resize(gmax_ - gmin_ + 1);
10,022✔
146
    int i_gout = 0;
10,022✔
147
    for (int gout = gmin_; gout <= gmax_; gout++) {
55,156✔
148
      sparse_scatter[gin][i_gout].resize(this_nuscatt_matrix.shape(2));
45,134✔
149
      for (int l = 0; l < this_nuscatt_matrix.shape(2); l++) {
159,426✔
150
        sparse_scatter[gin][i_gout][l] = this_nuscatt_matrix(gin, gout, l);
114,292✔
151
      }
152
      sparse_mult[gin][i_gout] = this_mult(gin, gout);
45,134✔
153
      i_gout++;
45,134✔
154
    }
155
  }
156
}
2,733✔
157

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

160
void ScattData::sample_energy(int gin, int& gout, int& i_gout, uint64_t* seed)
1,517,650,640✔
161
{
162
  // Sample the outgoing group
163
  double xi = prn(seed);
1,517,650,640✔
164
  double prob = 0.;
1,517,650,640✔
165
  i_gout = 0;
1,517,650,640✔
166
  for (gout = gmin[gin]; gout < gmax[gin]; ++gout) {
1,850,683,840✔
167
    prob += energy[gin][i_gout];
1,460,122,460✔
168
    if (xi < prob)
1,460,122,460✔
169
      break;
1,127,089,260✔
170
    ++i_gout;
333,033,200✔
171
  }
172
}
1,517,650,640✔
173

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

176
double ScattData::get_xs(
17,492,854✔
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;
17,492,854✔
181
  if (gout != nullptr) {
17,492,854!
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?
17,492,854✔
185
      return 0.;
581,860✔
186
    }
187
    i_gout = *gout - gmin[gin];
16,910,994✔
188
  }
189

190
  double val = scattxs[gin];
16,910,994✔
191
  switch (xstype) {
16,910,994!
192
  case MgxsType::NU_SCATTER:
88,564✔
193
    if (gout != nullptr)
88,564!
194
      val *= energy[gin][i_gout];
88,564✔
195
    break;
88,564✔
196
  case MgxsType::SCATTER:
46,590✔
197
    if (gout != nullptr) {
46,590!
198
      val *= energy[gin][i_gout] / mult[gin][i_gout];
46,590✔
199
    } else {
UNCOV
200
      val /= std::inner_product(
×
UNCOV
201
        mult[gin].begin(), mult[gin].end(), energy[gin].begin(), 0.0);
×
202
    }
203
    break;
46,590✔
204
  case MgxsType::NU_SCATTER_FMU:
8,387,920✔
205
    if ((gout != nullptr) && (mu != nullptr)) {
8,387,920!
206
      val *= energy[gin][i_gout] * calc_f(gin, *gout, *mu);
8,387,920✔
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;
8,387,920✔
213
  case MgxsType::SCATTER_FMU:
8,387,920✔
214
    if ((gout != nullptr) && (mu != nullptr)) {
8,387,920!
215
      val *= energy[gin][i_gout] * calc_f(gin, *gout, *mu) / mult[gin][i_gout];
8,387,920✔
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;
8,387,920✔
UNCOV
222
  default:
×
UNCOV
223
    break;
×
224
  }
225
  return val;
16,910,994✔
226
}
227

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

232
void ScattDataLegendre::init(const tensor::Tensor<int>& in_gmin,
3,349✔
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,349✔
237
  size_t order = coeffs[0][0].size();
3,349✔
238

239
  // make a copy of coeffs that we can use to both extract data and normalize
240
  double_3dvec matrix = coeffs;
3,349✔
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,349✔
245
  for (int gin = 0; gin < groups; gin++) {
14,561✔
246
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
11,212✔
247
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
58,138✔
248
      scattxs[gin] += matrix[gin][i_gout][0];
46,926✔
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,349✔
256
  in_energy.resize(groups);
3,349✔
257
  for (int gin = 0; gin < groups; gin++) {
14,561✔
258
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
11,212✔
259
    in_energy[gin].resize(num_groups);
11,212✔
260
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
58,138✔
261
      double norm = matrix[gin][i_gout][0];
46,926✔
262
      in_energy[gin][i_gout] = norm;
46,926✔
263
      if (norm != 0.) {
46,926✔
264
        for (auto& n : matrix[gin][i_gout])
84,808✔
265
          n /= norm;
43,706✔
266
      }
267
    }
268
  }
269

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

273
  // Set the distribution (sdata.dist) values and initialize max_val
274
  max_val.resize(groups);
3,349✔
275
  for (int gin = 0; gin < groups; gin++) {
14,561✔
276
    int num_groups = gmax[gin] - gmin[gin] + 1;
11,212✔
277
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
58,138✔
278
      dist[gin][i_gout] = matrix[gin][i_gout];
46,926✔
279
    }
280
    max_val[gin].resize(num_groups);
11,212✔
281
    for (auto& n : max_val[gin])
58,138✔
282
      n = 0.;
46,926✔
283
  }
284

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

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

291
void ScattDataLegendre::update_max_val()
3,349✔
292
{
293
  size_t groups = max_val.size();
3,349✔
294
  // Step through the polynomial with fixed number of points to identify the
295
  // maximal value
296
  int Nmu = 1001;
3,349✔
297
  double dmu = 2. / (Nmu - 1);
3,349✔
298
  for (int gin = 0; gin < groups; gin++) {
14,561✔
299
    int num_groups = gmax[gin] - gmin[gin] + 1;
11,212✔
300
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
58,138✔
301
      for (int imu = 0; imu < Nmu; imu++) {
47,019,852✔
302
        double mu;
303
        if (imu == 0) {
46,972,926✔
304
          mu = -1.;
46,926✔
305
        } else if (imu == (Nmu - 1)) {
46,926,000✔
306
          mu = 1.;
46,926✔
307
        } else {
308
          mu = -1. + (imu - 1) * dmu;
46,879,074✔
309
        }
310

311
        // Calculate probability
312
        double f = evaluate_legendre(
93,945,852✔
313
          dist[gin][i_gout].size() - 1, dist[gin][i_gout].data(), mu);
46,972,926✔
314

315
        // if this is a new maximum, store it
316
        if (f > max_val[gin][i_gout])
46,972,926✔
317
          max_val[gin][i_gout] = f;
1,225,026✔
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;
46,926✔
322
    }
323
  }
324
}
3,349✔
325

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

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

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

343
void ScattDataLegendre::sample(
14,222,090✔
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);
14,222,090✔
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];
14,222,090✔
353
  int samples;
354
  for (samples = 0; samples < MAX_SAMPLE; ++samples) {
25,188,030!
355
    mu = 2. * prn(seed) - 1.;
25,188,030✔
356
    double f = calc_f(gin, gout, mu);
25,188,030✔
357
    if (f > 0.) {
25,188,030!
358
      double u = prn(seed) * M;
25,188,030✔
359
      if (u <= f)
25,188,030✔
360
        break;
14,222,090✔
361
    }
362
  }
363
  if (samples == MAX_SAMPLE) {
14,222,090!
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];
14,222,090✔
369
}
14,222,090✔
370

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

494
      // Now re-normalize for integral to unity
495
      double norm = dist[gin][i_gout][order - 1];
336✔
496
      if (norm > 0.) {
336!
497
        for (int imu = 0; imu < order; imu++) {
9,828✔
498
          fmu[gin][i_gout][imu] /= norm;
9,492✔
499
          dist[gin][i_gout][imu] /= norm;
9,492✔
500
        }
501
      }
502
    }
503
  }
504
}
112✔
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(
28,880✔
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);
28,880✔
537

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

541
  int imu;
542
  if (xi < dist[gin][i_gout][0]) {
28,880✔
543
    imu = 0;
1,390✔
544
  } else {
545
    imu =
27,490✔
546
      std::upper_bound(dist[gin][i_gout].begin(), dist[gin][i_gout].end(), xi) -
27,490✔
547
      dist[gin][i_gout].begin();
54,980✔
548
  }
549

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

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

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

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

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

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

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

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

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

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

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

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

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

636
  // Build the angular distribution mu values
637
  mu = tensor::linspace(-1., 1., order);
2,495✔
638
  dmu = 2. / (order - 1);
2,495✔
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,495✔
643
  for (int gin = 0; gin < groups; gin++) {
12,041✔
644
    for (int i_gout = 0; i_gout < matrix[gin].size(); i_gout++) {
53,966✔
645
      for (int imu = 1; imu < order; imu++) {
111,142✔
646
        scattxs[gin] +=
66,722✔
647
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
66,722✔
648
      }
649
    }
650
  }
651

652
  // Build the energy transfer matrix from data in the variable matrix
653
  double_2dvec in_energy(groups);
2,495✔
654
  for (int gin = 0; gin < groups; gin++) {
12,041✔
655
    int num_groups = in_gmax[gin] - in_gmin[gin] + 1;
9,546✔
656
    in_energy[gin].resize(num_groups);
9,546✔
657
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
53,966✔
658
      double norm = 0.;
44,420✔
659
      for (int imu = 1; imu < order; imu++) {
111,142✔
660
        norm +=
66,722✔
661
          0.5 * dmu * (matrix[gin][i_gout][imu - 1] + matrix[gin][i_gout][imu]);
66,722✔
662
      }
663
      in_energy[gin][i_gout] = norm;
44,420✔
664
    }
665
  }
666

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

670
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
671
  fmu.resize(groups);
2,495✔
672
  for (int gin = 0; gin < groups; gin++) {
12,041✔
673
    int num_groups = gmax[gin] - gmin[gin] + 1;
9,546✔
674
    fmu[gin].resize(num_groups);
9,546✔
675
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
53,966✔
676
      fmu[gin][i_gout].resize(order);
44,420✔
677
      // The variable matrix contains f(mu); so directly assign it
678
      fmu[gin][i_gout] = matrix[gin][i_gout];
44,420✔
679

680
      // Ensure positivity
681
      for (auto& val : fmu[gin][i_gout]) {
155,562✔
682
        if (val < 0.)
111,142✔
683
          val = 0.;
1,540✔
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.;
44,420✔
689
      for (int imu = 1; imu < order; imu++) {
111,142✔
690
        norm += 0.5 * dmu * (fmu[gin][i_gout][imu - 1] + fmu[gin][i_gout][imu]);
66,722✔
691
        // incorporate to the CDF
692
        dist[gin][i_gout][imu] = norm;
66,722✔
693
      }
694

695
      // now do the normalization
696
      if (norm > 0.) {
44,420✔
697
        for (int imu = 0; imu < order; imu++) {
135,346✔
698
          fmu[gin][i_gout][imu] /= norm;
97,520✔
699
          dist[gin][i_gout][imu] /= norm;
97,520✔
700
        }
701
      }
702
    }
703
  }
704
}
2,495✔
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,503,399,670✔
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,503,399,670✔
738

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

743
  double c_k = dist[gin][i_gout][0];
1,503,399,670✔
744
  int k;
745
  for (k = 0; k < NP - 1; k++) {
1,515,879,770!
746
    double c_k1 = dist[gin][i_gout][k + 1];
1,515,879,770✔
747
    if (xi < c_k1)
1,515,879,770✔
748
      break;
1,503,399,670✔
749
    c_k = c_k1;
12,480,100✔
750
  }
751

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

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

761
  if (p0 == p1) {
1,503,399,670✔
762
    mu = mu0 + (xi - c_k) / p0;
1,502,756,970✔
763
  } else {
764
    double frac = (p1 - p0) / (mu1 - mu0);
642,700✔
765
    mu =
642,700✔
766
      mu0 +
642,700✔
767
      (std::sqrt(std::max(0., p0 * p0 + 2. * frac * (xi - c_k))) - p0) / frac;
642,700✔
768
  }
769

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

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

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

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

791
  for (int gin = 0; gin < groups; gin++) {
13,301✔
792
    for (int i_gout = 0; i_gout < energy[gin].size(); i_gout++) {
56,038✔
793
      int gout = i_gout + gmin[gin];
45,666✔
794
      for (int l = 0; l < order_dim; l++) {
156,612✔
795
        matrix(gin, gout, l) =
110,946✔
796
          scattxs[gin] * energy[gin][i_gout] * fmu[gin][i_gout][l];
110,946✔
797
      }
798
    }
799
  }
800
  return matrix;
2,929✔
801
}
802

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

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

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

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

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

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

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

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

864
  // Calculate f(mu) and integrate it so we can avoid rejection sampling
865
  size_t groups = tab.energy.size();
2,873✔
866
  tab.fmu.resize(groups);
2,873✔
867
  for (int gin = 0; gin < groups; gin++) {
13,133✔
868
    int num_groups = tab.gmax[gin] - tab.gmin[gin] + 1;
10,260✔
869
    tab.fmu[gin].resize(num_groups);
10,260✔
870
    for (int i_gout = 0; i_gout < num_groups; i_gout++) {
55,758✔
871
      tab.fmu[gin][i_gout].resize(n_mu);
45,498✔
872
      for (int imu = 0; imu < n_mu; imu++) {
153,420✔
873
        tab.fmu[gin][i_gout][imu] =
107,922✔
874
          evaluate_legendre(leg.dist[gin][i_gout].size() - 1,
107,922✔
875
            leg.dist[gin][i_gout].data(), tab.mu[imu]);
107,922✔
876
      }
877

878
      // Ensure positivity
879
      for (auto& val : tab.fmu[gin][i_gout]) {
153,420✔
880
        if (val < 0.)
107,922✔
881
          val = 0.;
504✔
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.;
45,498✔
887
      tab.dist[gin][i_gout][0] = 0.;
45,498✔
888
      for (int imu = 1; imu < n_mu; imu++) {
107,922✔
889
        norm += 0.5 * tab.dmu *
124,848✔
890
                (tab.fmu[gin][i_gout][imu - 1] + tab.fmu[gin][i_gout][imu]);
62,424✔
891
        // incorporate to the CDF
892
        tab.dist[gin][i_gout][imu] = norm;
62,424✔
893
      }
894

895
      // now do the normalization
896
      if (norm > 0.) {
45,498✔
897
        for (int imu = 0; imu < n_mu; imu++) {
135,514✔
898
          tab.fmu[gin][i_gout][imu] /= norm;
95,840✔
899
          tab.dist[gin][i_gout][imu] /= norm;
95,840✔
900
        }
901
      }
902
    }
903
  }
904
}
2,873✔
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