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

openmc-dev / openmc / 9617074623

21 Jun 2024 04:53PM UTC coverage: 84.715% (+0.03%) from 84.688%
9617074623

Pull #3042

github

web-flow
Merge 47e50a3a9 into 4bd0b09e6
Pull Request #3042: Rely on std::filesystem for file_utils

26 of 31 new or added lines in 5 files covered. (83.87%)

921 existing lines in 34 files now uncovered.

48900 of 57723 relevant lines covered (84.71%)

31496763.14 hits per line

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

87.94
/src/nuclide.cpp
1
#include "openmc/nuclide.h"
2

3
#include "openmc/capi.h"
4
#include "openmc/container_util.h"
5
#include "openmc/cross_sections.h"
6
#include "openmc/endf.h"
7
#include "openmc/error.h"
8
#include "openmc/hdf5_interface.h"
9
#include "openmc/message_passing.h"
10
#include "openmc/photon.h"
11
#include "openmc/random_lcg.h"
12
#include "openmc/search.h"
13
#include "openmc/settings.h"
14
#include "openmc/simulation.h"
15
#include "openmc/string_utils.h"
16
#include "openmc/thermal.h"
17

18
#include <fmt/core.h>
19

20
#include "xtensor/xbuilder.hpp"
21
#include "xtensor/xview.hpp"
22

23
#include <algorithm> // for sort, min_element
24
#include <string>    // for to_string, stoi
25

26
namespace openmc {
27

28
//==============================================================================
29
// Global variables
30
//==============================================================================
31

32
namespace data {
33
array<double, 2> energy_min {0.0, 0.0};
34
array<double, 2> energy_max {INFTY, INFTY};
35
double temperature_min {INFTY};
36
double temperature_max {0.0};
37
std::unordered_map<std::string, int> nuclide_map;
38
vector<unique_ptr<Nuclide>> nuclides;
39
} // namespace data
40

41
//==============================================================================
42
// Nuclide implementation
43
//==============================================================================
44

45
int Nuclide::XS_TOTAL {0};
46
int Nuclide::XS_ABSORPTION {1};
47
int Nuclide::XS_FISSION {2};
48
int Nuclide::XS_NU_FISSION {3};
49
int Nuclide::XS_PHOTON_PROD {4};
50

51
Nuclide::Nuclide(hid_t group, const vector<double>& temperature)
27,330✔
52
{
53
  // Set index of nuclide in global vector
54
  index_ = data::nuclides.size();
27,330✔
55

56
  // Get name of nuclide from group, removing leading '/'
57
  name_ = object_name(group).substr(1);
27,330✔
58
  data::nuclide_map[name_] = index_;
27,330✔
59

60
  read_attribute(group, "Z", Z_);
27,330✔
61
  read_attribute(group, "A", A_);
27,330✔
62
  read_attribute(group, "metastable", metastable_);
27,330✔
63
  read_attribute(group, "atomic_weight_ratio", awr_);
27,330✔
64

65
  if (settings::run_mode == RunMode::VOLUME) {
27,330✔
66
    // Determine whether nuclide is fissionable and then exit
67
    int mt;
68
    hid_t rxs_group = open_group(group, "reactions");
523✔
69
    for (auto name : group_names(rxs_group)) {
18,837✔
70
      if (starts_with(name, "reaction_")) {
18,493✔
71
        hid_t rx_group = open_group(rxs_group, name.c_str());
18,493✔
72
        read_attribute(rx_group, "mt", mt);
18,493✔
73
        if (is_fission(mt)) {
18,493✔
74
          fissionable_ = true;
179✔
75
          break;
179✔
76
        }
77
      }
78
    }
18,837✔
79
    return;
523✔
80
  }
81

82
  // Determine temperatures available
83
  hid_t kT_group = open_group(group, "kTs");
26,807✔
84
  auto dset_names = dataset_names(kT_group);
26,807✔
85
  vector<double> temps_available;
26,807✔
86
  for (const auto& name : dset_names) {
53,974✔
87
    double T;
88
    read_dataset(kT_group, name.c_str(), T);
27,167✔
89
    temps_available.push_back(T / K_BOLTZMANN);
27,167✔
90
  }
91
  std::sort(temps_available.begin(), temps_available.end());
26,807✔
92

93
  // If only one temperature is available, revert to nearest temperature
94
  if (temps_available.size() == 1 &&
53,434✔
95
      settings::temperature_method == TemperatureMethod::INTERPOLATION) {
26,627✔
UNCOV
96
    if (mpi::master) {
×
UNCOV
97
      warning("Cross sections for " + name_ +
×
98
              " are only available at one "
99
              "temperature. Reverting to nearest temperature method.");
100
    }
UNCOV
101
    settings::temperature_method = TemperatureMethod::NEAREST;
×
102
  }
103

104
  // Determine actual temperatures to read -- start by checking whether a
105
  // temperature range was given (indicated by T_max > 0), in which case all
106
  // temperatures in the range are loaded irrespective of what temperatures
107
  // actually appear in the model
108
  vector<int> temps_to_read;
26,807✔
109
  int n = temperature.size();
26,807✔
110
  double T_min = n > 0 ? settings::temperature_range[0] : 0.0;
26,807✔
111
  double T_max = n > 0 ? settings::temperature_range[1] : INFTY;
26,807✔
112
  if (T_max > 0.0) {
26,807✔
113
    // Determine first available temperature below or equal to T_min
114
    auto T_min_it =
115
      std::upper_bound(temps_available.begin(), temps_available.end(), T_min);
1,439✔
116
    if (T_min_it != temps_available.begin())
1,439✔
UNCOV
117
      --T_min_it;
×
118

119
    // Determine first available temperature above or equal to T_max
120
    auto T_max_it =
121
      std::lower_bound(temps_available.begin(), temps_available.end(), T_max);
1,439✔
122
    if (T_max_it != temps_available.end())
1,439✔
UNCOV
123
      ++T_max_it;
×
124

125
    // Add corresponding temperatures to vector
126
    for (auto it = T_min_it; it != T_max_it; ++it) {
2,878✔
127
      temps_to_read.push_back(std::round(*it));
1,439✔
128
    }
129
  }
130

131
  switch (settings::temperature_method) {
26,807✔
132
  case TemperatureMethod::NEAREST:
26,675✔
133
    // Find nearest temperatures
134
    for (double T_desired : temperature) {
51,989✔
135

136
      // Determine closest temperature
137
      double min_delta_T = INFTY;
25,314✔
138
      double T_actual = 0.0;
25,314✔
139
      for (auto T : temps_available) {
50,724✔
140
        double delta_T = std::abs(T - T_desired);
25,410✔
141
        if (delta_T < min_delta_T) {
25,410✔
142
          T_actual = T;
25,350✔
143
          min_delta_T = delta_T;
25,350✔
144
        }
145
      }
146

147
      if (std::abs(T_actual - T_desired) < settings::temperature_tolerance) {
25,314✔
148
        if (!contains(temps_to_read, std::round(T_actual))) {
25,314✔
149
          temps_to_read.push_back(std::round(T_actual));
25,236✔
150

151
          // Write warning for resonance scattering data if 0K is not available
152
          if (std::abs(T_actual - T_desired) > 0 && T_desired == 0 &&
25,236✔
153
              mpi::master) {
UNCOV
154
            warning(name_ +
×
155
                    " does not contain 0K data needed for resonance "
UNCOV
156
                    "scattering options selected. Using data at " +
×
UNCOV
157
                    std::to_string(T_actual) + " K instead.");
×
158
          }
159
        }
160
      } else {
UNCOV
161
        fatal_error(
×
UNCOV
162
          "Nuclear data library does not contain cross sections for " + name_ +
×
UNCOV
163
          " at or near " + std::to_string(T_desired) + " K.");
×
164
      }
165
    }
166
    break;
26,675✔
167

168
  case TemperatureMethod::INTERPOLATION:
132✔
169
    // If temperature interpolation or multipole is selected, get a list of
170
    // bounding temperatures for each actual temperature present in the model
171
    for (double T_desired : temperature) {
276✔
172
      bool found_pair = false;
144✔
173
      for (int j = 0; j < temps_available.size() - 1; ++j) {
432✔
174
        if (temps_available[j] <= T_desired &&
468✔
175
            T_desired < temps_available[j + 1]) {
180✔
176
          int T_j = std::round(temps_available[j]);
84✔
177
          int T_j1 = std::round(temps_available[j + 1]);
84✔
178
          if (!contains(temps_to_read, T_j)) {
84✔
179
            temps_to_read.push_back(T_j);
84✔
180
          }
181
          if (!contains(temps_to_read, T_j1)) {
84✔
182
            temps_to_read.push_back(T_j1);
72✔
183
          }
184
          found_pair = true;
84✔
185
        }
186
      }
187

188
      if (!found_pair) {
144✔
189
        // If no pairs found, check if the desired temperature falls just
190
        // outside of data
191
        if (std::abs(T_desired - temps_available.front()) <=
60✔
192
            settings::temperature_tolerance) {
193
          if (!contains(temps_to_read, temps_available.front())) {
36✔
194
            temps_to_read.push_back(std::round(temps_available.front()));
36✔
195
          }
196
          continue;
36✔
197
        }
198
        if (std::abs(T_desired - temps_available.back()) <=
24✔
199
            settings::temperature_tolerance) {
200
          if (!contains(temps_to_read, temps_available.back())) {
24✔
201
            temps_to_read.push_back(std::round(temps_available.back()));
24✔
202
          }
203
          continue;
24✔
204
        }
UNCOV
205
        fatal_error(
×
UNCOV
206
          "Nuclear data library does not contain cross sections for " + name_ +
×
UNCOV
207
          " at temperatures that bound " + std::to_string(T_desired) + " K.");
×
208
      }
209
    }
210
    break;
132✔
211
  }
212

213
  // Sort temperatures to read
214
  std::sort(temps_to_read.begin(), temps_to_read.end());
26,807✔
215

216
  data::temperature_min =
26,807✔
217
    std::min(data::temperature_min, static_cast<double>(temps_to_read.front()));
26,807✔
218
  data::temperature_max =
26,807✔
219
    std::max(data::temperature_max, static_cast<double>(temps_to_read.back()));
26,807✔
220

221
  hid_t energy_group = open_group(group, "energy");
26,807✔
222
  for (const auto& T : temps_to_read) {
53,698✔
223
    std::string dset {std::to_string(T) + "K"};
26,891✔
224

225
    // Determine exact kT values
226
    double kT;
227
    read_dataset(kT_group, dset.c_str(), kT);
26,891✔
228
    kTs_.push_back(kT);
26,891✔
229

230
    // Read energy grid
231
    grid_.emplace_back();
26,891✔
232
    read_dataset(energy_group, dset.c_str(), grid_.back().energy);
26,891✔
233
  }
26,891✔
234
  close_group(kT_group);
26,807✔
235

236
  // Check for 0K energy grid
237
  if (object_exists(energy_group, "0K")) {
26,807✔
238
    read_dataset(energy_group, "0K", energy_0K_);
5,057✔
239
  }
240
  close_group(energy_group);
26,807✔
241

242
  // Read reactions
243
  hid_t rxs_group = open_group(group, "reactions");
26,807✔
244
  for (auto name : group_names(rxs_group)) {
1,266,713✔
245
    if (starts_with(name, "reaction_")) {
1,239,906✔
246
      hid_t rx_group = open_group(rxs_group, name.c_str());
1,239,906✔
247
      reactions_.push_back(make_unique<Reaction>(rx_group, temps_to_read));
1,239,906✔
248

249
      // Check for 0K elastic scattering
250
      const auto& rx = reactions_.back();
1,239,906✔
251
      if (rx->mt_ == ELASTIC) {
1,239,906✔
252
        if (object_exists(rx_group, "0K")) {
26,807✔
253
          hid_t temp_group = open_group(rx_group, "0K");
5,057✔
254
          read_dataset(temp_group, "xs", elastic_0K_);
5,057✔
255
          close_group(temp_group);
5,057✔
256
        }
257
      }
258
      close_group(rx_group);
1,239,906✔
259

260
      // Determine reaction indices for inelastic scattering reactions
261
      if (is_inelastic_scatter(rx->mt_) && !rx->redundant_) {
1,239,906✔
262
        index_inelastic_scatter_.push_back(reactions_.size() - 1);
625,033✔
263
      }
264
    }
265
  }
1,266,713✔
266
  close_group(rxs_group);
26,807✔
267

268
  // Read unresolved resonance probability tables if present
269
  if (object_exists(group, "urr")) {
26,807✔
270
    urr_present_ = true;
12,854✔
271
    urr_data_.reserve(temps_to_read.size());
12,854✔
272

273
    for (int i = 0; i < temps_to_read.size(); i++) {
25,708✔
274
      // Get temperature as a string
275
      std::string temp_str {std::to_string(temps_to_read[i]) + "K"};
12,854✔
276

277
      // Read probability tables for i-th temperature
278
      hid_t urr_group = open_group(group, ("urr/" + temp_str).c_str());
12,854✔
279
      urr_data_.emplace_back(urr_group);
12,854✔
280
      close_group(urr_group);
12,854✔
281

282
      // Check for negative values
283
      if (urr_data_[i].has_negative() && mpi::master) {
12,854✔
UNCOV
284
        warning("Negative value(s) found on probability table for nuclide " +
×
UNCOV
285
                name_ + " at " + temp_str);
×
286
      }
287
    }
12,854✔
288

289
    // If the inelastic competition flag indicates that the inelastic cross
290
    // section should be determined from a normal reaction cross section, we
291
    // need to get the index of the reaction.
292
    if (temps_to_read.size() > 0) {
12,854✔
293
      // Make sure inelastic flags are consistent for different temperatures
294
      for (int i = 0; i < urr_data_.size() - 1; ++i) {
12,854✔
UNCOV
295
        if (urr_data_[i].inelastic_flag_ != urr_data_[i + 1].inelastic_flag_) {
×
UNCOV
296
          fatal_error(fmt::format(
×
297
            "URR inelastic flag is not consistent for "
298
            "multiple temperatures in nuclide {}. This most likely indicates "
299
            "a problem in how the data was processed.",
300
            name_));
×
301
        }
302
      }
303

304
      if (urr_data_[0].inelastic_flag_ > 0) {
12,854✔
305
        for (int i = 0; i < reactions_.size(); i++) {
406,808✔
306
          if (reactions_[i]->mt_ == urr_data_[0].inelastic_flag_) {
399,477✔
307
            urr_inelastic_ = i;
7,331✔
308
          }
309
        }
310

311
        // Abort if no corresponding inelastic reaction was found
312
        if (urr_inelastic_ == C_NONE) {
7,331✔
UNCOV
313
          fatal_error("Could no find inelastic reaction specified on "
×
314
                      "unresolved resonance probability table.");
315
        }
316
      }
317
    }
318
  }
319

320
  // Check for total nu data
321
  if (object_exists(group, "total_nu")) {
26,807✔
322
    // Read total nu data
323
    hid_t nu_group = open_group(group, "total_nu");
7,008✔
324
    total_nu_ = read_function(nu_group, "yield");
7,008✔
325
    close_group(nu_group);
7,008✔
326
  }
327

328
  // Read fission energy release data if present
329
  if (object_exists(group, "fission_energy_release")) {
26,807✔
330
    hid_t fer_group = open_group(group, "fission_energy_release");
7,008✔
331
    fission_q_prompt_ = read_function(fer_group, "q_prompt");
7,008✔
332
    fission_q_recov_ = read_function(fer_group, "q_recoverable");
7,008✔
333

334
    // Read fission fragment and delayed beta energy release. This is needed for
335
    // energy normalization in k-eigenvalue calculations
336
    fragments_ = read_function(fer_group, "fragments");
7,008✔
337
    betas_ = read_function(fer_group, "betas");
7,008✔
338

339
    // We need prompt/delayed photon energy release for scaling fission photon
340
    // production
341
    prompt_photons_ = read_function(fer_group, "prompt_photons");
7,008✔
342
    delayed_photons_ = read_function(fer_group, "delayed_photons");
7,008✔
343
    close_group(fer_group);
7,008✔
344
  }
345

346
  this->create_derived(prompt_photons_.get(), delayed_photons_.get());
26,807✔
347
}
26,807✔
348

349
Nuclide::~Nuclide()
27,330✔
350
{
351
  data::nuclide_map.erase(name_);
27,330✔
352
}
27,330✔
353

354
void Nuclide::create_derived(
26,807✔
355
  const Function1D* prompt_photons, const Function1D* delayed_photons)
356
{
357
  for (const auto& grid : grid_) {
53,698✔
358
    // Allocate and initialize cross section
359
    array<size_t, 2> shape {grid.energy.size(), 5};
26,891✔
360
    xs_.emplace_back(shape, 0.0);
26,891✔
361
  }
362

363
  reaction_index_.fill(C_NONE);
26,807✔
364
  for (int i = 0; i < reactions_.size(); ++i) {
1,266,713✔
365
    const auto& rx {reactions_[i]};
1,239,906✔
366

367
    // Set entry in direct address table for reaction
368
    reaction_index_[rx->mt_] = i;
1,239,906✔
369

370
    for (int t = 0; t < kTs_.size(); ++t) {
2,480,064✔
371
      int j = rx->xs_[t].threshold;
1,240,158✔
372
      int n = rx->xs_[t].value.size();
1,240,158✔
373
      auto xs = xt::adapt(rx->xs_[t].value);
1,240,158✔
374

375
      for (const auto& p : rx->products_) {
4,470,296✔
376
        if (p.particle_ == ParticleType::photon) {
3,230,138✔
377
          auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD);
2,491,949✔
378
          for (int k = 0; k < n; ++k) {
2,147,483,647✔
379
            double E = grid_[t].energy[k + j];
2,147,483,647✔
380

381
            // For fission, artificially increase the photon yield to account
382
            // for delayed photons
383
            double f = 1.0;
2,147,483,647✔
384
            if (settings::delayed_photon_scaling) {
2,147,483,647✔
385
              if (is_fission(rx->mt_)) {
2,147,483,647✔
386
                if (prompt_photons && delayed_photons) {
559,035,927✔
387
                  double energy_prompt = (*prompt_photons)(E);
559,035,927✔
388
                  double energy_delayed = (*delayed_photons)(E);
559,035,927✔
389
                  f = (energy_prompt + energy_delayed) / (energy_prompt);
559,035,927✔
390
                }
391
              }
392
            }
393

394
            pprod[k] += f * xs[k] * (*p.yield_)(E);
2,147,483,647✔
395
          }
396
        }
2,491,949✔
397
      }
398

399
      // Skip redundant reactions
400
      if (rx->redundant_)
1,240,158✔
401
        continue;
167,338✔
402

403
      // Add contribution to total cross section
404
      auto total = xt::view(xs_[t], xt::range(j, j + n), XS_TOTAL);
1,072,820✔
405
      total += xs;
1,072,820✔
406

407
      // Add contribution to absorption cross section
408
      auto absorption = xt::view(xs_[t], xt::range(j, j + n), XS_ABSORPTION);
1,072,820✔
409
      if (is_disappearance(rx->mt_)) {
1,072,820✔
410
        absorption += xs;
408,317✔
411
      }
412

413
      if (is_fission(rx->mt_)) {
1,072,820✔
414
        fissionable_ = true;
12,579✔
415
        auto fission = xt::view(xs_[t], xt::range(j, j + n), XS_FISSION);
12,579✔
416
        fission += xs;
12,579✔
417
        absorption += xs;
12,579✔
418

419
        // Keep track of fission reactions
420
        if (t == 0) {
12,579✔
421
          fission_rx_.push_back(rx.get());
12,495✔
422
          if (rx->mt_ == N_F)
12,495✔
423
            has_partial_fission_ = true;
1,769✔
424
        }
425
      }
12,579✔
426
    }
1,240,158✔
427
  }
428

429
  // Determine number of delayed neutron precursors
430
  if (fissionable_) {
26,807✔
431
    for (const auto& product : fission_rx_[0]->products_) {
61,408✔
432
      if (product.emission_mode_ == EmissionMode::delayed) {
54,220✔
433
        ++n_precursor_;
41,844✔
434
      }
435
    }
436
  }
437

438
  // Calculate nu-fission cross section
439
  for (int t = 0; t < kTs_.size(); ++t) {
53,698✔
440
    if (fissionable_) {
26,891✔
441
      int n = grid_[t].energy.size();
7,272✔
442
      for (int i = 0; i < n; ++i) {
560,015,378✔
443
        double E = grid_[t].energy[i];
560,008,106✔
444
        xs_[t](i, XS_NU_FISSION) =
560,008,106✔
445
          nu(E, EmissionMode::total) * xs_[t](i, XS_FISSION);
560,008,106✔
446
      }
447
    }
448
  }
449

450
  if (settings::res_scat_on) {
26,807✔
451
    // Determine if this nuclide should be treated as a resonant scatterer
452
    if (!settings::res_scat_nuclides.empty()) {
68✔
453
      // If resonant nuclides were specified, check the list explicitly
454
      for (const auto& name : settings::res_scat_nuclides) {
170✔
455
        if (name_ == name) {
153✔
456
          resonant_ = true;
51✔
457

458
          // Make sure nuclide has 0K data
459
          if (energy_0K_.empty()) {
51✔
UNCOV
460
            fatal_error("Cannot treat " + name_ +
×
461
                        " as a resonant scatterer "
462
                        "because 0 K elastic scattering data is not present.");
463
          }
464
          break;
51✔
465
        }
466
      }
467
    } else {
468
      // Otherwise, assume that any that have 0 K elastic scattering data are
469
      // resonant
UNCOV
470
      resonant_ = !energy_0K_.empty();
×
471
    }
472

473
    if (resonant_) {
68✔
474
      // Build CDF for 0K elastic scattering
475
      double xs_cdf_sum = 0.0;
51✔
476
      xs_cdf_.resize(energy_0K_.size());
51✔
477
      xs_cdf_[0] = 0.0;
51✔
478

479
      const auto& E = energy_0K_;
51✔
480
      auto& xs = elastic_0K_;
51✔
481
      for (int i = 0; i < E.size() - 1; ++i) {
9,469,612✔
482
        // Negative cross sections result in a CDF that is not monotonically
483
        // increasing. Set all negative xs values to zero.
484
        if (xs[i] < 0.0)
9,469,561✔
UNCOV
485
          xs[i] = 0.0;
×
486

487
        // build xs cdf
488
        xs_cdf_sum +=
9,469,561✔
489
          (std::sqrt(E[i]) * xs[i] + std::sqrt(E[i + 1]) * xs[i + 1]) / 2.0 *
9,469,561✔
490
          (E[i + 1] - E[i]);
9,469,561✔
491
        xs_cdf_[i + 1] = xs_cdf_sum;
9,469,561✔
492
      }
493
    }
494
  }
495
}
26,807✔
496

497
void Nuclide::init_grid()
28,678✔
498
{
499
  int neutron = static_cast<int>(ParticleType::neutron);
28,678✔
500
  double E_min = data::energy_min[neutron];
28,678✔
501
  double E_max = data::energy_max[neutron];
28,678✔
502
  int M = settings::n_log_bins;
28,678✔
503

504
  // Determine equal-logarithmic energy spacing
505
  double spacing = std::log(E_max / E_min) / M;
28,678✔
506

507
  // Create equally log-spaced energy grid
508
  auto umesh = xt::linspace(0.0, M * spacing, M + 1);
28,678✔
509

510
  for (auto& grid : grid_) {
57,440✔
511
    // Resize array for storing grid indices
512
    grid.grid_index.resize(M + 1);
28,762✔
513

514
    // Determine corresponding indices in nuclide grid to energies on
515
    // equal-logarithmic grid
516
    int j = 0;
28,762✔
517
    for (int k = 0; k <= M; ++k) {
230,765,524✔
518
      while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) {
951,213,074✔
519
        // Ensure that for isotopes where maxval(grid.energy) << E_max that
520
        // there are no out-of-bounds issues.
521
        if (j + 2 == grid.energy.size())
720,493,912✔
522
          break;
17,600✔
523
        ++j;
720,476,312✔
524
      }
525
      grid.grid_index[k] = j;
230,736,762✔
526
    }
527
  }
528
}
28,678✔
529

530
double Nuclide::nu(double E, EmissionMode mode, int group) const
881,463,353✔
531
{
532
  if (!fissionable_)
881,463,353✔
533
    return 0.0;
31,816,896✔
534

535
  switch (mode) {
849,646,457✔
536
  case EmissionMode::prompt:
8,198,016✔
537
    return (*fission_rx_[0]->products_[0].yield_)(E);
8,198,016✔
538
  case EmissionMode::delayed:
147,765,856✔
539
    if (n_precursor_ > 0 && settings::create_delayed_neutrons) {
147,765,856✔
540
      auto rx = fission_rx_[0];
145,732,624✔
541
      if (group >= 1 && group < rx->products_.size()) {
145,732,624✔
542
        // If delayed group specified, determine yield immediately
543
        return (*rx->products_[group].yield_)(E);
117,120,960✔
544
      } else {
545
        double nu {0.0};
28,611,664✔
546

547
        for (int i = 1; i < rx->products_.size(); ++i) {
227,592,589✔
548
          // Skip any non-neutron products
549
          const auto& product = rx->products_[i];
198,980,925✔
550
          if (product.particle_ != ParticleType::neutron)
198,980,925✔
551
            continue;
27,310,941✔
552

553
          // Evaluate yield
554
          if (product.emission_mode_ == EmissionMode::delayed) {
171,669,984✔
555
            nu += (*product.yield_)(E);
171,669,984✔
556
          }
557
        }
558
        return nu;
28,611,664✔
559
      }
560
    } else {
561
      return 0.0;
2,033,232✔
562
    }
563
  case EmissionMode::total:
693,682,585✔
564
    if (total_nu_ && settings::create_delayed_neutrons) {
693,682,585✔
565
      return (*total_nu_)(E);
691,791,433✔
566
    } else {
567
      return (*fission_rx_[0]->products_[0].yield_)(E);
1,891,152✔
568
    }
569
  }
UNCOV
570
  UNREACHABLE();
×
571
}
572

573
void Nuclide::calculate_elastic_xs(Particle& p) const
798,281,455✔
574
{
575
  // Get temperature index, grid index, and interpolation factor
576
  auto& micro {p.neutron_xs(index_)};
798,281,455✔
577
  int i_temp = micro.index_temp;
798,281,455✔
578
  int i_grid = micro.index_grid;
798,281,455✔
579
  double f = micro.interp_factor;
798,281,455✔
580

581
  if (i_temp >= 0) {
798,281,455✔
582
    const auto& xs = reactions_[0]->xs_[i_temp].value;
794,769,463✔
583
    micro.elastic = (1.0 - f) * xs[i_grid] + f * xs[i_grid + 1];
794,769,463✔
584
  }
585
}
798,281,455✔
586

587
double Nuclide::elastic_xs_0K(double E) const
×
588
{
589
  // Determine index on nuclide energy grid
590
  int i_grid;
UNCOV
591
  if (E < energy_0K_.front()) {
×
592
    i_grid = 0;
×
UNCOV
593
  } else if (E > energy_0K_.back()) {
×
UNCOV
594
    i_grid = energy_0K_.size() - 2;
×
595
  } else {
UNCOV
596
    i_grid = lower_bound_index(energy_0K_.begin(), energy_0K_.end(), E);
×
597
  }
598

599
  // check for rare case where two energy points are the same
UNCOV
600
  if (energy_0K_[i_grid] == energy_0K_[i_grid + 1])
×
UNCOV
601
    ++i_grid;
×
602

603
  // calculate interpolation factor
604
  double f =
UNCOV
605
    (E - energy_0K_[i_grid]) / (energy_0K_[i_grid + 1] - energy_0K_[i_grid]);
×
606

607
  // Calculate microscopic nuclide elastic cross section
UNCOV
608
  return (1.0 - f) * elastic_0K_[i_grid] + f * elastic_0K_[i_grid + 1];
×
609
}
610

611
void Nuclide::calculate_xs(
2,147,483,647✔
612
  int i_sab, int i_log_union, double sab_frac, Particle& p)
613
{
614
  auto& micro {p.neutron_xs(index_)};
2,147,483,647✔
615

616
  // Initialize cached cross sections to zero
617
  micro.elastic = CACHE_INVALID;
2,147,483,647✔
618
  micro.thermal = 0.0;
2,147,483,647✔
619
  micro.thermal_elastic = 0.0;
2,147,483,647✔
620

621
  // Check to see if there is multipole data present at this energy
622
  bool use_mp = false;
2,147,483,647✔
623
  if (multipole_) {
2,147,483,647✔
624
    use_mp = (p.E() >= multipole_->E_min_ && p.E() <= multipole_->E_max_);
9,948,480✔
625
  }
626

627
  // Evaluate multipole or interpolate
628
  if (use_mp) {
2,147,483,647✔
629
    // Call multipole kernel
630
    double sig_s, sig_a, sig_f;
631
    std::tie(sig_s, sig_a, sig_f) = multipole_->evaluate(p.E(), p.sqrtkT());
9,579,864✔
632

633
    micro.total = sig_s + sig_a;
9,579,864✔
634
    micro.elastic = sig_s;
9,579,864✔
635
    micro.absorption = sig_a;
9,579,864✔
636
    micro.fission = sig_f;
9,579,864✔
637
    micro.nu_fission =
9,579,864✔
638
      fissionable_ ? sig_f * this->nu(p.E(), EmissionMode::total) : 0.0;
9,579,864✔
639

640
    if (simulation::need_depletion_rx) {
9,579,864✔
641
      // Only non-zero reaction is (n,gamma)
UNCOV
642
      micro.reaction[0] = sig_a - sig_f;
×
643

644
      // Set all other reaction cross sections to zero
UNCOV
645
      for (int i = 1; i < DEPLETION_RX.size(); ++i) {
×
UNCOV
646
        micro.reaction[i] = 0.0;
×
647
      }
648
    }
649

650
    /*
651
     * index_temp, index_grid, and interp_factor are used only in the
652
     * following places:
653
     *   1. physics.cpp - scatter - For inelastic scatter.
654
     *   2. physics.cpp - sample_fission - For partial fissions.
655
     *   3. tallies/tally_scoring.cpp - score_general -
656
     *        For tallying on MTxxx reactions.
657
     *   4. nuclide.cpp - calculate_urr_xs - For unresolved purposes.
658
     * It is worth noting that none of these occur in the resolved resonance
659
     * range, so the value here does not matter.  index_temp is set to -1 to
660
     * force a segfault in case a developer messes up and tries to use it with
661
     * multipole.
662
     *
663
     * However, a segfault is not necessarily guaranteed with an out-of-bounds
664
     * access, so this technique should be replaced by something more robust
665
     * in the future.
666
     */
667
    micro.index_temp = -1;
9,579,864✔
668
    micro.index_grid = -1;
9,579,864✔
669
    micro.interp_factor = 0.0;
9,579,864✔
670

671
  } else {
672
    // Find the appropriate temperature index.
673
    double kT = p.sqrtkT() * p.sqrtkT();
2,147,483,647✔
674
    double f;
675
    int i_temp = -1;
2,147,483,647✔
676
    switch (settings::temperature_method) {
2,147,483,647✔
677
    case TemperatureMethod::NEAREST: {
2,147,483,647✔
678
      double max_diff = INFTY;
2,147,483,647✔
679
      for (int t = 0; t < kTs_.size(); ++t) {
2,147,483,647✔
680
        double diff = std::abs(kTs_[t] - kT);
2,147,483,647✔
681
        if (diff < max_diff) {
2,147,483,647✔
682
          i_temp = t;
2,147,483,647✔
683
          max_diff = diff;
2,147,483,647✔
684
        }
685
      }
686
    } break;
2,147,483,647✔
687

688
    case TemperatureMethod::INTERPOLATION:
1,654,860✔
689
      // If current kT outside of the bounds of available, snap to the bound
690
      if (kT < kTs_.front()) {
1,654,860✔
691
        i_temp = 0;
450,792✔
692
        break;
450,792✔
693
      }
694
      if (kT > kTs_.back()) {
1,204,068✔
695
        i_temp = kTs_.size() - 1;
236,976✔
696
        break;
236,976✔
697
      }
698

699
      // Find temperatures that bound the actual temperature
700
      for (i_temp = 0; i_temp < kTs_.size() - 1; ++i_temp) {
967,092✔
701
        if (kTs_[i_temp] <= kT && kT < kTs_[i_temp + 1])
967,092✔
702
          break;
967,092✔
703
      }
704

705
      // Randomly sample between temperature i and i+1
706
      f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
967,092✔
707
      if (f > prn(p.current_seed()))
967,092✔
708
        ++i_temp;
449,316✔
709
      break;
967,092✔
710
    }
711

712
    // Determine the energy grid index using a logarithmic mapping to
713
    // reduce the energy range over which a binary search needs to be
714
    // performed
715

716
    const auto& grid {grid_[i_temp]};
2,147,483,647✔
717
    const auto& xs {xs_[i_temp]};
2,147,483,647✔
718

719
    int i_grid;
720
    if (p.E() < grid.energy.front()) {
2,147,483,647✔
721
      i_grid = 0;
884✔
722
    } else if (p.E() > grid.energy.back()) {
2,147,483,647✔
UNCOV
723
      i_grid = grid.energy.size() - 2;
×
724
    } else {
725
      // Determine bounding indices based on which equal log-spaced
726
      // interval the energy is in
727
      int i_low = grid.grid_index[i_log_union];
2,147,483,647✔
728
      int i_high = grid.grid_index[i_log_union + 1] + 1;
2,147,483,647✔
729

730
      // Perform binary search over reduced range
731
      i_grid = i_low + lower_bound_index(
2,147,483,647✔
732
                         &grid.energy[i_low], &grid.energy[i_high], p.E());
2,147,483,647✔
733
    }
734

735
    // check for rare case where two energy points are the same
736
    if (grid.energy[i_grid] == grid.energy[i_grid + 1])
2,147,483,647✔
UNCOV
737
      ++i_grid;
×
738

739
    // calculate interpolation factor
740
    f = (p.E() - grid.energy[i_grid]) /
2,147,483,647✔
741
        (grid.energy[i_grid + 1] - grid.energy[i_grid]);
2,147,483,647✔
742

743
    micro.index_temp = i_temp;
2,147,483,647✔
744
    micro.index_grid = i_grid;
2,147,483,647✔
745
    micro.interp_factor = f;
2,147,483,647✔
746

747
    // Calculate microscopic nuclide total cross section
748
    micro.total =
2,147,483,647✔
749
      (1.0 - f) * xs(i_grid, XS_TOTAL) + f * xs(i_grid + 1, XS_TOTAL);
2,147,483,647✔
750

751
    // Calculate microscopic nuclide absorption cross section
752
    micro.absorption =
2,147,483,647✔
753
      (1.0 - f) * xs(i_grid, XS_ABSORPTION) + f * xs(i_grid + 1, XS_ABSORPTION);
2,147,483,647✔
754

755
    if (fissionable_) {
2,147,483,647✔
756
      // Calculate microscopic nuclide total cross section
757
      micro.fission =
649,915,136✔
758
        (1.0 - f) * xs(i_grid, XS_FISSION) + f * xs(i_grid + 1, XS_FISSION);
649,915,136✔
759

760
      // Calculate microscopic nuclide nu-fission cross section
761
      micro.nu_fission = (1.0 - f) * xs(i_grid, XS_NU_FISSION) +
649,915,136✔
762
                         f * xs(i_grid + 1, XS_NU_FISSION);
649,915,136✔
763
    } else {
764
      micro.fission = 0.0;
2,147,483,647✔
765
      micro.nu_fission = 0.0;
2,147,483,647✔
766
    }
767

768
    // Calculate microscopic nuclide photon production cross section
769
    micro.photon_prod = (1.0 - f) * xs(i_grid, XS_PHOTON_PROD) +
2,147,483,647✔
770
                        f * xs(i_grid + 1, XS_PHOTON_PROD);
2,147,483,647✔
771

772
    // Depletion-related reactions
773
    if (simulation::need_depletion_rx) {
2,147,483,647✔
774
      // Initialize all reaction cross sections to zero
775
      for (double& xs_i : micro.reaction) {
2,147,483,647✔
776
        xs_i = 0.0;
2,147,483,647✔
777
      }
778

779
      for (int j = 0; j < DEPLETION_RX.size(); ++j) {
2,147,483,647✔
780
        // If reaction is present and energy is greater than threshold, set the
781
        // reaction xs appropriately
782
        int i_rx = reaction_index_[DEPLETION_RX[j]];
2,147,483,647✔
783
        if (i_rx >= 0) {
2,147,483,647✔
784
          const auto& rx = reactions_[i_rx];
2,147,483,647✔
785
          const auto& rx_xs = rx->xs_[i_temp].value;
2,147,483,647✔
786

787
          // Physics says that (n,gamma) is not a threshold reaction, so we
788
          // don't need to specifically check its threshold index
789
          if (j == 0) {
2,147,483,647✔
790
            micro.reaction[0] =
949,149,756✔
791
              (1.0 - f) * rx_xs[i_grid] + f * rx_xs[i_grid + 1];
949,149,756✔
792
            continue;
949,149,756✔
793
          }
794

795
          int threshold = rx->xs_[i_temp].threshold;
1,993,186,406✔
796
          if (i_grid >= threshold) {
1,993,186,406✔
797
            micro.reaction[j] = (1.0 - f) * rx_xs[i_grid - threshold] +
306,998,892✔
798
                                f * rx_xs[i_grid - threshold + 1];
306,998,892✔
799
          } else if (j >= 3) {
1,686,187,514✔
800
            // One can show that the the threshold for (n,(x+1)n) is always
801
            // higher than the threshold for (n,xn). Thus, if we are below
802
            // the threshold for, e.g., (n,2n), there is no reason to check
803
            // the threshold for (n,3n) and (n,4n).
804
            break;
774,059,671✔
805
          }
806
        }
807
      }
808
    }
809
  }
810

811
  // Initialize sab treatment to false
812
  micro.index_sab = C_NONE;
2,147,483,647✔
813
  micro.sab_frac = 0.0;
2,147,483,647✔
814

815
  // Initialize URR probability table treatment to false
816
  micro.use_ptable = false;
2,147,483,647✔
817

818
  // If there is S(a,b) data for this nuclide, we need to set the sab_scatter
819
  // and sab_elastic cross sections and correct the total and elastic cross
820
  // sections.
821

822
  if (i_sab >= 0)
2,147,483,647✔
823
    this->calculate_sab_xs(i_sab, sab_frac, p);
92,157,272✔
824

825
  // If the particle is in the unresolved resonance range and there are
826
  // probability tables, we need to determine cross sections from the table
827
  if (settings::urr_ptables_on && urr_present_ && !use_mp) {
2,147,483,647✔
828
    if (urr_data_[micro.index_temp].energy_in_bounds(p.E()))
1,225,858,282✔
829
      this->calculate_urr_xs(micro.index_temp, p);
155,758,230✔
830
  }
831

832
  micro.last_E = p.E();
2,147,483,647✔
833
  micro.last_sqrtkT = p.sqrtkT();
2,147,483,647✔
834
}
2,147,483,647✔
835

836
void Nuclide::calculate_sab_xs(int i_sab, double sab_frac, Particle& p)
92,157,272✔
837
{
838
  auto& micro {p.neutron_xs(index_)};
92,157,272✔
839

840
  // Set flag that S(a,b) treatment should be used for scattering
841
  micro.index_sab = i_sab;
92,157,272✔
842

843
  // Calculate the S(a,b) cross section
844
  int i_temp;
845
  double elastic;
846
  double inelastic;
847
  data::thermal_scatt[i_sab]->calculate_xs(
184,314,544✔
848
    p.E(), p.sqrtkT(), &i_temp, &elastic, &inelastic, p.current_seed());
92,157,272✔
849

850
  // Store the S(a,b) cross sections.
851
  micro.thermal = sab_frac * (elastic + inelastic);
92,157,272✔
852
  micro.thermal_elastic = sab_frac * elastic;
92,157,272✔
853

854
  // Calculate free atom elastic cross section
855
  this->calculate_elastic_xs(p);
92,157,272✔
856

857
  // Correct total and elastic cross sections
858
  micro.total = micro.total + micro.thermal - sab_frac * micro.elastic;
92,157,272✔
859
  micro.elastic = micro.thermal + (1.0 - sab_frac) * micro.elastic;
92,157,272✔
860

861
  // Save temperature index and thermal fraction
862
  micro.index_temp_sab = i_temp;
92,157,272✔
863
  micro.sab_frac = sab_frac;
92,157,272✔
864
}
92,157,272✔
865

866
void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const
155,758,230✔
867
{
868
  auto& micro = p.neutron_xs(index_);
155,758,230✔
869
  micro.use_ptable = true;
155,758,230✔
870

871
  // Create a shorthand for the URR data
872
  const auto& urr = urr_data_[i_temp];
155,758,230✔
873

874
  // Determine the energy table
875
  int i_energy =
876
    lower_bound_index(urr.energy_.begin(), urr.energy_.end(), p.E());
155,758,230✔
877

878
  // Sample the probability table using the cumulative distribution
879

880
  // Random numbers for the xs calculation are sampled from a separate stream.
881
  // This guarantees the randomness and, at the same time, makes sure we
882
  // reuse random numbers for the same nuclide at different temperatures,
883
  // therefore preserving correlation of temperature in probability tables.
884
  double r =
885
    future_prn(static_cast<int64_t>(index_), p.seeds(STREAM_URR_PTABLE));
155,758,230✔
886

887
  // Warning: this assumes row-major order of cdf_values_
888
  int i_low = upper_bound_index(&urr.cdf_values_(i_energy, 0),
155,758,230✔
889
                &urr.cdf_values_(i_energy, 0) + urr.n_cdf(), r) +
155,758,230✔
890
              1;
155,758,230✔
891
  int i_up = upper_bound_index(&urr.cdf_values_(i_energy + 1, 0),
155,758,230✔
892
               &urr.cdf_values_(i_energy + 1, 0) + urr.n_cdf(), r) +
155,758,230✔
893
             1;
155,758,230✔
894

895
  // Determine elastic, fission, and capture cross sections from the
896
  // probability table
897
  double elastic = 0.;
155,758,230✔
898
  double fission = 0.;
155,758,230✔
899
  double capture = 0.;
155,758,230✔
900
  double f;
901
  if (urr.interp_ == Interpolation::lin_lin) {
155,758,230✔
902
    // Determine the interpolation factor on the table
903
    f = (p.E() - urr.energy_[i_energy]) /
64,650,815✔
904
        (urr.energy_[i_energy + 1] - urr.energy_[i_energy]);
64,650,815✔
905

906
    elastic = (1. - f) * urr.xs_values_(i_energy, i_low).elastic +
64,650,815✔
907
              f * urr.xs_values_(i_energy + 1, i_up).elastic;
64,650,815✔
908
    fission = (1. - f) * urr.xs_values_(i_energy, i_low).fission +
64,650,815✔
909
              f * urr.xs_values_(i_energy + 1, i_up).fission;
64,650,815✔
910
    capture = (1. - f) * urr.xs_values_(i_energy, i_low).n_gamma +
64,650,815✔
911
              f * urr.xs_values_(i_energy + 1, i_up).n_gamma;
64,650,815✔
912
  } else if (urr.interp_ == Interpolation::log_log) {
91,107,415✔
913
    // Determine interpolation factor on the table
914
    f = std::log(p.E() / urr.energy_[i_energy]) /
91,107,415✔
915
        std::log(urr.energy_[i_energy + 1] / urr.energy_[i_energy]);
91,107,415✔
916

917
    // Calculate the elastic cross section/factor
918
    if ((urr.xs_values_(i_energy, i_low).elastic > 0.) &&
182,214,830✔
919
        (urr.xs_values_(i_energy + 1, i_up).elastic > 0.)) {
91,107,415✔
920
      elastic =
921
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).elastic) +
91,107,415✔
922
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).elastic));
91,107,415✔
923
    } else {
UNCOV
924
      elastic = 0.;
×
925
    }
926

927
    // Calculate the fission cross section/factor
928
    if ((urr.xs_values_(i_energy, i_low).fission > 0.) &&
156,583,120✔
929
        (urr.xs_values_(i_energy + 1, i_up).fission > 0.)) {
65,475,705✔
930
      fission =
931
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).fission) +
65,475,705✔
932
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).fission));
65,475,705✔
933
    } else {
934
      fission = 0.;
25,631,710✔
935
    }
936

937
    // Calculate the capture cross section/factor
938
    if ((urr.xs_values_(i_energy, i_low).n_gamma > 0.) &&
182,214,830✔
939
        (urr.xs_values_(i_energy + 1, i_up).n_gamma > 0.)) {
91,107,415✔
940
      capture =
941
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).n_gamma) +
91,107,415✔
942
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).n_gamma));
91,107,415✔
943
    } else {
UNCOV
944
      capture = 0.;
×
945
    }
946
  }
947

948
  // Determine the treatment of inelastic scattering
949
  double inelastic = 0.;
155,758,230✔
950
  if (urr.inelastic_flag_ != C_NONE) {
155,758,230✔
951
    // get interpolation factor
952
    f = micro.interp_factor;
111,736,902✔
953

954
    // Determine inelastic scattering cross section
955
    Reaction* rx = reactions_[urr_inelastic_].get();
111,736,902✔
956
    int xs_index = micro.index_grid - rx->xs_[i_temp].threshold;
111,736,902✔
957
    if (xs_index >= 0) {
111,736,902✔
958
      inelastic = (1. - f) * rx->xs_[i_temp].value[xs_index] +
64,193,125✔
959
                  f * rx->xs_[i_temp].value[xs_index + 1];
64,193,125✔
960
    }
961
  }
962

963
  // Multiply by smooth cross-section if needed
964
  if (urr.multiply_smooth_) {
155,758,230✔
965
    calculate_elastic_xs(p);
69,116,668✔
966
    elastic *= micro.elastic;
69,116,668✔
967
    capture *= (micro.absorption - micro.fission);
69,116,668✔
968
    fission *= micro.fission;
69,116,668✔
969
  }
970

971
  // Check for negative values
972
  if (elastic < 0.) {
155,758,230✔
UNCOV
973
    elastic = 0.;
×
974
  }
975
  if (fission < 0.) {
155,758,230✔
UNCOV
976
    fission = 0.;
×
977
  }
978
  if (capture < 0.) {
155,758,230✔
UNCOV
979
    capture = 0.;
×
980
  }
981

982
  // Set elastic, absorption, fission, total, and capture x/s. Note that the
983
  // total x/s is calculated as a sum of partials instead of the table-provided
984
  // value
985
  micro.elastic = elastic;
155,758,230✔
986
  micro.absorption = capture + fission;
155,758,230✔
987
  micro.fission = fission;
155,758,230✔
988
  micro.total = elastic + inelastic + capture + fission;
155,758,230✔
989
  if (simulation::need_depletion_rx) {
155,758,230✔
990
    micro.reaction[0] = capture;
41,885,785✔
991
  }
992

993
  // Determine nu-fission cross-section
994
  if (fissionable_) {
155,758,230✔
995
    micro.nu_fission = nu(p.E(), EmissionMode::total) * micro.fission;
107,703,343✔
996
  }
997
}
155,758,230✔
998

999
std::pair<gsl::index, double> Nuclide::find_temperature(double T) const
816✔
1000
{
1001
  Expects(T >= 0.0);
816✔
1002

1003
  // Determine temperature index
1004
  gsl::index i_temp = 0;
816✔
1005
  double f = 0.0;
816✔
1006
  double kT = K_BOLTZMANN * T;
816✔
1007
  gsl::index n = kTs_.size();
816✔
1008
  switch (settings::temperature_method) {
816✔
1009
  case TemperatureMethod::NEAREST: {
816✔
1010
    double max_diff = INFTY;
816✔
1011
    for (gsl::index t = 0; t < n; ++t) {
1,632✔
1012
      double diff = std::abs(kTs_[t] - kT);
816✔
1013
      if (diff < max_diff) {
816✔
1014
        i_temp = t;
816✔
1015
        max_diff = diff;
816✔
1016
      }
1017
    }
1018
  } break;
816✔
1019

UNCOV
1020
  case TemperatureMethod::INTERPOLATION:
×
1021
    // If current kT outside of the bounds of available, snap to the bound
1022
    if (kT < kTs_.front()) {
×
UNCOV
1023
      i_temp = 0;
×
UNCOV
1024
      break;
×
1025
    }
UNCOV
1026
    if (kT > kTs_.back()) {
×
UNCOV
1027
      i_temp = kTs_.size() - 1;
×
UNCOV
1028
      break;
×
1029
    }
1030
    // Find temperatures that bound the actual temperature
UNCOV
1031
    while (kTs_[i_temp + 1] < kT && i_temp + 1 < n - 1)
×
UNCOV
1032
      ++i_temp;
×
1033

1034
    // Determine interpolation factor
UNCOV
1035
    f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
×
1036
  }
1037

1038
  Ensures(i_temp >= 0 && i_temp < n);
816✔
1039

1040
  return {i_temp, f};
816✔
1041
}
1042

1043
double Nuclide::collapse_rate(int MT, double temperature,
1,200✔
1044
  gsl::span<const double> energy, gsl::span<const double> flux) const
1045
{
1046
  Expects(MT > 0);
1,200✔
1047
  Expects(energy.size() > 0);
1,200✔
1048
  Expects(energy.size() == flux.size() + 1);
1,200✔
1049

1050
  int i_rx = reaction_index_[MT];
1,200✔
1051
  if (i_rx < 0)
1,200✔
1052
    return 0.0;
384✔
1053
  const auto& rx = reactions_[i_rx];
816✔
1054

1055
  // Determine temperature index
1056
  gsl::index i_temp;
1057
  double f;
1058
  std::tie(i_temp, f) = this->find_temperature(temperature);
816✔
1059

1060
  // Get reaction rate at lower temperature
1061
  const auto& grid_low = grid_[i_temp].energy;
816✔
1062
  double rr_low = rx->collapse_rate(i_temp, energy, flux, grid_low);
816✔
1063

1064
  if (f > 0.0) {
816✔
1065
    // Interpolate between reaction rate at lower and higher temperature
UNCOV
1066
    const auto& grid_high = grid_[i_temp + 1].energy;
×
UNCOV
1067
    double rr_high = rx->collapse_rate(i_temp + 1, energy, flux, grid_high);
×
UNCOV
1068
    return rr_low + f * (rr_high - rr_low);
×
1069
  } else {
1070
    // If interpolation factor is zero, return reaction rate at lower
1071
    // temperature
1072
    return rr_low;
816✔
1073
  }
1074
}
1075

1076
//==============================================================================
1077
// Non-member functions
1078
//==============================================================================
1079

1080
void check_data_version(hid_t file_id)
29,045✔
1081
{
1082
  if (attribute_exists(file_id, "version")) {
29,045✔
1083
    vector<int> version;
29,045✔
1084
    read_attribute(file_id, "version", version);
29,045✔
1085
    if (version[0] != HDF5_VERSION[0]) {
29,045✔
UNCOV
1086
      fatal_error("HDF5 data format uses version " +
×
UNCOV
1087
                  std::to_string(version[0]) + "." +
×
UNCOV
1088
                  std::to_string(version[1]) +
×
1089
                  " whereas your installation of "
UNCOV
1090
                  "OpenMC expects version " +
×
UNCOV
1091
                  std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1092
    }
1093
  } else {
29,045✔
UNCOV
1094
    fatal_error("HDF5 data does not indicate a version. Your installation of "
×
UNCOV
1095
                "OpenMC expects version " +
×
UNCOV
1096
                std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1097
  }
1098
}
29,045✔
1099

1100
extern "C" size_t nuclides_size()
24✔
1101
{
1102
  return data::nuclides.size();
24✔
1103
}
1104

1105
//==============================================================================
1106
// C API
1107
//==============================================================================
1108

1109
extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n)
27,354✔
1110
{
1111
  if (data::nuclide_map.find(name) == data::nuclide_map.end() ||
80,822✔
1112
      data::nuclide_map.at(name) >= data::elements.size()) {
53,468✔
1113
    LibraryKey key {Library::Type::neutron, name};
27,354✔
1114
    const auto& it = data::library_map.find(key);
27,354✔
1115
    if (it == data::library_map.end()) {
27,354✔
1116
      set_errmsg(
24✔
1117
        "Nuclide '" + std::string {name} + "' is not present in library.");
48✔
1118
      return OPENMC_E_DATA;
24✔
1119
    }
1120

1121
    // Get filename for library containing nuclide
1122
    int idx = it->second;
27,330✔
1123
    const auto& filename = data::libraries[idx].path_;
27,330✔
1124
    write_message(6, "Reading {} from {}", name, filename);
27,330✔
1125

1126
    // Open file and make sure version is sufficient
1127
    hid_t file_id = file_open(filename, 'r');
27,330✔
1128
    check_data_version(file_id);
27,330✔
1129

1130
    // Read nuclide data from HDF5
1131
    hid_t group = open_group(file_id, name);
27,330✔
1132
    vector<double> temperature {temps, temps + n};
27,330✔
1133
    data::nuclides.push_back(make_unique<Nuclide>(group, temperature));
27,330✔
1134

1135
    close_group(group);
27,330✔
1136
    file_close(file_id);
27,330✔
1137

1138
    // Read multipole file into the appropriate entry on the nuclides array
1139
    int i_nuclide = data::nuclide_map.at(name);
27,330✔
1140
    if (settings::temperature_multipole)
27,330✔
1141
      read_multipole_data(i_nuclide);
442✔
1142

1143
    // Read elemental data, if necessary
1144
    if (settings::photon_transport) {
27,330✔
1145
      auto element = to_element(name);
2,610✔
1146
      if (data::element_map.find(element) == data::element_map.end() ||
2,610✔
1147
          data::element_map.at(element) >= data::elements.size()) {
1,305✔
1148
        // Read photon interaction data from HDF5 photon library
1149
        LibraryKey key {Library::Type::photon, element};
623✔
1150
        const auto& it = data::library_map.find(key);
623✔
1151
        if (it == data::library_map.end()) {
623✔
UNCOV
1152
          set_errmsg("Element '" + std::string {element} +
×
1153
                     "' is not present in library.");
UNCOV
1154
          return OPENMC_E_DATA;
×
1155
        }
1156

1157
        int idx = it->second;
623✔
1158
        const auto& filename = data::libraries[idx].path_;
623✔
1159
        write_message(6, "Reading {} from {} ", element, filename);
623✔
1160

1161
        // Open file and make sure version is sufficient
1162
        hid_t file_id = file_open(filename, 'r');
623✔
1163
        check_data_version(file_id);
623✔
1164

1165
        // Read element data from HDF5
1166
        hid_t group = open_group(file_id, element.c_str());
623✔
1167
        data::elements.push_back(make_unique<PhotonInteraction>(group));
623✔
1168

1169
        close_group(group);
623✔
1170
        file_close(file_id);
623✔
1171
      }
623✔
1172
    }
1,305✔
1173
  }
27,354✔
1174
  return 0;
27,330✔
1175
}
1176

1177
extern "C" int openmc_get_nuclide_index(const char* name, int* index)
936✔
1178
{
1179
  auto it = data::nuclide_map.find(name);
936✔
1180
  if (it == data::nuclide_map.end()) {
936✔
1181
    set_errmsg(
12✔
1182
      "No nuclide named '" + std::string {name} + "' has been loaded.");
24✔
1183
    return OPENMC_E_DATA;
12✔
1184
  }
1185
  *index = it->second;
924✔
1186
  return 0;
924✔
1187
}
1188

1189
extern "C" int openmc_nuclide_name(int index, const char** name)
1,620✔
1190
{
1191
  if (index >= 0 && index < data::nuclides.size()) {
1,620✔
1192
    *name = data::nuclides[index]->name_.data();
1,620✔
1193
    return 0;
1,620✔
1194
  } else {
UNCOV
1195
    set_errmsg("Index in nuclides vector is out of bounds.");
×
UNCOV
1196
    return OPENMC_E_OUT_OF_BOUNDS;
×
1197
  }
1198
}
1199

1200
extern "C" int openmc_nuclide_collapse_rate(int index, int MT,
1,200✔
1201
  double temperature, const double* energy, const double* flux, int n,
1202
  double* xs)
1203
{
1204
  if (index < 0 || index >= data::nuclides.size()) {
1,200✔
UNCOV
1205
    set_errmsg("Index in nuclides vector is out of bounds.");
×
UNCOV
1206
    return OPENMC_E_OUT_OF_BOUNDS;
×
1207
  }
1208

1209
  try {
1210
    *xs = data::nuclides[index]->collapse_rate(
2,400✔
1211
      MT, temperature, {energy, energy + n + 1}, {flux, flux + n});
1,200✔
UNCOV
1212
  } catch (const std::out_of_range& e) {
×
UNCOV
1213
    fmt::print("Caught error\n");
×
UNCOV
1214
    set_errmsg(e.what());
×
UNCOV
1215
    return OPENMC_E_OUT_OF_BOUNDS;
×
UNCOV
1216
  }
×
1217
  return 0;
1,200✔
1218
}
1219

1220
void nuclides_clear()
6,249✔
1221
{
1222
  data::nuclides.clear();
6,249✔
1223
  data::nuclide_map.clear();
6,249✔
1224
}
6,249✔
1225

1226
bool multipole_in_range(const Nuclide& nuc, double E)
1,065,300✔
1227
{
1228
  return E >= nuc.multipole_->E_min_ && E <= nuc.multipole_->E_max_;
1,065,300✔
1229
}
1230

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