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

openmc-dev / openmc / 20738425414

06 Jan 2026 04:51AM UTC coverage: 81.961% (-0.2%) from 82.154%
20738425414

Pull #3702

github

web-flow
Merge 8f77d8b3e into 60ddafa9b
Pull Request #3702: Random Ray Kinetic Simulation Mode

17517 of 24332 branches covered (71.99%)

Branch coverage included in aggregate %.

907 of 1117 new or added lines in 20 files covered. (81.2%)

103 existing lines in 8 files now uncovered.

55908 of 65253 relevant lines covered (85.68%)

50718541.16 hits per line

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

72.35
/src/material.cpp
1
#include "openmc/material.h"
2

3
#include <algorithm> // for min, max, sort, fill
4
#include <cassert>
5
#include <cmath>
6
#include <iterator>
7
#include <sstream>
8
#include <string>
9
#include <unordered_set>
10

11
#include "xtensor/xbuilder.hpp"
12
#include "xtensor/xoperation.hpp"
13
#include "xtensor/xview.hpp"
14

15
#include "openmc/capi.h"
16
#include "openmc/container_util.h"
17
#include "openmc/cross_sections.h"
18
#include "openmc/error.h"
19
#include "openmc/file_utils.h"
20
#include "openmc/hdf5_interface.h"
21
#include "openmc/math_functions.h"
22
#include "openmc/message_passing.h"
23
#include "openmc/mgxs_interface.h"
24
#include "openmc/nuclide.h"
25
#include "openmc/photon.h"
26
#include "openmc/search.h"
27
#include "openmc/settings.h"
28
#include "openmc/simulation.h"
29
#include "openmc/string_utils.h"
30
#include "openmc/thermal.h"
31
#include "openmc/xml_interface.h"
32

33
namespace openmc {
34

35
//==============================================================================
36
// Global variables
37
//==============================================================================
38

39
namespace model {
40

41
std::unordered_map<int32_t, int32_t> material_map;
42
vector<unique_ptr<Material>> materials;
43

44
} // namespace model
45

46
//==============================================================================
47
// Material implementation
48
//==============================================================================
49

50
Material::Material(pugi::xml_node node)
15,263✔
51
{
52
  index_ = model::materials.size(); // Avoids warning about narrowing
15,263✔
53

54
  if (check_for_node(node, "id")) {
15,263!
55
    this->set_id(std::stoi(get_node_value(node, "id")));
15,263✔
56
  } else {
57
    fatal_error("Must specify id of material in materials XML file.");
×
58
  }
59

60
  if (check_for_node(node, "name")) {
15,263✔
61
    name_ = get_node_value(node, "name");
7,789✔
62
  }
63

64
  if (check_for_node(node, "cfg")) {
15,263✔
65
    auto cfg = get_node_value(node, "cfg");
16✔
66
    write_message(
16✔
67
      5, "NCrystal config string for material #{}: '{}'", this->id(), cfg);
16✔
68
    ncrystal_mat_ = NCrystalMat(cfg);
16✔
69
  }
16✔
70

71
  if (check_for_node(node, "depletable")) {
15,263✔
72
    depletable_ = get_node_value_bool(node, "depletable");
5,349✔
73
  }
74

75
  bool sum_density {false};
15,263✔
76
  pugi::xml_node density_node = node.child("density");
15,263✔
77
  std::string units;
15,263✔
78
  if (density_node) {
15,263!
79
    units = get_node_value(density_node, "units");
15,263✔
80
    if (settings::kinetic_simulation) {
15,263✔
81
      if (check_for_node(density_node, "value_timeseries")) {
304✔
82
        if (units == "sum") {
112!
NEW
83
          fatal_error("Use of density_timeseries is incompatible with 'sum'"
×
84
                      "density unit");
85
        } else {
86
          density_timeseries_ =
87
            get_node_array<double>(density_node, "value_timeseries");
112✔
88
          if (density_timeseries_.size() >= settings::n_timesteps) {
112!
89
            warning(fmt::format(
112✔
90
              "Material {} has a density_timeseries (size={}) longer than "
91
              "n_timesteps ({}). Only the first {} entries of the density "
92
              "timeseries "
93
              "will be simulated.",
94
              this->id(), density_timeseries_.size(), settings::n_timesteps,
224✔
95
              settings::n_timesteps));
96
          } else {
NEW
97
            fatal_error(
×
NEW
98
              fmt::format("Material {} has a density_timeseries (size={}) "
×
99
                          "shorter than n_timesteps ({}). Not all "
100
                          "time steps can be simulated. Aborting.",
NEW
101
                this->id(), density_timeseries_.size(), settings::n_timesteps));
×
102
          }
103
        }
104
      }
105
    }
106
    if (units == "sum") {
15,263✔
107
      sum_density = true;
2,242✔
108
    } else if (units == "macro") {
13,021✔
109
      if (check_for_node(density_node, "value")) {
2,691!
110
        density_ = std::stod(get_node_value(density_node, "value"));
2,691✔
111
      } else {
112
        density_ = 1.0;
×
113
      }
114
    } else {
115
      double val = std::stod(get_node_value(density_node, "value"));
10,330✔
116
      if (val <= 0.0) {
10,330!
117
        fatal_error("Need to specify a positive density on material " +
×
118
                    std::to_string(id_) + ".");
×
119
      }
120

121
      double scale_factor;
122
      if (units == "g/cc" || units == "g/cm3") {
10,330✔
123
        density_ = -val;
9,961✔
124
        scale_factor = 1.0;
9,961✔
125
      } else if (units == "kg/m3") {
369✔
126
        density_ = -1.0e-3 * val;
16✔
127
        scale_factor = -1.0e-3;
16✔
128
      } else if (units == "atom/b-cm") {
353✔
129
        density_ = val;
337✔
130
        scale_factor = 1.0;
337✔
131
      } else if (units == "atom/cc" || units == "atom/cm3") {
16!
132
        density_ = 1.0e-24 * val;
16✔
133
        scale_factor = 1.0e-24;
16✔
134
      } else {
135
        fatal_error("Unknown units '" + units + "' specified on material " +
×
136
                    std::to_string(id_) + ".");
×
137
      }
138
      // Scale density_timeseries_ to the same units as density_
139
      if (density_timeseries_.size() > 0) {
10,330!
NEW
140
        for (double& p : density_timeseries_) {
×
NEW
141
          if (p <= 0) {
×
NEW
142
            fatal_error(
×
143
              "Zero or negative value detected in material timeseries. This "
144
              "will break any time-dependent simulations. Please fix the "
145
              "density timeseries in your input file. Aborting...");
146
          }
NEW
147
          p *= scale_factor;
×
148
        }
149

150
        // Check that all elements are the same sign.
NEW
151
        vector<double> zero_vector;
×
NEW
152
        for (int i = 0; i < density_timeseries_.size(); i++) {
×
NEW
153
          zero_vector.push_back(i);
×
154
        }
NEW
155
        if (!(density_timeseries_ >= zero_vector ||
×
NEW
156
              density_timeseries_ <= zero_vector)) {
×
NEW
157
          fatal_error(
×
158
            "Cannot mix atom and weight percents for density timeseries "
NEW
159
            "in material " +
×
NEW
160
            std::to_string(id_));
×
NEW
161
        } else if ((density_timeseries_ >= zero_vector && density_ <= 0) ||
×
NEW
162
                   (density_timeseries_ <= zero_vector && density_ >= 0)) {
×
NEW
163
          fatal_error("density_timeseries_ must be same type as density_");
×
164
        }
NEW
165
      }
×
166
    }
167
  } else {
168
    fatal_error("Must specify <density> element in material " +
×
169
                std::to_string(id_) + ".");
×
170
  }
171

172
  if (node.child("element")) {
15,263!
173
    fatal_error(
×
174
      "Unable to add an element to material " + std::to_string(id_) +
×
175
      " since the element option has been removed from the xml input. "
176
      "Elements can only be added via the Python API, which will expand "
177
      "elements into their natural nuclides.");
178
  }
179

180
  // =======================================================================
181
  // READ AND PARSE <nuclide> TAGS
182

183
  // Check to ensure material has at least one nuclide
184
  if (!check_for_node(node, "nuclide") &&
17,954!
185
      !check_for_node(node, "macroscopic")) {
2,691!
186
    fatal_error("No macroscopic data or nuclides specified on material " +
×
187
                std::to_string(id_));
×
188
  }
189

190
  // Create list of macroscopic x/s based on those specified, just treat
191
  // them as nuclides. This is all really a facade so the user thinks they
192
  // are entering in macroscopic data but the code treats them the same
193
  // as nuclides internally.
194
  // Get pointer list of XML <macroscopic>
195
  auto node_macros = node.children("macroscopic");
15,263✔
196
  int num_macros = std::distance(node_macros.begin(), node_macros.end());
15,263✔
197

198
  vector<std::string> names;
15,263✔
199
  vector<double> densities;
15,263✔
200
  if (settings::run_CE && num_macros > 0) {
15,263!
201
    fatal_error("Macroscopic can not be used in continuous-energy mode.");
×
202
  } else if (num_macros > 1) {
15,263!
203
    fatal_error("Only one macroscopic object permitted per material, " +
×
204
                std::to_string(id_));
×
205
  } else if (num_macros == 1) {
15,263✔
206
    pugi::xml_node node_nuc = *node_macros.begin();
2,691✔
207

208
    // Check for empty name on nuclide
209
    if (!check_for_node(node_nuc, "name")) {
2,691!
210
      fatal_error("No name specified on macroscopic data in material " +
×
211
                  std::to_string(id_));
×
212
    }
213

214
    // store nuclide name
215
    std::string name = get_node_value(node_nuc, "name", false, true);
2,691✔
216
    names.push_back(name);
2,691✔
217

218
    // Set density for macroscopic data
219
    if (units == "macro") {
2,691!
220
      densities.push_back(density_);
2,691✔
221
    } else {
222
      fatal_error("Units can only be macro for macroscopic data " + name);
×
223
    }
224
  } else {
2,691✔
225
    // Create list of nuclides based on those specified
226
    for (auto node_nuc : node.children("nuclide")) {
61,404✔
227
      // Check for empty name on nuclide
228
      if (!check_for_node(node_nuc, "name")) {
48,832!
229
        fatal_error(
×
230
          "No name specified on nuclide in material " + std::to_string(id_));
×
231
      }
232

233
      // store nuclide name
234
      std::string name = get_node_value(node_nuc, "name", false, true);
48,832✔
235
      names.push_back(name);
48,832✔
236

237
      // Check if no atom/weight percents were specified or if both atom and
238
      // weight percents were specified
239
      if (units == "macro") {
48,832!
240
        densities.push_back(density_);
×
241
      } else {
242
        bool has_ao = check_for_node(node_nuc, "ao");
48,832✔
243
        bool has_wo = check_for_node(node_nuc, "wo");
48,832✔
244

245
        if (!has_ao && !has_wo) {
48,832!
246
          fatal_error(
×
247
            "No atom or weight percent specified for nuclide: " + name);
×
248
        } else if (has_ao && has_wo) {
48,832!
249
          fatal_error("Cannot specify both atom and weight percents for a "
×
250
                      "nuclide: " +
×
251
                      name);
252
        }
253

254
        // Copy atom/weight percents
255
        if (has_ao) {
48,832✔
256
          densities.push_back(std::stod(get_node_value(node_nuc, "ao")));
40,369✔
257
        } else {
258
          densities.push_back(-std::stod(get_node_value(node_nuc, "wo")));
8,463✔
259
        }
260
      }
261
    }
48,832✔
262
  }
263

264
  // =======================================================================
265
  // READ AND PARSE <isotropic> element
266

267
  vector<std::string> iso_lab;
15,263✔
268
  if (check_for_node(node, "isotropic")) {
15,263✔
269
    iso_lab = get_node_array<std::string>(node, "isotropic");
192✔
270
  }
271

272
  // ========================================================================
273
  // COPY NUCLIDES TO ARRAYS IN MATERIAL
274

275
  // allocate arrays in Material object
276
  auto n = names.size();
15,263✔
277
  nuclide_.reserve(n);
15,263✔
278
  atom_density_ = xt::empty<double>({n});
15,263✔
279
  if (settings::photon_transport)
15,263✔
280
    element_.reserve(n);
341✔
281

282
  for (int i = 0; i < n; ++i) {
66,786✔
283
    const auto& name {names[i]};
51,523✔
284

285
    // Check that this nuclide is listed in the nuclear data library
286
    // (cross_sections.xml for CE and the MGXS HDF5 for MG)
287
    if (settings::run_mode != RunMode::PLOTTING) {
51,523✔
288
      LibraryKey key {Library::Type::neutron, name};
50,658✔
289
      if (data::library_map.find(key) == data::library_map.end()) {
50,658!
290
        fatal_error("Could not find nuclide " + name +
×
291
                    " in the "
292
                    "nuclear data library.");
293
      }
294
    }
50,658✔
295

296
    // If this nuclide hasn't been encountered yet, we need to add its name
297
    // and alias to the nuclide_dict
298
    if (data::nuclide_map.find(name) == data::nuclide_map.end()) {
51,523✔
299
      int index = data::nuclide_map.size();
30,992✔
300
      data::nuclide_map[name] = index;
30,992✔
301
      nuclide_.push_back(index);
30,992✔
302
    } else {
303
      nuclide_.push_back(data::nuclide_map[name]);
20,531✔
304
    }
305

306
    // If the corresponding element hasn't been encountered yet and photon
307
    // transport will be used, we need to add its symbol to the element_dict
308
    if (settings::photon_transport) {
51,523✔
309
      std::string element = to_element(name);
1,517✔
310

311
      // Make sure photon cross section data is available
312
      if (settings::run_mode != RunMode::PLOTTING) {
1,517!
313
        LibraryKey key {Library::Type::photon, element};
1,517✔
314
        if (data::library_map.find(key) == data::library_map.end()) {
1,517!
315
          fatal_error(
×
316
            "Could not find element " + element + " in cross_sections.xml.");
×
317
        }
318
      }
1,517✔
319

320
      if (data::element_map.find(element) == data::element_map.end()) {
1,517✔
321
        int index = data::element_map.size();
732✔
322
        data::element_map[element] = index;
732✔
323
        element_.push_back(index);
732✔
324
      } else {
325
        element_.push_back(data::element_map[element]);
785✔
326
      }
327
    }
1,517✔
328

329
    // Copy atom/weight percent
330
    atom_density_(i) = densities[i];
51,523✔
331
  }
332

333
  if (settings::run_CE) {
15,263✔
334
    // By default, isotropic-in-lab is not used
335
    if (iso_lab.size() > 0) {
12,364✔
336
      p0_.resize(n);
192✔
337

338
      // Apply isotropic-in-lab treatment to specified nuclides
339
      for (int j = 0; j < n; ++j) {
1,808✔
340
        for (const auto& nuc : iso_lab) {
8,400!
341
          if (names[j] == nuc) {
8,400✔
342
            p0_[j] = true;
1,616✔
343
            break;
1,616✔
344
          }
345
        }
346
      }
347
    }
348
  }
349

350
  // Check to make sure either all atom percents or all weight percents are
351
  // given
352
  if (!(xt::all(atom_density_ >= 0.0) || xt::all(atom_density_ <= 0.0))) {
15,263!
353
    fatal_error(
×
354
      "Cannot mix atom and weight percents in material " + std::to_string(id_));
×
355
  }
356

357
  // Determine density if it is a sum value
358
  if (sum_density)
15,263✔
359
    density_ = xt::sum(atom_density_)();
2,242✔
360

361
  if (check_for_node(node, "temperature")) {
15,263✔
362
    temperature_ = std::stod(get_node_value(node, "temperature"));
1,451✔
363
  }
364

365
  if (check_for_node(node, "volume")) {
15,263✔
366
    volume_ = std::stod(get_node_value(node, "volume"));
1,861✔
367
  }
368

369
  // =======================================================================
370
  // READ AND PARSE <sab> TAG FOR THERMAL SCATTERING DATA
371
  if (settings::run_CE) {
15,263✔
372
    // Loop over <sab> elements
373

374
    vector<std::string> sab_names;
12,364✔
375
    for (auto node_sab : node.children("sab")) {
14,393✔
376
      // Determine name of thermal scattering table
377
      if (!check_for_node(node_sab, "name")) {
2,029!
378
        fatal_error("Need to specify <name> for thermal scattering table.");
×
379
      }
380
      std::string name = get_node_value(node_sab, "name");
2,029✔
381
      sab_names.push_back(name);
2,029✔
382

383
      // Read the fraction of nuclei affected by this thermal scattering table
384
      double fraction = 1.0;
2,029✔
385
      if (check_for_node(node_sab, "fraction")) {
2,029✔
386
        fraction = std::stod(get_node_value(node_sab, "fraction"));
16✔
387
      }
388

389
      // Check that the thermal scattering table is listed in the
390
      // cross_sections.xml file
391
      if (settings::run_mode != RunMode::PLOTTING) {
2,029✔
392
        LibraryKey key {Library::Type::thermal, name};
1,976✔
393
        if (data::library_map.find(key) == data::library_map.end()) {
1,976!
394
          fatal_error("Could not find thermal scattering data " + name +
×
395
                      " in cross_sections.xml file.");
396
        }
397
      }
1,976✔
398

399
      // Determine index of thermal scattering data in global
400
      // data::thermal_scatt array
401
      int index_table;
402
      if (data::thermal_scatt_map.find(name) == data::thermal_scatt_map.end()) {
2,029✔
403
        index_table = data::thermal_scatt_map.size();
1,273✔
404
        data::thermal_scatt_map[name] = index_table;
1,273✔
405
      } else {
406
        index_table = data::thermal_scatt_map[name];
756✔
407
      }
408

409
      // Add entry to thermal tables vector. For now, we put the nuclide index
410
      // as zero since we don't know which nuclides the table is being applied
411
      // to yet (this is assigned in init_thermal)
412
      thermal_tables_.push_back({index_table, 0, fraction});
2,029✔
413
    }
2,029✔
414
  }
12,364✔
415
}
15,263✔
416

417
Material::~Material()
15,307✔
418
{
419
  model::material_map.erase(id_);
15,307✔
420
}
15,307✔
421

422
Material& Material::clone()
×
423
{
424
  std::unique_ptr<Material> mat = std::make_unique<Material>();
×
425

426
  // set all other parameters to whatever the calling Material has
427
  mat->name_ = name_;
×
428
  mat->nuclide_ = nuclide_;
×
429
  mat->element_ = element_;
×
430
  mat->ncrystal_mat_ = ncrystal_mat_.clone();
×
431
  mat->atom_density_ = atom_density_;
×
432
  mat->density_ = density_;
×
NEW
433
  if (settings::kinetic_simulation)
×
NEW
434
    mat->density_timeseries_ = density_timeseries_;
×
435
  mat->density_gpcc_ = density_gpcc_;
×
436
  mat->volume_ = volume_;
×
437
  mat->fissionable() = fissionable_;
×
438
  mat->depletable() = depletable_;
×
439
  mat->p0_ = p0_;
×
440
  mat->mat_nuclide_index_ = mat_nuclide_index_;
×
441
  mat->thermal_tables_ = thermal_tables_;
×
442
  mat->temperature_ = temperature_;
×
443

444
  if (ttb_)
×
445
    mat->ttb_ = std::make_unique<Bremsstrahlung>(*ttb_);
×
446

447
  mat->index_ = model::materials.size();
×
448
  mat->set_id(C_NONE);
×
449
  model::materials.push_back(std::move(mat));
×
450
  return *model::materials.back();
×
451
}
×
452

453
void Material::finalize()
14,974✔
454
{
455
  // Set fissionable if any nuclide is fissionable
456
  if (settings::run_CE) {
14,974✔
457
    for (const auto& i_nuc : nuclide_) {
43,107✔
458
      if (data::nuclides[i_nuc]->fissionable_) {
36,467✔
459
        fissionable_ = true;
5,435✔
460
        break;
5,435✔
461
      }
462
    }
463

464
    // Generate material bremsstrahlung data for electrons and positrons
465
    if (settings::photon_transport &&
12,075✔
466
        settings::electron_treatment == ElectronTreatment::TTB) {
341✔
467
      this->init_bremsstrahlung();
275✔
468
    }
469

470
    // Assign thermal scattering tables
471
    this->init_thermal();
12,075✔
472
  }
473

474
  // Normalize density
475
  this->normalize_density();
14,974✔
476
}
14,974✔
477

478
void Material::normalize_density()
14,974✔
479
{
480
  bool percent_in_atom = (atom_density_(0) >= 0.0);
14,974✔
481
  bool density_in_atom = (density_ >= 0.0);
14,974✔
482

483
  for (int i = 0; i < nuclide_.size(); ++i) {
65,635✔
484
    // determine atomic weight ratio
485
    int i_nuc = nuclide_[i];
50,661✔
486
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
53,944✔
487
                                  : data::mg.nuclides_[i_nuc].awr;
3,283✔
488

489
    // if given weight percent, convert all values so that they are divided
490
    // by awr. thus, when a sum is done over the values, it's actually
491
    // sum(w/awr)
492
    if (!percent_in_atom)
50,661✔
493
      atom_density_(i) = -atom_density_(i) / awr;
8,463✔
494
  }
495

496
  // determine normalized atom percents. if given atom percents, this is
497
  // straightforward. if given weight percents, the value is w/awr and is
498
  // divided by sum(w/awr)
499
  atom_density_ /= xt::sum(atom_density_)();
14,974✔
500

501
  // Change density in g/cm^3 to atom/b-cm. Since all values are now in
502
  // atom percent, the sum needs to be re-evaluated as 1/sum(x*awr)
503
  if (!density_in_atom) {
14,974✔
504
    double sum_percent = 0.0;
9,684✔
505
    for (int i = 0; i < nuclide_.size(); ++i) {
42,807✔
506
      int i_nuc = nuclide_[i];
33,123✔
507
      double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
33,203✔
508
                                    : data::mg.nuclides_[i_nuc].awr;
80✔
509
      sum_percent += atom_density_(i) * awr;
33,123✔
510
    }
511
    sum_percent = 1.0 / sum_percent;
9,684✔
512
    density_ = -density_ * N_AVOGADRO / MASS_NEUTRON * sum_percent;
9,684✔
513

514
    for (double& p : density_timeseries_)
9,684!
NEW
515
      p = -p * N_AVOGADRO / MASS_NEUTRON * sum_percent;
×
516
  }
517

518
  // Calculate nuclide atom densities
519
  atom_density_ *= density_;
14,974✔
520

521
  // Calculate density in [g/cm^3] and charge density in [e/b-cm]
522
  density_gpcc_ = 0.0;
14,974✔
523
  charge_density_ = 0.0;
14,974✔
524
  for (int i = 0; i < nuclide_.size(); ++i) {
65,635✔
525
    int i_nuc = nuclide_[i];
50,661✔
526
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_ : 1.0;
50,661✔
527
    int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0;
50,661✔
528
    density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
50,661✔
529
    charge_density_ += atom_density_(i) * z;
50,661✔
530
  }
531
}
14,974✔
532

533
void Material::init_thermal()
17,851✔
534
{
535
  vector<ThermalTable> tables;
17,851✔
536

537
  std::unordered_set<int> already_checked;
17,851✔
538
  for (const auto& table : thermal_tables_) {
19,839✔
539
    // Make sure each S(a,b) table only gets checked once
540
    if (already_checked.find(table.index_table) != already_checked.end()) {
1,988!
541
      continue;
×
542
    }
543
    already_checked.insert(table.index_table);
1,988✔
544

545
    // In order to know which nuclide the S(a,b) table applies to, we need
546
    // to search through the list of nuclides for one which has a matching
547
    // name
548
    bool found = false;
1,988✔
549
    for (int j = 0; j < nuclide_.size(); ++j) {
12,896✔
550
      const auto& name {data::nuclides[nuclide_[j]]->name_};
10,908✔
551
      if (contains(data::thermal_scatt[table.index_table]->nuclides_, name)) {
10,908✔
552
        tables.push_back({table.index_table, j, table.fraction});
2,052✔
553
        found = true;
2,052✔
554
      }
555
    }
556

557
    // Check to make sure thermal scattering table matched a nuclide
558
    if (!found) {
1,988!
559
      fatal_error("Thermal scattering table " +
×
560
                  data::thermal_scatt[table.index_table]->name_ +
×
561
                  " did not match any nuclide on material " +
×
562
                  std::to_string(id_));
×
563
    }
564
  }
565

566
  // Make sure each nuclide only appears in one table.
567
  for (int j = 0; j < tables.size(); ++j) {
19,903✔
568
    for (int k = j + 1; k < tables.size(); ++k) {
2,308✔
569
      if (tables[j].index_nuclide == tables[k].index_nuclide) {
256!
570
        int index = nuclide_[tables[j].index_nuclide];
×
571
        auto name = data::nuclides[index]->name_;
×
572
        fatal_error(
×
573
          name + " in material " + std::to_string(id_) +
×
574
          " was found "
575
          "in multiple thermal scattering tables. Each nuclide can appear in "
576
          "only one table per material.");
577
      }
×
578
    }
579
  }
580

581
  // If there are multiple S(a,b) tables, we need to make sure that the
582
  // entries in i_sab_nuclides are sorted or else they won't be applied
583
  // correctly in the cross_section module.
584
  std::sort(tables.begin(), tables.end(), [](ThermalTable a, ThermalTable b) {
17,851✔
585
    return a.index_nuclide < b.index_nuclide;
176✔
586
  });
587

588
  // Update the list of thermal tables
589
  thermal_tables_ = tables;
17,851✔
590
}
17,851✔
591

592
void Material::collision_stopping_power(double* s_col, bool positron)
550✔
593
{
594
  // Average electron number and average atomic weight
595
  double electron_density = 0.0;
550✔
596
  double mass_density = 0.0;
550✔
597

598
  // Log of the mean excitation energy of the material
599
  double log_I = 0.0;
550✔
600

601
  // Effective number of conduction electrons in the material
602
  double n_conduction = 0.0;
550✔
603

604
  // Oscillator strength and square of the binding energy for each oscillator
605
  // in material
606
  vector<double> f;
550✔
607
  vector<double> e_b_sq;
550✔
608

609
  for (int i = 0; i < element_.size(); ++i) {
2,814✔
610
    const auto& elm = *data::elements[element_[i]];
2,264✔
611
    double awr = data::nuclides[nuclide_[i]]->awr_;
2,264✔
612

613
    // Get atomic density of nuclide given atom/weight percent
614
    double atom_density =
615
      (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
2,264!
616

617
    electron_density += atom_density * elm.Z_;
2,264✔
618
    mass_density += atom_density * awr * MASS_NEUTRON;
2,264✔
619
    log_I += atom_density * elm.Z_ * std::log(elm.I_);
2,264✔
620

621
    for (int j = 0; j < elm.n_electrons_.size(); ++j) {
17,970✔
622
      if (elm.n_electrons_[j] < 0) {
15,706✔
623
        n_conduction -= elm.n_electrons_[j] * atom_density;
1,616✔
624
        continue;
1,616✔
625
      }
626
      e_b_sq.push_back(elm.ionization_energy_[j] * elm.ionization_energy_[j]);
14,090✔
627
      f.push_back(elm.n_electrons_[j] * atom_density);
14,090✔
628
    }
629
  }
630
  log_I /= electron_density;
550✔
631
  n_conduction /= electron_density;
550✔
632
  for (auto& f_i : f)
14,640✔
633
    f_i /= electron_density;
14,090✔
634

635
  // Get density in g/cm^3 if it is given in atom/b-cm
636
  double density = (density_ < 0.0) ? -density_ : mass_density / N_AVOGADRO;
550✔
637

638
  // Calculate the square of the plasma energy
639
  double e_p_sq =
550✔
640
    PLANCK_C * PLANCK_C * PLANCK_C * N_AVOGADRO * electron_density * density /
550✔
641
    (2.0 * PI * PI * FINE_STRUCTURE * MASS_ELECTRON_EV * mass_density);
550✔
642

643
  // Get the Sternheimer adjustment factor
644
  double rho =
645
    sternheimer_adjustment(f, e_b_sq, e_p_sq, n_conduction, log_I, 1.0e-6, 100);
550✔
646

647
  // Classical electron radius in cm
648
  constexpr double CM_PER_ANGSTROM {1.0e-8};
550✔
649
  constexpr double r_e =
550✔
650
    CM_PER_ANGSTROM * PLANCK_C / (2.0 * PI * FINE_STRUCTURE * MASS_ELECTRON_EV);
651

652
  // Constant in expression for collision stopping power
653
  constexpr double BARN_PER_CM_SQ {1.0e24};
550✔
654
  double c =
550✔
655
    BARN_PER_CM_SQ * 2.0 * PI * r_e * r_e * MASS_ELECTRON_EV * electron_density;
656

657
  // Loop over incident charged particle energies
658
  for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
110,202✔
659
    double E = data::ttb_e_grid(i);
109,652✔
660

661
    // Get the density effect correction
662
    double delta =
663
      density_effect(f, e_b_sq, e_p_sq, n_conduction, rho, E, 1.0e-6, 100);
109,652✔
664

665
    // Square of the ratio of the speed of light to the velocity of the charged
666
    // particle
667
    double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
109,652✔
668
                     ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
109,652✔
669

670
    double tau = E / MASS_ELECTRON_EV;
109,652✔
671

672
    double F;
673
    if (positron) {
109,652✔
674
      double t = tau + 2.0;
54,826✔
675
      F = std::log(4.0) - (beta_sq / 12.0) * (23.0 + 14.0 / t + 10.0 / (t * t) +
54,826✔
676
                                               4.0 / (t * t * t));
54,826✔
677
    } else {
678
      F = (1.0 - beta_sq) *
54,826✔
679
          (1.0 + tau * tau / 8.0 - (2.0 * tau + 1.0) * std::log(2.0));
54,826✔
680
    }
681

682
    // Calculate the collision stopping power for this energy
683
    s_col[i] =
109,652✔
684
      c / beta_sq *
109,652✔
685
      (2.0 * (std::log(E) - log_I) + std::log(1.0 + tau / 2.0) + F - delta);
109,652✔
686
  }
687
}
550✔
688

689
void Material::init_bremsstrahlung()
275✔
690
{
691
  // Create new object
692
  ttb_ = make_unique<Bremsstrahlung>();
275✔
693

694
  // Get the size of the energy grids
695
  auto n_k = data::ttb_k_grid.size();
275✔
696
  auto n_e = data::ttb_e_grid.size();
275✔
697

698
  // Determine number of elements
699
  int n = element_.size();
275✔
700

701
  for (int particle = 0; particle < 2; ++particle) {
825✔
702
    // Loop over logic twice, once for electron, once for positron
703
    BremsstrahlungData* ttb =
704
      (particle == 0) ? &ttb_->electron : &ttb_->positron;
550✔
705
    bool positron = (particle == 1);
550✔
706

707
    // Allocate arrays for TTB data
708
    ttb->pdf = xt::zeros<double>({n_e, n_e});
550✔
709
    ttb->cdf = xt::zeros<double>({n_e, n_e});
550✔
710
    ttb->yield = xt::zeros<double>({n_e});
550✔
711

712
    // Allocate temporary arrays
713
    xt::xtensor<double, 1> stopping_power_collision({n_e}, 0.0);
550✔
714
    xt::xtensor<double, 1> stopping_power_radiative({n_e}, 0.0);
550✔
715
    xt::xtensor<double, 2> dcs({n_e, n_k}, 0.0);
550✔
716

717
    double Z_eq_sq = 0.0;
550✔
718
    double sum_density = 0.0;
550✔
719

720
    // Get the collision stopping power of the material
721
    this->collision_stopping_power(stopping_power_collision.data(), positron);
550✔
722

723
    // Calculate the molecular DCS and the molecular radiative stopping power
724
    // using Bragg's additivity rule.
725
    for (int i = 0; i < n; ++i) {
2,814✔
726
      // Get pointer to current element
727
      const auto& elm = *data::elements[element_[i]];
2,264✔
728
      double awr = data::nuclides[nuclide_[i]]->awr_;
2,264✔
729

730
      // Get atomic density and mass density of nuclide given atom/weight
731
      // percent
732
      double atom_density =
733
        (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
2,264!
734

735
      // Calculate the "equivalent" atomic number Zeq of the material
736
      Z_eq_sq += atom_density * elm.Z_ * elm.Z_;
2,264✔
737
      sum_density += atom_density;
2,264✔
738

739
      // Accumulate material DCS
740
      dcs += (atom_density * elm.Z_ * elm.Z_) * elm.dcs_;
2,264✔
741

742
      // Accumulate material radiative stopping power
743
      stopping_power_radiative += atom_density * elm.stopping_power_radiative_;
2,264✔
744
    }
745
    Z_eq_sq /= sum_density;
550✔
746

747
    // Calculate the positron DCS and radiative stopping power. These are
748
    // obtained by multiplying the electron DCS and radiative stopping powers by
749
    // a factor r, which is a numerical approximation of the ratio of the
750
    // radiative stopping powers for positrons and electrons. Source: F. Salvat,
751
    // J. M. Fernández-Varea, and J. Sempau, "PENELOPE-2011: A Code System for
752
    // Monte Carlo Simulation of Electron and Photon Transport," OECD-NEA,
753
    // Issy-les-Moulineaux, France (2011).
754
    if (positron) {
550✔
755
      for (int i = 0; i < n_e; ++i) {
55,101✔
756
        double t = std::log(
54,826✔
757
          1.0 + 1.0e6 * data::ttb_e_grid(i) / (Z_eq_sq * MASS_ELECTRON_EV));
54,826✔
758
        double r =
759
          1.0 -
54,826✔
760
          std::exp(-1.2359e-1 * t + 6.1274e-2 * std::pow(t, 2) -
54,826✔
761
                   3.1516e-2 * std::pow(t, 3) + 7.7446e-3 * std::pow(t, 4) -
54,826✔
762
                   1.0595e-3 * std::pow(t, 5) + 7.0568e-5 * std::pow(t, 6) -
54,826✔
763
                   1.808e-6 * std::pow(t, 7));
54,826✔
764
        stopping_power_radiative(i) *= r;
54,826✔
765
        auto dcs_i = xt::view(dcs, i, xt::all());
54,826✔
766
        dcs_i *= r;
54,826✔
767
      }
54,826✔
768
    }
769

770
    // Total material stopping power
771
    xt::xtensor<double, 1> stopping_power =
772
      stopping_power_collision + stopping_power_radiative;
550✔
773

774
    // Loop over photon energies
775
    xt::xtensor<double, 1> f({n_e}, 0.0);
550✔
776
    xt::xtensor<double, 1> z({n_e}, 0.0);
550✔
777
    for (int i = 0; i < n_e - 1; ++i) {
109,652✔
778
      double w = data::ttb_e_grid(i);
109,102✔
779

780
      // Loop over incident particle energies
781
      for (int j = i; j < n_e; ++j) {
11,097,104✔
782
        double e = data::ttb_e_grid(j);
10,988,002✔
783

784
        // Reduced photon energy
785
        double k = w / e;
10,988,002✔
786

787
        // Find the lower bounding index of the reduced photon energy
788
        int i_k = lower_bound_index(
10,988,002✔
789
          data::ttb_k_grid.cbegin(), data::ttb_k_grid.cend(), k);
10,988,002✔
790

791
        // Get the interpolation bounds
792
        double k_l = data::ttb_k_grid(i_k);
10,988,002✔
793
        double k_r = data::ttb_k_grid(i_k + 1);
10,988,002✔
794
        double x_l = dcs(j, i_k);
10,988,002✔
795
        double x_r = dcs(j, i_k + 1);
10,988,002✔
796

797
        // Find the value of the DCS using linear interpolation in reduced
798
        // photon energy k
799
        double x = x_l + (k - k_l) * (x_r - x_l) / (k_r - k_l);
10,988,002✔
800

801
        // Square of the ratio of the speed of light to the velocity of the
802
        // charged particle
803
        double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
10,988,002✔
804
                         ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
10,988,002✔
805

806
        // Compute the integrand of the PDF
807
        f(j) = x / (beta_sq * stopping_power(j) * w);
10,988,002✔
808
      }
809

810
      // Number of points to integrate
811
      int n = n_e - i;
109,102✔
812

813
      // Integrate the PDF using cubic spline integration over the incident
814
      // particle energy
815
      if (n > 2) {
109,102✔
816
        spline(n, &data::ttb_e_grid(i), &f(i), &z(i));
108,552✔
817

818
        double c = 0.0;
108,552✔
819
        for (int j = i; j < n_e - 1; ++j) {
10,986,902✔
820
          c += spline_integrate(n, &data::ttb_e_grid(i), &f(i), &z(i),
10,878,350✔
821
            data::ttb_e_grid(j), data::ttb_e_grid(j + 1));
10,878,350✔
822

823
          ttb->pdf(j + 1, i) = c;
10,878,350✔
824
        }
825

826
        // Integrate the last two points using trapezoidal rule in log-log space
827
      } else {
828
        double e_l = std::log(data::ttb_e_grid(i));
550✔
829
        double e_r = std::log(data::ttb_e_grid(i + 1));
550✔
830
        double x_l = std::log(f(i));
550✔
831
        double x_r = std::log(f(i + 1));
550✔
832

833
        ttb->pdf(i + 1, i) =
550✔
834
          0.5 * (e_r - e_l) * (std::exp(e_l + x_l) + std::exp(e_r + x_r));
550✔
835
      }
836
    }
837

838
    // Loop over incident particle energies
839
    for (int j = 1; j < n_e; ++j) {
109,652✔
840
      // Set last element of PDF to small non-zero value to enable log-log
841
      // interpolation
842
      ttb->pdf(j, j) = std::exp(-500.0);
109,102✔
843

844
      // Loop over photon energies
845
      double c = 0.0;
109,102✔
846
      for (int i = 0; i < j; ++i) {
10,988,002✔
847
        // Integrate the CDF from the PDF using the fact that the PDF is linear
848
        // in log-log space
849
        double w_l = std::log(data::ttb_e_grid(i));
10,878,900✔
850
        double w_r = std::log(data::ttb_e_grid(i + 1));
10,878,900✔
851
        double x_l = std::log(ttb->pdf(j, i));
10,878,900✔
852
        double x_r = std::log(ttb->pdf(j, i + 1));
10,878,900✔
853
        double beta = (x_r - x_l) / (w_r - w_l);
10,878,900✔
854
        double a = beta + 1.0;
10,878,900✔
855
        c += std::exp(w_l + x_l) / a * std::expm1(a * (w_r - w_l));
10,878,900✔
856
        ttb->cdf(j, i + 1) = c;
10,878,900✔
857
      }
858

859
      // Set photon number yield
860
      ttb->yield(j) = c;
109,102✔
861
    }
862

863
    // Use logarithm of number yield since it is log-log interpolated
864
    ttb->yield = xt::where(ttb->yield > 0.0, xt::log(ttb->yield), -500.0);
550✔
865
  }
550✔
866
}
275✔
867

868
void Material::init_nuclide_index()
20,092✔
869
{
870
  int n = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
20,092✔
871
  mat_nuclide_index_.resize(n);
20,092✔
872
  std::fill(mat_nuclide_index_.begin(), mat_nuclide_index_.end(), C_NONE);
20,092✔
873
  for (int i = 0; i < nuclide_.size(); ++i) {
112,616✔
874
    mat_nuclide_index_[nuclide_[i]] = i;
92,524✔
875
  }
876
}
20,092✔
877

878
void Material::calculate_xs(Particle& p) const
1,554,148,656✔
879
{
880
  // Set all material macroscopic cross sections to zero
881
  p.macro_xs().total = 0.0;
1,554,148,656✔
882
  p.macro_xs().absorption = 0.0;
1,554,148,656✔
883
  p.macro_xs().fission = 0.0;
1,554,148,656✔
884
  p.macro_xs().nu_fission = 0.0;
1,554,148,656✔
885

886
  if (p.type() == ParticleType::neutron) {
1,554,148,656✔
887
    this->calculate_neutron_xs(p);
1,481,976,362✔
888
  } else if (p.type() == ParticleType::photon) {
72,172,294✔
889
    this->calculate_photon_xs(p);
18,186,610✔
890
  }
891
}
1,554,148,656✔
892

893
void Material::calculate_neutron_xs(Particle& p) const
1,481,976,362✔
894
{
895
  // Find energy index on energy grid
896
  int neutron = static_cast<int>(ParticleType::neutron);
1,481,976,362✔
897
  int i_grid =
898
    std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing;
1,481,976,362✔
899

900
  // Determine if this material has S(a,b) tables
901
  bool check_sab = (thermal_tables_.size() > 0);
1,481,976,362✔
902

903
  // Initialize position in i_sab_nuclides
904
  int j = 0;
1,481,976,362✔
905

906
  // Calculate NCrystal cross section
907
  double ncrystal_xs = -1.0;
1,481,976,362✔
908
  if (ncrystal_mat_ && p.E() < NCRYSTAL_MAX_ENERGY) {
1,481,976,362!
909
    ncrystal_xs = ncrystal_mat_.xs(p);
11,158,829✔
910
  }
911

912
  // Add contribution from each nuclide in material
913
  for (int i = 0; i < nuclide_.size(); ++i) {
2,147,483,647✔
914
    // ======================================================================
915
    // CHECK FOR S(A,B) TABLE
916

917
    int i_sab = C_NONE;
2,147,483,647✔
918
    double sab_frac = 0.0;
2,147,483,647✔
919

920
    // Check if this nuclide matches one of the S(a,b) tables specified.
921
    // This relies on thermal_tables_ being sorted by .index_nuclide
922
    if (check_sab) {
2,147,483,647✔
923
      const auto& sab {thermal_tables_[j]};
762,420,074✔
924
      if (i == sab.index_nuclide) {
762,420,074✔
925
        // Get index in sab_tables
926
        i_sab = sab.index_table;
351,647,514✔
927
        sab_frac = sab.fraction;
351,647,514✔
928

929
        // If particle energy is greater than the highest energy for the
930
        // S(a,b) table, then don't use the S(a,b) table
931
        if (p.E() > data::thermal_scatt[i_sab]->energy_max_)
351,647,514✔
932
          i_sab = C_NONE;
217,939,573✔
933

934
        // Increment position in thermal_tables_
935
        ++j;
351,647,514✔
936

937
        // Don't check for S(a,b) tables if there are no more left
938
        if (j == thermal_tables_.size())
351,647,514✔
939
          check_sab = false;
351,413,973✔
940
      }
941
    }
942

943
    // ======================================================================
944
    // CALCULATE MICROSCOPIC CROSS SECTION
945

946
    // Get nuclide index
947
    int i_nuclide = nuclide_[i];
2,147,483,647✔
948

949
    // Update microscopic cross section for this nuclide
950
    p.update_neutron_xs(i_nuclide, i_grid, i_sab, sab_frac, ncrystal_xs);
2,147,483,647✔
951
    auto& micro = p.neutron_xs(i_nuclide);
2,147,483,647✔
952

953
    // ======================================================================
954
    // ADD TO MACROSCOPIC CROSS SECTION
955

956
    // Copy atom density of nuclide in material
957
    double atom_density = this->atom_density(i, p.density_mult());
2,147,483,647✔
958

959
    // Add contributions to cross sections
960
    p.macro_xs().total += atom_density * micro.total;
2,147,483,647✔
961
    p.macro_xs().absorption += atom_density * micro.absorption;
2,147,483,647✔
962
    p.macro_xs().fission += atom_density * micro.fission;
2,147,483,647✔
963
    p.macro_xs().nu_fission += atom_density * micro.nu_fission;
2,147,483,647✔
964
  }
965
}
1,481,976,362✔
966

967
void Material::calculate_photon_xs(Particle& p) const
18,186,610✔
968
{
969
  p.macro_xs().coherent = 0.0;
18,186,610✔
970
  p.macro_xs().incoherent = 0.0;
18,186,610✔
971
  p.macro_xs().photoelectric = 0.0;
18,186,610✔
972
  p.macro_xs().pair_production = 0.0;
18,186,610✔
973

974
  // Add contribution from each nuclide in material
975
  for (int i = 0; i < nuclide_.size(); ++i) {
101,992,886✔
976
    // ========================================================================
977
    // CALCULATE MICROSCOPIC CROSS SECTION
978

979
    // Determine microscopic cross sections for this nuclide
980
    int i_element = element_[i];
83,806,276✔
981

982
    // Calculate microscopic cross section for this nuclide
983
    const auto& micro {p.photon_xs(i_element)};
83,806,276✔
984
    if (p.E() != micro.last_E) {
83,806,276✔
985
      data::elements[i_element]->calculate_xs(p);
41,845,730✔
986
    }
987

988
    // ========================================================================
989
    // ADD TO MACROSCOPIC CROSS SECTION
990

991
    // Copy atom density of nuclide in material
992
    double atom_density = this->atom_density(i, p.density_mult());
83,806,276✔
993

994
    // Add contributions to material macroscopic cross sections
995
    p.macro_xs().total += atom_density * micro.total;
83,806,276✔
996
    p.macro_xs().coherent += atom_density * micro.coherent;
83,806,276✔
997
    p.macro_xs().incoherent += atom_density * micro.incoherent;
83,806,276✔
998
    p.macro_xs().photoelectric += atom_density * micro.photoelectric;
83,806,276✔
999
    p.macro_xs().pair_production += atom_density * micro.pair_production;
83,806,276✔
1000
  }
1001
}
18,186,610✔
1002

1003
void Material::set_id(int32_t id)
15,307✔
1004
{
1005
  assert(id >= 0 || id == C_NONE);
12,481!
1006

1007
  // Clear entry in material map if an ID was already assigned before
1008
  if (id_ != C_NONE) {
15,307!
1009
    model::material_map.erase(id_);
×
1010
    id_ = C_NONE;
×
1011
  }
1012

1013
  // Make sure no other material has same ID
1014
  if (model::material_map.find(id) != model::material_map.end()) {
15,307!
1015
    throw std::runtime_error {
×
1016
      "Two materials have the same ID: " + std::to_string(id)};
×
1017
  }
1018

1019
  // If no ID specified, auto-assign next ID in sequence
1020
  if (id == C_NONE) {
15,307!
1021
    id = 0;
×
1022
    for (const auto& m : model::materials) {
×
1023
      id = std::max(id, m->id_);
×
1024
    }
1025
    ++id;
×
1026
  }
1027

1028
  // Update ID and entry in material map
1029
  id_ = id;
15,307✔
1030
  model::material_map[id] = index_;
15,307✔
1031
}
15,307✔
1032

1033
void Material::set_density(double density, const std::string& units)
5,974✔
1034
{
1035
  assert(density >= 0.0);
4,914!
1036

1037
  if (nuclide_.empty()) {
5,974!
1038
    throw std::runtime_error {"No nuclides exist in material yet."};
×
1039
  }
1040

1041
  if (units == "atom/b-cm") {
5,974✔
1042
    // Set total density based on value provided
1043
    density_ = density;
5,930✔
1044

1045
    // Determine normalized atom percents
1046
    double sum_percent = xt::sum(atom_density_)();
5,930✔
1047
    atom_density_ /= sum_percent;
5,930✔
1048

1049
    // Recalculate nuclide atom densities based on given density
1050
    atom_density_ *= density;
5,930✔
1051

1052
    // Calculate density in g/cm^3 and charge density in [e/b-cm]
1053
    density_gpcc_ = 0.0;
5,930✔
1054
    charge_density_ = 0.0;
5,930✔
1055
    for (int i = 0; i < nuclide_.size(); ++i) {
61,049✔
1056
      int i_nuc = nuclide_[i];
55,119✔
1057
      double awr = data::nuclides[i_nuc]->awr_;
55,119✔
1058
      int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0;
55,119!
1059
      density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
55,119✔
1060
      charge_density_ += atom_density_(i) * z;
55,119✔
1061
    }
1062
  } else if (units == "g/cm3" || units == "g/cc") {
44!
1063
    // Determine factor by which to change densities
1064
    double previous_density_gpcc = density_gpcc_;
33✔
1065
    double f = density / previous_density_gpcc;
33✔
1066

1067
    // Update densities
1068
    density_gpcc_ = density;
33✔
1069
    density_ *= f;
33✔
1070
    atom_density_ *= f;
33✔
1071
    charge_density_ *= f;
33✔
1072
  } else {
1073
    throw std::invalid_argument {
11✔
1074
      "Invalid units '" + std::string(units.data()) + "' specified."};
22✔
1075
  }
1076
}
5,963✔
1077

1078
void Material::set_densities(
5,776✔
1079
  const vector<std::string>& name, const vector<double>& density)
1080
{
1081
  auto n = name.size();
5,776✔
1082
  assert(n > 0);
4,752!
1083
  assert(n == density.size());
4,752!
1084

1085
  if (n != nuclide_.size()) {
5,776✔
1086
    nuclide_.resize(n);
1,356✔
1087
    atom_density_ = xt::zeros<double>({n});
1,356✔
1088
    if (settings::photon_transport)
1,356!
1089
      element_.resize(n);
×
1090
  }
1091

1092
  double sum_density = 0.0;
5,776✔
1093
  for (int64_t i = 0; i < n; ++i) {
60,213✔
1094
    const auto& nuc {name[i]};
54,437✔
1095
    if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) {
54,437!
1096
      int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
×
1097
      if (err < 0)
×
1098
        throw std::runtime_error {openmc_err_msg};
×
1099
    }
1100

1101
    nuclide_[i] = data::nuclide_map.at(nuc);
54,437✔
1102
    assert(density[i] > 0.0);
44,807!
1103
    atom_density_(i) = density[i];
54,437✔
1104
    sum_density += density[i];
54,437✔
1105

1106
    if (settings::photon_transport) {
54,437!
1107
      auto element_name = to_element(nuc);
×
1108
      element_[i] = data::element_map.at(element_name);
×
1109
    }
×
1110
  }
1111

1112
  // Set total density to the sum of the vector
1113
  this->set_density(sum_density, "atom/b-cm");
5,776✔
1114

1115
  // Generate material bremsstrahlung data for electrons and positrons
1116
  if (settings::photon_transport &&
5,776!
1117
      settings::electron_treatment == ElectronTreatment::TTB) {
×
1118
    this->init_bremsstrahlung();
×
1119
  }
1120

1121
  // Assign S(a,b) tables
1122
  this->init_thermal();
5,776✔
1123
}
5,776✔
1124

1125
double Material::volume() const
55✔
1126
{
1127
  if (volume_ < 0.0) {
55✔
1128
    throw std::runtime_error {
11✔
1129
      "Volume for material with ID=" + std::to_string(id_) + " not set."};
22✔
1130
  }
1131
  return volume_;
44✔
1132
}
1133

1134
double Material::temperature() const
19,463✔
1135
{
1136
  // If material doesn't have an assigned temperature, use global default
1137
  return temperature_ >= 0 ? temperature_ : settings::temperature_default;
19,463✔
1138
}
1139

1140
void Material::to_hdf5(hid_t group) const
12,164✔
1141
{
1142
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
12,164✔
1143

1144
  write_attribute(material_group, "depletable", static_cast<int>(depletable()));
12,164✔
1145
  if (volume_ > 0.0) {
12,164✔
1146
    write_attribute(material_group, "volume", volume_);
1,850✔
1147
  }
1148
  if (temperature_ > 0.0) {
12,164✔
1149
    write_attribute(material_group, "temperature", temperature_);
1,436✔
1150
  }
1151
  write_dataset(material_group, "name", name_);
12,164✔
1152
  write_dataset(material_group, "atom_density", density_);
12,164✔
1153

1154
  // Copy nuclide/macro name for each nuclide to vector
1155
  vector<std::string> nuc_names;
12,164✔
1156
  vector<std::string> macro_names;
12,164✔
1157
  vector<double> nuc_densities;
12,164✔
1158
  if (settings::run_CE) {
12,164✔
1159
    for (int i = 0; i < nuclide_.size(); ++i) {
51,710✔
1160
      int i_nuc = nuclide_[i];
41,273✔
1161
      nuc_names.push_back(data::nuclides[i_nuc]->name_);
41,273✔
1162
      nuc_densities.push_back(atom_density_(i));
41,273✔
1163
    }
1164
  } else {
1165
    for (int i = 0; i < nuclide_.size(); ++i) {
3,487✔
1166
      int i_nuc = nuclide_[i];
1,760✔
1167
      if (data::mg.nuclides_[i_nuc].awr != MACROSCOPIC_AWR) {
1,760✔
1168
        nuc_names.push_back(data::mg.nuclides_[i_nuc].name);
88✔
1169
        nuc_densities.push_back(atom_density_(i));
88✔
1170
      } else {
1171
        macro_names.push_back(data::mg.nuclides_[i_nuc].name);
1,672✔
1172
      }
1173
    }
1174
  }
1175

1176
  // Write vector to 'nuclides'
1177
  if (!nuc_names.empty()) {
12,164✔
1178
    write_dataset(material_group, "nuclides", nuc_names);
10,492✔
1179
    write_dataset(material_group, "nuclide_densities", nuc_densities);
10,492✔
1180
  }
1181

1182
  // Write vector to 'macroscopics'
1183
  if (!macro_names.empty()) {
12,164✔
1184
    write_dataset(material_group, "macroscopics", macro_names);
1,672✔
1185
  }
1186

1187
  if (!thermal_tables_.empty()) {
12,164✔
1188
    vector<std::string> sab_names;
1,515✔
1189
    for (const auto& table : thermal_tables_) {
3,096✔
1190
      sab_names.push_back(data::thermal_scatt[table.index_table]->name_);
1,581✔
1191
    }
1192
    write_dataset(material_group, "sab_names", sab_names);
1,515✔
1193
  }
1,515✔
1194

1195
  close_group(material_group);
12,164✔
1196
}
12,164✔
1197

1198
void Material::export_properties_hdf5(hid_t group) const
132✔
1199
{
1200
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
132✔
1201
  write_attribute(material_group, "atom_density", density_);
132✔
1202
  write_attribute(material_group, "mass_density", density_gpcc_);
132✔
1203
  close_group(material_group);
132✔
1204
}
132✔
1205

1206
void Material::import_properties_hdf5(hid_t group)
99✔
1207
{
1208
  hid_t material_group = open_group(group, "material " + std::to_string(id_));
99✔
1209
  double density;
1210
  read_attribute(material_group, "atom_density", density);
99✔
1211
  this->set_density(density, "atom/b-cm");
99✔
1212
  close_group(material_group);
99✔
1213
}
99✔
1214

1215
void Material::add_nuclide(const std::string& name, double density)
11✔
1216
{
1217
  // Check if nuclide is already in material
1218
  for (int i = 0; i < nuclide_.size(); ++i) {
55✔
1219
    int i_nuc = nuclide_[i];
44✔
1220
    if (data::nuclides[i_nuc]->name_ == name) {
44!
1221
      double awr = data::nuclides[i_nuc]->awr_;
×
1222
      density_ += density - atom_density_(i);
×
1223
      density_gpcc_ +=
×
1224
        (density - atom_density_(i)) * awr * MASS_NEUTRON / N_AVOGADRO;
×
1225
      atom_density_(i) = density;
×
1226
      return;
×
1227
    }
1228
  }
1229

1230
  // If nuclide wasn't found, extend nuclide/density arrays
1231
  int err = openmc_load_nuclide(name.c_str(), nullptr, 0);
11✔
1232
  if (err < 0)
11!
1233
    throw std::runtime_error {openmc_err_msg};
×
1234

1235
  // Append new nuclide/density
1236
  int i_nuc = data::nuclide_map[name];
11✔
1237
  nuclide_.push_back(i_nuc);
11✔
1238

1239
  // Append new element if photon transport is on
1240
  if (settings::photon_transport) {
11!
1241
    int i_elem = data::element_map[to_element(name)];
×
1242
    element_.push_back(i_elem);
×
1243
  }
1244

1245
  auto n = nuclide_.size();
11✔
1246

1247
  // Create copy of atom_density_ array with one extra entry
1248
  xt::xtensor<double, 1> atom_density = xt::zeros<double>({n});
11✔
1249
  xt::view(atom_density, xt::range(0, n - 1)) = atom_density_;
11✔
1250
  atom_density(n - 1) = density;
11✔
1251
  atom_density_ = atom_density;
11✔
1252

1253
  density_ += density;
11✔
1254
  density_gpcc_ +=
11✔
1255
    density * data::nuclides[i_nuc]->awr_ * MASS_NEUTRON / N_AVOGADRO;
11✔
1256
}
11✔
1257

1258
//==============================================================================
1259
// Non-method functions
1260
//==============================================================================
1261

1262
double sternheimer_adjustment(const vector<double>& f,
550✔
1263
  const vector<double>& e_b_sq, double e_p_sq, double n_conduction,
1264
  double log_I, double tol, int max_iter)
1265
{
1266
  // Get the total number of oscillators
1267
  int n = f.size();
550✔
1268

1269
  // Calculate the Sternheimer adjustment factor using Newton's method
1270
  double rho = 2.0;
550✔
1271
  int iter;
1272
  for (iter = 0; iter < max_iter; ++iter) {
2,200!
1273
    double rho_0 = rho;
2,200✔
1274

1275
    // Function to find the root of and its derivative
1276
    double g = 0.0;
2,200✔
1277
    double gp = 0.0;
2,200✔
1278

1279
    for (int i = 0; i < n; ++i) {
59,144✔
1280
      // Square of resonance energy of a bound-shell oscillator
1281
      double e_r_sq = e_b_sq[i] * rho * rho + 2.0 / 3.0 * f[i] * e_p_sq;
56,944✔
1282
      g += f[i] * std::log(e_r_sq);
56,944✔
1283
      gp += e_b_sq[i] * f[i] * rho / e_r_sq;
56,944✔
1284
    }
1285
    // Include conduction electrons
1286
    if (n_conduction > 0.0) {
2,200✔
1287
      g += n_conduction * std::log(n_conduction * e_p_sq);
1,822✔
1288
    }
1289

1290
    // Set the next guess: rho_n+1 = rho_n - g(rho_n)/g'(rho_n)
1291
    rho -= (g - 2.0 * log_I) / (2.0 * gp);
2,200✔
1292

1293
    // If the initial guess is too large, rho can be negative
1294
    if (rho < 0.0)
2,200!
1295
      rho = rho_0 / 2.0;
×
1296

1297
    // Check for convergence
1298
    if (std::abs(rho - rho_0) / rho_0 < tol)
2,200✔
1299
      break;
550✔
1300
  }
1301
  // Did not converge
1302
  if (iter >= max_iter) {
550!
1303
    warning("Maximum Newton-Raphson iterations exceeded.");
×
1304
    rho = 1.0e-6;
×
1305
  }
1306
  return rho;
550✔
1307
}
1308

1309
double density_effect(const vector<double>& f, const vector<double>& e_b_sq,
109,652✔
1310
  double e_p_sq, double n_conduction, double rho, double E, double tol,
1311
  int max_iter)
1312
{
1313
  // Get the total number of oscillators
1314
  int n = f.size();
109,652✔
1315

1316
  // Square of the ratio of the speed of light to the velocity of the charged
1317
  // particle
1318
  double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
109,652✔
1319
                   ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
109,652✔
1320

1321
  // For nonmetals, delta = 0 for beta < beta_0, where beta_0 is obtained by
1322
  // setting the frequency w = 0.
1323
  double beta_0_sq = 0.0;
109,652✔
1324
  if (n_conduction == 0.0) {
109,652✔
1325
    for (int i = 0; i < n; ++i) {
142,400✔
1326
      beta_0_sq += f[i] * e_p_sq / (e_b_sq[i] * rho * rho);
120,800✔
1327
    }
1328
    beta_0_sq = 1.0 / (1.0 + beta_0_sq);
21,600✔
1329
  }
1330
  double delta = 0.0;
109,652✔
1331
  if (beta_sq < beta_0_sq)
109,652✔
1332
    return delta;
11,094✔
1333

1334
  // Compute the square of the frequency w^2 using Newton's method, with the
1335
  // initial guess of w^2 equal to beta^2 * gamma^2
1336
  double w_sq = E / MASS_ELECTRON_EV * (E / MASS_ELECTRON_EV + 2);
98,558✔
1337
  int iter;
1338
  for (iter = 0; iter < max_iter; ++iter) {
661,422!
1339
    double w_sq_0 = w_sq;
661,422✔
1340

1341
    // Function to find the root of and its derivative
1342
    double g = 0.0;
661,422✔
1343
    double gp = 0.0;
661,422✔
1344

1345
    for (int i = 0; i < n; ++i) {
20,924,910✔
1346
      double c = e_b_sq[i] * rho * rho / e_p_sq + w_sq;
20,263,488✔
1347
      g += f[i] / c;
20,263,488✔
1348
      gp -= f[i] / (c * c);
20,263,488✔
1349
    }
1350
    // Include conduction electrons
1351
    g += n_conduction / w_sq;
661,422✔
1352
    gp -= n_conduction / (w_sq * w_sq);
661,422✔
1353

1354
    // Set the next guess: w_n+1 = w_n - g(w_n)/g'(w_n)
1355
    w_sq -= (g + 1.0 - 1.0 / beta_sq) / gp;
661,422✔
1356

1357
    // If the initial guess is too large, w can be negative
1358
    if (w_sq < 0.0)
661,422✔
1359
      w_sq = w_sq_0 / 2.0;
141,600✔
1360

1361
    // Check for convergence
1362
    if (std::abs(w_sq - w_sq_0) / w_sq_0 < tol)
661,422✔
1363
      break;
98,558✔
1364
  }
1365
  // Did not converge
1366
  if (iter >= max_iter) {
98,558!
1367
    warning("Maximum Newton-Raphson iterations exceeded: setting density "
×
1368
            "effect correction to zero.");
1369
    return delta;
×
1370
  }
1371

1372
  // Solve for the density effect correction
1373
  for (int i = 0; i < n; ++i) {
2,849,546✔
1374
    double l_sq = e_b_sq[i] * rho * rho / e_p_sq + 2.0 / 3.0 * f[i];
2,750,988✔
1375
    delta += f[i] * std::log((l_sq + w_sq) / l_sq);
2,750,988✔
1376
  }
1377
  // Include conduction electrons
1378
  if (n_conduction > 0.0) {
98,558✔
1379
    delta += n_conduction * std::log((n_conduction + w_sq) / n_conduction);
88,052✔
1380
  }
1381

1382
  return delta - w_sq * (1.0 - beta_sq);
98,558✔
1383
}
1384

1385
void read_materials_xml()
1,371✔
1386
{
1387
  write_message("Reading materials XML file...", 5);
1,371✔
1388

1389
  pugi::xml_document doc;
1,371✔
1390

1391
  // Check if materials.xml exists
1392
  std::string filename = settings::path_input + "materials.xml";
1,371✔
1393
  if (!file_exists(filename)) {
1,371!
1394
    fatal_error("Material XML file '" + filename + "' does not exist!");
×
1395
  }
1396

1397
  // Parse materials.xml file and get root element
1398
  doc.load_file(filename.c_str());
1,371✔
1399

1400
  // Loop over XML material elements and populate the array.
1401
  pugi::xml_node root = doc.document_element();
1,371✔
1402

1403
  read_materials_xml(root);
1,371✔
1404
}
1,371✔
1405

1406
void read_materials_xml(pugi::xml_node root)
7,783✔
1407
{
1408
  for (pugi::xml_node material_node : root.children("material")) {
23,038✔
1409
    model::materials.push_back(make_unique<Material>(material_node));
15,255✔
1410
  }
1411
  model::materials.shrink_to_fit();
7,783✔
1412
}
7,783✔
1413

1414
void free_memory_material()
7,901✔
1415
{
1416
  model::materials.clear();
7,901✔
1417
  model::material_map.clear();
7,901✔
1418
}
7,901✔
1419

1420
//==============================================================================
1421
// C API
1422
//==============================================================================
1423

1424
extern "C" int openmc_get_material_index(int32_t id, int32_t* index)
7,729✔
1425
{
1426
  auto it = model::material_map.find(id);
7,729✔
1427
  if (it == model::material_map.end()) {
7,729✔
1428
    set_errmsg("No material exists with ID=" + std::to_string(id) + ".");
11✔
1429
    return OPENMC_E_INVALID_ID;
11✔
1430
  } else {
1431
    *index = it->second;
7,718✔
1432
    return 0;
7,718✔
1433
  }
1434
}
1435

1436
extern "C" int openmc_material_add_nuclide(
11✔
1437
  int32_t index, const char* name, double density)
1438
{
1439
  int err = 0;
11✔
1440
  if (index >= 0 && index < model::materials.size()) {
11!
1441
    try {
1442
      model::materials[index]->add_nuclide(name, density);
11✔
1443
    } catch (const std::runtime_error& e) {
×
1444
      return OPENMC_E_DATA;
×
1445
    }
×
1446
  } else {
1447
    set_errmsg("Index in materials array is out of bounds.");
×
1448
    return OPENMC_E_OUT_OF_BOUNDS;
×
1449
  }
1450
  return err;
11✔
1451
}
1452

1453
extern "C" int openmc_material_get_densities(
165✔
1454
  int32_t index, const int** nuclides, const double** densities, int* n)
1455
{
1456
  if (index >= 0 && index < model::materials.size()) {
165!
1457
    auto& mat = model::materials[index];
165✔
1458
    if (!mat->nuclides().empty()) {
165!
1459
      *nuclides = mat->nuclides().data();
165✔
1460
      *densities = mat->densities().data();
165✔
1461
      *n = mat->nuclides().size();
165✔
1462
      return 0;
165✔
1463
    } else {
1464
      set_errmsg("Material atom density array has not been allocated.");
×
1465
      return OPENMC_E_ALLOCATE;
×
1466
    }
1467
  } else {
1468
    set_errmsg("Index in materials array is out of bounds.");
×
1469
    return OPENMC_E_OUT_OF_BOUNDS;
×
1470
  }
1471
}
1472

1473
extern "C" int openmc_material_get_density(int32_t index, double* density)
33✔
1474
{
1475
  if (index >= 0 && index < model::materials.size()) {
33!
1476
    auto& mat = model::materials[index];
33✔
1477
    *density = mat->density_gpcc();
33✔
1478
    return 0;
33✔
1479
  } else {
1480
    set_errmsg("Index in materials array is out of bounds.");
×
1481
    return OPENMC_E_OUT_OF_BOUNDS;
×
1482
  }
1483
}
1484

1485
extern "C" int openmc_material_get_fissionable(int32_t index, bool* fissionable)
×
1486
{
1487
  if (index >= 0 && index < model::materials.size()) {
×
1488
    *fissionable = model::materials[index]->fissionable();
×
1489
    return 0;
×
1490
  } else {
1491
    set_errmsg("Index in materials array is out of bounds.");
×
1492
    return OPENMC_E_OUT_OF_BOUNDS;
×
1493
  }
1494
}
1495

1496
extern "C" int openmc_material_get_id(int32_t index, int32_t* id)
8,809✔
1497
{
1498
  if (index >= 0 && index < model::materials.size()) {
8,809!
1499
    *id = model::materials[index]->id();
8,809✔
1500
    return 0;
8,809✔
1501
  } else {
1502
    set_errmsg("Index in materials array is out of bounds.");
×
1503
    return OPENMC_E_OUT_OF_BOUNDS;
×
1504
  }
1505
}
1506

1507
extern "C" int openmc_material_get_temperature(
66✔
1508
  int32_t index, double* temperature)
1509
{
1510
  if (index < 0 || index >= model::materials.size()) {
66!
1511
    set_errmsg("Index in materials array is out of bounds.");
×
1512
    return OPENMC_E_OUT_OF_BOUNDS;
×
1513
  }
1514
  *temperature = model::materials[index]->temperature();
66✔
1515
  return 0;
66✔
1516
}
1517

1518
extern "C" int openmc_material_get_volume(int32_t index, double* volume)
55✔
1519
{
1520
  if (index >= 0 && index < model::materials.size()) {
55!
1521
    try {
1522
      *volume = model::materials[index]->volume();
55✔
1523
    } catch (const std::exception& e) {
11!
1524
      set_errmsg(e.what());
11✔
1525
      return OPENMC_E_UNASSIGNED;
11✔
1526
    }
11✔
1527
    return 0;
44✔
1528
  } else {
1529
    set_errmsg("Index in materials array is out of bounds.");
×
1530
    return OPENMC_E_OUT_OF_BOUNDS;
×
1531
  }
1532
}
1533

1534
extern "C" int openmc_material_set_density(
99✔
1535
  int32_t index, double density, const char* units)
1536
{
1537
  if (index >= 0 && index < model::materials.size()) {
99!
1538
    try {
1539
      model::materials[index]->set_density(density, units);
121✔
1540
    } catch (const std::exception& e) {
11!
1541
      set_errmsg(e.what());
11✔
1542
      return OPENMC_E_UNASSIGNED;
11✔
1543
    }
11✔
1544
  } else {
1545
    set_errmsg("Index in materials array is out of bounds.");
×
1546
    return OPENMC_E_OUT_OF_BOUNDS;
×
1547
  }
1548
  return 0;
88✔
1549
}
1550

1551
extern "C" int openmc_material_set_densities(
5,776✔
1552
  int32_t index, int n, const char** name, const double* density)
1553
{
1554
  if (index >= 0 && index < model::materials.size()) {
5,776!
1555
    try {
1556
      model::materials[index]->set_densities(
17,328✔
1557
        {name, name + n}, {density, density + n});
11,552✔
1558
    } catch (const std::exception& e) {
×
1559
      set_errmsg(e.what());
×
1560
      return OPENMC_E_UNASSIGNED;
×
1561
    }
×
1562
  } else {
1563
    set_errmsg("Index in materials array is out of bounds.");
×
1564
    return OPENMC_E_OUT_OF_BOUNDS;
×
1565
  }
1566
  return 0;
5,776✔
1567
}
1568

1569
extern "C" int openmc_material_set_id(int32_t index, int32_t id)
44✔
1570
{
1571
  if (index >= 0 && index < model::materials.size()) {
44!
1572
    try {
1573
      model::materials.at(index)->set_id(id);
44✔
1574
    } catch (const std::exception& e) {
×
1575
      set_errmsg(e.what());
×
1576
      return OPENMC_E_UNASSIGNED;
×
1577
    }
×
1578
  } else {
1579
    set_errmsg("Index in materials array is out of bounds.");
×
1580
    return OPENMC_E_OUT_OF_BOUNDS;
×
1581
  }
1582
  return 0;
44✔
1583
}
1584

1585
extern "C" int openmc_material_get_name(int32_t index, const char** name)
33✔
1586
{
1587
  if (index < 0 || index >= model::materials.size()) {
33!
1588
    set_errmsg("Index in materials array is out of bounds.");
×
1589
    return OPENMC_E_OUT_OF_BOUNDS;
×
1590
  }
1591

1592
  *name = model::materials[index]->name().data();
33✔
1593

1594
  return 0;
33✔
1595
}
1596

1597
extern "C" int openmc_material_set_name(int32_t index, const char* name)
11✔
1598
{
1599
  if (index < 0 || index >= model::materials.size()) {
11!
1600
    set_errmsg("Index in materials array is out of bounds.");
×
1601
    return OPENMC_E_OUT_OF_BOUNDS;
×
1602
  }
1603

1604
  model::materials[index]->set_name(name);
11✔
1605

1606
  return 0;
11✔
1607
}
1608

1609
extern "C" int openmc_material_set_volume(int32_t index, double volume)
47✔
1610
{
1611
  if (index >= 0 && index < model::materials.size()) {
47!
1612
    auto& m {model::materials[index]};
47✔
1613
    if (volume >= 0.0) {
47!
1614
      m->volume_ = volume;
47✔
1615
      return 0;
47✔
1616
    } else {
1617
      set_errmsg("Volume must be non-negative");
×
1618
      return OPENMC_E_INVALID_ARGUMENT;
×
1619
    }
1620
  } else {
1621
    set_errmsg("Index in materials array is out of bounds.");
×
1622
    return OPENMC_E_OUT_OF_BOUNDS;
×
1623
  }
1624
}
1625

1626
extern "C" int openmc_material_get_depletable(int32_t index, bool* depletable)
22✔
1627
{
1628
  if (index < 0 || index >= model::materials.size()) {
22!
1629
    set_errmsg("Index in materials array is out of bounds.");
×
1630
    return OPENMC_E_OUT_OF_BOUNDS;
×
1631
  }
1632

1633
  *depletable = model::materials[index]->depletable();
22✔
1634

1635
  return 0;
22✔
1636
}
1637

1638
extern "C" int openmc_material_set_depletable(int32_t index, bool depletable)
11✔
1639
{
1640
  if (index < 0 || index >= model::materials.size()) {
11!
1641
    set_errmsg("Index in materials array is out of bounds.");
×
1642
    return OPENMC_E_OUT_OF_BOUNDS;
×
1643
  }
1644

1645
  model::materials[index]->depletable() = depletable;
11✔
1646

1647
  return 0;
11✔
1648
}
1649

1650
extern "C" int openmc_extend_materials(
44✔
1651
  int32_t n, int32_t* index_start, int32_t* index_end)
1652
{
1653
  if (index_start)
44!
1654
    *index_start = model::materials.size();
44✔
1655
  if (index_end)
44!
1656
    *index_end = model::materials.size() + n - 1;
×
1657
  for (int32_t i = 0; i < n; i++) {
88✔
1658
    model::materials.push_back(make_unique<Material>());
44✔
1659
  }
1660
  return 0;
44✔
1661
}
1662

1663
extern "C" size_t n_materials()
66✔
1664
{
1665
  return model::materials.size();
66✔
1666
}
1667

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