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

openmc-dev / openmc / 28164258238

25 Jun 2026 10:37AM UTC coverage: 80.898% (-0.4%) from 81.267%
28164258238

Pull #3979

github

web-flow
Merge 670fee26c into 4d6244d93
Pull Request #3979: in memory cross-section alteration

18140 of 26525 branches covered (68.39%)

Branch coverage included in aggregate %.

24 of 272 new or added lines in 2 files covered. (8.82%)

59283 of 69180 relevant lines covered (85.69%)

48219588.76 hits per line

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

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

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

18
#include <fmt/core.h>
19

20
#include "openmc/tensor.h"
21

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

26
namespace openmc {
27

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

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

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

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

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

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

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

65
  if (settings::run_mode == RunMode::VOLUME) {
33,261✔
66
    // Determine whether nuclide is fissionable and then exit
67
    int mt;
2,923✔
68
    hid_t rxs_group = open_group(group, "reactions");
2,923✔
69
    for (auto name : group_names(rxs_group)) {
111,748✔
70
      if (starts_with(name, "reaction_")) {
109,527!
71
        hid_t rx_group = open_group(rxs_group, name.c_str());
109,527✔
72
        read_attribute(rx_group, "mt", mt);
109,527✔
73
        if (is_fission(mt)) {
109,527✔
74
          fissionable_ = true;
702✔
75
          break;
702✔
76
        }
77
      }
78
    }
111,748✔
79
    return;
2,923✔
80
  }
81

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

93
  // If only one temperature is available, revert to nearest temperature
94
  if (temps_available.size() == 1 &&
30,338✔
95
      settings::temperature_method == TemperatureMethod::INTERPOLATION) {
30,173!
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;
30,338✔
109
  int n = temperature.size();
30,338✔
110
  double T_min = n > 0 ? settings::temperature_range[0] : 0.0;
30,338✔
111
  double T_max = n > 0 ? settings::temperature_range[1] : INFTY;
30,338✔
112
  if (T_max > 0.0) {
30,338✔
113
    // Determine first available temperature below or equal to T_min
114
    auto T_min_it =
3,381✔
115
      std::upper_bound(temps_available.begin(), temps_available.end(), T_min);
3,381✔
116
    if (T_min_it != temps_available.begin())
3,381!
117
      --T_min_it;
×
118

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

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

131
  switch (settings::temperature_method) {
30,338!
132
  case TemperatureMethod::NEAREST:
30,217✔
133
    // Find nearest temperatures
134
    for (double T_desired : temperature) {
57,960✔
135

136
      // Determine closest temperature
137
      double min_delta_T = INFTY;
27,743✔
138
      double T_actual = 0.0;
27,743✔
139
      for (auto T : temps_available) {
55,574✔
140
        double delta_T = std::abs(T - T_desired);
27,831✔
141
        if (delta_T < min_delta_T) {
27,831✔
142
          T_actual = T;
27,776✔
143
          min_delta_T = delta_T;
27,776✔
144
        }
145
      }
146

147
      if (std::abs(T_actual - T_desired) < settings::temperature_tolerance) {
27,743!
148
        if (!contains(temps_to_read, std::round(T_actual))) {
27,743✔
149
          temps_to_read.push_back(std::round(T_actual));
26,836✔
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 &&
26,836!
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;
170

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

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

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

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

224
  hid_t energy_group = open_group(group, "energy");
30,338✔
225
  for (const auto& T : temps_to_read) {
60,753✔
226
    std::string dset {std::to_string(T) + "K"};
30,415✔
227

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

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

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

245
  // Read reactions
246
  hid_t rxs_group = open_group(group, "reactions");
30,338✔
247
  for (auto name : group_names(rxs_group)) {
1,436,902✔
248
    if (starts_with(name, "reaction_")) {
1,406,564!
249
      hid_t rx_group = open_group(rxs_group, name.c_str());
1,406,564✔
250
      reactions_.push_back(
2,813,128✔
251
        make_unique<Reaction>(rx_group, temps_to_read, name_));
2,813,128✔
252

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

264
      // Determine reaction indices for inelastic scattering reactions
265
      if (is_inelastic_scatter(rx->mt_) && !rx->redundant_) {
1,406,564!
266
        index_inelastic_scatter_.push_back(reactions_.size() - 1);
700,386✔
267
      }
268
    }
269
  }
1,436,902✔
270
  close_group(rxs_group);
30,338✔
271

272
  // Read unresolved resonance probability tables if present
273
  if (object_exists(group, "urr")) {
30,338✔
274
    urr_present_ = true;
14,203✔
275
    urr_data_.reserve(temps_to_read.size());
14,203✔
276

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

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

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

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

308
      if (urr_data_[0].inelastic_flag_ > 0) {
14,203✔
309
        for (int i = 0; i < reactions_.size(); i++) {
452,966✔
310
          if (reactions_[i]->mt_ == urr_data_[0].inelastic_flag_) {
444,743✔
311
            urr_inelastic_ = i;
8,223✔
312
          }
313
        }
314

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

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

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

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

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

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

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

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

366
  reaction_index_.fill(C_NONE);
30,338✔
367
  for (int i = 0; i < reactions_.size(); ++i) {
1,436,902✔
368
    const auto& rx {reactions_[i]};
1,406,564✔
369

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

373
    for (int t = 0; t < kTs_.size(); ++t) {
2,813,359✔
374
      int j = rx->xs_[t].threshold;
1,406,795✔
375
      int n = rx->xs_[t].value.size();
1,406,795✔
376
      auto xs = tensor::Tensor<double>(
1,406,795✔
377
        rx->xs_[t].value.data(), rx->xs_[t].value.size());
1,406,795✔
378
      for (const auto& p : rx->products_) {
5,065,589✔
379
        if (p.particle_.is_photon()) {
3,658,794✔
380
          for (int k = 0; k < n; ++k) {
2,147,483,647✔
381
            double E = grid_[t].energy[k + j];
2,147,483,647!
382

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

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

401
      // Skip redundant reactions
402
      if (rx->redundant_)
1,406,795✔
403
        continue;
189,457✔
404

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

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

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

418
        // Keep track of fission reactions
419
        if (t == 0) {
14,399✔
420
          fission_rx_.push_back(rx.get());
14,322✔
421
          if (rx->mt_ == N_F)
14,322✔
422
            has_partial_fission_ = true;
2,073✔
423
        }
424
      }
425
    }
1,406,795✔
426
  }
427

428
  // Determine number of delayed neutron precursors
429
  if (fissionable_) {
30,338✔
430
    for (const auto& product : fission_rx_[0]->products_) {
69,474✔
431
      if (product.emission_mode_ == EmissionMode::delayed) {
61,371✔
432
        ++n_precursor_;
47,448✔
433
      }
434
    }
435
  }
436

437
  // Calculate nu-fission cross section
438
  for (int t = 0; t < kTs_.size(); ++t) {
60,753✔
439
    if (fissionable_) {
30,415✔
440
      int n = grid_[t].energy.size();
8,180✔
441
      for (int i = 0; i < n; ++i) {
625,617,491✔
442
        double E = grid_[t].energy[i];
625,609,311✔
443
        xs_[t](i, XS_NU_FISSION) =
625,609,311✔
444
          nu(E, EmissionMode::total) * xs_[t](i, XS_FISSION);
625,609,311✔
445
      }
446
    }
447
  }
448

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

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

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

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

486
        // build xs cdf
487
        xs_cdf_sum +=
16,710,990✔
488
          (std::sqrt(E[i]) * xs[i] + std::sqrt(E[i + 1]) * xs[i + 1]) / 2.0 *
8,355,495✔
489
          (E[i + 1] - E[i]);
8,355,495✔
490
        xs_cdf_[i + 1] = xs_cdf_sum;
8,355,495✔
491
      }
492
    }
493
  }
494
}
30,338✔
495

496
//======================================================
497
//===================== get current xs ========
498

NEW
499
std::vector<double> Nuclide::get_xs(int MT, int T_index) const
×
500
{
501
  // Find reaction index
NEW
502
  size_t i_rx = reaction_index_.at(MT);
×
NEW
503
  if (i_rx == C_NONE) {
×
NEW
504
    throw std::runtime_error("No reaction with MT = " + std::to_string(MT));
×
505
  }
506

NEW
507
  const auto& rx = *reactions_[i_rx];
×
NEW
508
  if (T_index >= rx.xs_.size()) {
×
NEW
509
    throw std::runtime_error("Temperature index out of range");
×
510
  }
511

NEW
512
  return rx.xs_[T_index].value;
×
513
}
514

515
//===================== end of code get current xs =====
516
//======================================================
517

518
//======================================================
519
//== Rebuild derived xs_ from reactions_ data =
520

NEW
521
void Nuclide::rebuild_derived_xs()
×
522
{
523
  // Loop over temperature indices
NEW
524
  for (int t = 0; t < static_cast<int>(kTs_.size()); ++t) {
×
525

NEW
526
    auto& xs_t = xs_[t];
×
527

528
    // Reset derived cross sections
NEW
529
    xs_t.fill(0.0);
×
530

531
    // Loop over all reactions
NEW
532
    for (const auto& rx_ptr : reactions_) {
×
NEW
533
      const auto& rx = *rx_ptr;
×
534

535
      // Skip redundant reactions
NEW
536
      if (rx.redundant_) {
×
NEW
537
        continue;
×
538
      }
539

NEW
540
      const int j = rx.xs_[t].threshold;
×
NEW
541
      const int n = static_cast<int>(rx.xs_[t].value.size());
×
542

NEW
543
      const auto& vals = rx.xs_[t].value;
×
544

545
      // -----------------------------
546
      // TOTAL CROSS SECTION
547
      // -----------------------------
NEW
548
      for (int k = 0; k < n; ++k) {
×
NEW
549
        xs_t(j + k, XS_TOTAL) += vals[k];
×
550
      }
551

552
      // -----------------------------
553
      // ABSORPTION (disappearance)
554
      // -----------------------------
NEW
555
      if (is_disappearance(rx.mt_)) {
×
NEW
556
        for (int k = 0; k < n; ++k) {
×
NEW
557
          xs_t(j + k, XS_ABSORPTION) += vals[k];
×
558
        }
559
      }
560

561
      // -----------------------------
562
      // FISSION
563
      // -----------------------------
NEW
564
      if (is_fission(rx.mt_)) {
×
NEW
565
        for (int k = 0; k < n; ++k) {
×
NEW
566
          xs_t(j + k, XS_FISSION) += vals[k];
×
NEW
567
          xs_t(j + k, XS_ABSORPTION) += vals[k];
×
568
        }
569
      }
570

571
      // -----------------------------
572
      // PHOTON
573
      // -----------------------------
NEW
574
      for (const auto& p : rx.products_) {
×
NEW
575
        if (p.particle_.is_photon()) {
×
NEW
576
          for (int k = 0; k < n; ++k) {
×
NEW
577
            double E = grid_[t].energy[j + k];
×
NEW
578
            xs_t(j + k, XS_PHOTON_PROD) += vals[k] * (*p.yield_)(E);
×
579
          }
580
        }
581
      }
582
    }
583

584
    // -----------------------------
585
    // NU-FISSION RECONSTRUCTION
586
    // -----------------------------
NEW
587
    if (fissionable_) {
×
NEW
588
      const int ngrid = static_cast<int>(grid_[t].energy.size());
×
NEW
589
      for (int i = 0; i < ngrid; ++i) {
×
NEW
590
        double E = grid_[t].energy[i];
×
NEW
591
        xs_t(i, XS_NU_FISSION) =
×
NEW
592
          nu(E, EmissionMode::total) * xs_t(i, XS_FISSION);
×
593
      }
594
    }
595
  }
NEW
596
}
×
597

598
//=end of code Rebuild derived xs_ from reactions_ data=
599
//======================================================
600

601
void Nuclide::init_grid()
32,874✔
602
{
603
  int neutron = ParticleType::neutron().transport_index();
32,874✔
604
  double E_min = data::energy_min[neutron];
32,874✔
605
  double E_max = data::energy_max[neutron];
32,874✔
606
  int M = settings::n_log_bins;
32,874✔
607

608
  // Determine equal-logarithmic energy spacing
609
  double spacing = std::log(E_max / E_min) / M;
32,874✔
610

611
  // Create equally log-spaced energy grid
612
  auto umesh = tensor::linspace(0.0, M * spacing, M + 1);
32,874✔
613

614
  for (auto& grid : grid_) {
65,825✔
615
    // Resize array for storing grid indices
616
    grid.grid_index.resize(M + 1);
32,951✔
617

618
    // Determine corresponding indices in nuclide grid to energies on
619
    // equal-logarithmic grid
620
    int j = 0;
621
    for (int k = 0; k <= M; ++k) {
264,213,902✔
622
      while (std::log(grid.energy[j + 1] / E_min) <= umesh(k)) {
1,083,124,547✔
623
        // Ensure that for isotopes where maxval(grid.energy) << E_max that
624
        // there are no out-of-bounds issues.
625
        if (j + 2 == grid.energy.size())
818,963,914✔
626
          break;
627
        ++j;
628
      }
629
      grid.grid_index[k] = j;
264,180,951✔
630
    }
631
  }
632
}
32,874✔
633

634
double Nuclide::nu(double E, EmissionMode mode, int group) const
1,079,929,300✔
635
{
636
  if (!fissionable_)
1,079,929,300✔
637
    return 0.0;
638

639
  switch (mode) {
1,050,656,353!
640
  case EmissionMode::prompt:
7,632,658✔
641
    return (*fission_rx_[0]->products_[0].yield_)(E);
7,632,658✔
642
  case EmissionMode::delayed:
151,482,036✔
643
    if (n_precursor_ > 0 && settings::create_delayed_neutrons) {
151,482,036!
644
      auto rx = fission_rx_[0];
149,616,667✔
645
      if (group >= 1 && group < rx->products_.size()) {
149,616,667!
646
        // If delayed group specified, determine yield immediately
647
        return (*rx->products_[group].yield_)(E);
107,901,156✔
648
      } else {
649
        double nu {0.0};
650

651
        for (int i = 1; i < rx->products_.size(); ++i) {
332,491,675✔
652
          // Skip any non-neutron products
653
          const auto& product = rx->products_[i];
290,776,164✔
654
          if (!product.particle_.is_neutron())
290,776,164✔
655
            continue;
40,483,098✔
656

657
          // Evaluate yield
658
          if (product.emission_mode_ == EmissionMode::delayed) {
250,293,066!
659
            nu += (*product.yield_)(E);
250,293,066✔
660
          }
661
        }
662
        return nu;
41,715,511✔
663
      }
664
    } else {
665
      return 0.0;
666
    }
667
  case EmissionMode::total:
891,541,659✔
668
    if (total_nu_ && settings::create_delayed_neutrons) {
891,541,659!
669
      return (*total_nu_)(E);
889,807,344✔
670
    } else {
671
      return (*fission_rx_[0]->products_[0].yield_)(E);
1,734,315✔
672
    }
673
  }
674
  UNREACHABLE();
×
675
}
676

677
void Nuclide::calculate_elastic_xs(Particle& p) const
1,380,266,933✔
678
{
679
  // Get temperature index, grid index, and interpolation factor
680
  auto& micro {p.neutron_xs(index_)};
1,380,266,933✔
681
  int i_temp = micro.index_temp;
1,380,266,933✔
682
  int i_grid = micro.index_grid;
1,380,266,933✔
683
  double f = micro.interp_factor;
1,380,266,933✔
684

685
  if (i_temp >= 0) {
1,380,266,933✔
686
    const auto& xs = reactions_[0]->xs_[i_temp].value;
1,352,179,742✔
687
    micro.elastic = (1.0 - f) * xs[i_grid] + f * xs[i_grid + 1];
1,352,179,742✔
688
  }
689
}
1,380,266,933✔
690

691
double Nuclide::elastic_xs_0K(double E) const
×
692
{
693
  // Determine index on nuclide energy grid
694
  int i_grid;
×
695
  if (E < energy_0K_.front()) {
×
696
    i_grid = 0;
697
  } else if (E > energy_0K_.back()) {
×
698
    i_grid = energy_0K_.size() - 2;
×
699
  } else {
700
    i_grid = lower_bound_index(energy_0K_.begin(), energy_0K_.end(), E);
×
701
  }
702

703
  // check for rare case where two energy points are the same
704
  if (energy_0K_[i_grid] == energy_0K_[i_grid + 1])
×
705
    ++i_grid;
×
706

707
  // calculate interpolation factor
708
  double f =
×
709
    (E - energy_0K_[i_grid]) / (energy_0K_[i_grid + 1] - energy_0K_[i_grid]);
×
710

711
  // Calculate microscopic nuclide elastic cross section
712
  return (1.0 - f) * elastic_0K_[i_grid] + f * elastic_0K_[i_grid + 1];
×
713
}
714

715
void Nuclide::calculate_xs(
2,147,483,647✔
716
  int i_sab, int i_log_union, double sab_frac, Particle& p)
717
{
718
  auto& micro {p.neutron_xs(index_)};
2,147,483,647✔
719

720
  // Initialize cached cross sections to zero
721
  micro.elastic = CACHE_INVALID;
2,147,483,647✔
722
  micro.thermal = 0.0;
2,147,483,647✔
723
  micro.thermal_elastic = 0.0;
2,147,483,647✔
724

725
  // Check to see if there is multipole data present at this energy
726
  bool use_mp = false;
2,147,483,647✔
727
  if (multipole_) {
2,147,483,647✔
728
    use_mp = (p.E() >= multipole_->E_min_ && p.E() <= multipole_->E_max_);
646,800,781!
729
  }
730

731
  // Evaluate multipole or interpolate
732
  if (use_mp) {
443,896,068✔
733
    // Call multipole kernel
734
    double sig_s, sig_a, sig_f;
443,896,068✔
735
    std::tie(sig_s, sig_a, sig_f) = multipole_->evaluate(p.E(), p.sqrtkT());
443,896,068✔
736

737
    micro.total = sig_s + sig_a;
443,896,068✔
738
    micro.elastic = sig_s;
443,896,068✔
739
    micro.absorption = sig_a;
443,896,068✔
740
    micro.fission = sig_f;
443,896,068✔
741
    micro.nu_fission =
887,792,136✔
742
      fissionable_ ? sig_f * this->nu(p.E(), EmissionMode::total) : 0.0;
443,896,068✔
743

744
    if (simulation::need_depletion_rx) {
443,896,068!
745
      // Only non-zero reaction is (n,gamma)
746
      micro.reaction[0] = sig_a - sig_f;
×
747

748
      // Set all other reaction cross sections to zero
749
      for (int i = 1; i < DEPLETION_RX.size(); ++i) {
×
750
        micro.reaction[i] = 0.0;
×
751
      }
752
    }
753

754
    /*
755
     * index_temp, index_grid, and interp_factor are used only in the
756
     * following places:
757
     *   1. physics.cpp - scatter - For inelastic scatter.
758
     *   2. physics.cpp - sample_fission - For partial fissions.
759
     *   3. tallies/tally_scoring.cpp - score_general -
760
     *        For tallying on MTxxx reactions.
761
     *   4. nuclide.cpp - calculate_urr_xs - For unresolved purposes.
762
     * It is worth noting that none of these occur in the resolved resonance
763
     * range, so the value here does not matter.  index_temp is set to -1 to
764
     * force a segfault in case a developer messes up and tries to use it with
765
     * multipole.
766
     *
767
     * However, a segfault is not necessarily guaranteed with an out-of-bounds
768
     * access, so this technique should be replaced by something more robust
769
     * in the future.
770
     */
771
    micro.index_temp = -1;
443,896,068✔
772
    micro.index_grid = -1;
443,896,068✔
773
    micro.interp_factor = 0.0;
443,896,068✔
774

775
  } else {
776
    // Find the appropriate temperature index.
777
    double kT = p.sqrtkT() * p.sqrtkT();
2,147,483,647✔
778
    double f;
2,147,483,647✔
779
    int i_temp = -1;
2,147,483,647✔
780
    switch (settings::temperature_method) {
2,147,483,647!
781
    case TemperatureMethod::NEAREST: {
782
      double max_diff = INFTY;
783
      for (int t = 0; t < kTs_.size(); ++t) {
2,147,483,647✔
784
        double diff = std::abs(kTs_[t] - kT);
2,147,483,647!
785
        if (diff < max_diff) {
2,147,483,647!
786
          i_temp = t;
2,147,483,647✔
787
          max_diff = diff;
2,147,483,647✔
788
        }
789
      }
790
    } break;
791

792
    case TemperatureMethod::INTERPOLATION:
1,517,109✔
793
      // If current kT outside of the bounds of available, snap to the bound
794
      if (kT < kTs_.front()) {
1,517,109✔
795
        i_temp = 0;
796
        break;
797
      }
798
      if (kT > kTs_.back()) {
1,103,883✔
799
        i_temp = kTs_.size() - 1;
215,644✔
800
        break;
215,644✔
801
      }
802

803
      // Find temperatures that bound the actual temperature
804
      for (i_temp = 0; i_temp < kTs_.size() - 1; ++i_temp) {
888,239!
805
        if (kTs_[i_temp] <= kT && kT < kTs_[i_temp + 1])
888,239!
806
          break;
807
      }
808

809
      // Randomly sample between temperature i and i+1
810
      f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
888,239✔
811
      if (f > prn(p.current_seed()))
888,239✔
812
        ++i_temp;
411,873✔
813
      break;
814
    }
815

816
    // Determine the energy grid index using a logarithmic mapping to
817
    // reduce the energy range over which a binary search needs to be
818
    // performed
819

820
    const auto& grid {grid_[i_temp]};
2,147,483,647✔
821
    const auto& xs {xs_[i_temp]};
2,147,483,647✔
822

823
    int i_grid;
2,147,483,647✔
824
    if (p.E() < grid.energy.front()) {
2,147,483,647✔
825
      i_grid = 0;
826
    } else if (p.E() > grid.energy.back()) {
2,147,483,647!
827
      i_grid = grid.energy.size() - 2;
×
828
    } else {
829
      // Determine bounding indices based on which equal log-spaced
830
      // interval the energy is in
831
      int i_low = grid.grid_index[i_log_union];
2,147,483,647✔
832
      int i_high = grid.grid_index[i_log_union + 1] + 1;
2,147,483,647✔
833

834
      // Perform binary search over reduced range
835
      i_grid = i_low + lower_bound_index(
2,147,483,647✔
836
                         &grid.energy[i_low], &grid.energy[i_high], p.E());
2,147,483,647✔
837
    }
838

839
    // check for rare case where two energy points are the same
840
    if (grid.energy[i_grid] == grid.energy[i_grid + 1])
2,147,483,647!
841
      ++i_grid;
×
842

843
    // calculate interpolation factor
844
    f = (p.E() - grid.energy[i_grid]) /
2,147,483,647✔
845
        (grid.energy[i_grid + 1] - grid.energy[i_grid]);
2,147,483,647✔
846

847
    micro.index_temp = i_temp;
2,147,483,647✔
848
    micro.index_grid = i_grid;
2,147,483,647✔
849
    micro.interp_factor = f;
2,147,483,647✔
850

851
    // Calculate microscopic nuclide total cross section
852
    micro.total =
2,147,483,647✔
853
      (1.0 - f) * xs(i_grid, XS_TOTAL) + f * xs(i_grid + 1, XS_TOTAL);
2,147,483,647✔
854

855
    // Calculate microscopic nuclide absorption cross section
856
    micro.absorption =
2,147,483,647✔
857
      (1.0 - f) * xs(i_grid, XS_ABSORPTION) + f * xs(i_grid + 1, XS_ABSORPTION);
2,147,483,647✔
858

859
    if (fissionable_) {
2,147,483,647✔
860
      // Calculate microscopic nuclide total cross section
861
      micro.fission =
1,134,667,216✔
862
        (1.0 - f) * xs(i_grid, XS_FISSION) + f * xs(i_grid + 1, XS_FISSION);
1,134,667,216✔
863

864
      // Calculate microscopic nuclide nu-fission cross section
865
      micro.nu_fission = (1.0 - f) * xs(i_grid, XS_NU_FISSION) +
1,134,667,216✔
866
                         f * xs(i_grid + 1, XS_NU_FISSION);
1,134,667,216✔
867
    } else {
868
      micro.fission = 0.0;
2,147,483,647✔
869
      micro.nu_fission = 0.0;
2,147,483,647✔
870
    }
871

872
    // Calculate microscopic nuclide photon production cross section
873
    micro.photon_prod = (1.0 - f) * xs(i_grid, XS_PHOTON_PROD) +
2,147,483,647✔
874
                        f * xs(i_grid + 1, XS_PHOTON_PROD);
2,147,483,647✔
875

876
    // Depletion-related reactions
877
    if (simulation::need_depletion_rx) {
2,147,483,647✔
878
      // Initialize all reaction cross sections to zero
879
      for (double& xs_i : micro.reaction) {
2,147,483,647✔
880
        xs_i = 0.0;
2,147,483,647✔
881
      }
882

883
      for (int j = 0; j < DEPLETION_RX.size(); ++j) {
2,147,483,647✔
884
        // If reaction is present and energy is greater than threshold, set
885
        // the reaction xs appropriately
886
        int i_rx = reaction_index_[DEPLETION_RX[j]];
2,147,483,647✔
887
        if (i_rx >= 0) {
2,147,483,647✔
888
          const auto& rx = reactions_[i_rx];
2,147,483,647✔
889
          const auto& rx_xs = rx->xs_[i_temp].value;
2,147,483,647✔
890

891
          // Physics says that (n,gamma) is not a threshold reaction, so we
892
          // don't need to specifically check its threshold index
893
          if (j == 0) {
2,147,483,647✔
894
            micro.reaction[0] =
2,147,483,647✔
895
              (1.0 - f) * rx_xs[i_grid] + f * rx_xs[i_grid + 1];
2,147,483,647✔
896
            continue;
2,147,483,647✔
897
          }
898

899
          int threshold = rx->xs_[i_temp].threshold;
2,147,483,647✔
900
          if (i_grid >= threshold) {
2,147,483,647✔
901
            micro.reaction[j] = (1.0 - f) * rx_xs[i_grid - threshold] +
743,615,104✔
902
                                f * rx_xs[i_grid - threshold + 1];
743,615,104✔
903
          } else if (j >= 3) {
2,147,483,647✔
904
            // One can show that the the threshold for (n,(x+1)n) is always
905
            // higher than the threshold for (n,xn). Thus, if we are below
906
            // the threshold for, e.g., (n,2n), there is no reason to check
907
            // the threshold for (n,3n) and (n,4n).
908
            break;
909
          }
910
        }
911
      }
912
    }
913
  }
914

915
  // Initialize sab treatment to false
916
  micro.index_sab = C_NONE;
2,147,483,647✔
917
  micro.sab_frac = 0.0;
2,147,483,647✔
918

919
  // Initialize URR probability table treatment to false
920
  micro.use_ptable = false;
2,147,483,647✔
921

922
  // If there is S(a,b) data for this nuclide, we need to set the
923
  // sab_scatter and sab_elastic cross sections and correct the total and
924
  // elastic cross sections.
925

926
  if (i_sab >= 0)
2,147,483,647✔
927
    this->calculate_sab_xs(i_sab, sab_frac, p);
148,784,377✔
928

929
  // If the particle is in the unresolved resonance range and there are
930
  // probability tables, we need to determine cross sections from the table
931
  if (settings::urr_ptables_on && urr_present_ && !use_mp) {
2,147,483,647✔
932
    if (urr_data_[micro.index_temp].energy_in_bounds(p.E()))
2,147,483,647✔
933
      this->calculate_urr_xs(micro.index_temp, p);
396,866,420✔
934
  }
935

936
  micro.last_E = p.E();
2,147,483,647✔
937
  micro.last_sqrtkT = p.sqrtkT();
2,147,483,647✔
938
}
2,147,483,647✔
939

940
void Nuclide::calculate_sab_xs(int i_sab, double sab_frac, Particle& p)
148,784,377✔
941
{
942
  auto& micro {p.neutron_xs(index_)};
148,784,377✔
943

944
  // Set flag that S(a,b) treatment should be used for scattering
945
  micro.index_sab = i_sab;
148,784,377✔
946

947
  // Calculate the S(a,b) cross section
948
  int i_temp;
148,784,377✔
949
  double elastic;
148,784,377✔
950
  double inelastic;
148,784,377✔
951
  data::thermal_scatt[i_sab]->calculate_xs(
148,784,377✔
952
    p.E(), p.sqrtkT(), &i_temp, &elastic, &inelastic, p.current_seed());
148,784,377✔
953

954
  // Store the S(a,b) cross sections.
955
  micro.thermal = sab_frac * (elastic + inelastic);
148,784,377✔
956
  micro.thermal_elastic = sab_frac * elastic;
148,784,377✔
957

958
  // Calculate free atom elastic cross section
959
  this->calculate_elastic_xs(p);
148,784,377✔
960

961
  // Correct total and elastic cross sections
962
  micro.total = micro.total + micro.thermal - sab_frac * micro.elastic;
148,784,377✔
963
  micro.elastic = micro.thermal + (1.0 - sab_frac) * micro.elastic;
148,784,377✔
964

965
  // Save temperature index and thermal fraction
966
  micro.index_temp_sab = i_temp;
148,784,377✔
967
  micro.sab_frac = sab_frac;
148,784,377✔
968
}
148,784,377✔
969

970
void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const
396,866,420✔
971
{
972
  auto& micro = p.neutron_xs(index_);
396,866,420✔
973
  micro.use_ptable = true;
396,866,420✔
974

975
  // Create a shorthand for the URR data
976
  const auto& urr = urr_data_[i_temp];
396,866,420✔
977

978
  // Determine the energy table
979
  int i_energy =
396,866,420✔
980
    lower_bound_index(urr.energy_.begin(), urr.energy_.end(), p.E());
396,866,420✔
981

982
  // Sample the probability table using the cumulative distribution
983

984
  // Random numbers for the xs calculation are sampled from a separate stream.
985
  // This guarantees the randomness and, at the same time, makes sure we
986
  // reuse random numbers for the same nuclide at different temperatures,
987
  // therefore preserving correlation of temperature in probability tables.
988
  double r =
396,866,420✔
989
    future_prn(static_cast<int64_t>(index_), p.seeds(STREAM_URR_PTABLE));
396,866,420✔
990

991
  // Warning: this assumes row-major order of cdf_values_
992
  int i_low = upper_bound_index(&urr.cdf_values_(i_energy, 0),
793,732,840✔
993
                &urr.cdf_values_(i_energy, 0) + urr.n_cdf(), r) +
793,732,840!
994
              1;
396,866,420✔
995
  int i_up = upper_bound_index(&urr.cdf_values_(i_energy + 1, 0),
396,866,420✔
996
               &urr.cdf_values_(i_energy + 1, 0) + urr.n_cdf(), r) +
396,866,420✔
997
             1;
396,866,420✔
998

999
  // Determine elastic, fission, and capture cross sections from the
1000
  // probability table
1001
  double elastic = 0.;
396,866,420✔
1002
  double fission = 0.;
396,866,420✔
1003
  double capture = 0.;
396,866,420✔
1004
  double f;
396,866,420✔
1005
  if (urr.interp_ == Interpolation::lin_lin) {
396,866,420✔
1006
    // Determine the interpolation factor on the table
1007
    f = (p.E() - urr.energy_[i_energy]) /
220,265,868✔
1008
        (urr.energy_[i_energy + 1] - urr.energy_[i_energy]);
220,265,868✔
1009

1010
    elastic = (1. - f) * urr.xs_values_(i_energy, i_low).elastic +
220,265,868✔
1011
              f * urr.xs_values_(i_energy + 1, i_up).elastic;
220,265,868✔
1012
    fission = (1. - f) * urr.xs_values_(i_energy, i_low).fission +
220,265,868✔
1013
              f * urr.xs_values_(i_energy + 1, i_up).fission;
220,265,868✔
1014
    capture = (1. - f) * urr.xs_values_(i_energy, i_low).n_gamma +
220,265,868✔
1015
              f * urr.xs_values_(i_energy + 1, i_up).n_gamma;
220,265,868✔
1016
  } else if (urr.interp_ == Interpolation::log_log) {
176,600,552!
1017
    // Determine interpolation factor on the table
1018
    f = std::log(p.E() / urr.energy_[i_energy]) /
176,600,552✔
1019
        std::log(urr.energy_[i_energy + 1] / urr.energy_[i_energy]);
176,600,552✔
1020

1021
    // Calculate the elastic cross section/factor
1022
    if ((urr.xs_values_(i_energy, i_low).elastic > 0.) &&
176,600,552!
1023
        (urr.xs_values_(i_energy + 1, i_up).elastic > 0.)) {
176,600,552!
1024
      elastic =
176,600,552✔
1025
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).elastic) +
176,600,552✔
1026
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).elastic));
176,600,552✔
1027
    } else {
1028
      elastic = 0.;
1029
    }
1030

1031
    // Calculate the fission cross section/factor
1032
    if ((urr.xs_values_(i_energy, i_low).fission > 0.) &&
176,600,552✔
1033
        (urr.xs_values_(i_energy + 1, i_up).fission > 0.)) {
113,870,861!
1034
      fission =
113,870,861✔
1035
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).fission) +
113,870,861✔
1036
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).fission));
113,870,861✔
1037
    } else {
1038
      fission = 0.;
1039
    }
1040

1041
    // Calculate the capture cross section/factor
1042
    if ((urr.xs_values_(i_energy, i_low).n_gamma > 0.) &&
176,600,552!
1043
        (urr.xs_values_(i_energy + 1, i_up).n_gamma > 0.)) {
176,600,552!
1044
      capture =
176,600,552✔
1045
        std::exp((1. - f) * std::log(urr.xs_values_(i_energy, i_low).n_gamma) +
176,600,552✔
1046
                 f * std::log(urr.xs_values_(i_energy + 1, i_up).n_gamma));
176,600,552✔
1047
    } else {
1048
      capture = 0.;
1049
    }
1050
  }
1051

1052
  // Determine the treatment of inelastic scattering
1053
  double inelastic = 0.;
396,866,420✔
1054
  if (urr.inelastic_flag_ != C_NONE) {
396,866,420✔
1055
    // get interpolation factor
1056
    f = micro.interp_factor;
219,411,018✔
1057

1058
    // Determine inelastic scattering cross section
1059
    Reaction* rx = reactions_[urr_inelastic_].get();
219,411,018✔
1060
    int xs_index = micro.index_grid - rx->xs_[i_temp].threshold;
219,411,018✔
1061
    if (xs_index >= 0) {
219,411,018✔
1062
      inelastic = (1. - f) * rx->xs_[i_temp].value[xs_index] +
108,485,408✔
1063
                  f * rx->xs_[i_temp].value[xs_index + 1];
108,485,408✔
1064
    }
1065
  }
1066

1067
  // Multiply by smooth cross-section if needed
1068
  if (urr.multiply_smooth_) {
396,866,420✔
1069
    calculate_elastic_xs(p);
121,510,439✔
1070
    elastic *= micro.elastic;
121,510,439✔
1071
    capture *= (micro.absorption - micro.fission);
121,510,439✔
1072
    fission *= micro.fission;
121,510,439✔
1073
  }
1074

1075
  // Check for negative values
1076
  if (elastic < 0.) {
396,866,420!
1077
    elastic = 0.;
×
1078
  }
1079
  if (fission < 0.) {
396,866,420!
1080
    fission = 0.;
×
1081
  }
1082
  if (capture < 0.) {
396,866,420!
1083
    capture = 0.;
×
1084
  }
1085

1086
  // Set elastic, absorption, fission, total, and capture x/s. Note that the
1087
  // total x/s is calculated as a sum of partials instead of the
1088
  // table-provided value
1089
  micro.elastic = elastic;
396,866,420✔
1090
  micro.absorption = capture + fission;
396,866,420✔
1091
  micro.fission = fission;
396,866,420✔
1092
  micro.total = elastic + inelastic + capture + fission;
396,866,420✔
1093
  if (simulation::need_depletion_rx) {
396,866,420✔
1094
    micro.reaction[0] = capture;
172,894,251✔
1095
  }
1096

1097
  // Determine nu-fission cross-section
1098
  if (fissionable_) {
396,866,420✔
1099
    micro.nu_fission = nu(p.E(), EmissionMode::total) * micro.fission;
205,193,613✔
1100
  }
1101
}
396,866,420✔
1102

1103
std::pair<int64_t, double> Nuclide::find_temperature(double T) const
2,167✔
1104
{
1105
  assert(T >= 0.0);
2,167!
1106

1107
  // Determine temperature index
1108
  int64_t i_temp = 0;
2,167✔
1109
  double f = 0.0;
2,167✔
1110
  double kT = K_BOLTZMANN * T;
2,167✔
1111
  int64_t n = kTs_.size();
2,167!
1112
  switch (settings::temperature_method) {
2,167!
1113
  case TemperatureMethod::NEAREST: {
1114
    double max_diff = INFTY;
1115
    for (int64_t t = 0; t < n; ++t) {
4,334✔
1116
      double diff = std::abs(kTs_[t] - kT);
2,167!
1117
      if (diff < max_diff) {
2,167!
1118
        i_temp = t;
2,167✔
1119
        max_diff = diff;
2,167✔
1120
      }
1121
    }
1122
  } break;
1123

1124
  case TemperatureMethod::INTERPOLATION:
×
1125
    // If current kT outside of the bounds of available, snap to the bound
1126
    if (kT < kTs_.front()) {
×
1127
      i_temp = 0;
1128
      break;
1129
    }
1130
    if (kT > kTs_.back()) {
×
1131
      i_temp = kTs_.size() - 1;
×
1132
      break;
×
1133
    }
1134
    // Find temperatures that bound the actual temperature
1135
    while (kTs_[i_temp + 1] < kT && i_temp + 1 < n - 1)
×
1136
      ++i_temp;
1137

1138
    // Determine interpolation factor
1139
    f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]);
×
1140
  }
1141

1142
  assert(i_temp >= 0 && i_temp < n);
2,167!
1143

1144
  return {i_temp, f};
2,167✔
1145
}
1146

1147
double Nuclide::collapse_rate(int MT, double temperature,
2,849✔
1148
  span<const double> energy, span<const double> flux) const
1149
{
1150
  assert(MT > 0);
2,849!
1151
  assert(energy.size() > 0);
2,849!
1152
  assert(energy.size() == flux.size() + 1);
2,849!
1153

1154
  int i_rx = reaction_index_[MT];
2,849✔
1155
  if (i_rx < 0)
2,849✔
1156
    return 0.0;
1157
  const auto& rx = reactions_[i_rx];
2,167✔
1158

1159
  // Determine temperature index
1160
  int64_t i_temp;
2,167✔
1161
  double f;
2,167✔
1162
  std::tie(i_temp, f) = this->find_temperature(temperature);
2,167✔
1163

1164
  // Get reaction rate at lower temperature
1165
  const auto& grid_low = grid_[i_temp].energy;
2,167✔
1166
  double rr_low = rx->collapse_rate(i_temp, energy, flux, grid_low);
2,167✔
1167

1168
  if (f > 0.0) {
2,167!
1169
    // Interpolate between reaction rate at lower and higher temperature
1170
    const auto& grid_high = grid_[i_temp + 1].energy;
×
1171
    double rr_high = rx->collapse_rate(i_temp + 1, energy, flux, grid_high);
×
1172
    return rr_low + f * (rr_high - rr_low);
×
1173
  } else {
1174
    // If interpolation factor is zero, return reaction rate at lower
1175
    // temperature
1176
    return rr_low;
1177
  }
1178
}
1179

1180
//==============================================================================
1181
// Non-member functions
1182
//==============================================================================
1183

1184
void check_data_version(hid_t file_id)
35,923✔
1185
{
1186
  if (attribute_exists(file_id, "version")) {
35,923!
1187
    vector<int> version;
35,923✔
1188
    read_attribute(file_id, "version", version);
35,923✔
1189
    if (version[0] != HDF5_VERSION[0]) {
35,923!
1190
      fatal_error("HDF5 data format uses version " +
×
1191
                  std::to_string(version[0]) + "." +
×
1192
                  std::to_string(version[1]) +
×
1193
                  " whereas your installation of "
1194
                  "OpenMC expects version " +
×
1195
                  std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1196
    }
1197
  } else {
×
1198
    fatal_error("HDF5 data does not indicate a version. Your installation of "
×
1199
                "OpenMC expects version " +
×
1200
                std::to_string(HDF5_VERSION[0]) + ".x data.");
×
1201
  }
1202
}
35,923✔
1203

1204
extern "C" size_t nuclides_size()
22✔
1205
{
1206
  return data::nuclides.size();
22✔
1207
}
1208

1209
//==============================================================================
1210
// C API
1211
//==============================================================================
1212

1213
extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n)
33,404✔
1214
{
1215
  if (data::nuclide_map.find(name) == data::nuclide_map.end() ||
97,822✔
1216
      data::nuclide_map.at(name) >= data::nuclides.size()) {
126,325!
1217
    LibraryKey key {Library::Type::neutron, name};
33,283✔
1218
    const auto& it = data::library_map.find(key);
33,283✔
1219
    if (it == data::library_map.end()) {
33,283✔
1220
      set_errmsg(
22✔
1221
        "Nuclide '" + std::string {name} + "' is not present in library.");
44✔
1222
      return OPENMC_E_DATA;
22✔
1223
    }
1224

1225
    // Get filename for library containing nuclide
1226
    int idx = it->second;
33,261✔
1227
    const auto& filename = data::libraries[idx].path_;
33,261✔
1228
    write_message(6, "Reading {} from {}", name, filename);
33,261✔
1229

1230
    // Open file and make sure version is sufficient
1231
    hid_t file_id = file_open(filename, 'r');
33,261✔
1232
    check_data_version(file_id);
33,261✔
1233

1234
    // Read nuclide data from HDF5
1235
    hid_t group = open_group(file_id, name);
33,261✔
1236
    vector<double> temperature {temps, temps + n};
33,261✔
1237
    data::nuclides.push_back(make_unique<Nuclide>(group, temperature));
66,522✔
1238

1239
    close_group(group);
33,261✔
1240
    file_close(file_id);
33,261✔
1241

1242
    // Read multipole file into the appropriate entry on the nuclides array
1243
    int i_nuclide = data::nuclide_map.at(name);
66,522✔
1244
    if (settings::temperature_multipole)
33,261✔
1245
      read_multipole_data(i_nuclide);
1,204✔
1246

1247
    // Read elemental data, if necessary
1248
    if (settings::photon_transport) {
33,261✔
1249
      auto element = to_element(name);
2,313✔
1250
      if (data::element_map.find(element) == data::element_map.end() ||
4,626!
1251
          data::element_map.at(element) >= data::elements.size()) {
2,313✔
1252
        // Read photon interaction data from HDF5 photon library
1253
        LibraryKey key {Library::Type::photon, element};
1,223✔
1254
        const auto& it = data::library_map.find(key);
1,223!
1255
        if (it == data::library_map.end()) {
1,223!
1256
          set_errmsg("Element '" + std::string {element} +
×
1257
                     "' is not present in library.");
1258
          return OPENMC_E_DATA;
×
1259
        }
1260

1261
        int idx = it->second;
1,223✔
1262
        const auto& filename = data::libraries[idx].path_;
1,223✔
1263
        write_message(6, "Reading {} from {} ", element, filename);
1,223✔
1264

1265
        // Open file and make sure version is sufficient
1266
        hid_t file_id = file_open(filename, 'r');
1,223✔
1267
        check_data_version(file_id);
1,223✔
1268

1269
        // Read element data from HDF5
1270
        hid_t group = open_group(file_id, element.c_str());
1,223✔
1271
        data::elements.push_back(make_unique<PhotonInteraction>(group));
2,446✔
1272

1273
        close_group(group);
1,223✔
1274
        file_close(file_id);
1,223✔
1275
      }
1,223✔
1276
    }
2,313✔
1277
  }
33,283✔
1278
  return 0;
1279
}
1280

1281
extern "C" int openmc_get_nuclide_index(const char* name, int* index)
1,419✔
1282
{
1283
  auto it = data::nuclide_map.find(name);
2,838✔
1284
  if (it == data::nuclide_map.end()) {
1,419✔
1285
    set_errmsg(
11✔
1286
      "No nuclide named '" + std::string {name} + "' has been loaded.");
22✔
1287
    return OPENMC_E_DATA;
11✔
1288
  }
1289
  *index = it->second;
1,408✔
1290
  return 0;
1,408✔
1291
}
1292

1293
extern "C" int openmc_nuclide_name(int index, const char** name)
3,367✔
1294
{
1295
  if (index >= 0 && index < data::nuclides.size()) {
3,367!
1296
    *name = data::nuclides[index]->name_.data();
3,367✔
1297
    return 0;
3,367✔
1298
  } else {
1299
    set_errmsg("Index in nuclides vector is out of bounds.");
×
1300
    return OPENMC_E_OUT_OF_BOUNDS;
×
1301
  }
1302
}
1303

1304
extern "C" int openmc_nuclide_collapse_rate(int index, int MT,
2,849✔
1305
  double temperature, const double* energy, const double* flux, int n,
1306
  double* xs)
1307
{
1308
  if (index < 0 || index >= data::nuclides.size()) {
2,849!
1309
    set_errmsg("Index in nuclides vector is out of bounds.");
×
1310
    return OPENMC_E_OUT_OF_BOUNDS;
×
1311
  }
1312

1313
  try {
2,849✔
1314
    *xs = data::nuclides[index]->collapse_rate(
5,698✔
1315
      MT, temperature, {energy, energy + n + 1}, {flux, flux + n});
2,849✔
1316
  } catch (const std::out_of_range& e) {
×
1317
    set_errmsg(e.what());
×
1318
    return OPENMC_E_OUT_OF_BOUNDS;
×
1319
  }
×
1320
  return 0;
2,849✔
1321
}
1322

1323
void nuclides_clear()
8,920✔
1324
{
1325
  data::nuclides.clear();
8,920✔
1326
  data::nuclide_map.clear();
8,920✔
1327
}
8,920✔
1328

1329
bool multipole_in_range(const Nuclide& nuc, double E)
981,794✔
1330
{
1331
  return E >= nuc.multipole_->E_min_ && E <= nuc.multipole_->E_max_;
981,794!
1332
}
1333

NEW
1334
extern "C" int openmc_nuclide_get_xs(
×
1335
  int i_nuclide, int MT, int T_index, double* xs_out, int n_values)
1336
{
NEW
1337
  using namespace openmc;
×
1338

NEW
1339
  if (i_nuclide < 0 || i_nuclide >= static_cast<int>(data::nuclides.size()))
×
NEW
1340
    return OPENMC_E_INVALID_ID;
×
1341

NEW
1342
  try {
×
NEW
1343
    const Nuclide* nuc = data::nuclides[i_nuclide].get();
×
NEW
1344
    auto xs = nuc->get_xs(MT, T_index);
×
NEW
1345
    if (xs.size() != static_cast<size_t>(n_values)) {
×
NEW
1346
      set_errmsg("Requested length does not match stored XS length");
×
NEW
1347
      return OPENMC_E_DATA;
×
1348
    }
1349

1350
    // std::memcpy(xs_out, xs.data(), n_values * sizeof(double));
NEW
1351
    for (int i = 0; i < n_values; ++i) {
×
NEW
1352
      xs_out[i] = xs[i];
×
1353
    }
1354

1355
    return 0;
NEW
1356
  } catch (const std::exception& e) {
×
NEW
1357
    set_errmsg(e.what());
×
NEW
1358
    return OPENMC_E_DATA;
×
NEW
1359
  }
×
1360
}
1361

1362
//======================================================
1363
//===================== new code get current xs size ===
1364

NEW
1365
extern "C" int openmc_nuclide_get_xs_size(
×
1366
  int index, int MT, int T_index, int* n)
1367
{
NEW
1368
  if (index < 0 || index >= data::nuclides.size()) {
×
NEW
1369
    set_errmsg("Index in nuclides vector is out of bounds.");
×
NEW
1370
    return OPENMC_E_OUT_OF_BOUNDS;
×
1371
  }
1372

NEW
1373
  try {
×
NEW
1374
    auto& nuc = *data::nuclides[index];
×
NEW
1375
    size_t i_rx = nuc.reaction_index_[MT];
×
NEW
1376
    if (i_rx == C_NONE) {
×
NEW
1377
      set_errmsg("No reaction with MT = " + std::to_string(MT));
×
NEW
1378
      return OPENMC_E_DATA;
×
1379
    }
1380

NEW
1381
    const auto& rx = *nuc.reactions_[i_rx];
×
NEW
1382
    if (T_index >= rx.xs_.size()) {
×
NEW
1383
      set_errmsg("Temperature index out of range");
×
NEW
1384
      return OPENMC_E_DATA;
×
1385
    }
1386

NEW
1387
    *n = static_cast<int>(rx.xs_[T_index].value.size());
×
NEW
1388
  } catch (const std::exception& e) {
×
NEW
1389
    set_errmsg(e.what());
×
NEW
1390
    return OPENMC_E_DATA;
×
NEW
1391
  }
×
1392

NEW
1393
  return 0;
×
1394
}
1395
//===================== end of code get current sizexs ===
1396
//========================================================
NEW
1397
extern "C" int openmc_nuclide_get_energy_grid(
×
1398
  int index, int T_index, double* E, int n)
1399
{
NEW
1400
  using namespace openmc;
×
1401

NEW
1402
  if (index < 0 || index >= data::nuclides.size())
×
NEW
1403
    return OPENMC_E_INVALID_ARGUMENT;
×
1404

NEW
1405
  try {
×
NEW
1406
    const auto& nuc = *data::nuclides[index];
×
NEW
1407
    const auto& grid = nuc.grid_.at(T_index).energy;
×
1408

NEW
1409
    if (static_cast<int>(grid.size()) != n)
×
NEW
1410
      return OPENMC_E_INVALID_ARGUMENT;
×
1411

NEW
1412
    std::memcpy(E, grid.data(), n * sizeof(double));
×
NEW
1413
  } catch (const std::exception& e) {
×
NEW
1414
    set_errmsg(e.what());
×
NEW
1415
    return OPENMC_E_DATA;
×
NEW
1416
  }
×
1417

NEW
1418
  return 0;
×
1419
}
1420

NEW
1421
extern "C" int openmc_nuclide_rebuild_derived_xs(int index)
×
1422
{
NEW
1423
  if (index < 0 || index >= data::nuclides.size()) {
×
NEW
1424
    set_errmsg("Index in nuclides vector out of bounds.");
×
NEW
1425
    return OPENMC_E_OUT_OF_BOUNDS;
×
1426
  }
NEW
1427
  try {
×
NEW
1428
    data::nuclides[index]->rebuild_derived_xs();
×
NEW
1429
  } catch (const std::exception& e) {
×
NEW
1430
    set_errmsg(e.what());
×
NEW
1431
    return OPENMC_E_DATA;
×
NEW
1432
  }
×
1433
  return 0;
1434
}
1435

NEW
1436
extern "C" int openmc_nuclide_get_energy_grid_size(
×
1437
  int index, int T_index, int* n)
1438
{
NEW
1439
  using namespace openmc;
×
1440

NEW
1441
  if (index < 0 || index >= data::nuclides.size())
×
NEW
1442
    return OPENMC_E_INVALID_ARGUMENT;
×
1443

NEW
1444
  try {
×
NEW
1445
    const auto& nuc = *data::nuclides[index];
×
NEW
1446
    const auto& grid = nuc.grid_.at(T_index).energy;
×
1447

NEW
1448
    *n = static_cast<int>(grid.size());
×
NEW
1449
  } catch (const std::exception& e) {
×
NEW
1450
    set_errmsg(e.what());
×
NEW
1451
    return OPENMC_E_DATA;
×
NEW
1452
  }
×
1453

NEW
1454
  return 0;
×
1455
}
1456

NEW
1457
extern "C" int openmc_nuclide_get_reaction_threshold_energy(
×
1458
  int index, int mt, int T_index, double* threshold)
1459
{
NEW
1460
  using namespace openmc;
×
1461

NEW
1462
  if (index < 0 || index >= data::nuclides.size())
×
NEW
1463
    return OPENMC_E_INVALID_ARGUMENT;
×
NEW
1464
  if (!threshold)
×
NEW
1465
    return OPENMC_E_INVALID_ARGUMENT;
×
1466

NEW
1467
  try {
×
NEW
1468
    const auto& nuc = *data::nuclides[index];
×
1469

1470
    // Find reaction
NEW
1471
    const Reaction* r = nullptr;
×
NEW
1472
    for (const auto& rx : nuc.reactions_) {
×
NEW
1473
      if (rx && rx->mt_ == mt) {
×
1474
        r = rx.get();
1475
        break;
1476
      }
1477
    }
NEW
1478
    if (!r)
×
NEW
1479
      return OPENMC_E_INVALID_ARGUMENT;
×
1480

1481
    // Access threshold index in NuclideMicroXS
NEW
1482
    if (T_index < 0 || T_index >= static_cast<int>(r->xs_.size()))
×
NEW
1483
      return OPENMC_E_INVALID_ARGUMENT;
×
1484

NEW
1485
    int idx = r->xs_[T_index].threshold;
×
1486

1487
    // Energy grid for this temperature
NEW
1488
    const auto& grid = nuc.grid_.at(T_index).energy;
×
1489

NEW
1490
    if (idx < 0 || idx >= static_cast<int>(grid.size()))
×
NEW
1491
      return OPENMC_E_INVALID_ARGUMENT;
×
1492

NEW
1493
    *threshold = grid[idx]; // <-- actual energy in eV
×
NEW
1494
  } catch (const std::exception& e) {
×
NEW
1495
    set_errmsg(e.what());
×
NEW
1496
    return OPENMC_E_DATA;
×
NEW
1497
  }
×
1498

NEW
1499
  return 0;
×
1500
}
1501

NEW
1502
extern "C" int openmc_nuclide_get_mt_numbers(int index, int* mts, int* n_mts)
×
1503
{
NEW
1504
  using namespace openmc;
×
1505

NEW
1506
  if (index < 0 || index >= data::nuclides.size())
×
NEW
1507
    return OPENMC_E_INVALID_ARGUMENT;
×
1508

NEW
1509
  try {
×
NEW
1510
    const auto& nuc = *data::nuclides[index];
×
NEW
1511
    int n = static_cast<int>(nuc.reactions_.size());
×
NEW
1512
    *n_mts = n;
×
1513

NEW
1514
    if (mts == nullptr)
×
1515
      return 0;
1516

NEW
1517
    for (int i = 0; i < n; ++i) {
×
NEW
1518
      mts[i] = nuc.reactions_[i]->mt_;
×
1519
    }
1520
  } catch (const std::exception& e) {
1521
    set_errmsg(e.what());
1522
    return OPENMC_E_DATA;
1523
  }
1524

1525
  return 0;
1526
}
1527

NEW
1528
extern "C" int openmc_nuclide_set_reaction_xs_with_threshold(int index, int mt,
×
1529
  int T_index, const double* energy, const double* values, int n,
1530
  double threshold_energy)
1531
{
NEW
1532
  using namespace openmc;
×
1533

NEW
1534
  if (index < 0 || index >= static_cast<int>(data::nuclides.size()))
×
NEW
1535
    return OPENMC_E_INVALID_ARGUMENT;
×
NEW
1536
  if (!energy || !values || n <= 0)
×
NEW
1537
    return OPENMC_E_INVALID_ARGUMENT;
×
1538

NEW
1539
  try {
×
NEW
1540
    auto& nuc = *data::nuclides[index];
×
1541

1542
    // --- Find reaction ---
NEW
1543
    Reaction* rx = nullptr;
×
NEW
1544
    for (auto& rx_ptr : nuc.reactions_) {
×
NEW
1545
      if (rx_ptr && rx_ptr->mt_ == mt) {
×
1546
        rx = rx_ptr.get();
1547
        break;
1548
      }
1549
    }
NEW
1550
    if (!rx)
×
NEW
1551
      return OPENMC_E_INVALID_ARGUMENT;
×
1552

NEW
1553
    if (T_index < 0 || T_index >= static_cast<int>(rx->xs_.size()))
×
NEW
1554
      return OPENMC_E_INVALID_ARGUMENT;
×
1555

NEW
1556
    auto& xs = rx->xs_[T_index];
×
1557

1558
    // --- Validate energy grid ---
NEW
1559
    const auto& nuc_grid = nuc.grid_.at(T_index).energy;
×
1560

NEW
1561
    if (static_cast<int>(nuc_grid.size()) != n)
×
NEW
1562
      return OPENMC_E_INVALID_ARGUMENT;
×
1563

NEW
1564
    for (int i = 0; i < n; ++i) {
×
NEW
1565
      if (energy[i] != nuc_grid[i])
×
NEW
1566
        return OPENMC_E_INVALID_ARGUMENT;
×
1567
    }
1568

1569
    // --- Compute threshold index ---
NEW
1570
    int thr_idx = -1;
×
NEW
1571
    for (int i = 0; i < n; ++i) {
×
NEW
1572
      if (energy[i] >= threshold_energy) {
×
1573
        thr_idx = i;
1574
        break;
1575
      }
1576
    }
NEW
1577
    if (thr_idx < 0)
×
NEW
1578
      return OPENMC_E_INVALID_ARGUMENT;
×
1579

1580
    // --- Enforce zero below threshold ---
NEW
1581
    for (int i = 0; i < thr_idx; ++i) {
×
NEW
1582
      if (values[i] != 0.0)
×
NEW
1583
        return OPENMC_E_INVALID_ARGUMENT;
×
1584
    }
1585

1586
    // --- Trim XS ---
NEW
1587
    std::vector<double> new_values;
×
NEW
1588
    new_values.reserve(n - thr_idx);
×
1589

NEW
1590
    for (int i = thr_idx; i < n; ++i)
×
NEW
1591
      new_values.push_back(values[i]);
×
1592

1593
    // --- Update reaction --
NEW
1594
    xs.value = std::move(new_values);
×
NEW
1595
    xs.threshold = thr_idx;
×
NEW
1596
  } catch (const std::exception& e) {
×
NEW
1597
    set_errmsg(e.what());
×
NEW
1598
    return OPENMC_E_DATA;
×
NEW
1599
  }
×
1600

NEW
1601
  return 0;
×
1602
}
1603

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