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

openmc-dev / openmc / 24201667475

09 Apr 2026 04:34PM UTC coverage: 81.306% (-0.03%) from 81.337%
24201667475

Pull #3702

github

web-flow
Merge 85e3d3a8b into 1eb368bbd
Pull Request #3702: Random Ray Kinetic Simulation Mode

18160 of 26198 branches covered (69.32%)

Branch coverage included in aggregate %.

935 of 1075 new or added lines in 20 files covered. (86.98%)

74 existing lines in 8 files now uncovered.

58936 of 68624 relevant lines covered (85.88%)

52648244.8 hits per line

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

80.33
/src/mgxs.cpp
1
#include "openmc/mgxs.h"
2

3
#include <algorithm>
4
#include <cmath>
5
#include <cstdlib>
6
#include <sstream>
7

8
#include "openmc/tensor.h"
9
#include <fmt/core.h>
10

11
#include "openmc/error.h"
12
#include "openmc/math_functions.h"
13
#include "openmc/mgxs_interface.h"
14
#include "openmc/nuclide.h"
15
#include "openmc/random_lcg.h"
16
#include "openmc/settings.h"
17
#include "openmc/string_utils.h"
18

19
namespace openmc {
20

21
//==============================================================================
22
// Mgxs base-class methods
23
//==============================================================================
24

25
void Mgxs::init(const std::string& in_name, double in_awr,
7,039✔
26
  const vector<double>& in_kTs, bool in_fissionable,
27
  AngleDistributionType in_scatter_format, bool in_is_isotropic,
28
  const vector<double>& in_polar, const vector<double>& in_azimuthal)
29
{
30
  // Set the metadata
31
  name = in_name;
7,039✔
32
  awr = in_awr;
7,039✔
33
  kTs = tensor::Tensor<double>(in_kTs.data(), in_kTs.size());
7,039✔
34
  fissionable = in_fissionable;
7,039✔
35
  scatter_format = in_scatter_format;
7,039✔
36
  xs.resize(in_kTs.size());
7,039✔
37
  is_isotropic = in_is_isotropic;
7,039✔
38
  n_pol = in_polar.size();
7,039✔
39
  n_azi = in_azimuthal.size();
7,039✔
40
  polar = in_polar;
7,039✔
41
  azimuthal = in_azimuthal;
7,039✔
42
}
7,039✔
43

44
//==============================================================================
45

46
void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector<double>& temperature,
3,707✔
47
  vector<int>& temps_to_read, int& order_dim)
48
{
49
  // get name
50
  std::string in_name;
3,707✔
51
  get_name(xs_id, in_name);
3,707✔
52
  // remove the leading '/'
53
  in_name = in_name.substr(1);
3,707✔
54

55
  // Get the AWR
56
  double in_awr;
3,707✔
57
  if (attribute_exists(xs_id, "atomic_weight_ratio")) {
3,707✔
58
    read_attr_double(xs_id, "atomic_weight_ratio", &in_awr);
120✔
59
  } else {
60
    in_awr = MACROSCOPIC_AWR;
3,587✔
61
  }
62

63
  // Determine the available temperatures
64
  hid_t kT_group = open_group(xs_id, "kTs");
3,707✔
65
  size_t num_temps = get_num_datasets(kT_group);
3,707✔
66
  char** dset_names = new char*[num_temps];
3,707!
67
  for (int i = 0; i < num_temps; i++) {
8,494✔
68
    dset_names[i] = new char[151];
4,787✔
69
  }
70
  get_datasets(kT_group, dset_names);
3,707✔
71
  vector<size_t> shape = {num_temps};
3,707✔
72
  tensor::Tensor<double> temps_available(shape);
3,707✔
73
  for (int i = 0; i < num_temps; i++) {
8,494✔
74
    read_double(kT_group, dset_names[i], &temps_available[i], true);
4,787✔
75

76
    // convert eV to Kelvin
77
    temps_available[i] = std::round(temps_available[i] / K_BOLTZMANN);
4,787!
78

79
    // Done with dset_names, so delete it
80
    delete[] dset_names[i];
4,787!
81
  }
82
  delete[] dset_names;
3,707✔
83
  std::sort(temps_available.begin(), temps_available.end());
3,707✔
84

85
  // Set the global upper and lower interpolation bounds to avoid errors
86
  // involving C-API functions.
87
  data::temperature_min =
3,707✔
88
    std::min(data::temperature_min, temps_available.front());
3,707✔
89
  data::temperature_max =
3,707✔
90
    std::max(data::temperature_max, temps_available.back());
3,707✔
91

92
  // If only one temperature is available, lets just use nearest temperature
93
  // interpolation
94
  if ((num_temps == 1) &&
3,707✔
95
      (settings::temperature_method == TemperatureMethod::INTERPOLATION)) {
3,077!
96
    warning("Cross sections for " + strtrim(name) + " are only available " +
×
97
            "at one temperature.  Reverting to the nearest temperature " +
×
98
            "method.");
99
    settings::temperature_method = TemperatureMethod::NEAREST;
×
100
  }
101

102
  switch (settings::temperature_method) {
3,707!
103
  case TemperatureMethod::NEAREST:
3,527✔
104
    // Determine actual temperatures to read
105
    for (const auto& T : temperature) {
7,024✔
106
      // Determine the closest temperature value
107
      auto i_closest = tensor::abs(temps_available - T).argmin();
6,994✔
108

109
      double temp_actual = temps_available[i_closest];
3,497!
110
      if (std::fabs(temp_actual - T) < settings::temperature_tolerance) {
3,497!
111
        if (std::find(temps_to_read.begin(), temps_to_read.end(),
3,497!
112
              std::round(temp_actual)) == temps_to_read.end()) {
3,497!
113
          temps_to_read.push_back(std::round(temp_actual));
3,497✔
114
        }
115
      } else {
116
        fatal_error(fmt::format(
×
117
          "MGXS library does not contain cross sections "
118
          "for {} at or near {} K. Available temperatures "
119
          "are {} K. Consider making use of openmc.Settings.temperature "
120
          "to specify how intermediate temperatures are treated.",
121
          in_name, std::round(T), concatenate(temps_available)));
×
122
      }
123
    }
124
    break;
125

126
  case TemperatureMethod::INTERPOLATION:
127
    for (int i = 0; i < temperature.size(); i++) {
360✔
128
      for (int j = 0; j < num_temps; j++) {
270!
129
        if (j == (num_temps - 1)) {
270!
130
          fatal_error("MGXS Library does not contain cross sections for " +
×
131
                      in_name + " at temperatures that bound " +
×
132
                      std::to_string(std::round(temperature[i])));
×
133
        }
134
        if ((temps_available[j] <= temperature[i]) &&
270!
135
            (temperature[i] < temps_available[j + 1])) {
270✔
136
          if (std::find(temps_to_read.begin(), temps_to_read.end(),
180!
137
                temps_available[j]) == temps_to_read.end()) {
180!
138
            temps_to_read.push_back(temps_available[j]);
180✔
139
          }
140

141
          if (std::find(temps_to_read.begin(), temps_to_read.end(),
180!
142
                temps_available[j + 1]) == temps_to_read.end()) {
180!
143
            temps_to_read.push_back(temps_available[j + 1]);
180✔
144
          }
145
          break;
146
        }
147
      }
148
    }
149
  }
150
  std::sort(temps_to_read.begin(), temps_to_read.end());
3,707✔
151

152
  // Get the library's temperatures
153
  int n_temperature = temps_to_read.size();
3,707✔
154
  vector<double> in_kTs(n_temperature);
7,414✔
155
  for (int i = 0; i < n_temperature; i++) {
7,564✔
156
    std::string temp_str(std::to_string(temps_to_read[i]) + "K");
3,857✔
157

158
    // read exact temperature value
159
    read_double(kT_group, temp_str.c_str(), &in_kTs[i], true);
3,857✔
160
  }
3,857✔
161
  close_group(kT_group);
3,707✔
162

163
  // Load the remaining metadata
164
  AngleDistributionType in_scatter_format;
3,707✔
165
  if (attribute_exists(xs_id, "scatter_format")) {
3,707!
166
    std::string temp_str;
3,707✔
167
    read_attribute(xs_id, "scatter_format", temp_str);
3,707✔
168
    to_lower(strtrim(temp_str));
3,707✔
169
    if (temp_str.compare(0, 8, "legendre") == 0) {
3,707✔
170
      in_scatter_format = AngleDistributionType::LEGENDRE;
171
    } else if (temp_str.compare(0, 9, "histogram") == 0) {
120✔
172
      in_scatter_format = AngleDistributionType::HISTOGRAM;
173
    } else if (temp_str.compare(0, 7, "tabular") == 0) {
60!
174
      in_scatter_format = AngleDistributionType::TABULAR;
175
    } else {
176
      fatal_error("Invalid scatter_format option!");
×
177
    }
178
  } else {
3,707✔
179
    in_scatter_format = AngleDistributionType::LEGENDRE;
180
  }
181

182
  if (attribute_exists(xs_id, "scatter_shape")) {
3,707!
183
    std::string temp_str;
3,707✔
184
    read_attribute(xs_id, "scatter_shape", temp_str);
3,707✔
185
    to_lower(strtrim(temp_str));
3,707✔
186
    if (temp_str.compare(0, 14, "[g][g\'][order]") != 0) {
3,707!
187
      fatal_error("Invalid scatter_shape option!");
×
188
    }
189
  }
3,707✔
190

191
  bool in_fissionable = false;
3,707✔
192
  if (attribute_exists(xs_id, "fissionable")) {
3,707!
193
    int int_fiss;
3,707✔
194
    read_attr_int(xs_id, "fissionable", &int_fiss);
3,707✔
195
    in_fissionable = int_fiss;
3,707✔
196
  } else {
197
    fatal_error("Fissionable element must be set!");
×
198
  }
199

200
  // Get the library's value for the order
201
  if (attribute_exists(xs_id, "order")) {
3,707!
202
    read_attr_int(xs_id, "order", &order_dim);
3,707✔
203
  } else {
204
    fatal_error("Order must be provided!");
×
205
  }
206

207
  // Store the dimensionality of the data in order_dim.
208
  // For Legendre data, we usually refer to it as Pn where n is the order.
209
  // However Pn has n+1 sets of points (since you need to count the P0
210
  // moment). Adjust for that. Histogram and Tabular formats dont need this
211
  // adjustment.
212
  if (in_scatter_format == AngleDistributionType::LEGENDRE) {
3,707✔
213
    ++order_dim;
3,587✔
214
  }
215

216
  // Get the angular information
217
  int in_n_pol;
3,707✔
218
  int in_n_azi;
3,707✔
219
  bool in_is_isotropic = true;
3,707✔
220
  if (attribute_exists(xs_id, "representation")) {
3,707!
221
    std::string temp_str;
3,707✔
222
    read_attribute(xs_id, "representation", temp_str);
3,707✔
223
    to_lower(strtrim(temp_str));
3,707✔
224
    if (temp_str.compare(0, 5, "angle") == 0) {
3,707✔
225
      in_is_isotropic = false;
226
    } else if (temp_str.compare(0, 9, "isotropic") != 0) {
3,677!
227
      fatal_error("Invalid Data Representation!");
×
228
    }
229
  }
×
230

231
  if (!in_is_isotropic) {
3,707✔
232
    if (attribute_exists(xs_id, "num_polar")) {
30!
233
      read_attr_int(xs_id, "num_polar", &in_n_pol);
30✔
234
    } else {
235
      fatal_error("num_polar must be provided!");
×
236
    }
237
    if (attribute_exists(xs_id, "num_azimuthal")) {
30!
238
      read_attr_int(xs_id, "num_azimuthal", &in_n_azi);
30✔
239
    } else {
240
      fatal_error("num_azimuthal must be provided!");
×
241
    }
242
  } else {
243
    in_n_pol = 1;
3,677✔
244
    in_n_azi = 1;
3,677✔
245
  }
246

247
  // Set the angular bins to use equally-spaced bins
248
  vector<double> in_polar(in_n_pol);
7,414✔
249
  double dangle = PI / in_n_pol;
3,707✔
250
  for (int p = 0; p < in_n_pol; p++) {
7,444✔
251
    in_polar[p] = (p + 0.5) * dangle;
3,737✔
252
  }
253
  vector<double> in_azimuthal(in_n_azi);
7,414✔
254
  dangle = 2. * PI / in_n_azi;
3,707✔
255
  for (int a = 0; a < in_n_azi; a++) {
7,444✔
256
    in_azimuthal[a] = (a + 0.5) * dangle - PI;
3,737✔
257
  }
258

259
  // Finally use this data to initialize the MGXS Object
260
  init(in_name, in_awr, in_kTs, in_fissionable, in_scatter_format,
3,707✔
261
    in_is_isotropic, in_polar, in_azimuthal);
262
}
3,707✔
263

264
//==============================================================================
265

266
Mgxs::Mgxs(
3,707✔
267
  hid_t xs_id, const vector<double>& temperature, int num_group, int num_delay)
3,707✔
268
  : num_groups(num_group), num_delayed_groups(num_delay)
3,707✔
269
{
270
  // Call generic data gathering routine (will populate the metadata)
271
  int order_data;
3,707✔
272
  vector<int> temps_to_read;
3,707✔
273
  metadata_from_hdf5(xs_id, temperature, temps_to_read, order_data);
3,707✔
274

275
  // Set number of energy and delayed groups
276
  AngleDistributionType final_scatter_format = scatter_format;
3,707✔
277
  if (settings::legendre_to_tabular) {
3,707✔
278
    if (scatter_format == AngleDistributionType::LEGENDRE)
3,467✔
279
      final_scatter_format = AngleDistributionType::TABULAR;
3,377✔
280
  }
281

282
  // Load the more specific XsData information
283
  for (int t = 0; t < temps_to_read.size(); t++) {
7,564✔
284
    xs[t] = XsData(fissionable, final_scatter_format, n_pol, n_azi, num_groups,
3,857✔
285
      num_delayed_groups);
3,857✔
286
    // Get the temperature as a string and then open the HDF5 group
287
    std::string temp_str = std::to_string(temps_to_read[t]) + "K";
3,857✔
288
    hid_t xsdata_grp = open_group(xs_id, temp_str.c_str());
3,857✔
289

290
    xs[t].from_hdf5(xsdata_grp, fissionable, scatter_format,
3,857✔
291
      final_scatter_format, order_data, is_isotropic, n_pol, n_azi);
3,857✔
292
    close_group(xsdata_grp);
3,857✔
293

294
  } // end temperature loop
3,857✔
295

296
  // Make sure the scattering format is updated to the final case
297
  scatter_format = final_scatter_format;
3,707✔
298
}
3,707✔
299

300
//==============================================================================
301

302
Mgxs::Mgxs(const std::string& in_name, const vector<double>& mat_kTs,
3,332✔
303
  const vector<Mgxs*>& micros, const vector<double>& atom_densities,
304
  int num_group, int num_delay)
3,332✔
305
  : num_groups(num_group), num_delayed_groups(num_delay)
3,332✔
306
{
307
  // Get the minimum data needed to initialize:
308
  // Dont need awr, but lets just initialize it anyways
309
  double in_awr = -1.;
3,332✔
310
  // start with the assumption it is not fissionable and set
311
  // the fissionable status if we learn differently
312
  bool in_fissionable = false;
3,332✔
313
  for (int m = 0; m < micros.size(); m++) {
7,024✔
314
    if (micros[m]->fissionable)
3,692✔
315
      in_fissionable = true;
1,301✔
316
  }
317
  // Force all of the following data to be the same; these will be verified
318
  // to be true later
319
  AngleDistributionType in_scatter_format = micros[0]->scatter_format;
3,332✔
320
  bool in_is_isotropic = micros[0]->is_isotropic;
3,332✔
321
  vector<double> in_polar = micros[0]->polar;
3,332✔
322
  vector<double> in_azimuthal = micros[0]->azimuthal;
3,332✔
323

324
  init(in_name, in_awr, mat_kTs, in_fissionable, in_scatter_format,
3,332✔
325
    in_is_isotropic, in_polar, in_azimuthal);
326

327
  // Create the xs data for each temperature
328
  for (int t = 0; t < mat_kTs.size(); t++) {
6,694✔
329
    xs[t] = XsData(in_fissionable, in_scatter_format, in_polar.size(),
3,362✔
330
      in_azimuthal.size(), num_groups, num_delayed_groups);
3,362✔
331

332
    // Find the right temperature index to use
333
    double temp_desired = mat_kTs[t];
3,362✔
334

335
    // Create the list of temperature indices and interpolation factors for
336
    // each microscopic data at the material temperature
337
    vector<int> micro_t(micros.size(), 0);
3,362✔
338
    vector<double> micro_t_interp(micros.size(), 0.);
3,362✔
339
    for (int m = 0; m < micros.size(); m++) {
7,084✔
340
      switch (settings::temperature_method) {
3,722!
341
      case TemperatureMethod::NEAREST: {
3,542✔
342
        micro_t[m] = tensor::abs(micros[m]->kTs - temp_desired).argmin();
7,084✔
343
        auto temp_actual = micros[m]->kTs[micro_t[m]];
3,542!
344

345
        if (std::abs(temp_actual - temp_desired) >=
3,542!
346
            K_BOLTZMANN * settings::temperature_tolerance) {
3,542!
347
          fatal_error(fmt::format("MGXS Library does not contain cross section "
×
348
                                  "for {} at or near {} K.",
349
            name, std::round(temp_desired / K_BOLTZMANN)));
×
350
        }
351
      } break;
352
      case TemperatureMethod::INTERPOLATION:
353
        // Get a list of bounding temperatures for each actual temperature
354
        // present in the model
355
        for (int k = 0; k < micros[m]->kTs.shape(0) - 1; k++) {
720!
356
          if ((micros[m]->kTs[k] <= temp_desired) &&
180!
357
              (temp_desired < micros[m]->kTs[k + 1])) {
180!
358
            micro_t[m] = k;
180!
359
            if (k == 0) {
180!
360
              micro_t_interp[m] = (temp_desired - micros[m]->kTs[k]) /
180✔
361
                                  (micros[m]->kTs[k + 1] - micros[m]->kTs[k]);
180✔
362
            } else {
363
              micro_t_interp[m] = 1.;
×
364
            }
365
          }
366
        }
367
      } // end switch
368
    }   // end microscopic temperature loop
369

370
    // Now combine the microscopic data at each relevant temperature
371
    // We will do this by treating the multiple temperatures of a nuclide as
372
    // a different nuclide. Mathematically this just means the temperature
373
    // interpolant is included in the number density.
374
    // These interpolants are contained within interpolant.
375
    vector<double> interpolant;    // the interpolant for the Mgxs
6,724✔
376
    vector<int> temp_indices;      // the temperature index for each Mgxs
3,362✔
377
    vector<Mgxs*> mgxs_to_combine; // The Mgxs to combine
3,362✔
378
    // Now go through and build the above vectors so that we can use them to
379
    // combine the data. We will step through each microscopic data and
380
    // add in its lower and upper temperature points
381
    for (int m = 0; m < micros.size(); m++) {
7,084✔
382
      if (settings::temperature_method == TemperatureMethod::NEAREST) {
3,722✔
383
        // Nearest interpolation only has one temperature point per isotope
384
        // and so we dont need to include a temperature interpolant in
385
        // the interpolant vector
386
        interpolant.push_back(atom_densities[m]);
3,542✔
387
        temp_indices.push_back(micro_t[m]);
3,542✔
388
        mgxs_to_combine.push_back(micros[m]);
3,542✔
389
      } else {
390
        // This will be an interpolation between two points so get both these
391
        // points
392
        // Start with the low point
393
        interpolant.push_back((1. - micro_t_interp[m]) * atom_densities[m]);
180✔
394
        temp_indices.push_back(micro_t[m]);
180✔
395
        mgxs_to_combine.push_back(micros[m]);
180✔
396
        // The higher point
397
        interpolant.push_back((micro_t_interp[m]) * atom_densities[m]);
180✔
398
        temp_indices.push_back(micro_t[m] + 1);
180✔
399
        mgxs_to_combine.push_back(micros[m]);
180✔
400
      }
401
    }
402

403
    // And finally, combine the data
404
    combine(mgxs_to_combine, interpolant, temp_indices, t);
3,362✔
405
  } // end temperature (t) loop
3,362✔
406
}
3,332✔
407

408
//==============================================================================
409

410
void Mgxs::combine(const vector<Mgxs*>& micros, const vector<double>& scalars,
3,362✔
411
  const vector<int>& micro_ts, int this_t)
412
{
413
  // Build the vector of pointers to the xs objects within micros
414
  vector<XsData*> those_xs(micros.size());
3,362✔
415
  for (int i = 0; i < micros.size(); i++) {
7,264✔
416
    those_xs[i] = &(micros[i]->xs[micro_ts[i]]);
3,902✔
417
  }
418

419
  xs[this_t].combine(those_xs, scalars);
3,362✔
420
}
3,362✔
421

422
//==============================================================================
423

424
double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu,
2,147,483,647✔
425
  const int* dg, int t, int a)
426
{
427
  XsData* xs_t = &xs[t];
2,147,483,647!
428
  double val;
2,147,483,647✔
429
  std::string mgxs_type;
2,147,483,647!
430
  switch (xstype) {
2,147,483,647!
431
  case MgxsType::TOTAL:
20,646,631✔
432
    val = xs_t->total(a, gin);
20,646,631✔
433
    mgxs_type = "total";
20,646,631✔
434
    break;
435
  case MgxsType::NU_FISSION:
10,205,297✔
436
    val = fissionable ? xs_t->nu_fission(a, gin) : 0.;
10,205,297✔
437
    mgxs_type = "nu-fission";
10,205,297✔
438
    break;
439
  case MgxsType::ABSORPTION:
5,825,710✔
440
    val = xs_t->absorption(a, gin);
5,825,710✔
441
    mgxs_type = "absorption";
5,825,710✔
442
    break;
443
  case MgxsType::FISSION:
11,178,137✔
444
    val = fissionable ? xs_t->fission(a, gin) : 0.;
11,178,137✔
445
    mgxs_type = "fission";
11,178,137✔
446
    break;
447
  case MgxsType::KAPPA_FISSION:
10,449,101✔
448
    val = fissionable ? xs_t->kappa_fission(a, gin) : 0.;
10,449,101✔
449
    mgxs_type = "kappa-fission";
10,449,101✔
450
    break;
451
  case MgxsType::NU_SCATTER:
19,154,061✔
452
  case MgxsType::SCATTER:
19,154,061✔
453
  case MgxsType::NU_SCATTER_FMU:
19,154,061✔
454
  case MgxsType::SCATTER_FMU:
19,154,061✔
455
    val = xs_t->scatter[a]->get_xs(xstype, gin, gout, mu);
19,154,061✔
456
    mgxs_type = "scatter_data";
19,154,061✔
457
    break;
458
  case MgxsType::PROMPT_NU_FISSION:
10,196,486✔
459
    val = fissionable ? xs_t->prompt_nu_fission(a, gin) : 0.;
10,196,486✔
460
    mgxs_type = "prompt-nu-fission";
10,196,486✔
461
    break;
462
  case MgxsType::DELAYED_NU_FISSION:
71,369,612✔
463
    if (fissionable) {
71,369,612✔
464
      if (dg != nullptr) {
71,337,212✔
465
        val = xs_t->delayed_nu_fission(a, *dg, gin);
61,148,616✔
466
      } else {
467
        val = 0.;
468
        for (int d = 0; d < xs_t->delayed_nu_fission.shape(1); d++) {
142,640,344!
469
          val += xs_t->delayed_nu_fission(a, d, gin);
61,131,576✔
470
        }
471
      }
472
    } else {
473
      val = 0.;
474
    }
475
    mgxs_type = "delayed-nu-fission";
71,369,612✔
476
    break;
477
  case MgxsType::CHI:
16,701✔
478
    if (fissionable) {
16,701✔
479
      if (gout != nullptr) {
5,747!
480
        val = xs_t->chi(a, gin, *gout);
5,747✔
481
      } else {
482
        // provide an outgoing group-wise sum
483
        val = 0.;
NEW
484
        for (int g = 0; g < xs_t->chi.shape()[2]; g++) {
×
NEW
485
          val += xs_t->chi(a, gin, g);
×
486
        }
487
      }
488
    } else {
489
      val = 0.;
490
    }
491
    mgxs_type = "chi";
16,701✔
492
    break;
493
  case MgxsType::CHI_PROMPT:
7,890✔
494
    if (fissionable) {
7,890✔
495
      if (gout != nullptr) {
2,700!
496
        val = xs_t->chi_prompt(a, gin, *gout);
2,700✔
497
      } else {
498
        // provide an outgoing group-wise sum
499
        val = 0.;
500
        for (int g = 0; g < xs_t->chi_prompt.shape(2); g++) {
×
501
          val += xs_t->chi_prompt(a, gin, g);
×
502
        }
503
      }
504
    } else {
505
      val = 0.;
506
    }
507
    mgxs_type = "chi-prompt";
7,890✔
508
    break;
509
  case MgxsType::CHI_DELAYED:
49,440✔
510
    if (fissionable) {
49,440✔
511
      if (gout != nullptr) {
17,040!
512
        if (dg != nullptr) {
17,040!
513
          val = xs_t->chi_delayed(a, *dg, gin, *gout);
17,040✔
514
        } else {
515
          val = xs_t->chi_delayed(a, 0, gin, *gout);
×
516
        }
517
      } else {
518
        if (dg != nullptr) {
×
519
          val = 0.;
520
          for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) {
×
521
            val += xs_t->delayed_nu_fission(a, *dg, gin, g);
×
522
          }
523
        } else {
524
          val = 0.;
525
          for (int g = 0; g < xs_t->delayed_nu_fission.shape(2); g++) {
×
526
            for (int d = 0; d < xs_t->delayed_nu_fission.shape(3); d++) {
×
527
              val += xs_t->delayed_nu_fission(a, d, gin, g);
×
528
            }
529
          }
530
        }
531
      }
532
    } else {
533
      val = 0.;
534
    }
535
    mgxs_type = "chi-delayed";
49,440✔
536
    break;
537
  case MgxsType::INVERSE_VELOCITY:
2,079,479,746✔
538
    val = xs_t->inverse_velocity(a, gin);
2,079,479,746✔
539
    mgxs_type = "inverse-velocity";
2,079,479,746✔
540
    if (!(val > 0))
2,079,479,746✔
541
      val = data::mg.default_inverse_velocity_[gin];
2,061,510,000✔
542
    break;
543
  case MgxsType::DECAY_RATE:
61,138,159✔
544
    if (dg != nullptr) {
61,138,159!
545
      val = xs_t->decay_rate(a, *dg);
61,138,159✔
546
    } else {
547
      val = xs_t->decay_rate(a, 0);
×
548
    }
549
    mgxs_type = "decay-rate";
61,138,159✔
550
    break;
551
  default:
552
    val = 0.;
553
  }
554
  // TODO: need to add more robust handling of zero-values cross sections that
555
  // produce NANs on normalization
556
  if (val != val) {
2,147,483,647✔
557
    warning(
1,620✔
558
      fmt::format("The {} cross section for this material has a nan present "
1,944✔
559
                  "(possibly due to a division by zero). Setting to zero...",
560
        mgxs_type));
561
    val = 0;
1,620✔
562
  }
563
  return val;
2,147,483,647✔
564
}
2,147,483,647✔
565

566
//==============================================================================
567

568
void Mgxs::sample_fission_energy(
111,089,935✔
569
  int gin, int& dg, int& gout, uint64_t* seed, int t, int a)
570
{
571
  XsData* xs_t = &xs[t];
111,089,935✔
572
  double nu_fission = xs_t->nu_fission(a, gin);
111,089,935✔
573

574
  // Find the probability of having a prompt neutron
575
  double prob_prompt = xs_t->prompt_nu_fission(a, gin);
111,089,935✔
576

577
  // sample random numbers
578
  double xi_pd = prn(seed) * nu_fission;
111,089,935✔
579
  double xi_gout = prn(seed);
111,089,935✔
580

581
  // Select whether the neutron is prompt or delayed
582
  if (xi_pd <= prob_prompt) {
111,089,935✔
583
    // the neutron is prompt
584

585
    // set the delayed group for the particle to be -1, indicating prompt
586
    dg = -1;
111,088,428✔
587

588
    // sample the outgoing energy group
589
    double prob_gout = 0.;
111,088,428✔
590
    for (gout = 0; gout < num_groups; ++gout) {
111,140,150!
591
      prob_gout += xs_t->chi_prompt(a, gin, gout);
111,140,150✔
592
      if (xi_gout < prob_gout)
111,140,150✔
593
        break;
594
    }
595

596
  } else {
597
    // the neutron is delayed
598

599
    // get the delayed group
600
    for (dg = 0; dg < num_delayed_groups; ++dg) {
3,795!
601
      prob_prompt += xs_t->delayed_nu_fission(a, dg, gin);
3,795✔
602
      if (xi_pd < prob_prompt)
3,795✔
603
        break;
604
    }
605

606
    // adjust dg in case of round-off error
607
    dg = std::min(dg, num_delayed_groups - 1);
1,507!
608

609
    // sample the outgoing energy group
610
    double prob_gout = 0.;
1,507✔
611
    for (gout = 0; gout < num_groups; ++gout) {
1,507!
612
      prob_gout += xs_t->chi_delayed(a, dg, gin, gout);
1,507✔
613
      if (xi_gout < prob_gout)
1,507!
614
        break;
615
    }
616
  }
617
}
111,089,935✔
618

619
//==============================================================================
620

621
void Mgxs::sample_scatter(
1,669,415,704✔
622
  int gin, int& gout, double& mu, double& wgt, uint64_t* seed, int t, int a)
623
{
624
  // Sample the data
625
  xs[t].scatter[a]->sample(gin, gout, mu, wgt, seed);
1,669,415,704✔
626
}
1,669,415,704✔
627

628
//==============================================================================
629

630
void Mgxs::calculate_xs(Particle& p)
2,063,937,744✔
631
{
632
  // If the material is different, then we need to do a full lookup
633
  if (p.material() != p.mg_xs_cache().material) {
2,063,937,744✔
634
    set_temperature_index(p);
112,278,953✔
635
    set_angle_index(p);
112,278,953✔
636
    p.mg_xs_cache().material = p.material();
112,278,953✔
637
  } else {
638
    // If material is the same, but temperature is different, need to
639
    // find the new temperature index
640
    if (p.sqrtkT() != p.mg_xs_cache().sqrtkT) {
1,951,658,791✔
641
      set_temperature_index(p);
2,227,056✔
642
    }
643
    // If the material is the same, but angle is different, need to
644
    // find the new angle index
645
    if (p.u_local() != p.mg_xs_cache().u) {
1,951,658,791✔
646
      set_angle_index(p);
1,951,658,791✔
647
    }
648
  }
649
  int temperature = p.mg_xs_cache().t;
2,063,937,744✔
650
  int angle = p.mg_xs_cache().a;
2,063,937,744✔
651
  p.macro_xs().total = xs[temperature].total(angle, p.g()) * p.density_mult();
2,063,937,744✔
652
  p.macro_xs().absorption =
2,063,937,744✔
653
    xs[temperature].absorption(angle, p.g()) * p.density_mult();
2,063,937,744✔
654
  p.macro_xs().nu_fission =
2,147,483,647✔
655
    fissionable ? xs[temperature].nu_fission(angle, p.g()) * p.density_mult()
2,063,937,744✔
656
                : 0.;
657
}
2,063,937,744✔
658

659
//==============================================================================
660

661
bool Mgxs::equiv(const Mgxs& that)
×
662
{
663
  return (
×
664
    (num_delayed_groups == that.num_delayed_groups) &&
×
665
    (num_groups == that.num_groups) && (n_pol == that.n_pol) &&
×
666
    (n_azi == that.n_azi) &&
×
667
    (std::equal(polar.begin(), polar.end(), that.polar.begin())) &&
×
668
    (std::equal(azimuthal.begin(), azimuthal.end(), that.azimuthal.begin())) &&
×
669
    (scatter_format == that.scatter_format));
×
670
}
671

672
//==============================================================================
673

674
int Mgxs::get_temperature_index(double sqrtkT) const
127,102,913✔
675
{
676
  return tensor::abs(kTs - sqrtkT * sqrtkT).argmin();
381,308,739✔
677
}
678

679
//==============================================================================
680

681
void Mgxs::set_temperature_index(Particle& p)
114,506,009✔
682
{
683
  p.mg_xs_cache().t = get_temperature_index(p.sqrtkT());
114,506,009✔
684
  p.mg_xs_cache().sqrtkT = p.sqrtkT();
114,506,009✔
685
}
114,506,009✔
686

687
//==============================================================================
688

689
int Mgxs::get_angle_index(const Direction& u) const
2,134,808,533✔
690
{
691
  if (is_isotropic) {
2,134,808,533✔
692
    return 0;
693
  } else {
694
    // convert direction to polar and azimuthal angles
695
    double my_pol = std::acos(u.z);
44,242✔
696
    double my_azi = std::atan2(u.y, u.x);
44,242✔
697

698
    // Find the location, assuming equal-bin angles
699
    double delta_angle = PI / n_pol;
44,242✔
700
    int p = std::floor(my_pol / delta_angle);
44,242✔
701
    delta_angle = 2. * PI / n_azi;
44,242✔
702
    int a = std::floor((my_azi + PI) / delta_angle);
44,242✔
703

704
    return n_azi * p + a;
44,242✔
705
  }
706
}
707

708
//==============================================================================
709

710
void Mgxs::set_angle_index(Particle& p)
2,063,937,744✔
711
{
712
  // See if we need to find the new index
713
  if (!is_isotropic) {
2,063,937,744✔
714
    p.mg_xs_cache().a = get_angle_index(p.u_local());
22,121✔
715
    p.mg_xs_cache().u = p.u_local();
22,121✔
716
  }
717
}
2,063,937,744✔
718

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