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

openmc-dev / openmc / 7016117814

28 Nov 2023 08:32AM UTC coverage: 84.538% (+0.005%) from 84.533%
7016117814

push

github

web-flow
In volume calculation mode, only load atomic weight ratio from data files (#2741)

2 of 2 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

47069 of 55678 relevant lines covered (84.54%)

34253958.6 hits per line

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

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

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

18
#include <fmt/core.h>
19

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

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

26
namespace openmc {
27

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

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

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

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

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

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

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

65
  if (settings::run_mode == RunMode::VOLUME) {
27,941✔
66
    return;
593✔
67
  }
68

69
  // Determine temperatures available
70
  hid_t kT_group = open_group(group, "kTs");
27,348✔
71
  auto dset_names = dataset_names(kT_group);
27,348✔
72
  vector<double> temps_available;
27,348✔
73
  for (const auto& name : dset_names) {
55,116✔
74
    double T;
75
    read_dataset(kT_group, name.c_str(), T);
27,768✔
76
    temps_available.push_back(T / K_BOLTZMANN);
27,768✔
77
  }
78
  std::sort(temps_available.begin(), temps_available.end());
27,348✔
79

80
  // If only one temperature is available, revert to nearest temperature
81
  if (temps_available.size() == 1 &&
54,486✔
82
      settings::temperature_method == TemperatureMethod::INTERPOLATION) {
27,138✔
83
    if (mpi::master) {
×
84
      warning("Cross sections for " + name_ +
×
85
              " are only available at one "
86
              "temperature. Reverting to nearest temperature method.");
87
    }
88
    settings::temperature_method = TemperatureMethod::NEAREST;
×
89
  }
90

91
  // Determine actual temperatures to read -- start by checking whether a
92
  // temperature range was given (indicated by T_max > 0), in which case all
93
  // temperatures in the range are loaded irrespective of what temperatures
94
  // actually appear in the model
95
  vector<int> temps_to_read;
27,348✔
96
  int n = temperature.size();
27,348✔
97
  double T_min = n > 0 ? settings::temperature_range[0] : 0.0;
27,348✔
98
  double T_max = n > 0 ? settings::temperature_range[1] : INFTY;
27,348✔
99
  if (T_max > 0.0) {
27,348✔
100
    // Determine first available temperature below or equal to T_min
101
    auto T_min_it =
102
      std::upper_bound(temps_available.begin(), temps_available.end(), T_min);
1,561✔
103
    if (T_min_it != temps_available.begin())
1,561✔
104
      --T_min_it;
×
105

106
    // Determine first available temperature above or equal to T_max
107
    auto T_max_it =
108
      std::lower_bound(temps_available.begin(), temps_available.end(), T_max);
1,561✔
109
    if (T_max_it != temps_available.end())
1,561✔
110
      ++T_max_it;
×
111

112
    // Add corresponding temperatures to vector
113
    for (auto it = T_min_it; it != T_max_it; ++it) {
3,122✔
114
      temps_to_read.push_back(std::round(*it));
1,561✔
115
    }
116
  }
117

118
  switch (settings::temperature_method) {
27,348✔
119
  case TemperatureMethod::NEAREST:
27,194✔
120
    // Find nearest temperatures
121
    for (double T_desired : temperature) {
52,898✔
122

123
      // Determine closest temperature
124
      double min_delta_T = INFTY;
25,704✔
125
      double T_actual = 0.0;
25,704✔
126
      for (auto T : temps_available) {
51,520✔
127
        double delta_T = std::abs(T - T_desired);
25,816✔
128
        if (delta_T < min_delta_T) {
25,816✔
129
          T_actual = T;
25,746✔
130
          min_delta_T = delta_T;
25,746✔
131
        }
132
      }
133

134
      if (std::abs(T_actual - T_desired) < settings::temperature_tolerance) {
25,704✔
135
        if (!contains(temps_to_read, std::round(T_actual))) {
25,704✔
136
          temps_to_read.push_back(std::round(T_actual));
25,633✔
137

138
          // Write warning for resonance scattering data if 0K is not available
139
          if (std::abs(T_actual - T_desired) > 0 && T_desired == 0 &&
25,633✔
140
              mpi::master) {
141
            warning(name_ +
×
142
                    " does not contain 0K data needed for resonance "
143
                    "scattering options selected. Using data at " +
×
144
                    std::to_string(T_actual) + " K instead.");
×
145
          }
146
        }
147
      } else {
148
        fatal_error(
×
149
          "Nuclear data library does not contain cross sections for " + name_ +
×
150
          " at or near " + std::to_string(T_desired) + " K.");
×
151
      }
152
    }
153
    break;
27,194✔
154

155
  case TemperatureMethod::INTERPOLATION:
154✔
156
    // If temperature interpolation or multipole is selected, get a list of
157
    // bounding temperatures for each actual temperature present in the model
158
    for (double T_desired : temperature) {
322✔
159
      bool found_pair = false;
168✔
160
      for (int j = 0; j < temps_available.size() - 1; ++j) {
504✔
161
        if (temps_available[j] <= T_desired &&
546✔
162
            T_desired < temps_available[j + 1]) {
210✔
163
          int T_j = std::round(temps_available[j]);
98✔
164
          int T_j1 = std::round(temps_available[j + 1]);
98✔
165
          if (!contains(temps_to_read, T_j)) {
98✔
166
            temps_to_read.push_back(T_j);
98✔
167
          }
168
          if (!contains(temps_to_read, T_j1)) {
98✔
169
            temps_to_read.push_back(T_j1);
84✔
170
          }
171
          found_pair = true;
98✔
172
        }
173
      }
174

175
      if (!found_pair) {
168✔
176
        // If no pairs found, check if the desired temperature falls just
177
        // outside of data
178
        if (std::abs(T_desired - temps_available.front()) <=
70✔
179
            settings::temperature_tolerance) {
180
          if (!contains(temps_to_read, temps_available.front())) {
42✔
181
            temps_to_read.push_back(std::round(temps_available.front()));
42✔
182
          }
183
          continue;
42✔
184
        }
185
        if (std::abs(T_desired - temps_available.back()) <=
28✔
186
            settings::temperature_tolerance) {
187
          if (!contains(temps_to_read, temps_available.back())) {
28✔
188
            temps_to_read.push_back(std::round(temps_available.back()));
28✔
189
          }
190
          continue;
28✔
191
        }
192
        fatal_error(
×
193
          "Nuclear data library does not contain cross sections for " + name_ +
×
194
          " at temperatures that bound " + std::to_string(T_desired) + " K.");
×
195
      }
196
    }
197
    break;
154✔
198
  }
199

200
  // Sort temperatures to read
201
  std::sort(temps_to_read.begin(), temps_to_read.end());
27,348✔
202

203
  data::temperature_min =
27,348✔
204
    std::min(data::temperature_min, static_cast<double>(temps_to_read.front()));
27,348✔
205
  data::temperature_max =
27,348✔
206
    std::max(data::temperature_max, static_cast<double>(temps_to_read.back()));
27,348✔
207

208
  hid_t energy_group = open_group(group, "energy");
27,348✔
209
  for (const auto& T : temps_to_read) {
54,794✔
210
    std::string dset {std::to_string(T) + "K"};
27,446✔
211

212
    // Determine exact kT values
213
    double kT;
214
    read_dataset(kT_group, dset.c_str(), kT);
27,446✔
215
    kTs_.push_back(kT);
27,446✔
216

217
    // Read energy grid
218
    grid_.emplace_back();
27,446✔
219
    read_dataset(energy_group, dset.c_str(), grid_.back().energy);
27,446✔
220
  }
27,446✔
221
  close_group(kT_group);
27,348✔
222

223
  // Check for 0K energy grid
224
  if (object_exists(energy_group, "0K")) {
27,348✔
225
    read_dataset(energy_group, "0K", energy_0K_);
4,937✔
226
  }
227
  close_group(energy_group);
27,348✔
228

229
  // Read reactions
230
  hid_t rxs_group = open_group(group, "reactions");
27,348✔
231
  for (auto name : group_names(rxs_group)) {
1,289,803✔
232
    if (starts_with(name, "reaction_")) {
1,262,455✔
233
      hid_t rx_group = open_group(rxs_group, name.c_str());
1,262,455✔
234
      reactions_.push_back(make_unique<Reaction>(rx_group, temps_to_read));
1,262,455✔
235

236
      // Check for 0K elastic scattering
237
      const auto& rx = reactions_.back();
1,262,455✔
238
      if (rx->mt_ == ELASTIC) {
1,262,455✔
239
        if (object_exists(rx_group, "0K")) {
27,348✔
240
          hid_t temp_group = open_group(rx_group, "0K");
4,937✔
241
          read_dataset(temp_group, "xs", elastic_0K_);
4,937✔
242
          close_group(temp_group);
4,937✔
243
        }
244
      }
245
      close_group(rx_group);
1,262,455✔
246

247
      // Determine reaction indices for inelastic scattering reactions
248
      if (is_inelastic_scatter(rx->mt_) && !rx->redundant_) {
1,262,455✔
249
        index_inelastic_scatter_.push_back(reactions_.size() - 1);
620,515✔
250
      }
251
    }
252
  }
1,289,803✔
253
  close_group(rxs_group);
27,348✔
254

255
  // Read unresolved resonance probability tables if present
256
  if (object_exists(group, "urr")) {
27,348✔
257
    urr_present_ = true;
12,622✔
258
    urr_data_.reserve(temps_to_read.size());
12,622✔
259

260
    for (int i = 0; i < temps_to_read.size(); i++) {
25,244✔
261
      // Get temperature as a string
262
      std::string temp_str {std::to_string(temps_to_read[i]) + "K"};
12,622✔
263

264
      // Read probability tables for i-th temperature
265
      hid_t urr_group = open_group(group, ("urr/" + temp_str).c_str());
12,622✔
266
      urr_data_.emplace_back(urr_group);
12,622✔
267
      close_group(urr_group);
12,622✔
268

269
      // Check for negative values
270
      if (urr_data_[i].has_negative() && mpi::master) {
12,622✔
UNCOV
271
        warning("Negative value(s) found on probability table for nuclide " +
×
UNCOV
272
                name_ + " at " + temp_str);
×
273
      }
274
    }
12,622✔
275

276
    // If the inelastic competition flag indicates that the inelastic cross
277
    // section should be determined from a normal reaction cross section, we
278
    // need to get the index of the reaction.
279
    if (temps_to_read.size() > 0) {
12,622✔
280
      // Make sure inelastic flags are consistent for different temperatures
281
      for (int i = 0; i < urr_data_.size() - 1; ++i) {
12,622✔
282
        if (urr_data_[i].inelastic_flag_ != urr_data_[i + 1].inelastic_flag_) {
×
283
          fatal_error(fmt::format(
×
284
            "URR inelastic flag is not consistent for "
285
            "multiple temperatures in nuclide {}. This most likely indicates "
286
            "a problem in how the data was processed.",
287
            name_));
×
288
        }
289
      }
290

291
      if (urr_data_[0].inelastic_flag_ > 0) {
12,622✔
292
        for (int i = 0; i < reactions_.size(); i++) {
372,032✔
293
          if (reactions_[i]->mt_ == urr_data_[0].inelastic_flag_) {
365,313✔
294
            urr_inelastic_ = i;
6,719✔
295
          }
296
        }
297

298
        // Abort if no corresponding inelastic reaction was found
299
        if (urr_inelastic_ == C_NONE) {
6,719✔
300
          fatal_error("Could no find inelastic reaction specified on "
×
301
                      "unresolved resonance probability table.");
302
        }
303
      }
304
    }
305
  }
306

307
  // Check for total nu data
308
  if (object_exists(group, "total_nu")) {
27,348✔
309
    // Read total nu data
310
    hid_t nu_group = open_group(group, "total_nu");
6,427✔
311
    total_nu_ = read_function(nu_group, "yield");
6,427✔
312
    close_group(nu_group);
6,427✔
313
  }
314

315
  // Read fission energy release data if present
316
  if (object_exists(group, "fission_energy_release")) {
27,348✔
317
    hid_t fer_group = open_group(group, "fission_energy_release");
6,427✔
318
    fission_q_prompt_ = read_function(fer_group, "q_prompt");
6,427✔
319
    fission_q_recov_ = read_function(fer_group, "q_recoverable");
6,427✔
320

321
    // Read fission fragment and delayed beta energy release. This is needed for
322
    // energy normalization in k-eigenvalue calculations
323
    fragments_ = read_function(fer_group, "fragments");
6,427✔
324
    betas_ = read_function(fer_group, "betas");
6,427✔
325

326
    // We need prompt/delayed photon energy release for scaling fission photon
327
    // production
328
    prompt_photons_ = read_function(fer_group, "prompt_photons");
6,427✔
329
    delayed_photons_ = read_function(fer_group, "delayed_photons");
6,427✔
330
    close_group(fer_group);
6,427✔
331
  }
332

333
  this->create_derived(prompt_photons_.get(), delayed_photons_.get());
27,348✔
334
}
27,348✔
335

336
Nuclide::~Nuclide()
27,941✔
337
{
338
  data::nuclide_map.erase(name_);
27,941✔
339
}
27,941✔
340

341
void Nuclide::create_derived(
27,348✔
342
  const Function1D* prompt_photons, const Function1D* delayed_photons)
343
{
344
  for (const auto& grid : grid_) {
54,794✔
345
    // Allocate and initialize cross section
346
    array<size_t, 2> shape {grid.energy.size(), 5};
27,446✔
347
    xs_.emplace_back(shape, 0.0);
27,446✔
348
  }
349

350
  reaction_index_.fill(C_NONE);
27,348✔
351
  for (int i = 0; i < reactions_.size(); ++i) {
1,289,803✔
352
    const auto& rx {reactions_[i]};
1,262,455✔
353

354
    // Set entry in direct address table for reaction
355
    reaction_index_[rx->mt_] = i;
1,262,455✔
356

357
    for (int t = 0; t < kTs_.size(); ++t) {
2,525,204✔
358
      int j = rx->xs_[t].threshold;
1,262,749✔
359
      int n = rx->xs_[t].value.size();
1,262,749✔
360
      auto xs = xt::adapt(rx->xs_[t].value);
1,262,749✔
361

362
      for (const auto& p : rx->products_) {
4,590,065✔
363
        if (p.particle_ == ParticleType::photon) {
3,327,316✔
364
          auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD);
2,607,301✔
365
          for (int k = 0; k < n; ++k) {
2,147,483,647✔
366
            double E = grid_[t].energy[k + j];
2,147,483,647✔
367

368
            // For fission, artificially increase the photon yield to account
369
            // for delayed photons
370
            double f = 1.0;
2,147,483,647✔
371
            if (settings::delayed_photon_scaling) {
2,147,483,647✔
372
              if (is_fission(rx->mt_)) {
2,147,483,647✔
373
                if (prompt_photons && delayed_photons) {
527,906,443✔
374
                  double energy_prompt = (*prompt_photons)(E);
527,906,443✔
375
                  double energy_delayed = (*delayed_photons)(E);
527,906,443✔
376
                  f = (energy_prompt + energy_delayed) / (energy_prompt);
527,906,443✔
377
                }
378
              }
379
            }
380

381
            pprod[k] += f * xs[k] * (*p.yield_)(E);
2,147,483,647✔
382
          }
383
        }
2,607,301✔
384
      }
385

386
      // Skip redundant reactions
387
      if (rx->redundant_)
1,262,749✔
388
        continue;
174,260✔
389

390
      // Add contribution to total cross section
391
      auto total = xt::view(xs_[t], xt::range(j, j + n), XS_TOTAL);
1,088,489✔
392
      total += xs;
1,088,489✔
393

394
      // Add contribution to absorption cross section
395
      auto absorption = xt::view(xs_[t], xt::range(j, j + n), XS_ABSORPTION);
1,088,489✔
396
      if (is_disappearance(rx->mt_)) {
1,088,489✔
397
        absorption += xs;
429,938✔
398
      }
399

400
      if (is_fission(rx->mt_)) {
1,088,489✔
401
        fissionable_ = true;
10,590✔
402
        auto fission = xt::view(xs_[t], xt::range(j, j + n), XS_FISSION);
10,590✔
403
        fission += xs;
10,590✔
404
        absorption += xs;
10,590✔
405

406
        // Keep track of fission reactions
407
        if (t == 0) {
10,590✔
408
          fission_rx_.push_back(rx.get());
10,492✔
409
          if (rx->mt_ == N_F)
10,492✔
410
            has_partial_fission_ = true;
1,285✔
411
        }
412
      }
10,590✔
413
    }
1,262,749✔
414
  }
415

416
  // Determine number of delayed neutron precursors
417
  if (fissionable_) {
27,348✔
418
    for (const auto& product : fission_rx_[0]->products_) {
56,693✔
419
      if (product.emission_mode_ == EmissionMode::delayed) {
50,056✔
420
        ++n_precursor_;
38,334✔
421
      }
422
    }
423
  }
424

425
  // Calculate nu-fission cross section
426
  for (int t = 0; t < kTs_.size(); ++t) {
54,794✔
427
    if (fissionable_) {
27,446✔
428
      int n = grid_[t].energy.size();
6,735✔
429
      for (int i = 0; i < n; ++i) {
529,033,721✔
430
        double E = grid_[t].energy[i];
529,026,986✔
431
        xs_[t](i, XS_NU_FISSION) =
529,026,986✔
432
          nu(E, EmissionMode::total) * xs_[t](i, XS_FISSION);
529,026,986✔
433
      }
434
    }
435
  }
436

437
  if (settings::res_scat_on) {
27,348✔
438
    // Determine if this nuclide should be treated as a resonant scatterer
439
    if (!settings::res_scat_nuclides.empty()) {
76✔
440
      // If resonant nuclides were specified, check the list explicitly
441
      for (const auto& name : settings::res_scat_nuclides) {
190✔
442
        if (name_ == name) {
171✔
443
          resonant_ = true;
57✔
444

445
          // Make sure nuclide has 0K data
446
          if (energy_0K_.empty()) {
57✔
447
            fatal_error("Cannot treat " + name_ +
×
448
                        " as a resonant scatterer "
449
                        "because 0 K elastic scattering data is not present.");
450
          }
451
          break;
57✔
452
        }
453
      }
454
    } else {
455
      // Otherwise, assume that any that have 0 K elastic scattering data are
456
      // resonant
457
      resonant_ = !energy_0K_.empty();
×
458
    }
459

460
    if (resonant_) {
76✔
461
      // Build CDF for 0K elastic scattering
462
      double xs_cdf_sum = 0.0;
57✔
463
      xs_cdf_.resize(energy_0K_.size());
57✔
464
      xs_cdf_[0] = 0.0;
57✔
465

466
      const auto& E = energy_0K_;
57✔
467
      auto& xs = elastic_0K_;
57✔
468
      for (int i = 0; i < E.size() - 1; ++i) {
10,583,684✔
469
        // Negative cross sections result in a CDF that is not monotonically
470
        // increasing. Set all negative xs values to zero.
471
        if (xs[i] < 0.0)
10,583,627✔
472
          xs[i] = 0.0;
×
473

474
        // build xs cdf
475
        xs_cdf_sum +=
10,583,627✔
476
          (std::sqrt(E[i]) * xs[i] + std::sqrt(E[i + 1]) * xs[i + 1]) / 2.0 *
10,583,627✔
477
          (E[i + 1] - E[i]);
10,583,627✔
478
        xs_cdf_[i + 1] = xs_cdf_sum;
10,583,627✔
479
      }
480
    }
481
  }
482
}
27,348✔
483

484
void Nuclide::init_grid()
30,195✔
485
{
486
  int neutron = static_cast<int>(ParticleType::neutron);
30,195✔
487
  double E_min = data::energy_min[neutron];
30,195✔
488
  double E_max = data::energy_max[neutron];
30,195✔
489
  int M = settings::n_log_bins;
30,195✔
490

491
  // Determine equal-logarithmic energy spacing
492
  double spacing = std::log(E_max / E_min) / M;
30,195✔
493

494
  // Create equally log-spaced energy grid
495
  auto umesh = xt::linspace(0.0, M * spacing, M + 1);
30,195✔
496

497
  for (auto& grid : grid_) {
60,488✔
498
    // Resize array for storing grid indices
499
    grid.grid_index.resize(M + 1);
30,293✔
500

501
    // Determine corresponding indices in nuclide grid to energies on
502
    // equal-logarithmic grid
503
    int j = 0;
30,293✔
504
    for (int k = 0; k <= M; ++k) {
243,088,586✔
505
      while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) {
967,933,735✔
506
        // Ensure that for isotopes where maxval(grid.energy) << E_max that
507
        // there are no out-of-bounds issues.
508
        if (j + 2 == grid.energy.size())
724,894,349✔
509
          break;
18,907✔
510
        ++j;
724,875,442✔
511
      }
512
      grid.grid_index[k] = j;
243,058,293✔
513
    }
514
  }
515
}
30,195✔
516

517
double Nuclide::nu(double E, EmissionMode mode, int group) const
908,317,197✔
518
{
519
  if (!fissionable_)
908,317,197✔
520
    return 0.0;
37,119,712✔
521

522
  switch (mode) {
871,197,485✔
523
  case EmissionMode::prompt:
9,564,352✔
524
    return (*fission_rx_[0]->products_[0].yield_)(E);
9,564,352✔
525
  case EmissionMode::delayed:
172,321,404✔
526
    if (n_precursor_ > 0 && settings::create_delayed_neutrons) {
172,321,404✔
527
      auto rx = fission_rx_[0];
169,949,300✔
528
      if (group >= 1 && group < rx->products_.size()) {
169,949,300✔
529
        // If delayed group specified, determine yield immediately
530
        return (*rx->products_[group].yield_)(E);
136,641,120✔
531
      } else {
532
        double nu {0.0};
33,308,180✔
533

534
        for (int i = 1; i < rx->products_.size(); ++i) {
264,948,337✔
535
          // Skip any non-neutron products
536
          const auto& product = rx->products_[i];
231,640,157✔
537
          if (product.particle_ != ParticleType::neutron)
231,640,157✔
538
            continue;
31,791,077✔
539

540
          // Evaluate yield
541
          if (product.emission_mode_ == EmissionMode::delayed) {
199,849,080✔
542
            nu += (*product.yield_)(E);
199,849,080✔
543
          }
544
        }
545
        return nu;
33,308,180✔
546
      }
547
    } else {
548
      return 0.0;
2,372,104✔
549
    }
550
  case EmissionMode::total:
689,311,729✔
551
    if (total_nu_ && settings::create_delayed_neutrons) {
689,311,729✔
552
      return (*total_nu_)(E);
687,105,385✔
553
    } else {
554
      return (*fission_rx_[0]->products_[0].yield_)(E);
2,206,344✔
555
    }
556
  }
557
  UNREACHABLE();
×
558
}
559

560
void Nuclide::calculate_elastic_xs(Particle& p) const
883,766,685✔
561
{
562
  // Get temperature index, grid index, and interpolation factor
563
  auto& micro {p.neutron_xs(index_)};
883,766,685✔
564
  int i_temp = micro.index_temp;
883,766,685✔
565
  int i_grid = micro.index_grid;
883,766,685✔
566
  double f = micro.interp_factor;
883,766,685✔
567

568
  if (i_temp >= 0) {
883,766,685✔
569
    const auto& xs = reactions_[0]->xs_[i_temp].value;
879,669,361✔
570
    micro.elastic = (1.0 - f) * xs[i_grid] + f * xs[i_grid + 1];
879,669,361✔
571
  }
572
}
883,766,685✔
573

574
double Nuclide::elastic_xs_0K(double E) const
×
575
{
576
  // Determine index on nuclide energy grid
577
  int i_grid;
578
  if (E < energy_0K_.front()) {
×
579
    i_grid = 0;
×
580
  } else if (E > energy_0K_.back()) {
×
581
    i_grid = energy_0K_.size() - 2;
×
582
  } else {
583
    i_grid = lower_bound_index(energy_0K_.begin(), energy_0K_.end(), E);
×
584
  }
585

586
  // check for rare case where two energy points are the same
587
  if (energy_0K_[i_grid] == energy_0K_[i_grid + 1])
×
588
    ++i_grid;
×
589

590
  // calculate interpolation factor
591
  double f =
592
    (E - energy_0K_[i_grid]) / (energy_0K_[i_grid + 1] - energy_0K_[i_grid]);
×
593

594
  // Calculate microscopic nuclide elastic cross section
595
  return (1.0 - f) * elastic_0K_[i_grid] + f * elastic_0K_[i_grid + 1];
×
596
}
597

598
void Nuclide::calculate_xs(
2,147,483,647✔
599
  int i_sab, int i_log_union, double sab_frac, Particle& p)
600
{
601
  auto& micro {p.neutron_xs(index_)};
2,147,483,647✔
602

603
  // Initialize cached cross sections to zero
604
  micro.elastic = CACHE_INVALID;
2,147,483,647✔
605
  micro.thermal = 0.0;
2,147,483,647✔
606
  micro.thermal_elastic = 0.0;
2,147,483,647✔
607

608
  // Check to see if there is multipole data present at this energy
609
  bool use_mp = false;
2,147,483,647✔
610
  if (multipole_) {
2,147,483,647✔
611
    use_mp = (p.E() >= multipole_->E_min_ && p.E() <= multipole_->E_max_);
11,606,560✔
612
  }
613

614
  // Evaluate multipole or interpolate
615
  if (use_mp) {
2,147,483,647✔
616
    // Call multipole kernel
617
    double sig_s, sig_a, sig_f;
618
    std::tie(sig_s, sig_a, sig_f) = multipole_->evaluate(p.E(), p.sqrtkT());
11,176,508✔
619

620
    micro.total = sig_s + sig_a;
11,176,508✔
621
    micro.elastic = sig_s;
11,176,508✔
622
    micro.absorption = sig_a;
11,176,508✔
623
    micro.fission = sig_f;
11,176,508✔
624
    micro.nu_fission =
11,176,508✔
625
      fissionable_ ? sig_f * this->nu(p.E(), EmissionMode::total) : 0.0;
11,176,508✔
626

627
    if (simulation::need_depletion_rx) {
11,176,508✔
628
      // Only non-zero reaction is (n,gamma)
629
      micro.reaction[0] = sig_a - sig_f;
×
630

631
      // Set all other reaction cross sections to zero
632
      for (int i = 1; i < DEPLETION_RX.size(); ++i) {
×
633
        micro.reaction[i] = 0.0;
×
634
      }
635
    }
636

637
    // Ensure these values are set
638
    // Note, the only time either is used is in one of 4 places:
639
    // 1. physics.cpp - scatter - For inelastic scatter.
640
    // 2. physics.cpp - sample_fission - For partial fissions.
641
    // 3. tally.F90 - score_general - For tallying on MTxxx reactions.
642
    // 4. nuclide.cpp - calculate_urr_xs - For unresolved purposes.
643
    // It is worth noting that none of these occur in the resolved
644
    // resonance range, so the value here does not matter.  index_temp is
645
    // set to -1 to force a segfault in case a developer messes up and tries
646
    // to use it with multipole.
647
    micro.index_temp = -1;
11,176,508✔
648
    micro.index_grid = -1;
11,176,508✔
649
    micro.interp_factor = 0.0;
11,176,508✔
650

651
  } else {
652
    // Find the appropriate temperature index.
653
    double kT = p.sqrtkT() * p.sqrtkT();
2,147,483,647✔
654
    double f;
655
    int i_temp = -1;
2,147,483,647✔
656
    switch (settings::temperature_method) {
2,147,483,647✔
657
    case TemperatureMethod::NEAREST: {
2,147,483,647✔
658
      double max_diff = INFTY;
2,147,483,647✔
659
      for (int t = 0; t < kTs_.size(); ++t) {
2,147,483,647✔
660
        double diff = std::abs(kTs_[t] - kT);
2,147,483,647✔
661
        if (diff < max_diff) {
2,147,483,647✔
662
          i_temp = t;
2,147,483,647✔
663
          max_diff = diff;
2,147,483,647✔
664
        }
665
      }
666
    } break;
2,147,483,647✔
667

668
    case TemperatureMethod::INTERPOLATION:
1,930,670✔
669
      // If current kT outside of the bounds of available, snap to the bound
670
      if (kT < kTs_.front()) {
1,930,670✔
671
        i_temp = 0;
525,924✔
672
        break;
525,924✔
673
      }
674
      if (kT > kTs_.back()) {
1,404,746✔
675
        i_temp = kTs_.size() - 1;
276,472✔
676
        break;
276,472✔
677
      }
678

679
      // Find temperatures that bound the actual temperature
680
      for (i_temp = 0; i_temp < kTs_.size() - 1; ++i_temp) {
1,128,274✔
681
        if (kTs_[i_temp] <= kT && kT < kTs_[i_temp + 1])
1,128,274✔
682
          break;
1,128,274✔
683
      }
684

685
      // Randomly sample between temperature i and i+1
686
      f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
1,128,274✔
687
      if (f > prn(p.current_seed()))
1,128,274✔
688
        ++i_temp;
524,202✔
689
      break;
1,128,274✔
690
    }
691

692
    // Determine the energy grid index using a logarithmic mapping to
693
    // reduce the energy range over which a binary search needs to be
694
    // performed
695

696
    const auto& grid {grid_[i_temp]};
2,147,483,647✔
697
    const auto& xs {xs_[i_temp]};
2,147,483,647✔
698

699
    int i_grid;
700
    if (p.E() < grid.energy.front()) {
2,147,483,647✔
701
      i_grid = 0;
802✔
702
    } else if (p.E() > grid.energy.back()) {
2,147,483,647✔
703
      i_grid = grid.energy.size() - 2;
×
704
    } else {
705
      // Determine bounding indices based on which equal log-spaced
706
      // interval the energy is in
707
      int i_low = grid.grid_index[i_log_union];
2,147,483,647✔
708
      int i_high = grid.grid_index[i_log_union + 1] + 1;
2,147,483,647✔
709

710
      // Perform binary search over reduced range
711
      i_grid = i_low + lower_bound_index(
2,147,483,647✔
712
                         &grid.energy[i_low], &grid.energy[i_high], p.E());
2,147,483,647✔
713
    }
714

715
    // check for rare case where two energy points are the same
716
    if (grid.energy[i_grid] == grid.energy[i_grid + 1])
2,147,483,647✔
717
      ++i_grid;
×
718

719
    // calculate interpolation factor
720
    f = (p.E() - grid.energy[i_grid]) /
2,147,483,647✔
721
        (grid.energy[i_grid + 1] - grid.energy[i_grid]);
2,147,483,647✔
722

723
    micro.index_temp = i_temp;
2,147,483,647✔
724
    micro.index_grid = i_grid;
2,147,483,647✔
725
    micro.interp_factor = f;
2,147,483,647✔
726

727
    // Calculate microscopic nuclide total cross section
728
    micro.total =
2,147,483,647✔
729
      (1.0 - f) * xs(i_grid, XS_TOTAL) + f * xs(i_grid + 1, XS_TOTAL);
2,147,483,647✔
730

731
    // Calculate microscopic nuclide absorption cross section
732
    micro.absorption =
2,147,483,647✔
733
      (1.0 - f) * xs(i_grid, XS_ABSORPTION) + f * xs(i_grid + 1, XS_ABSORPTION);
2,147,483,647✔
734

735
    if (fissionable_) {
2,147,483,647✔
736
      // Calculate microscopic nuclide total cross section
737
      micro.fission =
773,260,168✔
738
        (1.0 - f) * xs(i_grid, XS_FISSION) + f * xs(i_grid + 1, XS_FISSION);
773,260,168✔
739

740
      // Calculate microscopic nuclide nu-fission cross section
741
      micro.nu_fission = (1.0 - f) * xs(i_grid, XS_NU_FISSION) +
773,260,168✔
742
                         f * xs(i_grid + 1, XS_NU_FISSION);
773,260,168✔
743
    } else {
744
      micro.fission = 0.0;
2,147,483,647✔
745
      micro.nu_fission = 0.0;
2,147,483,647✔
746
    }
747

748
    // Calculate microscopic nuclide photon production cross section
749
    micro.photon_prod = (1.0 - f) * xs(i_grid, XS_PHOTON_PROD) +
2,147,483,647✔
750
                        f * xs(i_grid + 1, XS_PHOTON_PROD);
2,147,483,647✔
751

752
    // Depletion-related reactions
753
    if (simulation::need_depletion_rx) {
2,147,483,647✔
754
      // Initialize all reaction cross sections to zero
755
      for (double& xs_i : micro.reaction) {
2,147,483,647✔
756
        xs_i = 0.0;
2,147,483,647✔
757
      }
758

759
      for (int j = 0; j < DEPLETION_RX.size(); ++j) {
2,147,483,647✔
760
        // If reaction is present and energy is greater than threshold, set the
761
        // reaction xs appropriately
762
        int i_rx = reaction_index_[DEPLETION_RX[j]];
2,147,483,647✔
763
        if (i_rx >= 0) {
2,147,483,647✔
764
          const auto& rx = reactions_[i_rx];
2,147,483,647✔
765
          const auto& rx_xs = rx->xs_[i_temp].value;
2,147,483,647✔
766

767
          // Physics says that (n,gamma) is not a threshold reaction, so we
768
          // don't need to specifically check its threshold index
769
          if (j == 0) {
2,147,483,647✔
770
            micro.reaction[0] =
1,098,416,296✔
771
              (1.0 - f) * rx_xs[i_grid] + f * rx_xs[i_grid + 1];
1,098,416,296✔
772
            continue;
1,098,416,296✔
773
          }
774

775
          int threshold = rx->xs_[i_temp].threshold;
2,147,483,647✔
776
          if (i_grid >= threshold) {
2,147,483,647✔
777
            micro.reaction[j] = (1.0 - f) * rx_xs[i_grid - threshold] +
355,972,606✔
778
                                f * rx_xs[i_grid - threshold + 1];
355,972,606✔
779
          } else if (j >= 3) {
1,949,620,162✔
780
            // One can show that the the threshold for (n,(x+1)n) is always
781
            // higher than the threshold for (n,xn). Thus, if we are below
782
            // the threshold for, e.g., (n,2n), there is no reason to check
783
            // the threshold for (n,3n) and (n,4n).
784
            break;
894,724,189✔
785
          }
786
        }
787
      }
788
    }
789
  }
790

791
  // Initialize sab treatment to false
792
  micro.index_sab = C_NONE;
2,147,483,647✔
793
  micro.sab_frac = 0.0;
2,147,483,647✔
794

795
  // Initialize URR probability table treatment to false
796
  micro.use_ptable = false;
2,147,483,647✔
797

798
  // If there is S(a,b) data for this nuclide, we need to set the sab_scatter
799
  // and sab_elastic cross sections and correct the total and elastic cross
800
  // sections.
801

802
  if (i_sab >= 0)
2,147,483,647✔
803
    this->calculate_sab_xs(i_sab, sab_frac, p);
106,736,602✔
804

805
  // If the particle is in the unresolved resonance range and there are
806
  // probability tables, we need to determine cross sections from the table
807
  if (settings::urr_ptables_on && urr_present_ && !use_mp) {
2,147,483,647✔
808
    if (urr_data_[micro.index_temp].energy_in_bounds(p.E()))
1,448,481,216✔
809
      this->calculate_urr_xs(micro.index_temp, p);
186,760,248✔
810
  }
811

812
  micro.last_E = p.E();
2,147,483,647✔
813
  micro.last_sqrtkT = p.sqrtkT();
2,147,483,647✔
814
}
2,147,483,647✔
815

816
void Nuclide::calculate_sab_xs(int i_sab, double sab_frac, Particle& p)
106,736,602✔
817
{
818
  auto& micro {p.neutron_xs(index_)};
106,736,602✔
819

820
  // Set flag that S(a,b) treatment should be used for scattering
821
  micro.index_sab = i_sab;
106,736,602✔
822

823
  // Calculate the S(a,b) cross section
824
  int i_temp;
825
  double elastic;
826
  double inelastic;
827
  data::thermal_scatt[i_sab]->calculate_xs(
213,473,204✔
828
    p.E(), p.sqrtkT(), &i_temp, &elastic, &inelastic, p.current_seed());
106,736,602✔
829

830
  // Store the S(a,b) cross sections.
831
  micro.thermal = sab_frac * (elastic + inelastic);
106,736,602✔
832
  micro.thermal_elastic = sab_frac * elastic;
106,736,602✔
833

834
  // Calculate free atom elastic cross section
835
  this->calculate_elastic_xs(p);
106,736,602✔
836

837
  // Correct total and elastic cross sections
838
  micro.total = micro.total + micro.thermal - sab_frac * micro.elastic;
106,736,602✔
839
  micro.elastic = micro.thermal + (1.0 - sab_frac) * micro.elastic;
106,736,602✔
840

841
  // Save temperature index and thermal fraction
842
  micro.index_temp_sab = i_temp;
106,736,602✔
843
  micro.sab_frac = sab_frac;
106,736,602✔
844
}
106,736,602✔
845

846
void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const
186,760,248✔
847
{
848
  auto& micro = p.neutron_xs(index_);
186,760,248✔
849
  micro.use_ptable = true;
186,760,248✔
850

851
  // Create a shorthand for the URR data
852
  const auto& urr = urr_data_[i_temp];
186,760,248✔
853

854
  // Determine the energy table
855
  int i_energy =
856
    lower_bound_index(urr.energy_.begin(), urr.energy_.end(), p.E());
186,760,248✔
857

858
  // Sample the probability table using the cumulative distribution
859

860
  // Random numbers for the xs calculation are sampled from a separate stream.
861
  // This guarantees the randomness and, at the same time, makes sure we
862
  // reuse random numbers for the same nuclide at different temperatures,
863
  // therefore preserving correlation of temperature in probability tables.
864
  double r =
865
    future_prn(static_cast<int64_t>(index_), p.seeds(STREAM_URR_PTABLE));
186,760,248✔
866

867
  // Warning: this assumes row-major order of cdf_values_
868
  int i_low = upper_bound_index(&urr.cdf_values_(i_energy, 0),
186,760,248✔
869
                &urr.cdf_values_(i_energy, 0) + urr.n_cdf(), r) +
186,760,248✔
870
              1;
186,760,248✔
871
  int i_up = upper_bound_index(&urr.cdf_values_(i_energy + 1, 0),
186,760,248✔
872
               &urr.cdf_values_(i_energy + 1, 0) + urr.n_cdf(), r) +
186,760,248✔
873
             1;
186,760,248✔
874

875
  // Determine elastic, fission, and capture cross sections from the
876
  // probability table
877
  double elastic = 0.;
186,760,248✔
878
  double fission = 0.;
186,760,248✔
879
  double capture = 0.;
186,760,248✔
880
  double f;
881
  if (urr.interp_ == Interpolation::lin_lin) {
186,760,248✔
882
    // Determine the interpolation factor on the table
883
    f = (p.E() - urr.energy_[i_energy]) /
75,114,198✔
884
        (urr.energy_[i_energy + 1] - urr.energy_[i_energy]);
75,114,198✔
885

886
    elastic = (1. - f) * urr.xs_values_(i_energy, i_low).elastic +
75,114,198✔
887
              f * urr.xs_values_(i_energy + 1, i_up).elastic;
75,114,198✔
888
    fission = (1. - f) * urr.xs_values_(i_energy, i_low).fission +
75,114,198✔
889
              f * urr.xs_values_(i_energy + 1, i_up).fission;
75,114,198✔
890
    capture = (1. - f) * urr.xs_values_(i_energy, i_low).n_gamma +
75,114,198✔
891
              f * urr.xs_values_(i_energy + 1, i_up).n_gamma;
75,114,198✔
892
  } else if (urr.interp_ == Interpolation::log_log) {
111,646,050✔
893
    // Determine interpolation factor on the table
894
    f = std::log(p.E() / urr.energy_[i_energy]) /
111,646,050✔
895
        std::log(urr.energy_[i_energy + 1] / urr.energy_[i_energy]);
111,646,050✔
896

897
    // Calculate the elastic cross section/factor
898
    if ((urr.xs_values_(i_energy, i_low).elastic > 0.) &&
223,292,100✔
899
        (urr.xs_values_(i_energy + 1, i_up).elastic > 0.)) {
111,646,050✔
900
      elastic =
901
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).elastic) +
111,646,050✔
902
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).elastic));
111,646,050✔
903
    } else {
904
      elastic = 0.;
×
905
    }
906

907
    // Calculate the fission cross section/factor
908
    if ((urr.xs_values_(i_energy, i_low).fission > 0.) &&
192,187,224✔
909
        (urr.xs_values_(i_energy + 1, i_up).fission > 0.)) {
80,541,174✔
910
      fission =
911
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).fission) +
80,541,174✔
912
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).fission));
80,541,174✔
913
    } else {
914
      fission = 0.;
31,104,876✔
915
    }
916

917
    // Calculate the capture cross section/factor
918
    if ((urr.xs_values_(i_energy, i_low).n_gamma > 0.) &&
223,292,100✔
919
        (urr.xs_values_(i_energy + 1, i_up).n_gamma > 0.)) {
111,646,050✔
920
      capture =
921
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).n_gamma) +
111,646,050✔
922
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).n_gamma));
111,646,050✔
923
    } else {
924
      capture = 0.;
×
925
    }
926
  }
927

928
  // Determine the treatment of inelastic scattering
929
  double inelastic = 0.;
186,760,248✔
930
  if (urr.inelastic_flag_ != C_NONE) {
186,760,248✔
931
    // get interpolation factor
932
    f = micro.interp_factor;
134,880,482✔
933

934
    // Determine inelastic scattering cross section
935
    Reaction* rx = reactions_[urr_inelastic_].get();
134,880,482✔
936
    int xs_index = micro.index_grid - rx->xs_[i_temp].threshold;
134,880,482✔
937
    if (xs_index >= 0) {
134,880,482✔
938
      inelastic = (1. - f) * rx->xs_[i_temp].value[xs_index] +
78,618,533✔
939
                  f * rx->xs_[i_temp].value[xs_index + 1];
78,618,533✔
940
    }
941
  }
942

943
  // Multiply by smooth cross-section if needed
944
  if (urr.multiply_smooth_) {
186,760,248✔
945
    calculate_elastic_xs(p);
85,533,470✔
946
    elastic *= micro.elastic;
85,533,470✔
947
    capture *= (micro.absorption - micro.fission);
85,533,470✔
948
    fission *= micro.fission;
85,533,470✔
949
  }
950

951
  // Check for negative values
952
  if (elastic < 0.) {
186,760,248✔
953
    elastic = 0.;
×
954
  }
955
  if (fission < 0.) {
186,760,248✔
956
    fission = 0.;
×
957
  }
958
  if (capture < 0.) {
186,760,248✔
959
    capture = 0.;
×
960
  }
961

962
  // Set elastic, absorption, fission, total, and capture x/s. Note that the
963
  // total x/s is calculated as a sum of partials instead of the table-provided
964
  // value
965
  micro.elastic = elastic;
186,760,248✔
966
  micro.absorption = capture + fission;
186,760,248✔
967
  micro.fission = fission;
186,760,248✔
968
  micro.total = elastic + inelastic + capture + fission;
186,760,248✔
969
  if (simulation::need_depletion_rx) {
186,760,248✔
970
    micro.reaction[0] = capture;
47,470,101✔
971
  }
972

973
  // Determine nu-fission cross-section
974
  if (fissionable_) {
186,760,248✔
975
    micro.nu_fission = nu(p.E(), EmissionMode::total) * micro.fission;
130,057,179✔
976
  }
977
}
186,760,248✔
978

979
std::pair<gsl::index, double> Nuclide::find_temperature(double T) const
84✔
980
{
981
  Expects(T >= 0.0);
84✔
982

983
  // Determine temperature index
984
  gsl::index i_temp = 0;
84✔
985
  double f = 0.0;
84✔
986
  double kT = K_BOLTZMANN * T;
84✔
987
  gsl::index n = kTs_.size();
84✔
988
  switch (settings::temperature_method) {
84✔
989
  case TemperatureMethod::NEAREST: {
84✔
990
    double max_diff = INFTY;
84✔
991
    for (gsl::index t = 0; t < n; ++t) {
168✔
992
      double diff = std::abs(kTs_[t] - kT);
84✔
993
      if (diff < max_diff) {
84✔
994
        i_temp = t;
84✔
995
        max_diff = diff;
84✔
996
      }
997
    }
998
  } break;
84✔
999

1000
  case TemperatureMethod::INTERPOLATION:
×
1001
    // If current kT outside of the bounds of available, snap to the bound
1002
    if (kT < kTs_.front()) {
×
1003
      i_temp = 0;
×
1004
      break;
×
1005
    }
1006
    if (kT > kTs_.back()) {
×
1007
      i_temp = kTs_.size() - 1;
×
1008
      break;
×
1009
    }
1010
    // Find temperatures that bound the actual temperature
1011
    while (kTs_[i_temp + 1] < kT && i_temp + 1 < n - 1)
×
1012
      ++i_temp;
×
1013

1014
    // Determine interpolation factor
1015
    f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
×
1016
  }
1017

1018
  Ensures(i_temp >= 0 && i_temp < n);
84✔
1019

1020
  return {i_temp, f};
84✔
1021
}
1022

1023
double Nuclide::collapse_rate(int MT, double temperature,
84✔
1024
  gsl::span<const double> energy, gsl::span<const double> flux) const
1025
{
1026
  Expects(MT > 0);
84✔
1027
  Expects(energy.size() > 0);
84✔
1028
  Expects(energy.size() == flux.size() + 1);
84✔
1029

1030
  int i_rx = reaction_index_[MT];
84✔
1031
  if (i_rx < 0)
84✔
1032
    return 0.0;
×
1033
  const auto& rx = reactions_[i_rx];
84✔
1034

1035
  // Determine temperature index
1036
  gsl::index i_temp;
1037
  double f;
1038
  std::tie(i_temp, f) = this->find_temperature(temperature);
84✔
1039

1040
  // Get reaction rate at lower temperature
1041
  const auto& grid_low = grid_[i_temp].energy;
84✔
1042
  double rr_low = rx->collapse_rate(i_temp, energy, flux, grid_low);
84✔
1043

1044
  if (f > 0.0) {
84✔
1045
    // Interpolate between reaction rate at lower and higher temperature
1046
    const auto& grid_high = grid_[i_temp + 1].energy;
×
1047
    double rr_high = rx->collapse_rate(i_temp + 1, energy, flux, grid_high);
×
1048
    return rr_low + f * (rr_high - rr_low);
×
1049
  } else {
1050
    // If interpolation factor is zero, return reaction rate at lower
1051
    // temperature
1052
    return rr_low;
84✔
1053
  }
1054
}
1055

1056
//==============================================================================
1057
// Non-member functions
1058
//==============================================================================
1059

1060
void check_data_version(hid_t file_id)
29,850✔
1061
{
1062
  if (attribute_exists(file_id, "version")) {
29,850✔
1063
    vector<int> version;
29,850✔
1064
    read_attribute(file_id, "version", version);
29,850✔
1065
    if (version[0] != HDF5_VERSION[0]) {
29,850✔
1066
      fatal_error("HDF5 data format uses version " +
×
1067
                  std::to_string(version[0]) + "." +
×
1068
                  std::to_string(version[1]) +
×
1069
                  " whereas your installation of "
1070
                  "OpenMC expects version " +
×
1071
                  std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1072
    }
1073
  } else {
29,850✔
1074
    fatal_error("HDF5 data does not indicate a version. Your installation of "
×
1075
                "OpenMC expects version " +
×
1076
                std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1077
  }
1078
}
29,850✔
1079

1080
extern "C" size_t nuclides_size()
28✔
1081
{
1082
  return data::nuclides.size();
28✔
1083
}
1084

1085
//==============================================================================
1086
// C API
1087
//==============================================================================
1088

1089
extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n)
27,969✔
1090
{
1091
  if (data::nuclide_map.find(name) == data::nuclide_map.end() ||
82,567✔
1092
      data::nuclide_map.at(name) >= data::elements.size()) {
54,598✔
1093
    LibraryKey key {Library::Type::neutron, name};
27,969✔
1094
    const auto& it = data::library_map.find(key);
27,969✔
1095
    if (it == data::library_map.end()) {
27,969✔
1096
      set_errmsg(
28✔
1097
        "Nuclide '" + std::string {name} + "' is not present in library.");
56✔
1098
      return OPENMC_E_DATA;
28✔
1099
    }
1100

1101
    // Get filename for library containing nuclide
1102
    int idx = it->second;
27,941✔
1103
    const auto& filename = data::libraries[idx].path_;
27,941✔
1104
    write_message(6, "Reading {} from {}", name, filename);
27,941✔
1105

1106
    // Open file and make sure version is sufficient
1107
    hid_t file_id = file_open(filename, 'r');
27,941✔
1108
    check_data_version(file_id);
27,941✔
1109

1110
    // Read nuclide data from HDF5
1111
    hid_t group = open_group(file_id, name);
27,941✔
1112
    vector<double> temperature {temps, temps + n};
27,941✔
1113
    data::nuclides.push_back(make_unique<Nuclide>(group, temperature));
27,941✔
1114

1115
    close_group(group);
27,941✔
1116
    file_close(file_id);
27,941✔
1117

1118
    // Read multipole file into the appropriate entry on the nuclides array
1119
    int i_nuclide = data::nuclide_map.at(name);
27,941✔
1120
    if (settings::temperature_multipole)
27,941✔
1121
      read_multipole_data(i_nuclide);
494✔
1122

1123
    // Read elemental data, if necessary
1124
    if (settings::photon_transport) {
27,941✔
1125
      auto element = to_element(name);
2,990✔
1126
      if (data::element_map.find(element) == data::element_map.end() ||
2,990✔
1127
          data::element_map.at(element) >= data::elements.size()) {
1,495✔
1128
        // Read photon interaction data from HDF5 photon library
1129
        LibraryKey key {Library::Type::photon, element};
711✔
1130
        const auto& it = data::library_map.find(key);
711✔
1131
        if (it == data::library_map.end()) {
711✔
1132
          set_errmsg("Element '" + std::string {element} +
×
1133
                     "' is not present in library.");
1134
          return OPENMC_E_DATA;
×
1135
        }
1136

1137
        int idx = it->second;
711✔
1138
        const auto& filename = data::libraries[idx].path_;
711✔
1139
        write_message(6, "Reading {} from {} ", element, filename);
711✔
1140

1141
        // Open file and make sure version is sufficient
1142
        hid_t file_id = file_open(filename, 'r');
711✔
1143
        check_data_version(file_id);
711✔
1144

1145
        // Read element data from HDF5
1146
        hid_t group = open_group(file_id, element.c_str());
711✔
1147
        data::elements.push_back(make_unique<PhotonInteraction>(group));
711✔
1148

1149
        close_group(group);
711✔
1150
        file_close(file_id);
711✔
1151
      }
711✔
1152
    }
1,495✔
1153
  }
27,969✔
1154
  return 0;
27,941✔
1155
}
1156

1157
extern "C" int openmc_get_nuclide_index(const char* name, int* index)
434✔
1158
{
1159
  auto it = data::nuclide_map.find(name);
434✔
1160
  if (it == data::nuclide_map.end()) {
434✔
1161
    set_errmsg(
14✔
1162
      "No nuclide named '" + std::string {name} + "' has been loaded.");
28✔
1163
    return OPENMC_E_DATA;
14✔
1164
  }
1165
  *index = it->second;
420✔
1166
  return 0;
420✔
1167
}
1168

1169
extern "C" int openmc_nuclide_name(int index, const char** name)
1,890✔
1170
{
1171
  if (index >= 0 && index < data::nuclides.size()) {
1,890✔
1172
    *name = data::nuclides[index]->name_.data();
1,890✔
1173
    return 0;
1,890✔
1174
  } else {
1175
    set_errmsg("Index in nuclides vector is out of bounds.");
×
1176
    return OPENMC_E_OUT_OF_BOUNDS;
×
1177
  }
1178
}
1179

1180
extern "C" int openmc_nuclide_collapse_rate(int index, int MT,
84✔
1181
  double temperature, const double* energy, const double* flux, int n,
1182
  double* xs)
1183
{
1184
  if (index < 0 || index >= data::nuclides.size()) {
84✔
1185
    set_errmsg("Index in nuclides vector is out of bounds.");
×
1186
    return OPENMC_E_OUT_OF_BOUNDS;
×
1187
  }
1188

1189
  try {
1190
    *xs = data::nuclides[index]->collapse_rate(
168✔
1191
      MT, temperature, {energy, energy + n + 1}, {flux, flux + n});
84✔
1192
  } catch (const std::out_of_range& e) {
×
1193
    fmt::print("Caught error\n");
×
1194
    set_errmsg(e.what());
×
1195
    return OPENMC_E_OUT_OF_BOUNDS;
×
1196
  }
×
1197
  return 0;
84✔
1198
}
1199

1200
void nuclides_clear()
6,152✔
1201
{
1202
  data::nuclides.clear();
6,152✔
1203
  data::nuclide_map.clear();
6,152✔
1204
}
6,152✔
1205

1206
bool multipole_in_range(const Nuclide& nuc, double E)
1,242,850✔
1207
{
1208
  return E >= nuc.multipole_->E_min_ && E <= nuc.multipole_->E_max_;
1,242,850✔
1209
}
1210

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