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

openmc-dev / openmc / 13183757653

06 Feb 2025 04:49PM UTC coverage: 82.603% (-2.3%) from 84.867%
13183757653

Pull #3087

github

web-flow
Merge 97d1083b5 into 6e0f156d3
Pull Request #3087: wheel building with scikit build core

107125 of 129687 relevant lines covered (82.6%)

12608197.33 hits per line

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

76.95
/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)
17,079✔
52
{
53
  // Set index of nuclide in global vector
54
  index_ = data::nuclides.size();
17,079✔
55

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

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

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

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

93
  // If only one temperature is available, revert to nearest temperature
94
  if (temps_available.size() == 1 &&
32,956✔
95
      settings::temperature_method == TemperatureMethod::INTERPOLATION) {
16,388✔
96
    if (mpi::master) {
×
97
      warning("Cross sections for " + name_ +
×
98
              " are only available at one "
99
              "temperature. Reverting to nearest temperature method.");
100
    }
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;
16,568✔
109
  int n = temperature.size();
16,568✔
110
  double T_min = n > 0 ? settings::temperature_range[0] : 0.0;
16,568✔
111
  double T_max = n > 0 ? settings::temperature_range[1] : INFTY;
16,568✔
112
  if (T_max > 0.0) {
16,568✔
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);
367✔
116
    if (T_min_it != temps_available.begin())
367✔
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);
367✔
122
    if (T_max_it != temps_available.end())
367✔
123
      ++T_max_it;
×
124

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

131
  switch (settings::temperature_method) {
16,568✔
132
  case TemperatureMethod::NEAREST:
16,436✔
133
    // Find nearest temperatures
134
    for (double T_desired : temperature) {
32,580✔
135

136
      // Determine closest temperature
137
      double min_delta_T = INFTY;
16,144✔
138
      double T_actual = 0.0;
16,144✔
139
      for (auto T : temps_available) {
32,384✔
140
        double delta_T = std::abs(T - T_desired);
16,240✔
141
        if (delta_T < min_delta_T) {
16,240✔
142
          T_actual = T;
16,180✔
143
          min_delta_T = delta_T;
16,180✔
144
        }
145
      }
146

147
      if (std::abs(T_actual - T_desired) < settings::temperature_tolerance) {
16,144✔
148
        if (!contains(temps_to_read, std::round(T_actual))) {
16,144✔
149
          temps_to_read.push_back(std::round(T_actual));
16,069✔
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 &&
16,069✔
153
              mpi::master) {
154
            warning(name_ +
×
155
                    " does not contain 0K data needed for resonance "
156
                    "scattering options selected. Using data at " +
×
157
                    std::to_string(T_actual) + " K instead.");
×
158
          }
159
        }
160
      } else {
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.",
166
          name_, std::to_string(T_desired), concatenate(temps_available)));
×
167
      }
168
    }
169
    break;
16,436✔
170

171
  case TemperatureMethod::INTERPOLATION:
132✔
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) {
276✔
175
      bool found_pair = false;
144✔
176
      for (int j = 0; j < temps_available.size() - 1; ++j) {
432✔
177
        if (temps_available[j] <= T_desired &&
468✔
178
            T_desired < temps_available[j + 1]) {
180✔
179
          int T_j = temps_available[j];
84✔
180
          int T_j1 = temps_available[j + 1];
84✔
181
          if (!contains(temps_to_read, T_j)) {
84✔
182
            temps_to_read.push_back(T_j);
84✔
183
          }
184
          if (!contains(temps_to_read, T_j1)) {
84✔
185
            temps_to_read.push_back(T_j1);
72✔
186
          }
187
          found_pair = true;
84✔
188
        }
189
      }
190

191
      if (!found_pair) {
144✔
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()) <=
60✔
195
            settings::temperature_tolerance) {
196
          if (!contains(temps_to_read, temps_available.front())) {
36✔
197
            temps_to_read.push_back(temps_available.front());
36✔
198
          }
199
          continue;
36✔
200
        }
201
        if (std::abs(T_desired - temps_available.back()) <=
24✔
202
            settings::temperature_tolerance) {
203
          if (!contains(temps_to_read, temps_available.back())) {
24✔
204
            temps_to_read.push_back(temps_available.back());
24✔
205
          }
206
          continue;
24✔
207
        }
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;
132✔
214
  }
215

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

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

224
  hid_t energy_group = open_group(group, "energy");
16,568✔
225
  for (const auto& T : temps_to_read) {
33,220✔
226
    std::string dset {std::to_string(T) + "K"};
16,652✔
227

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

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

239
  // Check for 0K energy grid
240
  if (object_exists(energy_group, "0K")) {
16,568✔
241
    read_dataset(energy_group, "0K", energy_0K_);
3,459✔
242
  }
243
  close_group(energy_group);
16,568✔
244

245
  // Read reactions
246
  hid_t rxs_group = open_group(group, "reactions");
16,568✔
247
  for (auto name : group_names(rxs_group)) {
792,189✔
248
    if (starts_with(name, "reaction_")) {
775,621✔
249
      hid_t rx_group = open_group(rxs_group, name.c_str());
775,621✔
250
      reactions_.push_back(make_unique<Reaction>(rx_group, temps_to_read));
775,621✔
251

252
      // Check for 0K elastic scattering
253
      const auto& rx = reactions_.back();
775,621✔
254
      if (rx->mt_ == ELASTIC) {
775,621✔
255
        if (object_exists(rx_group, "0K")) {
16,568✔
256
          hid_t temp_group = open_group(rx_group, "0K");
3,459✔
257
          read_dataset(temp_group, "xs", elastic_0K_);
3,459✔
258
          close_group(temp_group);
3,459✔
259
        }
260
      }
261
      close_group(rx_group);
775,621✔
262

263
      // Determine reaction indices for inelastic scattering reactions
264
      if (is_inelastic_scatter(rx->mt_) && !rx->redundant_) {
775,621✔
265
        index_inelastic_scatter_.push_back(reactions_.size() - 1);
386,015✔
266
      }
267
    }
268
  }
792,189✔
269
  close_group(rxs_group);
16,568✔
270

271
  // Read unresolved resonance probability tables if present
272
  if (object_exists(group, "urr")) {
16,568✔
273
    urr_present_ = true;
7,793✔
274
    urr_data_.reserve(temps_to_read.size());
7,793✔
275

276
    for (int i = 0; i < temps_to_read.size(); i++) {
15,586✔
277
      // Get temperature as a string
278
      std::string temp_str {std::to_string(temps_to_read[i]) + "K"};
7,793✔
279

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

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

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

307
      if (urr_data_[0].inelastic_flag_ > 0) {
7,793✔
308
        for (int i = 0; i < reactions_.size(); i++) {
255,468✔
309
          if (reactions_[i]->mt_ == urr_data_[0].inelastic_flag_) {
250,779✔
310
            urr_inelastic_ = i;
4,689✔
311
          }
312
        }
313

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

323
  // Check for total nu data
324
  if (object_exists(group, "total_nu")) {
16,568✔
325
    // Read total nu data
326
    hid_t nu_group = open_group(group, "total_nu");
4,682✔
327
    total_nu_ = read_function(nu_group, "yield");
4,682✔
328
    close_group(nu_group);
4,682✔
329
  }
330

331
  // Read fission energy release data if present
332
  if (object_exists(group, "fission_energy_release")) {
16,568✔
333
    hid_t fer_group = open_group(group, "fission_energy_release");
4,682✔
334
    fission_q_prompt_ = read_function(fer_group, "q_prompt");
4,682✔
335
    fission_q_recov_ = read_function(fer_group, "q_recoverable");
4,682✔
336

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

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

349
  this->create_derived(prompt_photons_.get(), delayed_photons_.get());
16,568✔
350
}
16,568✔
351

352
Nuclide::~Nuclide()
17,079✔
353
{
354
  data::nuclide_map.erase(name_);
17,079✔
355
}
17,079✔
356

357
void Nuclide::create_derived(
16,568✔
358
  const Function1D* prompt_photons, const Function1D* delayed_photons)
359
{
360
  for (const auto& grid : grid_) {
33,220✔
361
    // Allocate and initialize cross section
362
    array<size_t, 2> shape {grid.energy.size(), 5};
16,652✔
363
    xs_.emplace_back(shape, 0.0);
16,652✔
364
  }
365

366
  reaction_index_.fill(C_NONE);
16,568✔
367
  for (int i = 0; i < reactions_.size(); ++i) {
792,189✔
368
    const auto& rx {reactions_[i]};
775,621✔
369

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

373
    for (int t = 0; t < kTs_.size(); ++t) {
1,551,494✔
374
      int j = rx->xs_[t].threshold;
775,873✔
375
      int n = rx->xs_[t].value.size();
775,873✔
376
      auto xs = xt::adapt(rx->xs_[t].value);
775,873✔
377

378
      for (const auto& p : rx->products_) {
2,733,343✔
379
        if (p.particle_ == ParticleType::photon) {
1,957,470✔
380
          auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD);
1,500,108✔
381
          for (int k = 0; k < n; ++k) {
2,147,483,647✔
382
            double E = grid_[t].energy[k + j];
2,147,483,647✔
383

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

397
            pprod[k] += f * xs[k] * (*p.yield_)(E);
2,147,483,647✔
398
          }
399
        }
1,500,108✔
400
      }
401

402
      // Skip redundant reactions
403
      if (rx->redundant_)
775,873✔
404
        continue;
103,533✔
405

406
      // Add contribution to total cross section
407
      auto total = xt::view(xs_[t], xt::range(j, j + n), XS_TOTAL);
672,340✔
408
      total += xs;
672,340✔
409

410
      // Add contribution to absorption cross section
411
      auto absorption = xt::view(xs_[t], xt::range(j, j + n), XS_ABSORPTION);
672,340✔
412
      if (is_disappearance(rx->mt_)) {
672,340✔
413
        absorption += xs;
261,604✔
414
      }
415

416
      if (is_fission(rx->mt_)) {
672,340✔
417
        fissionable_ = true;
8,069✔
418
        auto fission = xt::view(xs_[t], xt::range(j, j + n), XS_FISSION);
8,069✔
419
        fission += xs;
8,069✔
420
        absorption += xs;
8,069✔
421

422
        // Keep track of fission reactions
423
        if (t == 0) {
8,069✔
424
          fission_rx_.push_back(rx.get());
7,985✔
425
          if (rx->mt_ == N_F)
7,985✔
426
            has_partial_fission_ = true;
1,041✔
427
        }
428
      }
8,069✔
429
    }
775,873✔
430
  }
431

432
  // Determine number of delayed neutron precursors
433
  if (fissionable_) {
16,568✔
434
    for (const auto& product : fission_rx_[0]->products_) {
41,202✔
435
      if (product.emission_mode_ == EmissionMode::delayed) {
36,340✔
436
        ++n_precursor_;
27,888✔
437
      }
438
    }
439
  }
440

441
  // Calculate nu-fission cross section
442
  for (int t = 0; t < kTs_.size(); ++t) {
33,220✔
443
    if (fissionable_) {
16,652✔
444
      int n = grid_[t].energy.size();
4,946✔
445
      for (int i = 0; i < n; ++i) {
363,902,220✔
446
        double E = grid_[t].energy[i];
363,897,274✔
447
        xs_[t](i, XS_NU_FISSION) =
363,897,274✔
448
          nu(E, EmissionMode::total) * xs_[t](i, XS_FISSION);
363,897,274✔
449
      }
450
    }
451
  }
452

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

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

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

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

490
        // build xs cdf
491
        xs_cdf_sum +=
9,469,561✔
492
          (std::sqrt(E[i]) * xs[i] + std::sqrt(E[i + 1]) * xs[i + 1]) / 2.0 *
9,469,561✔
493
          (E[i + 1] - E[i]);
9,469,561✔
494
        xs_cdf_[i + 1] = xs_cdf_sum;
9,469,561✔
495
      }
496
    }
497
  }
498
}
16,568✔
499

500
void Nuclide::init_grid()
16,552✔
501
{
502
  int neutron = static_cast<int>(ParticleType::neutron);
16,552✔
503
  double E_min = data::energy_min[neutron];
16,552✔
504
  double E_max = data::energy_max[neutron];
16,552✔
505
  int M = settings::n_log_bins;
16,552✔
506

507
  // Determine equal-logarithmic energy spacing
508
  double spacing = std::log(E_max / E_min) / M;
16,552✔
509

510
  // Create equally log-spaced energy grid
511
  auto umesh = xt::linspace(0.0, M * spacing, M + 1);
16,552✔
512

513
  for (auto& grid : grid_) {
33,188✔
514
    // Resize array for storing grid indices
515
    grid.grid_index.resize(M + 1);
16,636✔
516

517
    // Determine corresponding indices in nuclide grid to energies on
518
    // equal-logarithmic grid
519
    int j = 0;
16,636✔
520
    for (int k = 0; k <= M; ++k) {
133,733,272✔
521
      while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) {
582,080,556✔
522
        // Ensure that for isotopes where maxval(grid.energy) << E_max that
523
        // there are no out-of-bounds issues.
524
        if (j + 2 == grid.energy.size())
448,374,269✔
525
          break;
10,349✔
526
        ++j;
448,363,920✔
527
      }
528
      grid.grid_index[k] = j;
133,716,636✔
529
    }
530
  }
531
}
16,552✔
532

533
double Nuclide::nu(double E, EmissionMode mode, int group) const
578,900,769✔
534
{
535
  if (!fissionable_)
578,900,769✔
536
    return 0.0;
31,816,896✔
537

538
  switch (mode) {
547,083,873✔
539
  case EmissionMode::prompt:
8,198,016✔
540
    return (*fission_rx_[0]->products_[0].yield_)(E);
8,198,016✔
541
  case EmissionMode::delayed:
138,193,922✔
542
    if (n_precursor_ > 0 && settings::create_delayed_neutrons) {
138,193,922✔
543
      auto rx = fission_rx_[0];
136,160,690✔
544
      if (group >= 1 && group < rx->products_.size()) {
136,160,690✔
545
        // If delayed group specified, determine yield immediately
546
        return (*rx->products_[group].yield_)(E);
117,120,960✔
547
      } else {
548
        double nu {0.0};
19,039,730✔
549

550
        for (int i = 1; i < rx->products_.size(); ++i) {
151,017,663✔
551
          // Skip any non-neutron products
552
          const auto& product = rx->products_[i];
131,977,933✔
553
          if (product.particle_ != ParticleType::neutron)
131,977,933✔
554
            continue;
17,739,553✔
555

556
          // Evaluate yield
557
          if (product.emission_mode_ == EmissionMode::delayed) {
114,238,380✔
558
            nu += (*product.yield_)(E);
114,238,380✔
559
          }
560
        }
561
        return nu;
19,039,730✔
562
      }
563
    } else {
564
      return 0.0;
2,033,232✔
565
    }
566
  case EmissionMode::total:
400,691,935✔
567
    if (total_nu_ && settings::create_delayed_neutrons) {
400,691,935✔
568
      return (*total_nu_)(E);
398,800,783✔
569
    } else {
570
      return (*fission_rx_[0]->products_[0].yield_)(E);
1,891,152✔
571
    }
572
  }
573
  UNREACHABLE();
×
574
}
575

576
void Nuclide::calculate_elastic_xs(Particle& p) const
487,480,868✔
577
{
578
  // Get temperature index, grid index, and interpolation factor
579
  auto& micro {p.neutron_xs(index_)};
487,480,868✔
580
  int i_temp = micro.index_temp;
487,480,868✔
581
  int i_grid = micro.index_grid;
487,480,868✔
582
  double f = micro.interp_factor;
487,480,868✔
583

584
  if (i_temp >= 0) {
487,480,868✔
585
    const auto& xs = reactions_[0]->xs_[i_temp].value;
483,968,876✔
586
    micro.elastic = (1.0 - f) * xs[i_grid] + f * xs[i_grid + 1];
483,968,876✔
587
  }
588
}
487,480,868✔
589

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

602
  // check for rare case where two energy points are the same
603
  if (energy_0K_[i_grid] == energy_0K_[i_grid + 1])
×
604
    ++i_grid;
×
605

606
  // calculate interpolation factor
607
  double f =
608
    (E - energy_0K_[i_grid]) / (energy_0K_[i_grid + 1] - energy_0K_[i_grid]);
×
609

610
  // Calculate microscopic nuclide elastic cross section
611
  return (1.0 - f) * elastic_0K_[i_grid] + f * elastic_0K_[i_grid + 1];
×
612
}
613

614
void Nuclide::calculate_xs(
1,958,012,770✔
615
  int i_sab, int i_log_union, double sab_frac, Particle& p)
616
{
617
  auto& micro {p.neutron_xs(index_)};
1,958,012,770✔
618

619
  // Initialize cached cross sections to zero
620
  micro.elastic = CACHE_INVALID;
1,958,012,770✔
621
  micro.thermal = 0.0;
1,958,012,770✔
622
  micro.thermal_elastic = 0.0;
1,958,012,770✔
623

624
  // Check to see if there is multipole data present at this energy
625
  bool use_mp = false;
1,958,012,770✔
626
  if (multipole_) {
1,958,012,770✔
627
    use_mp = (p.E() >= multipole_->E_min_ && p.E() <= multipole_->E_max_);
9,948,480✔
628
  }
629

630
  // Evaluate multipole or interpolate
631
  if (use_mp) {
1,958,012,770✔
632
    // Call multipole kernel
633
    double sig_s, sig_a, sig_f;
634
    std::tie(sig_s, sig_a, sig_f) = multipole_->evaluate(p.E(), p.sqrtkT());
9,579,864✔
635

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

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

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

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

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

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

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

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

715
    // Determine the energy grid index using a logarithmic mapping to
716
    // reduce the energy range over which a binary search needs to be
717
    // performed
718

719
    const auto& grid {grid_[i_temp]};
1,948,432,906✔
720
    const auto& xs {xs_[i_temp]};
1,948,432,906✔
721

722
    int i_grid;
723
    if (p.E() < grid.energy.front()) {
1,948,432,906✔
724
      i_grid = 0;
644✔
725
    } else if (p.E() > grid.energy.back()) {
1,948,432,262✔
726
      i_grid = grid.energy.size() - 2;
×
727
    } else {
728
      // Determine bounding indices based on which equal log-spaced
729
      // interval the energy is in
730
      int i_low = grid.grid_index[i_log_union];
1,948,432,262✔
731
      int i_high = grid.grid_index[i_log_union + 1] + 1;
1,948,432,262✔
732

733
      // Perform binary search over reduced range
734
      i_grid = i_low + lower_bound_index(
1,948,432,262✔
735
                         &grid.energy[i_low], &grid.energy[i_high], p.E());
1,948,432,262✔
736
    }
737

738
    // check for rare case where two energy points are the same
739
    if (grid.energy[i_grid] == grid.energy[i_grid + 1])
1,948,432,906✔
740
      ++i_grid;
×
741

742
    // calculate interpolation factor
743
    f = (p.E() - grid.energy[i_grid]) /
1,948,432,906✔
744
        (grid.energy[i_grid + 1] - grid.energy[i_grid]);
1,948,432,906✔
745

746
    micro.index_temp = i_temp;
1,948,432,906✔
747
    micro.index_grid = i_grid;
1,948,432,906✔
748
    micro.interp_factor = f;
1,948,432,906✔
749

750
    // Calculate microscopic nuclide total cross section
751
    micro.total =
1,948,432,906✔
752
      (1.0 - f) * xs(i_grid, XS_TOTAL) + f * xs(i_grid + 1, XS_TOTAL);
1,948,432,906✔
753

754
    // Calculate microscopic nuclide absorption cross section
755
    micro.absorption =
1,948,432,906✔
756
      (1.0 - f) * xs(i_grid, XS_ABSORPTION) + f * xs(i_grid + 1, XS_ABSORPTION);
1,948,432,906✔
757

758
    if (fissionable_) {
1,948,432,906✔
759
      // Calculate microscopic nuclide total cross section
760
      micro.fission =
173,201,216✔
761
        (1.0 - f) * xs(i_grid, XS_FISSION) + f * xs(i_grid + 1, XS_FISSION);
173,201,216✔
762

763
      // Calculate microscopic nuclide nu-fission cross section
764
      micro.nu_fission = (1.0 - f) * xs(i_grid, XS_NU_FISSION) +
173,201,216✔
765
                         f * xs(i_grid + 1, XS_NU_FISSION);
173,201,216✔
766
    } else {
767
      micro.fission = 0.0;
1,775,231,690✔
768
      micro.nu_fission = 0.0;
1,775,231,690✔
769
    }
770

771
    // Calculate microscopic nuclide photon production cross section
772
    micro.photon_prod = (1.0 - f) * xs(i_grid, XS_PHOTON_PROD) +
1,948,432,906✔
773
                        f * xs(i_grid + 1, XS_PHOTON_PROD);
1,948,432,906✔
774

775
    // Depletion-related reactions
776
    if (simulation::need_depletion_rx) {
1,948,432,906✔
777
      // Initialize all reaction cross sections to zero
778
      for (double& xs_i : micro.reaction) {
×
779
        xs_i = 0.0;
×
780
      }
781

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

790
          // Physics says that (n,gamma) is not a threshold reaction, so we
791
          // don't need to specifically check its threshold index
792
          if (j == 0) {
×
793
            micro.reaction[0] =
×
794
              (1.0 - f) * rx_xs[i_grid] + f * rx_xs[i_grid + 1];
×
795
            continue;
×
796
          }
797

798
          int threshold = rx->xs_[i_temp].threshold;
×
799
          if (i_grid >= threshold) {
×
800
            micro.reaction[j] = (1.0 - f) * rx_xs[i_grid - threshold] +
×
801
                                f * rx_xs[i_grid - threshold + 1];
×
802
          } else if (j >= 3) {
×
803
            // One can show that the the threshold for (n,(x+1)n) is always
804
            // higher than the threshold for (n,xn). Thus, if we are below
805
            // the threshold for, e.g., (n,2n), there is no reason to check
806
            // the threshold for (n,3n) and (n,4n).
807
            break;
×
808
          }
809
        }
810
      }
811
    }
812
  }
813

814
  // Initialize sab treatment to false
815
  micro.index_sab = C_NONE;
1,958,012,770✔
816
  micro.sab_frac = 0.0;
1,958,012,770✔
817

818
  // Initialize URR probability table treatment to false
819
  micro.use_ptable = false;
1,958,012,770✔
820

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

825
  if (i_sab >= 0)
1,958,012,770✔
826
    this->calculate_sab_xs(i_sab, sab_frac, p);
38,054,116✔
827

828
  // If the particle is in the unresolved resonance range and there are
829
  // probability tables, we need to determine cross sections from the table
830
  if (settings::urr_ptables_on && urr_present_ && !use_mp) {
1,958,012,770✔
831
    if (urr_data_[micro.index_temp].energy_in_bounds(p.E()))
323,597,110✔
832
      this->calculate_urr_xs(micro.index_temp, p);
30,455,362✔
833
  }
834

835
  micro.last_E = p.E();
1,958,012,770✔
836
  micro.last_sqrtkT = p.sqrtkT();
1,958,012,770✔
837
}
1,958,012,770✔
838

839
void Nuclide::calculate_sab_xs(int i_sab, double sab_frac, Particle& p)
38,054,116✔
840
{
841
  auto& micro {p.neutron_xs(index_)};
38,054,116✔
842

843
  // Set flag that S(a,b) treatment should be used for scattering
844
  micro.index_sab = i_sab;
38,054,116✔
845

846
  // Calculate the S(a,b) cross section
847
  int i_temp;
848
  double elastic;
849
  double inelastic;
850
  data::thermal_scatt[i_sab]->calculate_xs(
76,108,232✔
851
    p.E(), p.sqrtkT(), &i_temp, &elastic, &inelastic, p.current_seed());
38,054,116✔
852

853
  // Store the S(a,b) cross sections.
854
  micro.thermal = sab_frac * (elastic + inelastic);
38,054,116✔
855
  micro.thermal_elastic = sab_frac * elastic;
38,054,116✔
856

857
  // Calculate free atom elastic cross section
858
  this->calculate_elastic_xs(p);
38,054,116✔
859

860
  // Correct total and elastic cross sections
861
  micro.total = micro.total + micro.thermal - sab_frac * micro.elastic;
38,054,116✔
862
  micro.elastic = micro.thermal + (1.0 - sab_frac) * micro.elastic;
38,054,116✔
863

864
  // Save temperature index and thermal fraction
865
  micro.index_temp_sab = i_temp;
38,054,116✔
866
  micro.sab_frac = sab_frac;
38,054,116✔
867
}
38,054,116✔
868

869
void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const
30,455,362✔
870
{
871
  auto& micro = p.neutron_xs(index_);
30,455,362✔
872
  micro.use_ptable = true;
30,455,362✔
873

874
  // Create a shorthand for the URR data
875
  const auto& urr = urr_data_[i_temp];
30,455,362✔
876

877
  // Determine the energy table
878
  int i_energy =
879
    lower_bound_index(urr.energy_.begin(), urr.energy_.end(), p.E());
30,455,362✔
880

881
  // Sample the probability table using the cumulative distribution
882

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

890
  // Warning: this assumes row-major order of cdf_values_
891
  int i_low = upper_bound_index(&urr.cdf_values_(i_energy, 0),
30,455,362✔
892
                &urr.cdf_values_(i_energy, 0) + urr.n_cdf(), r) +
30,455,362✔
893
              1;
30,455,362✔
894
  int i_up = upper_bound_index(&urr.cdf_values_(i_energy + 1, 0),
30,455,362✔
895
               &urr.cdf_values_(i_energy + 1, 0) + urr.n_cdf(), r) +
30,455,362✔
896
             1;
30,455,362✔
897

898
  // Determine elastic, fission, and capture cross sections from the
899
  // probability table
900
  double elastic = 0.;
30,455,362✔
901
  double fission = 0.;
30,455,362✔
902
  double capture = 0.;
30,455,362✔
903
  double f;
904
  if (urr.interp_ == Interpolation::lin_lin) {
30,455,362✔
905
    // Determine the interpolation factor on the table
906
    f = (p.E() - urr.energy_[i_energy]) /
18,229,138✔
907
        (urr.energy_[i_energy + 1] - urr.energy_[i_energy]);
18,229,138✔
908

909
    elastic = (1. - f) * urr.xs_values_(i_energy, i_low).elastic +
18,229,138✔
910
              f * urr.xs_values_(i_energy + 1, i_up).elastic;
18,229,138✔
911
    fission = (1. - f) * urr.xs_values_(i_energy, i_low).fission +
18,229,138✔
912
              f * urr.xs_values_(i_energy + 1, i_up).fission;
18,229,138✔
913
    capture = (1. - f) * urr.xs_values_(i_energy, i_low).n_gamma +
18,229,138✔
914
              f * urr.xs_values_(i_energy + 1, i_up).n_gamma;
18,229,138✔
915
  } else if (urr.interp_ == Interpolation::log_log) {
12,226,224✔
916
    // Determine interpolation factor on the table
917
    f = std::log(p.E() / urr.energy_[i_energy]) /
12,226,224✔
918
        std::log(urr.energy_[i_energy + 1] / urr.energy_[i_energy]);
12,226,224✔
919

920
    // Calculate the elastic cross section/factor
921
    if ((urr.xs_values_(i_energy, i_low).elastic > 0.) &&
24,452,448✔
922
        (urr.xs_values_(i_energy + 1, i_up).elastic > 0.)) {
12,226,224✔
923
      elastic =
924
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).elastic) +
12,226,224✔
925
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).elastic));
12,226,224✔
926
    } else {
927
      elastic = 0.;
×
928
    }
929

930
    // Calculate the fission cross section/factor
931
    if ((urr.xs_values_(i_energy, i_low).fission > 0.) &&
21,715,999✔
932
        (urr.xs_values_(i_energy + 1, i_up).fission > 0.)) {
9,489,775✔
933
      fission =
934
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).fission) +
9,489,775✔
935
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).fission));
9,489,775✔
936
    } else {
937
      fission = 0.;
2,736,449✔
938
    }
939

940
    // Calculate the capture cross section/factor
941
    if ((urr.xs_values_(i_energy, i_low).n_gamma > 0.) &&
24,452,448✔
942
        (urr.xs_values_(i_energy + 1, i_up).n_gamma > 0.)) {
12,226,224✔
943
      capture =
944
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).n_gamma) +
12,226,224✔
945
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).n_gamma));
12,226,224✔
946
    } else {
947
      capture = 0.;
×
948
    }
949
  }
950

951
  // Determine the treatment of inelastic scattering
952
  double inelastic = 0.;
30,455,362✔
953
  if (urr.inelastic_flag_ != C_NONE) {
30,455,362✔
954
    // get interpolation factor
955
    f = micro.interp_factor;
20,682,931✔
956

957
    // Determine inelastic scattering cross section
958
    Reaction* rx = reactions_[urr_inelastic_].get();
20,682,931✔
959
    int xs_index = micro.index_grid - rx->xs_[i_temp].threshold;
20,682,931✔
960
    if (xs_index >= 0) {
20,682,931✔
961
      inelastic = (1. - f) * rx->xs_[i_temp].value[xs_index] +
10,242,761✔
962
                  f * rx->xs_[i_temp].value[xs_index + 1];
10,242,761✔
963
    }
964
  }
965

966
  // Multiply by smooth cross-section if needed
967
  if (urr.multiply_smooth_) {
30,455,362✔
968
    calculate_elastic_xs(p);
10,393,212✔
969
    elastic *= micro.elastic;
10,393,212✔
970
    capture *= (micro.absorption - micro.fission);
10,393,212✔
971
    fission *= micro.fission;
10,393,212✔
972
  }
973

974
  // Check for negative values
975
  if (elastic < 0.) {
30,455,362✔
976
    elastic = 0.;
×
977
  }
978
  if (fission < 0.) {
30,455,362✔
979
    fission = 0.;
×
980
  }
981
  if (capture < 0.) {
30,455,362✔
982
    capture = 0.;
×
983
  }
984

985
  // Set elastic, absorption, fission, total, and capture x/s. Note that the
986
  // total x/s is calculated as a sum of partials instead of the table-provided
987
  // value
988
  micro.elastic = elastic;
30,455,362✔
989
  micro.absorption = capture + fission;
30,455,362✔
990
  micro.fission = fission;
30,455,362✔
991
  micro.total = elastic + inelastic + capture + fission;
30,455,362✔
992
  if (simulation::need_depletion_rx) {
30,455,362✔
993
    micro.reaction[0] = capture;
×
994
  }
995

996
  // Determine nu-fission cross-section
997
  if (fissionable_) {
30,455,362✔
998
    micro.nu_fission = nu(p.E(), EmissionMode::total) * micro.fission;
20,395,459✔
999
  }
1000
}
30,455,362✔
1001

1002
std::pair<gsl::index, double> Nuclide::find_temperature(double T) const
×
1003
{
1004
  Expects(T >= 0.0);
×
1005

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

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

1037
    // Determine interpolation factor
1038
    f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
×
1039
  }
1040

1041
  Ensures(i_temp >= 0 && i_temp < n);
×
1042

1043
  return {i_temp, f};
×
1044
}
1045

1046
double Nuclide::collapse_rate(int MT, double temperature,
×
1047
  gsl::span<const double> energy, gsl::span<const double> flux) const
1048
{
1049
  Expects(MT > 0);
×
1050
  Expects(energy.size() > 0);
×
1051
  Expects(energy.size() == flux.size() + 1);
×
1052

1053
  int i_rx = reaction_index_[MT];
×
1054
  if (i_rx < 0)
×
1055
    return 0.0;
×
1056
  const auto& rx = reactions_[i_rx];
×
1057

1058
  // Determine temperature index
1059
  gsl::index i_temp;
1060
  double f;
1061
  std::tie(i_temp, f) = this->find_temperature(temperature);
×
1062

1063
  // Get reaction rate at lower temperature
1064
  const auto& grid_low = grid_[i_temp].energy;
×
1065
  double rr_low = rx->collapse_rate(i_temp, energy, flux, grid_low);
×
1066

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

1079
//==============================================================================
1080
// Non-member functions
1081
//==============================================================================
1082

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

1103
extern "C" size_t nuclides_size()
×
1104
{
1105
  return data::nuclides.size();
×
1106
}
1107

1108
//==============================================================================
1109
// C API
1110
//==============================================================================
1111

1112
extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n)
17,079✔
1113
{
1114
  if (data::nuclide_map.find(name) == data::nuclide_map.end() ||
51,081✔
1115
      data::nuclide_map.at(name) >= data::elements.size()) {
34,002✔
1116
    LibraryKey key {Library::Type::neutron, name};
17,079✔
1117
    const auto& it = data::library_map.find(key);
17,079✔
1118
    if (it == data::library_map.end()) {
17,079✔
1119
      set_errmsg(
×
1120
        "Nuclide '" + std::string {name} + "' is not present in library.");
×
1121
      return OPENMC_E_DATA;
×
1122
    }
1123

1124
    // Get filename for library containing nuclide
1125
    int idx = it->second;
17,079✔
1126
    const auto& filename = data::libraries[idx].path_;
17,079✔
1127
    write_message(6, "Reading {} from {}", name, filename);
17,079✔
1128

1129
    // Open file and make sure version is sufficient
1130
    hid_t file_id = file_open(filename, 'r');
17,079✔
1131
    check_data_version(file_id);
17,079✔
1132

1133
    // Read nuclide data from HDF5
1134
    hid_t group = open_group(file_id, name);
17,079✔
1135
    vector<double> temperature {temps, temps + n};
17,079✔
1136
    data::nuclides.push_back(make_unique<Nuclide>(group, temperature));
17,079✔
1137

1138
    close_group(group);
17,079✔
1139
    file_close(file_id);
17,079✔
1140

1141
    // Read multipole file into the appropriate entry on the nuclides array
1142
    int i_nuclide = data::nuclide_map.at(name);
17,079✔
1143
    if (settings::temperature_multipole)
17,079✔
1144
      read_multipole_data(i_nuclide);
442✔
1145

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

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

1164
        // Open file and make sure version is sufficient
1165
        hid_t file_id = file_open(filename, 'r');
563✔
1166
        check_data_version(file_id);
563✔
1167

1168
        // Read element data from HDF5
1169
        hid_t group = open_group(file_id, element.c_str());
563✔
1170
        data::elements.push_back(make_unique<PhotonInteraction>(group));
563✔
1171

1172
        close_group(group);
563✔
1173
        file_close(file_id);
563✔
1174
      }
563✔
1175
    }
1,101✔
1176
  }
17,079✔
1177
  return 0;
17,079✔
1178
}
1179

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

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

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

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

1223
void nuclides_clear()
5,426✔
1224
{
1225
  data::nuclides.clear();
5,426✔
1226
  data::nuclide_map.clear();
5,426✔
1227
}
5,426✔
1228

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

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

© 2025 Coveralls, Inc