• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

openmc-dev / openmc / 22210404096

20 Feb 2026 03:44AM UTC coverage: 81.804% (+0.08%) from 81.721%
22210404096

Pull #3809

github

web-flow
Merge d39f3220e into 53ce1910f
Pull Request #3809: Implement tally filter for filtering by reaction

17328 of 24423 branches covered (70.95%)

Branch coverage included in aggregate %.

125 of 149 new or added lines in 11 files covered. (83.89%)

1322 existing lines in 33 files now uncovered.

57670 of 67257 relevant lines covered (85.75%)

45506622.43 hits per line

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

79.51
/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 "openmc/tensor.h"
21

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

26
namespace openmc {
27

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

32
namespace data {
33
array<double, 4> energy_min {0.0, 0.0, 0.0, 0.0};
34
array<double, 4> energy_max {INFTY, INFTY, 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)
26,855✔
52
{
53
  // Set index of nuclide in global vector
54
  index_ = data::nuclides.size();
26,855✔
55

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

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

65
  if (settings::run_mode == RunMode::VOLUME) {
26,855✔
66
    // Determine whether nuclide is fissionable and then exit
67
    int mt;
68
    hid_t rxs_group = open_group(group, "reactions");
1,199✔
69
    for (auto name : group_names(rxs_group)) {
41,420✔
70
      if (starts_with(name, "reaction_")) {
40,529!
71
        hid_t rx_group = open_group(rxs_group, name.c_str());
40,529✔
72
        read_attribute(rx_group, "mt", mt);
40,529✔
73
        if (is_fission(mt)) {
40,529✔
74
          fissionable_ = true;
308✔
75
          break;
308✔
76
        }
77
      }
78
    }
41,420✔
79
    return;
1,199✔
80
  }
81

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

93
  // If only one temperature is available, revert to nearest temperature
94
  if (temps_available.size() == 1 &&
51,162!
95
      settings::temperature_method == TemperatureMethod::INTERPOLATION) {
25,506!
UNCOV
96
    if (mpi::master) {
×
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;
25,656✔
109
  int n = temperature.size();
25,656✔
110
  double T_min = n > 0 ? settings::temperature_range[0] : 0.0;
25,656✔
111
  double T_max = n > 0 ? settings::temperature_range[1] : INFTY;
25,656✔
112
  if (T_max > 0.0) {
25,656✔
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);
2,748✔
116
    if (T_min_it != temps_available.begin())
2,748!
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);
2,748✔
122
    if (T_max_it != temps_available.end())
2,748!
UNCOV
123
      ++T_max_it;
×
124

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

131
  switch (settings::temperature_method) {
25,656!
132
  case TemperatureMethod::NEAREST:
25,546✔
133
    // Find nearest temperatures
134
    for (double T_desired : temperature) {
49,183✔
135

136
      // Determine closest temperature
137
      double min_delta_T = INFTY;
23,637✔
138
      double T_actual = 0.0;
23,637✔
139
      for (auto T : temps_available) {
47,354✔
140
        double delta_T = std::abs(T - T_desired);
23,717✔
141
        if (delta_T < min_delta_T) {
23,717✔
142
          T_actual = T;
23,667✔
143
          min_delta_T = delta_T;
23,667✔
144
        }
145
      }
146

147
      if (std::abs(T_actual - T_desired) < settings::temperature_tolerance) {
23,637!
148
        if (!contains(temps_to_read, std::round(T_actual))) {
23,637✔
149
          temps_to_read.push_back(std::round(T_actual));
22,798✔
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 &&
22,798!
153
              mpi::master) {
UNCOV
154
            warning(name_ +
×
155
                    " does not contain 0K data needed for resonance "
UNCOV
156
                    "scattering options selected. Using data at " +
×
157
                    std::to_string(T_actual) + " K instead.");
×
158
          }
159
        }
160
      } else {
UNCOV
161
        fatal_error(fmt::format(
×
162
          "Nuclear data library does not contain cross sections "
163
          "for {}  at or near {} K. Available temperatures "
164
          "are {} K. Consider making use of openmc.Settings.temperature "
165
          "to specify how intermediate temperatures are treated.",
UNCOV
166
          name_, std::to_string(T_desired), concatenate(temps_available)));
×
167
      }
168
    }
169
    break;
25,546✔
170

171
  case TemperatureMethod::INTERPOLATION:
110✔
172
    // If temperature interpolation or multipole is selected, get a list of
173
    // bounding temperatures for each actual temperature present in the model
174
    for (double T_desired : temperature) {
230✔
175
      bool found_pair = false;
120✔
176
      for (int j = 0; j < temps_available.size() - 1; ++j) {
360✔
177
        if (temps_available[j] <= T_desired &&
390✔
178
            T_desired < temps_available[j + 1]) {
150✔
179
          int T_j = temps_available[j];
70✔
180
          int T_j1 = temps_available[j + 1];
70✔
181
          if (!contains(temps_to_read, T_j)) {
70!
182
            temps_to_read.push_back(T_j);
70✔
183
          }
184
          if (!contains(temps_to_read, T_j1)) {
70✔
185
            temps_to_read.push_back(T_j1);
60✔
186
          }
187
          found_pair = true;
70✔
188
        }
189
      }
190

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

216
  // Sort temperatures to read
217
  std::sort(temps_to_read.begin(), temps_to_read.end());
25,656✔
218

219
  data::temperature_min =
25,656✔
220
    std::min(data::temperature_min, static_cast<double>(temps_to_read.front()));
25,656✔
221
  data::temperature_max =
25,656✔
222
    std::max(data::temperature_max, static_cast<double>(temps_to_read.back()));
25,656✔
223

224
  hid_t energy_group = open_group(group, "energy");
25,656✔
225
  for (const auto& T : temps_to_read) {
51,382✔
226
    std::string dset {std::to_string(T) + "K"};
25,726✔
227

228
    // Determine exact kT values
229
    double kT;
230
    read_dataset(kT_group, dset.c_str(), kT);
25,726✔
231
    kTs_.push_back(kT);
25,726✔
232

233
    // Read energy grid
234
    grid_.emplace_back();
25,726✔
235
    read_dataset(energy_group, dset.c_str(), grid_.back().energy);
25,726✔
236
  }
25,726✔
237
  close_group(kT_group);
25,656✔
238

239
  // Check for 0K energy grid
240
  if (object_exists(energy_group, "0K")) {
25,656✔
241
    read_dataset(energy_group, "0K", energy_0K_);
5,075✔
242
  }
243
  close_group(energy_group);
25,656✔
244

245
  // Read reactions
246
  hid_t rxs_group = open_group(group, "reactions");
25,656✔
247
  for (auto name : group_names(rxs_group)) {
1,218,230✔
248
    if (starts_with(name, "reaction_")) {
1,192,574!
249
      hid_t rx_group = open_group(rxs_group, name.c_str());
1,192,574✔
250
      reactions_.push_back(
1,192,574✔
251
        make_unique<Reaction>(rx_group, temps_to_read, name_));
2,385,148✔
252

253
      // Check for 0K elastic scattering
254
      const auto& rx = reactions_.back();
1,192,574✔
255
      if (rx->mt_ == ELASTIC) {
1,192,574✔
256
        if (object_exists(rx_group, "0K")) {
25,656✔
257
          hid_t temp_group = open_group(rx_group, "0K");
5,075✔
258
          read_dataset(temp_group, "xs", elastic_0K_);
5,075✔
259
          close_group(temp_group);
5,075✔
260
        }
261
      }
262
      close_group(rx_group);
1,192,574✔
263

264
      // Determine reaction indices for inelastic scattering reactions
265
      if (is_inelastic_scatter(rx->mt_) && !rx->redundant_) {
1,192,574!
266
        index_inelastic_scatter_.push_back(reactions_.size() - 1);
600,605✔
267
      }
268
    }
269
  }
1,218,230✔
270
  close_group(rxs_group);
25,656✔
271

272
  // Read unresolved resonance probability tables if present
273
  if (object_exists(group, "urr")) {
25,656✔
274
    urr_present_ = true;
12,387✔
275
    urr_data_.reserve(temps_to_read.size());
12,387✔
276

277
    for (int i = 0; i < temps_to_read.size(); i++) {
24,774✔
278
      // Get temperature as a string
279
      std::string temp_str {std::to_string(temps_to_read[i]) + "K"};
12,387✔
280

281
      // Read probability tables for i-th temperature
282
      hid_t urr_group = open_group(group, ("urr/" + temp_str).c_str());
12,387✔
283
      urr_data_.emplace_back(urr_group);
12,387✔
284
      close_group(urr_group);
12,387✔
285

286
      // Check for negative values
287
      if (urr_data_[i].has_negative() && mpi::master) {
12,387!
UNCOV
288
        warning("Negative value(s) found on probability table for nuclide " +
×
289
                name_ + " at " + temp_str);
×
290
      }
291
    }
12,387✔
292

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

308
      if (urr_data_[0].inelastic_flag_ > 0) {
12,387✔
309
        for (int i = 0; i < reactions_.size(); i++) {
399,500✔
310
          if (reactions_[i]->mt_ == urr_data_[0].inelastic_flag_) {
392,229✔
311
            urr_inelastic_ = i;
7,271✔
312
          }
313
        }
314

315
        // Abort if no corresponding inelastic reaction was found
316
        if (urr_inelastic_ == C_NONE) {
7,271!
UNCOV
317
          fatal_error("Could no find inelastic reaction specified on "
×
318
                      "unresolved resonance probability table.");
319
        }
320
      }
321
    }
322
  }
323

324
  // Check for total nu data
325
  if (object_exists(group, "total_nu")) {
25,656✔
326
    // Read total nu data
327
    hid_t nu_group = open_group(group, "total_nu");
7,043✔
328
    total_nu_ = read_function(nu_group, "yield");
7,043✔
329
    close_group(nu_group);
7,043✔
330
  }
331

332
  // Read fission energy release data if present
333
  if (object_exists(group, "fission_energy_release")) {
25,656✔
334
    hid_t fer_group = open_group(group, "fission_energy_release");
7,043✔
335
    fission_q_prompt_ = read_function(fer_group, "q_prompt");
7,043✔
336
    fission_q_recov_ = read_function(fer_group, "q_recoverable");
7,043✔
337

338
    // Read fission fragment and delayed beta energy release. This is needed for
339
    // energy normalization in k-eigenvalue calculations
340
    fragments_ = read_function(fer_group, "fragments");
7,043✔
341
    betas_ = read_function(fer_group, "betas");
7,043✔
342

343
    // We need prompt/delayed photon energy release for scaling fission photon
344
    // production
345
    prompt_photons_ = read_function(fer_group, "prompt_photons");
7,043✔
346
    delayed_photons_ = read_function(fer_group, "delayed_photons");
7,043✔
347
    close_group(fer_group);
7,043✔
348
  }
349

350
  this->create_derived(prompt_photons_.get(), delayed_photons_.get());
25,656✔
351
}
25,656✔
352

353
Nuclide::~Nuclide()
26,855✔
354
{
355
  data::nuclide_map.erase(name_);
26,855✔
356
}
26,855✔
357

358
void Nuclide::create_derived(
25,656✔
359
  const Function1D* prompt_photons, const Function1D* delayed_photons)
360
{
361
  for (const auto& grid : grid_) {
51,382✔
362
    // Allocate and initialize cross section
363
    xs_.push_back(tensor::zeros<double>({grid.energy.size(), 5}));
25,726✔
364
  }
365

366
  reaction_index_.fill(C_NONE);
25,656✔
367
  for (int i = 0; i < reactions_.size(); ++i) {
1,218,230✔
368
    const auto& rx {reactions_[i]};
1,192,574✔
369

370
    // Set entry in direct address table for reaction
371
    reaction_index_[rx->mt_] = i;
1,192,574✔
372

373
    for (int t = 0; t < kTs_.size(); ++t) {
2,385,358✔
374
      int j = rx->xs_[t].threshold;
1,192,784✔
375
      int n = rx->xs_[t].value.size();
1,192,784✔
376
      auto xs = tensor::Tensor<double>(
377
        rx->xs_[t].value.data(), rx->xs_[t].value.size());
1,192,784✔
378
      for (const auto& p : rx->products_) {
4,254,483✔
379
        if (p.particle_.is_photon()) {
3,061,699✔
380
          for (int k = 0; k < n; ++k) {
2,147,483,647✔
381
            double E = grid_[t].energy[k + j];
2,147,483,647✔
382

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

396
            xs_[t](j + k, XS_PHOTON_PROD) += f * xs[k] * (*p.yield_)(E);
2,147,483,647✔
397
          }
398
        }
399
      }
400

401
      // Skip redundant reactions
402
      if (rx->redundant_)
1,192,784✔
403
        continue;
159,877✔
404

405
      // Add contribution to total cross section
406
      xs_[t].slice(tensor::range(j, j + n), XS_TOTAL) += xs;
1,032,907✔
407

408
      // Add contribution to absorption cross section
409
      if (is_disappearance(rx->mt_)) {
1,032,907✔
410
        xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs;
393,859✔
411
      }
412

413
      if (is_fission(rx->mt_)) {
1,032,907✔
414
        fissionable_ = true;
12,717✔
415
        xs_[t].slice(tensor::range(j, j + n), XS_FISSION) += xs;
12,717✔
416
        xs_[t].slice(tensor::range(j, j + n), XS_ABSORPTION) += xs;
12,717✔
417

418
        // Keep track of fission reactions
419
        if (t == 0) {
12,717✔
420
          fission_rx_.push_back(rx.get());
12,647✔
421
          if (rx->mt_ == N_F)
12,647✔
422
            has_partial_fission_ = true;
1,818✔
423
        }
424
      }
425
    }
1,192,784✔
426
  }
427

428
  // Determine number of delayed neutron precursors
429
  if (fissionable_) {
25,656✔
430
    for (const auto& product : fission_rx_[0]->products_) {
61,659✔
431
      if (product.emission_mode_ == EmissionMode::delayed) {
54,466✔
432
        ++n_precursor_;
42,090✔
433
      }
434
    }
435
  }
436

437
  // Calculate nu-fission cross section
438
  for (int t = 0; t < kTs_.size(); ++t) {
51,382✔
439
    if (fissionable_) {
25,726✔
440
      int n = grid_[t].energy.size();
7,263✔
441
      for (int i = 0; i < n; ++i) {
556,031,591✔
442
        double E = grid_[t].energy[i];
556,024,328✔
443
        xs_[t](i, XS_NU_FISSION) =
556,024,328✔
444
          nu(E, EmissionMode::total) * xs_[t](i, XS_FISSION);
556,024,328✔
445
      }
446
    }
447
  }
448

449
  if (settings::res_scat_on) {
25,656✔
450
    // Determine if this nuclide should be treated as a resonant scatterer
451
    if (!settings::res_scat_nuclides.empty()) {
56!
452
      // If resonant nuclides were specified, check the list explicitly
453
      for (const auto& name : settings::res_scat_nuclides) {
140✔
454
        if (name_ == name) {
126✔
455
          resonant_ = true;
42✔
456

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

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

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

486
        // build xs cdf
487
        xs_cdf_sum +=
7,798,462✔
488
          (std::sqrt(E[i]) * xs[i] + std::sqrt(E[i + 1]) * xs[i + 1]) / 2.0 *
7,798,462✔
489
          (E[i + 1] - E[i]);
7,798,462✔
490
        xs_cdf_[i + 1] = xs_cdf_sum;
7,798,462✔
491
      }
492
    }
493
  }
494
}
25,656✔
495

496
void Nuclide::init_grid()
26,425✔
497
{
498
  int neutron = ParticleType::neutron().transport_index();
26,425✔
499
  double E_min = data::energy_min[neutron];
26,425✔
500
  double E_max = data::energy_max[neutron];
26,425✔
501
  int M = settings::n_log_bins;
26,425✔
502

503
  // Determine equal-logarithmic energy spacing
504
  double spacing = std::log(E_max / E_min) / M;
26,425✔
505

506
  // Create equally log-spaced energy grid
507
  auto umesh = tensor::linspace(0.0, M * spacing, M + 1);
26,425✔
508

509
  for (auto& grid : grid_) {
52,920✔
510
    // Resize array for storing grid indices
511
    grid.grid_index.resize(M + 1);
26,495✔
512

513
    // Determine corresponding indices in nuclide grid to energies on
514
    // equal-logarithmic grid
515
    int j = 0;
26,495✔
516
    for (int k = 0; k <= M; ++k) {
212,516,990✔
517
      while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) {
893,869,402✔
518
        // Ensure that for isotopes where maxval(grid.energy) << E_max that
519
        // there are no out-of-bounds issues.
520
        if (j + 2 == grid.energy.size())
681,395,366✔
521
          break;
16,459✔
522
        ++j;
681,378,907✔
523
      }
524
      grid.grid_index[k] = j;
212,490,495✔
525
    }
526
  }
527
}
26,425✔
528

529
double Nuclide::nu(double E, EmissionMode mode, int group) const
889,285,605✔
530
{
531
  if (!fissionable_)
889,285,605✔
532
    return 0.0;
26,611,770✔
533

534
  switch (mode) {
862,673,835!
535
  case EmissionMode::prompt:
6,938,780✔
536
    return (*fission_rx_[0]->products_[0].yield_)(E);
6,938,780✔
537
  case EmissionMode::delayed:
129,971,900✔
538
    if (n_precursor_ > 0 && settings::create_delayed_neutrons) {
129,971,900!
539
      auto rx = fission_rx_[0];
128,276,110✔
540
      if (group >= 1 && group < rx->products_.size()) {
128,276,110!
541
        // If delayed group specified, determine yield immediately
542
        return (*rx->products_[group].yield_)(E);
98,091,960✔
543
      } else {
544
        double nu {0.0};
30,184,150✔
545

546
        for (int i = 1; i < rx->products_.size(); ++i) {
240,354,087✔
547
          // Skip any non-neutron products
548
          const auto& product = rx->products_[i];
210,169,937✔
549
          if (!product.particle_.is_neutron())
210,169,937✔
550
            continue;
29,065,037✔
551

552
          // Evaluate yield
553
          if (product.emission_mode_ == EmissionMode::delayed) {
181,104,900!
554
            nu += (*product.yield_)(E);
181,104,900✔
555
          }
556
        }
557
        return nu;
30,184,150✔
558
      }
559
    } else {
560
      return 0.0;
1,695,790✔
561
    }
562
  case EmissionMode::total:
725,763,155✔
563
    if (total_nu_ && settings::create_delayed_neutrons) {
725,763,155!
564
      return (*total_nu_)(E);
724,186,505✔
565
    } else {
566
      return (*fission_rx_[0]->products_[0].yield_)(E);
1,576,650✔
567
    }
568
  }
UNCOV
569
  UNREACHABLE();
×
570
}
571

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

580
  if (i_temp >= 0) {
920,681,187✔
581
    const auto& xs = reactions_[0]->xs_[i_temp].value;
895,147,377✔
582
    micro.elastic = (1.0 - f) * xs[i_grid] + f * xs[i_grid + 1];
895,147,377✔
583
  }
584
}
920,681,187✔
585

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

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

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

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

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

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

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

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

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

639
    if (simulation::need_depletion_rx) {
403,541,880!
640
      // Only non-zero reaction is (n,gamma)
UNCOV
641
      micro.reaction[0] = sig_a - sig_f;
×
642

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

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

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

687
    case TemperatureMethod::INTERPOLATION:
1,379,190✔
688
      // If current kT outside of the bounds of available, snap to the bound
689
      if (kT < kTs_.front()) {
1,379,190✔
690
        i_temp = 0;
375,660✔
691
        break;
375,660✔
692
      }
693
      if (kT > kTs_.back()) {
1,003,530✔
694
        i_temp = kTs_.size() - 1;
196,040✔
695
        break;
196,040✔
696
      }
697

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

704
      // Randomly sample between temperature i and i+1
705
      f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
807,490✔
706
      if (f > prn(p.current_seed()))
807,490✔
707
        ++i_temp;
374,430✔
708
      break;
807,490✔
709
    }
710

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

794
          int threshold = rx->xs_[i_temp].threshold;
1,695,136,210✔
795
          if (i_grid >= threshold) {
1,695,136,210✔
796
            micro.reaction[j] = (1.0 - f) * rx_xs[i_grid - threshold] +
259,553,986✔
797
                                f * rx_xs[i_grid - threshold + 1];
259,553,986✔
798
          } else if (j >= 3) {
1,435,582,224✔
799
            // One can show that the the threshold for (n,(x+1)n) is always
800
            // higher than the threshold for (n,xn). Thus, if we are below
801
            // the threshold for, e.g., (n,2n), there is no reason to check
802
            // the threshold for (n,3n) and (n,4n).
803
            break;
665,129,508✔
804
          }
805
        }
806
      }
807
    }
808
  }
809

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

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

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

821
  if (i_sab >= 0)
2,147,483,647✔
822
    this->calculate_sab_xs(i_sab, sab_frac, p);
135,139,924✔
823

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

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

835
void Nuclide::calculate_sab_xs(int i_sab, double sab_frac, Particle& p)
135,139,924✔
836
{
837
  auto& micro {p.neutron_xs(index_)};
135,139,924✔
838

839
  // Set flag that S(a,b) treatment should be used for scattering
840
  micro.index_sab = i_sab;
135,139,924✔
841

842
  // Calculate the S(a,b) cross section
843
  int i_temp;
844
  double elastic;
845
  double inelastic;
846
  data::thermal_scatt[i_sab]->calculate_xs(
270,279,848✔
847
    p.E(), p.sqrtkT(), &i_temp, &elastic, &inelastic, p.current_seed());
135,139,924✔
848

849
  // Store the S(a,b) cross sections.
850
  micro.thermal = sab_frac * (elastic + inelastic);
135,139,924✔
851
  micro.thermal_elastic = sab_frac * elastic;
135,139,924✔
852

853
  // Calculate free atom elastic cross section
854
  this->calculate_elastic_xs(p);
135,139,924✔
855

856
  // Correct total and elastic cross sections
857
  micro.total = micro.total + micro.thermal - sab_frac * micro.elastic;
135,139,924✔
858
  micro.elastic = micro.thermal + (1.0 - sab_frac) * micro.elastic;
135,139,924✔
859

860
  // Save temperature index and thermal fraction
861
  micro.index_temp_sab = i_temp;
135,139,924✔
862
  micro.sab_frac = sab_frac;
135,139,924✔
863
}
135,139,924✔
864

865
void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const
227,351,196✔
866
{
867
  auto& micro = p.neutron_xs(index_);
227,351,196✔
868
  micro.use_ptable = true;
227,351,196✔
869

870
  // Create a shorthand for the URR data
871
  const auto& urr = urr_data_[i_temp];
227,351,196✔
872

873
  // Determine the energy table
874
  int i_energy =
875
    lower_bound_index(urr.energy_.begin(), urr.energy_.end(), p.E());
227,351,196✔
876

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

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

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

894
  // Determine elastic, fission, and capture cross sections from the
895
  // probability table
896
  double elastic = 0.;
227,351,196✔
897
  double fission = 0.;
227,351,196✔
898
  double capture = 0.;
227,351,196✔
899
  double f;
900
  if (urr.interp_ == Interpolation::lin_lin) {
227,351,196✔
901
    // Determine the interpolation factor on the table
902
    f = (p.E() - urr.energy_[i_energy]) /
143,312,214✔
903
        (urr.energy_[i_energy + 1] - urr.energy_[i_energy]);
143,312,214✔
904

905
    elastic = (1. - f) * urr.xs_values_(i_energy, i_low).elastic +
143,312,214✔
906
              f * urr.xs_values_(i_energy + 1, i_up).elastic;
143,312,214✔
907
    fission = (1. - f) * urr.xs_values_(i_energy, i_low).fission +
143,312,214✔
908
              f * urr.xs_values_(i_energy + 1, i_up).fission;
143,312,214✔
909
    capture = (1. - f) * urr.xs_values_(i_energy, i_low).n_gamma +
143,312,214✔
910
              f * urr.xs_values_(i_energy + 1, i_up).n_gamma;
143,312,214✔
911
  } else if (urr.interp_ == Interpolation::log_log) {
84,038,982!
912
    // Determine interpolation factor on the table
913
    f = std::log(p.E() / urr.energy_[i_energy]) /
84,038,982✔
914
        std::log(urr.energy_[i_energy + 1] / urr.energy_[i_energy]);
84,038,982✔
915

916
    // Calculate the elastic cross section/factor
917
    if ((urr.xs_values_(i_energy, i_low).elastic > 0.) &&
168,077,964!
918
        (urr.xs_values_(i_energy + 1, i_up).elastic > 0.)) {
84,038,982!
919
      elastic =
920
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).elastic) +
84,038,982✔
921
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).elastic));
84,038,982✔
922
    } else {
UNCOV
923
      elastic = 0.;
×
924
    }
925

926
    // Calculate the fission cross section/factor
927
    if ((urr.xs_values_(i_energy, i_low).fission > 0.) &&
140,856,119✔
928
        (urr.xs_values_(i_energy + 1, i_up).fission > 0.)) {
56,817,137!
929
      fission =
930
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).fission) +
56,817,137✔
931
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).fission));
56,817,137✔
932
    } else {
933
      fission = 0.;
27,221,845✔
934
    }
935

936
    // Calculate the capture cross section/factor
937
    if ((urr.xs_values_(i_energy, i_low).n_gamma > 0.) &&
168,077,964!
938
        (urr.xs_values_(i_energy + 1, i_up).n_gamma > 0.)) {
84,038,982!
939
      capture =
940
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).n_gamma) +
84,038,982✔
941
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).n_gamma));
84,038,982✔
942
    } else {
UNCOV
943
      capture = 0.;
×
944
    }
945
  }
946

947
  // Determine the treatment of inelastic scattering
948
  double inelastic = 0.;
227,351,196✔
949
  if (urr.inelastic_flag_ != C_NONE) {
227,351,196✔
950
    // get interpolation factor
951
    f = micro.interp_factor;
125,331,895✔
952

953
    // Determine inelastic scattering cross section
954
    Reaction* rx = reactions_[urr_inelastic_].get();
125,331,895✔
955
    int xs_index = micro.index_grid - rx->xs_[i_temp].threshold;
125,331,895✔
956
    if (xs_index >= 0) {
125,331,895✔
957
      inelastic = (1. - f) * rx->xs_[i_temp].value[xs_index] +
68,625,577✔
958
                  f * rx->xs_[i_temp].value[xs_index + 1];
68,625,577✔
959
    }
960
  }
961

962
  // Multiply by smooth cross-section if needed
963
  if (urr.multiply_smooth_) {
227,351,196✔
964
    calculate_elastic_xs(p);
69,563,393✔
965
    elastic *= micro.elastic;
69,563,393✔
966
    capture *= (micro.absorption - micro.fission);
69,563,393✔
967
    fission *= micro.fission;
69,563,393✔
968
  }
969

970
  // Check for negative values
971
  if (elastic < 0.) {
227,351,196!
UNCOV
972
    elastic = 0.;
×
973
  }
974
  if (fission < 0.) {
227,351,196!
UNCOV
975
    fission = 0.;
×
976
  }
977
  if (capture < 0.) {
227,351,196!
978
    capture = 0.;
×
979
  }
980

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

992
  // Determine nu-fission cross-section
993
  if (fissionable_) {
227,351,196✔
994
    micro.nu_fission = nu(p.E(), EmissionMode::total) * micro.fission;
122,260,837✔
995
  }
996
}
227,351,196✔
997

998
std::pair<int64_t, double> Nuclide::find_temperature(double T) const
1,870✔
999
{
1000
  assert(T >= 0.0);
1,496!
1001

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

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

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

1037
  assert(i_temp >= 0 && i_temp < n);
1,496!
1038

1039
  return {i_temp, f};
1,870✔
1040
}
1041

1042
double Nuclide::collapse_rate(int MT, double temperature,
2,440✔
1043
  span<const double> energy, span<const double> flux) const
1044
{
1045
  assert(MT > 0);
1,952!
1046
  assert(energy.size() > 0);
1,952!
1047
  assert(energy.size() == flux.size() + 1);
1,952!
1048

1049
  int i_rx = reaction_index_[MT];
2,440✔
1050
  if (i_rx < 0)
2,440✔
1051
    return 0.0;
570✔
1052
  const auto& rx = reactions_[i_rx];
1,870✔
1053

1054
  // Determine temperature index
1055
  int64_t i_temp;
1056
  double f;
1057
  std::tie(i_temp, f) = this->find_temperature(temperature);
1,870✔
1058

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

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

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

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

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

1104
//==============================================================================
1105
// C API
1106
//==============================================================================
1107

1108
extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n)
26,985✔
1109
{
1110
  if (data::nuclide_map.find(name) == data::nuclide_map.end() ||
79,113!
1111
      data::nuclide_map.at(name) >= data::nuclides.size()) {
52,128!
1112
    LibraryKey key {Library::Type::neutron, name};
26,875✔
1113
    const auto& it = data::library_map.find(key);
26,875✔
1114
    if (it == data::library_map.end()) {
26,875✔
1115
      set_errmsg(
20✔
1116
        "Nuclide '" + std::string {name} + "' is not present in library.");
40✔
1117
      return OPENMC_E_DATA;
20✔
1118
    }
1119

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

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

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

1134
    close_group(group);
26,855✔
1135
    file_close(file_id);
26,855✔
1136

1137
    // Read multipole file into the appropriate entry on the nuclides array
1138
    int i_nuclide = data::nuclide_map.at(name);
26,855✔
1139
    if (settings::temperature_multipole)
26,855✔
1140
      read_multipole_data(i_nuclide);
1,104✔
1141

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

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

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

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

1168
        close_group(group);
624✔
1169
        file_close(file_id);
624✔
1170
      }
624!
1171
    }
1,206!
1172
  }
26,875!
1173
  return 0;
26,965✔
1174
}
1175

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

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

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

1208
  try {
1209
    *xs = data::nuclides[index]->collapse_rate(
4,880✔
1210
      MT, temperature, {energy, energy + n + 1}, {flux, flux + n});
2,440✔
1211
  } catch (const std::out_of_range& e) {
×
UNCOV
1212
    set_errmsg(e.what());
×
UNCOV
1213
    return OPENMC_E_OUT_OF_BOUNDS;
×
UNCOV
1214
  }
×
1215
  return 0;
2,440✔
1216
}
1217

1218
void nuclides_clear()
7,536✔
1219
{
1220
  data::nuclides.clear();
7,536✔
1221
  data::nuclide_map.clear();
7,536✔
1222
}
7,536✔
1223

1224
bool multipole_in_range(const Nuclide& nuc, double E)
892,540✔
1225
{
1226
  return E >= nuc.multipole_->E_min_ && E <= nuc.multipole_->E_max_;
892,540!
1227
}
1228

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