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

openmc-dev / openmc / 15199515869

23 May 2025 12:28AM UTC coverage: 85.224% (+0.004%) from 85.22%
15199515869

Pull #3416

github

web-flow
Merge 2293fef34 into a7d1ceba3
Pull Request #3416: Fixed a bug in charged particle energy deposition.

15 of 15 new or added lines in 2 files covered. (100.0%)

263 existing lines in 3 files now uncovered.

52359 of 61437 relevant lines covered (85.22%)

37291466.71 hits per line

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

82.43
/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)
13,847 ✔
51
{
52
  index_ = model::materials.size(); // Avoids warning about narrowing
13,847 ✔
53

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

60
  if (check_for_node(node, "name")) {
13,847 ✔
61
    name_ = get_node_value(node, "name");
6,583 ✔
62
  }
63

64
  if (check_for_node(node, "cfg")) {
13,847 ✔
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")) {
13,847 ✔
72
    depletable_ = get_node_value_bool(node, "depletable");
5,139 ✔
73
  }
74

75
  bool sum_density {false};
13,847 ✔
76
  pugi::xml_node density_node = node.child("density");
13,847 ✔
77
  std::string units;
13,847 ✔
78
  if (density_node) {
13,847 ✔
79
    units = get_node_value(density_node, "units");
13,847 ✔
80
    if (units == "sum") {
13,847 ✔
81
      sum_density = true;
2,477 ✔
82
    } else if (units == "macro") {
11,370 ✔
83
      if (check_for_node(density_node, "value")) {
2,146 ✔
84
        density_ = std::stod(get_node_value(density_node, "value"));
2,146 ✔
85
      } else {
86
        density_ = 1.0;
×
87
      }
88
    } else {
89
      double val = std::stod(get_node_value(density_node, "value"));
9,224 ✔
90
      if (val <= 0.0) {
9,224 ✔
91
        fatal_error("Need to specify a positive density on material " +
×
92
                    std::to_string(id_) + ".");
×
93
      }
94

95
      if (units == "g/cc" || units == "g/cm3") {
9,224 ✔
96
        density_ = -val;
8,789 ✔
97
      } else if (units == "kg/m3") {
435 ✔
98
        density_ = -1.0e-3 * val;
16 ✔
99
      } else if (units == "atom/b-cm") {
419 ✔
100
        density_ = val;
403 ✔
101
      } else if (units == "atom/cc" || units == "atom/cm3") {
16 ✔
102
        density_ = 1.0e-24 * val;
16 ✔
103
      } else {
104
        fatal_error("Unknown units '" + units + "' specified on material " +
×
105
                    std::to_string(id_) + ".");
×
106
      }
107
    }
108
  } else {
109
    fatal_error("Must specify <density> element in material " +
×
110
                std::to_string(id_) + ".");
×
111
  }
112

113
  if (node.child("element")) {
13,847 ✔
114
    fatal_error(
×
115
      "Unable to add an element to material " + std::to_string(id_) +
×
116
      " since the element option has been removed from the xml input. "
117
      "Elements can only be added via the Python API, which will expand "
118
      "elements into their natural nuclides.");
119
  }
120

121
  // =======================================================================
122
  // READ AND PARSE <nuclide> TAGS
123

124
  // Check to ensure material has at least one nuclide
125
  if (!check_for_node(node, "nuclide") &&
15,993 ✔
126
      !check_for_node(node, "macroscopic")) {
2,146 ✔
127
    fatal_error("No macroscopic data or nuclides specified on material " +
×
128
                std::to_string(id_));
×
129
  }
130

131
  // Create list of macroscopic x/s based on those specified, just treat
132
  // them as nuclides. This is all really a facade so the user thinks they
133
  // are entering in macroscopic data but the code treats them the same
134
  // as nuclides internally.
135
  // Get pointer list of XML <macroscopic>
136
  auto node_macros = node.children("macroscopic");
13,847 ✔
137
  int num_macros = std::distance(node_macros.begin(), node_macros.end());
13,847 ✔
138

139
  vector<std::string> names;
13,847 ✔
140
  vector<double> densities;
13,847 ✔
141
  if (settings::run_CE && num_macros > 0) {
13,847 ✔
142
    fatal_error("Macroscopic can not be used in continuous-energy mode.");
×
143
  } else if (num_macros > 1) {
13,847 ✔
144
    fatal_error("Only one macroscopic object permitted per material, " +
×
145
                std::to_string(id_));
×
146
  } else if (num_macros == 1) {
13,847 ✔
147
    pugi::xml_node node_nuc = *node_macros.begin();
2,146 ✔
148

149
    // Check for empty name on nuclide
150
    if (!check_for_node(node_nuc, "name")) {
2,146 ✔
151
      fatal_error("No name specified on macroscopic data in material " +
×
152
                  std::to_string(id_));
×
153
    }
154

155
    // store nuclide name
156
    std::string name = get_node_value(node_nuc, "name", false, true);
2,146 ✔
157
    names.push_back(name);
2,146 ✔
158

159
    // Set density for macroscopic data
160
    if (units == "macro") {
2,146 ✔
161
      densities.push_back(density_);
2,146 ✔
162
    } else {
163
      fatal_error("Units can only be macro for macroscopic data " + name);
×
164
    }
165
  } else {
2,146 ✔
166
    // Create list of nuclides based on those specified
167
    for (auto node_nuc : node.children("nuclide")) {
61,504 ✔
168
      // Check for empty name on nuclide
169
      if (!check_for_node(node_nuc, "name")) {
49,803 ✔
170
        fatal_error(
×
171
          "No name specified on nuclide in material " + std::to_string(id_));
×
172
      }
173

174
      // store nuclide name
175
      std::string name = get_node_value(node_nuc, "name", false, true);
49,803 ✔
176
      names.push_back(name);
49,803 ✔
177

178
      // Check if no atom/weight percents were specified or if both atom and
179
      // weight percents were specified
180
      if (units == "macro") {
49,803 ✔
181
        densities.push_back(density_);
×
182
      } else {
183
        bool has_ao = check_for_node(node_nuc, "ao");
49,803 ✔
184
        bool has_wo = check_for_node(node_nuc, "wo");
49,803 ✔
185

186
        if (!has_ao && !has_wo) {
49,803 ✔
187
          fatal_error(
×
188
            "No atom or weight percent specified for nuclide: " + name);
×
189
        } else if (has_ao && has_wo) {
49,803 ✔
190
          fatal_error("Cannot specify both atom and weight percents for a "
×
191
                      "nuclide: " +
×
192
                      name);
193
        }
194

195
        // Copy atom/weight percents
196
        if (has_ao) {
49,803 ✔
197
          densities.push_back(std::stod(get_node_value(node_nuc, "ao")));
41,061 ✔
198
        } else {
199
          densities.push_back(-std::stod(get_node_value(node_nuc, "wo")));
8,742 ✔
200
        }
201
      }
202
    }
49,803 ✔
203
  }
204

205
  // =======================================================================
206
  // READ AND PARSE <isotropic> element
207

208
  vector<std::string> iso_lab;
13,847 ✔
209
  if (check_for_node(node, "isotropic")) {
13,847 ✔
210
    iso_lab = get_node_array<std::string>(node, "isotropic");
192 ✔
211
  }
212

213
  // ========================================================================
214
  // COPY NUCLIDES TO ARRAYS IN MATERIAL
215

216
  // allocate arrays in Material object
217
  auto n = names.size();
13,847 ✔
218
  nuclide_.reserve(n);
13,847 ✔
219
  atom_density_ = xt::empty<double>({n});
13,847 ✔
220
  if (settings::photon_transport)
13,847 ✔
221
    element_.reserve(n);
275 ✔
222

223
  for (int i = 0; i < n; ++i) {
65,796 ✔
224
    const auto& name {names[i]};
51,949 ✔
225

226
    // Check that this nuclide is listed in the nuclear data library
227
    // (cross_sections.xml for CE and the MGXS HDF5 for MG)
228
    if (settings::run_mode != RunMode::PLOTTING) {
51,949 ✔
229
      LibraryKey key {Library::Type::neutron, name};
50,608 ✔
230
      if (data::library_map.find(key) == data::library_map.end()) {
50,608 ✔
231
        fatal_error("Could not find nuclide " + name +
×
232
                    " in the "
233
                    "nuclear data library.");
234
      }
235
    }
50,608 ✔
236

237
    // If this nuclide hasn't been encountered yet, we need to add its name
238
    // and alias to the nuclide_dict
239
    if (data::nuclide_map.find(name) == data::nuclide_map.end()) {
51,949 ✔
240
      int index = data::nuclide_map.size();
28,355 ✔
241
      data::nuclide_map[name] = index;
28,355 ✔
242
      nuclide_.push_back(index);
28,355 ✔
243
    } else {
244
      nuclide_.push_back(data::nuclide_map[name]);
23,594 ✔
245
    }
246

247
    // If the corresponding element hasn't been encountered yet and photon
248
    // transport will be used, we need to add its symbol to the element_dict
249
    if (settings::photon_transport) {
51,949 ✔
250
      std::string element = to_element(name);
1,319 ✔
251

252
      // Make sure photon cross section data is available
253
      if (settings::run_mode != RunMode::PLOTTING) {
1,319 ✔
254
        LibraryKey key {Library::Type::photon, element};
1,319 ✔
255
        if (data::library_map.find(key) == data::library_map.end()) {
1,319 ✔
256
          fatal_error(
×
257
            "Could not find element " + element + " in cross_sections.xml.");
×
258
        }
259
      }
1,319 ✔
260

261
      if (data::element_map.find(element) == data::element_map.end()) {
1,319 ✔
262
        int index = data::element_map.size();
628 ✔
263
        data::element_map[element] = index;
628 ✔
264
        element_.push_back(index);
628 ✔
265
      } else {
266
        element_.push_back(data::element_map[element]);
691 ✔
267
      }
268
    }
1,319 ✔
269

270
    // Copy atom/weight percent
271
    atom_density_(i) = densities[i];
51,949 ✔
272
  }
273

274
  if (settings::run_CE) {
13,847 ✔
275
    // By default, isotropic-in-lab is not used
276
    if (iso_lab.size() > 0) {
11,493 ✔
277
      p0_.resize(n);
192 ✔
278

279
      // Apply isotropic-in-lab treatment to specified nuclides
280
      for (int j = 0; j < n; ++j) {
1,808 ✔
281
        for (const auto& nuc : iso_lab) {
8,400 ✔
282
          if (names[j] == nuc) {
8,400 ✔
283
            p0_[j] = true;
1,616 ✔
284
            break;
1,616 ✔
285
          }
286
        }
287
      }
288
    }
289
  }
290

291
  // Check to make sure either all atom percents or all weight percents are
292
  // given
293
  if (!(xt::all(atom_density_ >= 0.0) || xt::all(atom_density_ <= 0.0))) {
13,847 ✔
294
    fatal_error(
×
295
      "Cannot mix atom and weight percents in material " + std::to_string(id_));
×
296
  }
297

298
  // Determine density if it is a sum value
299
  if (sum_density)
13,847 ✔
300
    density_ = xt::sum(atom_density_)();
2,477 ✔
301

302
  if (check_for_node(node, "temperature")) {
13,847 ✔
303
    temperature_ = std::stod(get_node_value(node, "temperature"));
1,931 ✔
304
  }
305

306
  if (check_for_node(node, "volume")) {
13,847 ✔
307
    volume_ = std::stod(get_node_value(node, "volume"));
2,292 ✔
308
  }
309

310
  // =======================================================================
311
  // READ AND PARSE <sab> TAG FOR THERMAL SCATTERING DATA
312
  if (settings::run_CE) {
13,847 ✔
313
    // Loop over <sab> elements
314

315
    vector<std::string> sab_names;
11,493 ✔
316
    for (auto node_sab : node.children("sab")) {
13,348 ✔
317
      // Determine name of thermal scattering table
318
      if (!check_for_node(node_sab, "name")) {
1,855 ✔
319
        fatal_error("Need to specify <name> for thermal scattering table.");
×
320
      }
321
      std::string name = get_node_value(node_sab, "name");
1,855 ✔
322
      sab_names.push_back(name);
1,855 ✔
323

324
      // Read the fraction of nuclei affected by this thermal scattering table
325
      double fraction = 1.0;
1,855 ✔
326
      if (check_for_node(node_sab, "fraction")) {
1,855 ✔
327
        fraction = std::stod(get_node_value(node_sab, "fraction"));
16 ✔
328
      }
329

330
      // Check that the thermal scattering table is listed in the
331
      // cross_sections.xml file
332
      if (settings::run_mode != RunMode::PLOTTING) {
1,855 ✔
333
        LibraryKey key {Library::Type::thermal, name};
1,769 ✔
334
        if (data::library_map.find(key) == data::library_map.end()) {
1,769 ✔
335
          fatal_error("Could not find thermal scattering data " + name +
×
336
                      " in cross_sections.xml file.");
337
        }
338
      }
1,769 ✔
339

340
      // Determine index of thermal scattering data in global
341
      // data::thermal_scatt array
342
      int index_table;
343
      if (data::thermal_scatt_map.find(name) == data::thermal_scatt_map.end()) {
1,855 ✔
344
        index_table = data::thermal_scatt_map.size();
1,141 ✔
345
        data::thermal_scatt_map[name] = index_table;
1,141 ✔
346
      } else {
347
        index_table = data::thermal_scatt_map[name];
714 ✔
348
      }
349

350
      // Add entry to thermal tables vector. For now, we put the nuclide index
351
      // as zero since we don't know which nuclides the table is being applied
352
      // to yet (this is assigned in init_thermal)
353
      thermal_tables_.push_back({index_table, 0, fraction});
1,855 ✔
354
    }
1,855 ✔
355
  }
11,493 ✔
356
}
13,847 ✔
357

358
Material::~Material()
13,891 ✔
359
{
360
  model::material_map.erase(id_);
13,891 ✔
361
}
13,891 ✔
362

363
Material& Material::clone()
×
364
{
365
  std::unique_ptr<Material> mat = std::make_unique<Material>();
×
366

367
  // set all other parameters to whatever the calling Material has
368
  mat->name_ = name_;
×
369
  mat->nuclide_ = nuclide_;
×
370
  mat->element_ = element_;
×
371
  mat->ncrystal_mat_ = ncrystal_mat_.clone();
×
372
  mat->atom_density_ = atom_density_;
×
373
  mat->density_ = density_;
×
374
  mat->density_gpcc_ = density_gpcc_;
×
375
  mat->volume_ = volume_;
×
376
  mat->fissionable() = fissionable_;
×
377
  mat->depletable() = depletable_;
×
378
  mat->p0_ = p0_;
×
379
  mat->mat_nuclide_index_ = mat_nuclide_index_;
×
380
  mat->thermal_tables_ = thermal_tables_;
×
381
  mat->temperature_ = temperature_;
×
382

383
  if (ttb_)
×
384
    mat->ttb_ = std::make_unique<Bremsstrahlung>(*ttb_);
×
385

386
  mat->index_ = model::materials.size();
×
387
  mat->set_id(C_NONE);
×
388
  model::materials.push_back(std::move(mat));
×
389
  return *model::materials.back();
×
390
}
391

392
void Material::finalize()
13,412 ✔
393
{
394
  // Set fissionable if any nuclide is fissionable
395
  if (settings::run_CE) {
13,412 ✔
396
    for (const auto& i_nuc : nuclide_) {
41,987 ✔
397
      if (data::nuclides[i_nuc]->fissionable_) {
36,246 ✔
398
        fissionable_ = true;
5,317 ✔
399
        break;
5,317 ✔
400
      }
401
    }
402

403
    // Generate material bremsstrahlung data for electrons and positrons
404
    if (settings::photon_transport &&
11,058 ✔
405
        settings::electron_treatment == ElectronTreatment::TTB) {
275 ✔
406
      this->init_bremsstrahlung();
264 ✔
407
    }
408

409
    // Assign thermal scattering tables
410
    this->init_thermal();
11,058 ✔
411
  }
412

413
  // Normalize density
414
  this->normalize_density();
13,412 ✔
415
}
13,412 ✔
416

417
void Material::normalize_density()
13,412 ✔
418
{
419
  bool percent_in_atom = (atom_density_(0) >= 0.0);
13,412 ✔
420
  bool density_in_atom = (density_ >= 0.0);
13,412 ✔
421

422
  for (int i = 0; i < nuclide_.size(); ++i) {
64,023 ✔
423
    // determine atomic weight ratio
424
    int i_nuc = nuclide_[i];
50,611 ✔
425
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
53,349 ✔
426
                                  : data::mg.nuclides_[i_nuc].awr;
2,738 ✔
427

428
    // if given weight percent, convert all values so that they are divided
429
    // by awr. thus, when a sum is done over the values, it's actually
430
    // sum(w/awr)
431
    if (!percent_in_atom)
50,611 ✔
432
      atom_density_(i) = -atom_density_(i) / awr;
8,742 ✔
433
  }
434

435
  // determine normalized atom percents. if given atom percents, this is
436
  // straightforward. if given weight percents, the value is w/awr and is
437
  // divided by sum(w/awr)
438
  atom_density_ /= xt::sum(atom_density_)();
13,412 ✔
439

440
  // Change density in g/cm^3 to atom/b-cm. Since all values are now in
441
  // atom percent, the sum needs to be re-evaluated as 1/sum(x*awr)
442
  if (!density_in_atom) {
13,412 ✔
443
    double sum_percent = 0.0;
8,366 ✔
444
    for (int i = 0; i < nuclide_.size(); ++i) {
37,785 ✔
445
      int i_nuc = nuclide_[i];
29,419 ✔
446
      double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
29,499 ✔
447
                                    : data::mg.nuclides_[i_nuc].awr;
80 ✔
448
      sum_percent += atom_density_(i) * awr;
29,419 ✔
449
    }
450
    sum_percent = 1.0 / sum_percent;
8,366 ✔
451
    density_ = -density_ * N_AVOGADRO / MASS_NEUTRON * sum_percent;
8,366 ✔
452
  }
453

454
  // Calculate nuclide atom densities
455
  atom_density_ *= density_;
13,412 ✔
456

457
  // Calculate density in g/cm^3.
458
  density_gpcc_ = 0.0;
13,412 ✔
459
  for (int i = 0; i < nuclide_.size(); ++i) {
64,023 ✔
460
    int i_nuc = nuclide_[i];
50,611 ✔
461
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_ : 1.0;
50,611 ✔
462
    density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
50,611 ✔
463
  }
464
}
13,412 ✔
465

466
void Material::init_thermal()
18,254 ✔
467
{
468
  vector<ThermalTable> tables;
18,254 ✔
469

470
  std::unordered_set<int> already_checked;
18,254 ✔
471
  for (const auto& table : thermal_tables_) {
20,035 ✔
472
    // Make sure each S(a,b) table only gets checked once
473
    if (already_checked.find(table.index_table) != already_checked.end()) {
1,781 ✔
UNCOV
474
      continue;
×
475
    }
476
    already_checked.insert(table.index_table);
1,781 ✔
477

478
    // In order to know which nuclide the S(a,b) table applies to, we need
479
    // to search through the list of nuclides for one which has a matching
480
    // name
481
    bool found = false;
1,781 ✔
482
    for (int j = 0; j < nuclide_.size(); ++j) {
12,053 ✔
483
      const auto& name {data::nuclides[nuclide_[j]]->name_};
10,272 ✔
484
      if (contains(data::thermal_scatt[table.index_table]->nuclides_, name)) {
10,272 ✔
485
        tables.push_back({table.index_table, j, table.fraction});
1,845 ✔
486
        found = true;
1,845 ✔
487
      }
488
    }
489

490
    // Check to make sure thermal scattering table matched a nuclide
491
    if (!found) {
1,781 ✔
UNCOV
492
      fatal_error("Thermal scattering table " +
×
UNCOV
493
                  data::thermal_scatt[table.index_table]->name_ +
×
UNCOV
494
                  " did not match any nuclide on material " +
×
495
                  std::to_string(id_));
×
496
    }
497
  }
498

499
  // Make sure each nuclide only appears in one table.
500
  for (int j = 0; j < tables.size(); ++j) {
20,099 ✔
501
    for (int k = j + 1; k < tables.size(); ++k) {
2,101 ✔
502
      if (tables[j].index_nuclide == tables[k].index_nuclide) {
256 ✔
UNCOV
503
        int index = nuclide_[tables[j].index_nuclide];
×
UNCOV
504
        auto name = data::nuclides[index]->name_;
×
UNCOV
505
        fatal_error(
×
506
          name + " in material " + std::to_string(id_) +
×
507
          " was found "
508
          "in multiple thermal scattering tables. Each nuclide can appear in "
509
          "only one table per material.");
UNCOV
510
      }
×
511
    }
512
  }
513

514
  // If there are multiple S(a,b) tables, we need to make sure that the
515
  // entries in i_sab_nuclides are sorted or else they won't be applied
516
  // correctly in the cross_section module.
517
  std::sort(tables.begin(), tables.end(), [](ThermalTable a, ThermalTable b) {
18,254 ✔
518
    return a.index_nuclide < b.index_nuclide;
176 ✔
519
  });
520

521
  // Update the list of thermal tables
522
  thermal_tables_ = tables;
18,254 ✔
523
}
18,254 ✔
524

525
void Material::collision_stopping_power(double* s_col, bool positron)
528 ✔
526
{
527
  // Average electron number and average atomic weight
528
  double electron_density = 0.0;
528 ✔
529
  double mass_density = 0.0;
528 ✔
530

531
  // Log of the mean excitation energy of the material
532
  double log_I = 0.0;
528 ✔
533

534
  // Effective number of conduction electrons in the material
535
  double n_conduction = 0.0;
528 ✔
536

537
  // Oscillator strength and square of the binding energy for each oscillator
538
  // in material
539
  vector<double> f;
528 ✔
540
  vector<double> e_b_sq;
528 ✔
541

542
  for (int i = 0; i < element_.size(); ++i) {
2,748 ✔
543
    const auto& elm = *data::elements[element_[i]];
2,220 ✔
544
    double awr = data::nuclides[nuclide_[i]]->awr_;
2,220 ✔
545

546
    // Get atomic density of nuclide given atom/weight percent
547
    double atom_density =
548
      (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
2,220 ✔
549

550
    electron_density += atom_density * elm.Z_;
2,220 ✔
551
    mass_density += atom_density * awr * MASS_NEUTRON;
2,220 ✔
552
    log_I += atom_density * elm.Z_ * std::log(elm.I_);
2,220 ✔
553

554
    for (int j = 0; j < elm.n_electrons_.size(); ++j) {
18,350 ✔
555
      if (elm.n_electrons_[j] < 0) {
16,130 ✔
556
        n_conduction -= elm.n_electrons_[j] * atom_density;
1,636 ✔
557
        continue;
1,636 ✔
558
      }
559
      e_b_sq.push_back(elm.ionization_energy_[j] * elm.ionization_energy_[j]);
14,494 ✔
560
      f.push_back(elm.n_electrons_[j] * atom_density);
14,494 ✔
561
    }
562
  }
563
  log_I /= electron_density;
528 ✔
564
  n_conduction /= electron_density;
528 ✔
565
  for (auto& f_i : f)
15,022 ✔
566
    f_i /= electron_density;
14,494 ✔
567

568
  // Get density in g/cm^3 if it is given in atom/b-cm
569
  double density = (density_ < 0.0) ? -density_ : mass_density / N_AVOGADRO;
528 ✔
570

571
  // Calculate the square of the plasma energy
572
  double e_p_sq =
528 ✔
573
    PLANCK_C * PLANCK_C * PLANCK_C * N_AVOGADRO * electron_density * density /
528 ✔
574
    (2.0 * PI * PI * FINE_STRUCTURE * MASS_ELECTRON_EV * mass_density);
528 ✔
575

576
  // Get the Sternheimer adjustment factor
577
  double rho =
578
    sternheimer_adjustment(f, e_b_sq, e_p_sq, n_conduction, log_I, 1.0e-6, 100);
528 ✔
579

580
  // Classical electron radius in cm
581
  constexpr double CM_PER_ANGSTROM {1.0e-8};
528 ✔
582
  constexpr double r_e =
528 ✔
583
    CM_PER_ANGSTROM * PLANCK_C / (2.0 * PI * FINE_STRUCTURE * MASS_ELECTRON_EV);
584

585
  // Constant in expression for collision stopping power
586
  constexpr double BARN_PER_CM_SQ {1.0e24};
528 ✔
587
  double c =
528 ✔
588
    BARN_PER_CM_SQ * 2.0 * PI * r_e * r_e * MASS_ELECTRON_EV * electron_density;
589

590
  // Loop over incident charged particle energies
591
  for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
105,842 ✔
592
    double E = data::ttb_e_grid(i);
105,314 ✔
593

594
    // Get the density effect correction
595
    double delta =
596
      density_effect(f, e_b_sq, e_p_sq, n_conduction, rho, E, 1.0e-6, 100);
105,314 ✔
597

598
    // Square of the ratio of the speed of light to the velocity of the charged
599
    // particle
600
    double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
105,314 ✔
601
                     ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
105,314 ✔
602

603
    double tau = E / MASS_ELECTRON_EV;
105,314 ✔
604

605
    double F;
606
    if (positron) {
105,314 ✔
607
      double t = tau + 2.0;
52,657 ✔
608
      F = std::log(4.0) - (beta_sq / 12.0) * (23.0 + 14.0 / t + 10.0 / (t * t) +
52,657 ✔
609
                                               4.0 / (t * t * t));
52,657 ✔
610
    } else {
611
      F = (1.0 - beta_sq) *
52,657 ✔
612
          (1.0 + tau * tau / 8.0 - (2.0 * tau + 1.0) * std::log(2.0));
52,657 ✔
613
    }
614

615
    // Calculate the collision stopping power for this energy
616
    s_col[i] =
105,314 ✔
617
      c / beta_sq *
105,314 ✔
618
      (2.0 * (std::log(E) - log_I) + std::log(1.0 + tau / 2.0) + F - delta);
105,314 ✔
619
  }
620
}
528 ✔
621

622
void Material::init_bremsstrahlung()
264 ✔
623
{
624
  // Create new object
625
  ttb_ = make_unique<Bremsstrahlung>();
264 ✔
626

627
  // Get the size of the energy grids
628
  auto n_k = data::ttb_k_grid.size();
264 ✔
629
  auto n_e = data::ttb_e_grid.size();
264 ✔
630

631
  // Determine number of elements
632
  int n = element_.size();
264 ✔
633

634
  for (int particle = 0; particle < 2; ++particle) {
792 ✔
635
    // Loop over logic twice, once for electron, once for positron
636
    BremsstrahlungData* ttb =
637
      (particle == 0) ? &ttb_->electron : &ttb_->positron;
528 ✔
638
    bool positron = (particle == 1);
528 ✔
639

640
    // Allocate arrays for TTB data
641
    ttb->pdf = xt::zeros<double>({n_e, n_e});
528 ✔
642
    ttb->cdf = xt::zeros<double>({n_e, n_e});
528 ✔
643
    ttb->yield = xt::zeros<double>({n_e});
528 ✔
644

645
    // Allocate temporary arrays
646
    xt::xtensor<double, 1> stopping_power_collision({n_e}, 0.0);
528 ✔
647
    xt::xtensor<double, 1> stopping_power_radiative({n_e}, 0.0);
528 ✔
648
    xt::xtensor<double, 2> dcs({n_e, n_k}, 0.0);
528 ✔
649

650
    double Z_eq_sq = 0.0;
528 ✔
651
    double sum_density = 0.0;
528 ✔
652

653
    // Get the collision stopping power of the material
654
    this->collision_stopping_power(stopping_power_collision.data(), positron);
528 ✔
655

656
    // Calculate the molecular DCS and the molecular radiative stopping power
657
    // using Bragg's additivity rule.
658
    for (int i = 0; i < n; ++i) {
2,748 ✔
659
      // Get pointer to current element
660
      const auto& elm = *data::elements[element_[i]];
2,220 ✔
661
      double awr = data::nuclides[nuclide_[i]]->awr_;
2,220 ✔
662

663
      // Get atomic density and mass density of nuclide given atom/weight
664
      // percent
665
      double atom_density =
666
        (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
2,220 ✔
667

668
      // Calculate the "equivalent" atomic number Zeq of the material
669
      Z_eq_sq += atom_density * elm.Z_ * elm.Z_;
2,220 ✔
670
      sum_density += atom_density;
2,220 ✔
671

672
      // Accumulate material DCS
673
      dcs += (atom_density * elm.Z_ * elm.Z_) * elm.dcs_;
2,220 ✔
674

675
      // Accumulate material radiative stopping power
676
      stopping_power_radiative += atom_density * elm.stopping_power_radiative_;
2,220 ✔
677
    }
678
    Z_eq_sq /= sum_density;
528 ✔
679

680
    // Calculate the positron DCS and radiative stopping power. These are
681
    // obtained by multiplying the electron DCS and radiative stopping powers by
682
    // a factor r, which is a numerical approximation of the ratio of the
683
    // radiative stopping powers for positrons and electrons. Source: F. Salvat,
684
    // J. M. Fernández-Varea, and J. Sempau, "PENELOPE-2011: A Code System for
685
    // Monte Carlo Simulation of Electron and Photon Transport," OECD-NEA,
686
    // Issy-les-Moulineaux, France (2011).
687
    if (positron) {
528 ✔
688
      for (int i = 0; i < n_e; ++i) {
52,921 ✔
689
        double t = std::log(
52,657 ✔
690
          1.0 + 1.0e6 * data::ttb_e_grid(i) / (Z_eq_sq * MASS_ELECTRON_EV));
52,657 ✔
691
        double r =
692
          1.0 -
52,657 ✔
693
          std::exp(-1.2359e-1 * t + 6.1274e-2 * std::pow(t, 2) -
52,657 ✔
694
                   3.1516e-2 * std::pow(t, 3) + 7.7446e-3 * std::pow(t, 4) -
52,657 ✔
695
                   1.0595e-3 * std::pow(t, 5) + 7.0568e-5 * std::pow(t, 6) -
52,657 ✔
696
                   1.808e-6 * std::pow(t, 7));
52,657 ✔
697
        stopping_power_radiative(i) *= r;
52,657 ✔
698
        auto dcs_i = xt::view(dcs, i, xt::all());
52,657 ✔
699
        dcs_i *= r;
52,657 ✔
700
      }
52,657 ✔
701
    }
702

703
    // Total material stopping power
704
    xt::xtensor<double, 1> stopping_power =
705
      stopping_power_collision + stopping_power_radiative;
528 ✔
706

707
    // Loop over photon energies
708
    xt::xtensor<double, 1> f({n_e}, 0.0);
528 ✔
709
    xt::xtensor<double, 1> z({n_e}, 0.0);
528 ✔
710
    for (int i = 0; i < n_e - 1; ++i) {
105,314 ✔
711
      double w = data::ttb_e_grid(i);
104,786 ✔
712

713
      // Loop over incident particle energies
714
      for (int j = i; j < n_e; ++j) {
10,661,574 ✔
715
        double e = data::ttb_e_grid(j);
10,556,788 ✔
716

717
        // Reduced photon energy
718
        double k = w / e;
10,556,788 ✔
719

720
        // Find the lower bounding index of the reduced photon energy
721
        int i_k = lower_bound_index(
10,556,788 ✔
722
          data::ttb_k_grid.cbegin(), data::ttb_k_grid.cend(), k);
10,556,788 ✔
723

724
        // Get the interpolation bounds
725
        double k_l = data::ttb_k_grid(i_k);
10,556,788 ✔
726
        double k_r = data::ttb_k_grid(i_k + 1);
10,556,788 ✔
727
        double x_l = dcs(j, i_k);
10,556,788 ✔
728
        double x_r = dcs(j, i_k + 1);
10,556,788 ✔
729

730
        // Find the value of the DCS using linear interpolation in reduced
731
        // photon energy k
732
        double x = x_l + (k - k_l) * (x_r - x_l) / (k_r - k_l);
10,556,788 ✔
733

734
        // Square of the ratio of the speed of light to the velocity of the
735
        // charged particle
736
        double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
10,556,788 ✔
737
                         ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
10,556,788 ✔
738

739
        // Compute the integrand of the PDF
740
        f(j) = x / (beta_sq * stopping_power(j) * w);
10,556,788 ✔
741
      }
742

743
      // Number of points to integrate
744
      int n = n_e - i;
104,786 ✔
745

746
      // Integrate the PDF using cubic spline integration over the incident
747
      // particle energy
748
      if (n > 2) {
104,786 ✔
749
        spline(n, &data::ttb_e_grid(i), &f(i), &z(i));
104,258 ✔
750

751
        double c = 0.0;
104,258 ✔
752
        for (int j = i; j < n_e - 1; ++j) {
10,555,732 ✔
753
          c += spline_integrate(n, &data::ttb_e_grid(i), &f(i), &z(i),
10,451,474 ✔
754
            data::ttb_e_grid(j), data::ttb_e_grid(j + 1));
10,451,474 ✔
755

756
          ttb->pdf(j + 1, i) = c;
10,451,474 ✔
757
        }
758

759
        // Integrate the last two points using trapezoidal rule in log-log space
760
      } else {
761
        double e_l = std::log(data::ttb_e_grid(i));
528 ✔
762
        double e_r = std::log(data::ttb_e_grid(i + 1));
528 ✔
763
        double x_l = std::log(f(i));
528 ✔
764
        double x_r = std::log(f(i + 1));
528 ✔
765

766
        ttb->pdf(i + 1, i) =
528 ✔
767
          0.5 * (e_r - e_l) * (std::exp(e_l + x_l) + std::exp(e_r + x_r));
528 ✔
768
      }
769
    }
770

771
    // Loop over incident particle energies
772
    for (int j = 1; j < n_e; ++j) {
105,314 ✔
773
      // Set last element of PDF to small non-zero value to enable log-log
774
      // interpolation
775
      ttb->pdf(j, j) = std::exp(-500.0);
104,786 ✔
776

777
      // Loop over photon energies
778
      double c = 0.0;
104,786 ✔
779
      for (int i = 0; i < j; ++i) {
10,556,788 ✔
780
        // Integrate the CDF from the PDF using the fact that the PDF is linear
781
        // in log-log space
782
        double w_l = std::log(data::ttb_e_grid(i));
10,452,002 ✔
783
        double w_r = std::log(data::ttb_e_grid(i + 1));
10,452,002 ✔
784
        double x_l = std::log(ttb->pdf(j, i));
10,452,002 ✔
785
        double x_r = std::log(ttb->pdf(j, i + 1));
10,452,002 ✔
786
        double beta = (x_r - x_l) / (w_r - w_l);
10,452,002 ✔
787
        double a = beta + 1.0;
10,452,002 ✔
788
        c += std::exp(w_l + x_l) / a * std::expm1(a * (w_r - w_l));
10,452,002 ✔
789
        ttb->cdf(j, i + 1) = c;
10,452,002 ✔
790
      }
791

792
      // Set photon number yield
793
      ttb->yield(j) = c;
104,786 ✔
794
    }
795

796
    // Use logarithm of number yield since it is log-log interpolated
797
    ttb->yield = xt::where(ttb->yield > 0.0, xt::log(ttb->yield), -500.0);
528 ✔
798
  }
528 ✔
799
}
264 ✔
800

801
void Material::init_nuclide_index()
18,099 ✔
802
{
803
  int n = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
18,099 ✔
804
  mat_nuclide_index_.resize(n);
18,099 ✔
805
  std::fill(mat_nuclide_index_.begin(), mat_nuclide_index_.end(), C_NONE);
18,099 ✔
806
  for (int i = 0; i < nuclide_.size(); ++i) {
120,947 ✔
807
    mat_nuclide_index_[nuclide_[i]] = i;
102,848 ✔
808
  }
809
}
18,099 ✔
810

811
void Material::calculate_xs(Particle& p) const
1,324,947,560 ✔
812
{
813
  // Set all material macroscopic cross sections to zero
814
  p.macro_xs().total = 0.0;
1,324,947,560 ✔
815
  p.macro_xs().absorption = 0.0;
1,324,947,560 ✔
816
  p.macro_xs().fission = 0.0;
1,324,947,560 ✔
817
  p.macro_xs().nu_fission = 0.0;
1,324,947,560 ✔
818

819
  if (p.type() == ParticleType::neutron) {
1,324,947,560 ✔
820
    this->calculate_neutron_xs(p);
1,254,630,449 ✔
821
  } else if (p.type() == ParticleType::photon) {
70,317,111 ✔
822
    this->calculate_photon_xs(p);
15,843,290 ✔
823
  }
824
}
1,324,947,560 ✔
825

826
void Material::calculate_neutron_xs(Particle& p) const
1,254,630,449 ✔
827
{
828
  // Find energy index on energy grid
829
  int neutron = static_cast<int>(ParticleType::neutron);
1,254,630,449 ✔
830
  int i_grid =
831
    std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing;
1,254,630,449 ✔
832

833
  // Determine if this material has S(a,b) tables
834
  bool check_sab = (thermal_tables_.size() > 0);
1,254,630,449 ✔
835

836
  // Initialize position in i_sab_nuclides
837
  int j = 0;
1,254,630,449 ✔
838

839
  // Calculate NCrystal cross section
840
  double ncrystal_xs = -1.0;
1,254,630,449 ✔
841
  if (ncrystal_mat_ && p.E() < NCRYSTAL_MAX_ENERGY) {
1,254,630,449 ✔
842
    ncrystal_xs = ncrystal_mat_.xs(p);
11,158,829 ✔
843
  }
844

845
  // Add contribution from each nuclide in material
846
  for (int i = 0; i < nuclide_.size(); ++i) {
2,147,483,647 ✔
847
    // ======================================================================
848
    // CHECK FOR S(A,B) TABLE
849

850
    int i_sab = C_NONE;
2,147,483,647 ✔
851
    double sab_frac = 0.0;
2,147,483,647 ✔
852

853
    // Check if this nuclide matches one of the S(a,b) tables specified.
854
    // This relies on thermal_tables_ being sorted by .index_nuclide
855
    if (check_sab) {
2,147,483,647 ✔
856
      const auto& sab {thermal_tables_[j]};
738,025,682 ✔
857
      if (i == sab.index_nuclide) {
738,025,682 ✔
858
        // Get index in sab_tables
859
        i_sab = sab.index_table;
300,864,079 ✔
860
        sab_frac = sab.fraction;
300,864,079 ✔
861

862
        // If particle energy is greater than the highest energy for the
863
        // S(a,b) table, then don't use the S(a,b) table
864
        if (p.E() > data::thermal_scatt[i_sab]->energy_max_)
300,864,079 ✔
865
          i_sab = C_NONE;
196,213,369 ✔
866

867
        // Increment position in thermal_tables_
868
        ++j;
300,864,079 ✔
869

870
        // Don't check for S(a,b) tables if there are no more left
871
        if (j == thermal_tables_.size())
300,864,079 ✔
872
          check_sab = false;
300,629,460 ✔
873
      }
874
    }
875

876
    // ======================================================================
877
    // CALCULATE MICROSCOPIC CROSS SECTION
878

879
    // Get nuclide index
880
    int i_nuclide = nuclide_[i];
2,147,483,647 ✔
881

882
    // Update microscopic cross section for this nuclide
883
    p.update_neutron_xs(i_nuclide, i_grid, i_sab, sab_frac, ncrystal_xs);
2,147,483,647 ✔
884
    auto& micro = p.neutron_xs(i_nuclide);
2,147,483,647 ✔
885

886
    // ======================================================================
887
    // ADD TO MACROSCOPIC CROSS SECTION
888

889
    // Copy atom density of nuclide in material
890
    double atom_density = atom_density_(i);
2,147,483,647 ✔
891

892
    // Add contributions to cross sections
893
    p.macro_xs().total += atom_density * micro.total;
2,147,483,647 ✔
894
    p.macro_xs().absorption += atom_density * micro.absorption;
2,147,483,647 ✔
895
    p.macro_xs().fission += atom_density * micro.fission;
2,147,483,647 ✔
896
    p.macro_xs().nu_fission += atom_density * micro.nu_fission;
2,147,483,647 ✔
897
  }
898
}
1,254,630,449 ✔
899

900
void Material::calculate_photon_xs(Particle& p) const
15,843,290 ✔
901
{
902
  p.macro_xs().coherent = 0.0;
15,843,290 ✔
903
  p.macro_xs().incoherent = 0.0;
15,843,290 ✔
904
  p.macro_xs().photoelectric = 0.0;
15,843,290 ✔
905
  p.macro_xs().pair_production = 0.0;
15,843,290 ✔
906

907
  // Add contribution from each nuclide in material
908
  for (int i = 0; i < nuclide_.size(); ++i) {
94,417,095 ✔
909
    // ========================================================================
910
    // CALCULATE MICROSCOPIC CROSS SECTION
911

912
    // Determine microscopic cross sections for this nuclide
913
    int i_element = element_[i];
78,573,805 ✔
914

915
    // Calculate microscopic cross section for this nuclide
916
    const auto& micro {p.photon_xs(i_element)};
78,573,805 ✔
917
    if (p.E() != micro.last_E) {
78,573,805 ✔
918
      data::elements[i_element]->calculate_xs(p);
36,415,686 ✔
919
    }
920

921
    // ========================================================================
922
    // ADD TO MACROSCOPIC CROSS SECTION
923

924
    // Copy atom density of nuclide in material
925
    double atom_density = atom_density_(i);
78,573,805 ✔
926

927
    // Add contributions to material macroscopic cross sections
928
    p.macro_xs().total += atom_density * micro.total;
78,573,805 ✔
929
    p.macro_xs().coherent += atom_density * micro.coherent;
78,573,805 ✔
930
    p.macro_xs().incoherent += atom_density * micro.incoherent;
78,573,805 ✔
931
    p.macro_xs().photoelectric += atom_density * micro.photoelectric;
78,573,805 ✔
932
    p.macro_xs().pair_production += atom_density * micro.pair_production;
78,573,805 ✔
933
  }
934
}
15,843,290 ✔
935

936
double Material::charge_density()
12,973,026 ✔
937
{
938
  double val = 0.0;
12,973,026 ✔
939
  // Add contribution from each nuclide in material
940
  for (int i = 0; i < nuclide_.size(); ++i) {
38,794,558 ✔
941

942
    // Get nuclide index
943
    int i_nuclide = nuclide_[i];
25,821,532 ✔
944
    int z = data::nuclides[i_nuclide]->Z_;
25,821,532 ✔
945
    val += atom_density_(i) * z;
25,821,532 ✔
946
  }
947
  return val;
12,973,026 ✔
948
}
949

950
void Material::set_id(int32_t id)
13,891 ✔
951
{
952
  assert(id >= 0 || id == C_NONE);
11,350 ✔
953

954
  // Clear entry in material map if an ID was already assigned before
955
  if (id_ != C_NONE) {
13,891 ✔
UNCOV
956
    model::material_map.erase(id_);
×
957
    id_ = C_NONE;
×
958
  }
959

960
  // Make sure no other material has same ID
961
  if (model::material_map.find(id) != model::material_map.end()) {
13,891 ✔
UNCOV
962
    throw std::runtime_error {
×
UNCOV
963
      "Two materials have the same ID: " + std::to_string(id)};
×
964
  }
965

966
  // If no ID specified, auto-assign next ID in sequence
967
  if (id == C_NONE) {
13,891 ✔
UNCOV
968
    id = 0;
×
UNCOV
969
    for (const auto& m : model::materials) {
×
UNCOV
970
      id = std::max(id, m->id_);
×
971
    }
UNCOV
972
    ++id;
×
973
  }
974

975
  // Update ID and entry in material map
976
  id_ = id;
13,891 ✔
977
  model::material_map[id] = index_;
13,891 ✔
978
}
13,891 ✔
979

980
void Material::set_density(double density, const std::string& units)
7,361 ✔
981
{
982
  assert(density >= 0.0);
6,043 ✔
983

984
  if (nuclide_.empty()) {
7,361 ✔
UNCOV
985
    throw std::runtime_error {"No nuclides exist in material yet."};
×
986
  }
987

988
  if (units == "atom/b-cm") {
7,361 ✔
989
    // Set total density based on value provided
990
    density_ = density;
7,317 ✔
991

992
    // Determine normalized atom percents
993
    double sum_percent = xt::sum(atom_density_)();
7,317 ✔
994
    atom_density_ /= sum_percent;
7,317 ✔
995

996
    // Recalculate nuclide atom densities based on given density
997
    atom_density_ *= density;
7,317 ✔
998

999
    // Calculate density in g/cm^3.
1000
    density_gpcc_ = 0.0;
7,317 ✔
1001
    for (int i = 0; i < nuclide_.size(); ++i) {
76,983 ✔
1002
      int i_nuc = nuclide_[i];
69,666 ✔
1003
      double awr = data::nuclides[i_nuc]->awr_;
69,666 ✔
1004
      density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
69,666 ✔
1005
    }
1006
  } else if (units == "g/cm3" || units == "g/cc") {
44 ✔
1007
    // Determine factor by which to change densities
1008
    double previous_density_gpcc = density_gpcc_;
33 ✔
1009
    double f = density / previous_density_gpcc;
33 ✔
1010

1011
    // Update densities
1012
    density_gpcc_ = density;
33 ✔
1013
    density_ *= f;
33 ✔
1014
    atom_density_ *= f;
33 ✔
1015
  } else {
1016
    throw std::invalid_argument {
11 ✔
1017
      "Invalid units '" + std::string(units.data()) + "' specified."};
22 ✔
1018
  }
1019
}
7,350 ✔
1020

1021
void Material::set_densities(
7,196 ✔
1022
  const vector<std::string>& name, const vector<double>& density)
1023
{
1024
  auto n = name.size();
7,196 ✔
1025
  assert(n > 0);
5,908 ✔
1026
  assert(n == density.size());
5,908 ✔
1027

1028
  if (n != nuclide_.size()) {
7,196 ✔
1029
    nuclide_.resize(n);
1,680 ✔
1030
    atom_density_ = xt::zeros<double>({n});
1,680 ✔
1031
    if (settings::photon_transport)
1,680 ✔
1032
      element_.resize(n);
×
1033
  }
1034

1035
  double sum_density = 0.0;
7,196 ✔
1036
  for (int64_t i = 0; i < n; ++i) {
76,323 ✔
1037
    const auto& nuc {name[i]};
69,127 ✔
1038
    if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) {
69,127 ✔
UNCOV
1039
      int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
×
UNCOV
1040
      if (err < 0)
×
UNCOV
1041
        throw std::runtime_error {openmc_err_msg};
×
1042
    }
1043

1044
    nuclide_[i] = data::nuclide_map.at(nuc);
69,127 ✔
1045
    assert(density[i] > 0.0);
56,715 ✔
1046
    atom_density_(i) = density[i];
69,127 ✔
1047
    sum_density += density[i];
69,127 ✔
1048

1049
    if (settings::photon_transport) {
69,127 ✔
UNCOV
1050
      auto element_name = to_element(nuc);
×
UNCOV
1051
      element_[i] = data::element_map.at(element_name);
×
1052
    }
1053
  }
1054

1055
  // Set total density to the sum of the vector
1056
  this->set_density(sum_density, "atom/b-cm");
7,196 ✔
1057

1058
  // Generate material bremsstrahlung data for electrons and positrons
1059
  if (settings::photon_transport &&
7,196 ✔
UNCOV
1060
      settings::electron_treatment == ElectronTreatment::TTB) {
×
UNCOV
1061
    this->init_bremsstrahlung();
×
1062
  }
1063

1064
  // Assign S(a,b) tables
1065
  this->init_thermal();
7,196 ✔
1066
}
7,196 ✔
1067

1068
double Material::volume() const
55 ✔
1069
{
1070
  if (volume_ < 0.0) {
55 ✔
1071
    throw std::runtime_error {
11 ✔
1072
      "Volume for material with ID=" + std::to_string(id_) + " not set."};
22 ✔
1073
  }
1074
  return volume_;
44 ✔
1075
}
1076

1077
double Material::temperature() const
17,620 ✔
1078
{
1079
  // If material doesn't have an assigned temperature, use global default
1080
  return temperature_ >= 0 ? temperature_ : settings::temperature_default;
17,620 ✔
1081
}
1082

1083
void Material::to_hdf5(hid_t group) const
10,855 ✔
1084
{
1085
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
10,855 ✔
1086

1087
  write_attribute(material_group, "depletable", static_cast<int>(depletable()));
10,855 ✔
1088
  if (volume_ > 0.0) {
10,855 ✔
1089
    write_attribute(material_group, "volume", volume_);
2,281 ✔
1090
  }
1091
  if (temperature_ > 0.0) {
10,855 ✔
1092
    write_attribute(material_group, "temperature", temperature_);
1,916 ✔
1093
  }
1094
  write_dataset(material_group, "name", name_);
10,855 ✔
1095
  write_dataset(material_group, "atom_density", density_);
10,855 ✔
1096

1097
  // Copy nuclide/macro name for each nuclide to vector
1098
  vector<std::string> nuc_names;
10,855 ✔
1099
  vector<std::string> macro_names;
10,855 ✔
1100
  vector<double> nuc_densities;
10,855 ✔
1101
  if (settings::run_CE) {
10,855 ✔
1102
    for (int i = 0; i < nuclide_.size(); ++i) {
51,150 ✔
1103
      int i_nuc = nuclide_[i];
41,648 ✔
1104
      nuc_names.push_back(data::nuclides[i_nuc]->name_);
41,648 ✔
1105
      nuc_densities.push_back(atom_density_(i));
41,648 ✔
1106
    }
1107
  } else {
1108
    for (int i = 0; i < nuclide_.size(); ++i) {
2,739 ✔
1109
      int i_nuc = nuclide_[i];
1,386 ✔
1110
      if (data::mg.nuclides_[i_nuc].awr != MACROSCOPIC_AWR) {
1,386 ✔
1111
        nuc_names.push_back(data::mg.nuclides_[i_nuc].name);
88 ✔
1112
        nuc_densities.push_back(atom_density_(i));
88 ✔
1113
      } else {
1114
        macro_names.push_back(data::mg.nuclides_[i_nuc].name);
1,298 ✔
1115
      }
1116
    }
1117
  }
1118

1119
  // Write vector to 'nuclides'
1120
  if (!nuc_names.empty()) {
10,855 ✔
1121
    write_dataset(material_group, "nuclides", nuc_names);
9,557 ✔
1122
    write_dataset(material_group, "nuclide_densities", nuc_densities);
9,557 ✔
1123
  }
1124

1125
  // Write vector to 'macroscopics'
1126
  if (!macro_names.empty()) {
10,855 ✔
1127
    write_dataset(material_group, "macroscopics", macro_names);
1,298 ✔
1128
  }
1129

1130
  if (!thermal_tables_.empty()) {
10,855 ✔
1131
    vector<std::string> sab_names;
1,338 ✔
1132
    for (const auto& table : thermal_tables_) {
2,742 ✔
1133
      sab_names.push_back(data::thermal_scatt[table.index_table]->name_);
1,404 ✔
1134
    }
1135
    write_dataset(material_group, "sab_names", sab_names);
1,338 ✔
1136
  }
1,338 ✔
1137

1138
  close_group(material_group);
10,855 ✔
1139
}
10,855 ✔
1140

1141
void Material::export_properties_hdf5(hid_t group) const
99 ✔
1142
{
1143
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
99 ✔
1144
  write_attribute(material_group, "atom_density", density_);
99 ✔
1145
  write_attribute(material_group, "mass_density", density_gpcc_);
99 ✔
1146
  close_group(material_group);
99 ✔
1147
}
99 ✔
1148

1149
void Material::import_properties_hdf5(hid_t group)
66 ✔
1150
{
1151
  hid_t material_group = open_group(group, "material " + std::to_string(id_));
66 ✔
1152
  double density;
1153
  read_attribute(material_group, "atom_density", density);
66 ✔
1154
  this->set_density(density, "atom/b-cm");
66 ✔
1155
  close_group(material_group);
66 ✔
1156
}
66 ✔
1157

1158
void Material::add_nuclide(const std::string& name, double density)
11 ✔
1159
{
1160
  // Check if nuclide is already in material
1161
  for (int i = 0; i < nuclide_.size(); ++i) {
55 ✔
1162
    int i_nuc = nuclide_[i];
44 ✔
1163
    if (data::nuclides[i_nuc]->name_ == name) {
44 ✔
UNCOV
1164
      double awr = data::nuclides[i_nuc]->awr_;
×
UNCOV
1165
      density_ += density - atom_density_(i);
×
UNCOV
1166
      density_gpcc_ +=
×
UNCOV
1167
        (density - atom_density_(i)) * awr * MASS_NEUTRON / N_AVOGADRO;
×
UNCOV
1168
      atom_density_(i) = density;
×
1169
      return;
×
1170
    }
1171
  }
1172

1173
  // If nuclide wasn't found, extend nuclide/density arrays
1174
  int err = openmc_load_nuclide(name.c_str(), nullptr, 0);
11 ✔
1175
  if (err < 0)
11 ✔
UNCOV
1176
    throw std::runtime_error {openmc_err_msg};
×
1177

1178
  // Append new nuclide/density
1179
  int i_nuc = data::nuclide_map[name];
11 ✔
1180
  nuclide_.push_back(i_nuc);
11 ✔
1181

1182
  // Append new element if photon transport is on
1183
  if (settings::photon_transport) {
11 ✔
UNCOV
1184
    int i_elem = data::element_map[to_element(name)];
×
UNCOV
1185
    element_.push_back(i_elem);
×
1186
  }
1187

1188
  auto n = nuclide_.size();
11 ✔
1189

1190
  // Create copy of atom_density_ array with one extra entry
1191
  xt::xtensor<double, 1> atom_density = xt::zeros<double>({n});
11 ✔
1192
  xt::view(atom_density, xt::range(0, n - 1)) = atom_density_;
11 ✔
1193
  atom_density(n - 1) = density;
11 ✔
1194
  atom_density_ = atom_density;
11 ✔
1195

1196
  density_ += density;
11 ✔
1197
  density_gpcc_ +=
11 ✔
1198
    density * data::nuclides[i_nuc]->awr_ * MASS_NEUTRON / N_AVOGADRO;
11 ✔
1199
}
11 ✔
1200

1201
//==============================================================================
1202
// Non-method functions
1203
//==============================================================================
1204

1205
double sternheimer_adjustment(const vector<double>& f,
528 ✔
1206
  const vector<double>& e_b_sq, double e_p_sq, double n_conduction,
1207
  double log_I, double tol, int max_iter)
1208
{
1209
  // Get the total number of oscillators
1210
  int n = f.size();
528 ✔
1211

1212
  // Calculate the Sternheimer adjustment factor using Newton's method
1213
  double rho = 2.0;
528 ✔
1214
  int iter;
1215
  for (iter = 0; iter < max_iter; ++iter) {
2,154 ✔
1216
    double rho_0 = rho;
2,154 ✔
1217

1218
    // Function to find the root of and its derivative
1219
    double g = 0.0;
2,154 ✔
1220
    double gp = 0.0;
2,154 ✔
1221

1222
    for (int i = 0; i < n; ++i) {
61,374 ✔
1223
      // Square of resonance energy of a bound-shell oscillator
1224
      double e_r_sq = e_b_sq[i] * rho * rho + 2.0 / 3.0 * f[i] * e_p_sq;
59,220 ✔
1225
      g += f[i] * std::log(e_r_sq);
59,220 ✔
1226
      gp += e_b_sq[i] * f[i] * rho / e_r_sq;
59,220 ✔
1227
    }
1228
    // Include conduction electrons
1229
    if (n_conduction > 0.0) {
2,154 ✔
1230
      g += n_conduction * std::log(n_conduction * e_p_sq);
1,872 ✔
1231
    }
1232

1233
    // Set the next guess: rho_n+1 = rho_n - g(rho_n)/g'(rho_n)
1234
    rho -= (g - 2.0 * log_I) / (2.0 * gp);
2,154 ✔
1235

1236
    // If the initial guess is too large, rho can be negative
1237
    if (rho < 0.0)
2,154 ✔
UNCOV
1238
      rho = rho_0 / 2.0;
×
1239

1240
    // Check for convergence
1241
    if (std::abs(rho - rho_0) / rho_0 < tol)
2,154 ✔
1242
      break;
528 ✔
1243
  }
1244
  // Did not converge
1245
  if (iter >= max_iter) {
528 ✔
UNCOV
1246
    warning("Maximum Newton-Raphson iterations exceeded.");
×
UNCOV
1247
    rho = 1.0e-6;
×
1248
  }
1249
  return rho;
528 ✔
1250
}
1251

1252
double density_effect(const vector<double>& f, const vector<double>& e_b_sq,
105,314 ✔
1253
  double e_p_sq, double n_conduction, double rho, double E, double tol,
1254
  int max_iter)
1255
{
1256
  // Get the total number of oscillators
1257
  int n = f.size();
105,314 ✔
1258

1259
  // Square of the ratio of the speed of light to the velocity of the charged
1260
  // particle
1261
  double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
105,314 ✔
1262
                   ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
105,314 ✔
1263

1264
  // For nonmetals, delta = 0 for beta < beta_0, where beta_0 is obtained by
1265
  // setting the frequency w = 0.
1266
  double beta_0_sq = 0.0;
105,314 ✔
1267
  if (n_conduction == 0.0) {
105,314 ✔
1268
    for (int i = 0; i < n; ++i) {
110,400 ✔
1269
      beta_0_sq += f[i] * e_p_sq / (e_b_sq[i] * rho * rho);
95,200 ✔
1270
    }
1271
    beta_0_sq = 1.0 / (1.0 + beta_0_sq);
15,200 ✔
1272
  }
1273
  double delta = 0.0;
105,314 ✔
1274
  if (beta_sq < beta_0_sq)
105,314 ✔
1275
    return delta;
8,278 ✔
1276

1277
  // Compute the square of the frequency w^2 using Newton's method, with the
1278
  // initial guess of w^2 equal to beta^2 * gamma^2
1279
  double w_sq = E / MASS_ELECTRON_EV * (E / MASS_ELECTRON_EV + 2);
97,036 ✔
1280
  int iter;
1281
  for (iter = 0; iter < max_iter; ++iter) {
677,596 ✔
1282
    double w_sq_0 = w_sq;
677,596 ✔
1283

1284
    // Function to find the root of and its derivative
1285
    double g = 0.0;
677,596 ✔
1286
    double gp = 0.0;
677,596 ✔
1287

1288
    for (int i = 0; i < n; ++i) {
21,791,832 ✔
1289
      double c = e_b_sq[i] * rho * rho / e_p_sq + w_sq;
21,114,236 ✔
1290
      g += f[i] / c;
21,114,236 ✔
1291
      gp -= f[i] / (c * c);
21,114,236 ✔
1292
    }
1293
    // Include conduction electrons
1294
    g += n_conduction / w_sq;
677,596 ✔
1295
    gp -= n_conduction / (w_sq * w_sq);
677,596 ✔
1296

1297
    // Set the next guess: w_n+1 = w_n - g(w_n)/g'(w_n)
1298
    w_sq -= (g + 1.0 - 1.0 / beta_sq) / gp;
677,596 ✔
1299

1300
    // If the initial guess is too large, w can be negative
1301
    if (w_sq < 0.0)
677,596 ✔
1302
      w_sq = w_sq_0 / 2.0;
152,876 ✔
1303

1304
    // Check for convergence
1305
    if (std::abs(w_sq - w_sq_0) / w_sq_0 < tol)
677,596 ✔
1306
      break;
97,036 ✔
1307
  }
1308
  // Did not converge
1309
  if (iter >= max_iter) {
97,036 ✔
UNCOV
1310
    warning("Maximum Newton-Raphson iterations exceeded: setting density "
×
1311
            "effect correction to zero.");
UNCOV
1312
    return delta;
×
1313
  }
1314

1315
  // Solve for the density effect correction
1316
  for (int i = 0; i < n; ++i) {
2,940,832 ✔
1317
    double l_sq = e_b_sq[i] * rho * rho / e_p_sq + 2.0 / 3.0 * f[i];
2,843,796 ✔
1318
    delta += f[i] * std::log((l_sq + w_sq) / l_sq);
2,843,796 ✔
1319
  }
1320
  // Include conduction electrons
1321
  if (n_conduction > 0.0) {
97,036 ✔
1322
    delta += n_conduction * std::log((n_conduction + w_sq) / n_conduction);
90,114 ✔
1323
  }
1324

1325
  return delta - w_sq * (1.0 - beta_sq);
97,036 ✔
1326
}
1327

1328
void read_materials_xml()
1,366 ✔
1329
{
1330
  write_message("Reading materials XML file...", 5);
1,366 ✔
1331

1332
  pugi::xml_document doc;
1,366 ✔
1333

1334
  // Check if materials.xml exists
1335
  std::string filename = settings::path_input + "materials.xml";
1,366 ✔
1336
  if (!file_exists(filename)) {
1,366 ✔
UNCOV
1337
    fatal_error("Material XML file '" + filename + "' does not exist!");
×
1338
  }
1339

1340
  // Parse materials.xml file and get root element
1341
  doc.load_file(filename.c_str());
1,366 ✔
1342

1343
  // Loop over XML material elements and populate the array.
1344
  pugi::xml_node root = doc.document_element();
1,366 ✔
1345

1346
  read_materials_xml(root);
1,366 ✔
1347
}
1,366 ✔
1348

1349
void read_materials_xml(pugi::xml_node root)
6,648 ✔
1350
{
1351
  for (pugi::xml_node material_node : root.children("material")) {
20,487 ✔
1352
    model::materials.push_back(make_unique<Material>(material_node));
13,839 ✔
1353
  }
1354
  model::materials.shrink_to_fit();
6,648 ✔
1355
}
6,648 ✔
1356

1357
void free_memory_material()
6,751 ✔
1358
{
1359
  model::materials.clear();
6,751 ✔
1360
  model::material_map.clear();
6,751 ✔
1361
}
6,751 ✔
1362

1363
//==============================================================================
1364
// C API
1365
//==============================================================================
1366

1367
extern "C" int openmc_get_material_index(int32_t id, int32_t* index)
9,534 ✔
1368
{
1369
  auto it = model::material_map.find(id);
9,534 ✔
1370
  if (it == model::material_map.end()) {
9,534 ✔
1371
    set_errmsg("No material exists with ID=" + std::to_string(id) + ".");
11 ✔
1372
    return OPENMC_E_INVALID_ID;
11 ✔
1373
  } else {
1374
    *index = it->second;
9,523 ✔
1375
    return 0;
9,523 ✔
1376
  }
1377
}
1378

1379
extern "C" int openmc_material_add_nuclide(
11 ✔
1380
  int32_t index, const char* name, double density)
1381
{
1382
  int err = 0;
11 ✔
1383
  if (index >= 0 && index < model::materials.size()) {
11 ✔
1384
    try {
1385
      model::materials[index]->add_nuclide(name, density);
11 ✔
UNCOV
1386
    } catch (const std::runtime_error& e) {
×
UNCOV
1387
      return OPENMC_E_DATA;
×
UNCOV
1388
    }
×
1389
  } else {
UNCOV
1390
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1391
    return OPENMC_E_OUT_OF_BOUNDS;
×
1392
  }
1393
  return err;
11 ✔
1394
}
1395

1396
extern "C" int openmc_material_get_densities(
165 ✔
1397
  int32_t index, const int** nuclides, const double** densities, int* n)
1398
{
1399
  if (index >= 0 && index < model::materials.size()) {
165 ✔
1400
    auto& mat = model::materials[index];
165 ✔
1401
    if (!mat->nuclides().empty()) {
165 ✔
1402
      *nuclides = mat->nuclides().data();
165 ✔
1403
      *densities = mat->densities().data();
165 ✔
1404
      *n = mat->nuclides().size();
165 ✔
1405
      return 0;
165 ✔
1406
    } else {
UNCOV
1407
      set_errmsg("Material atom density array has not been allocated.");
×
UNCOV
1408
      return OPENMC_E_ALLOCATE;
×
1409
    }
1410
  } else {
UNCOV
1411
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1412
    return OPENMC_E_OUT_OF_BOUNDS;
×
1413
  }
1414
}
1415

1416
extern "C" int openmc_material_get_density(int32_t index, double* density)
33 ✔
1417
{
1418
  if (index >= 0 && index < model::materials.size()) {
33 ✔
1419
    auto& mat = model::materials[index];
33 ✔
1420
    *density = mat->density_gpcc();
33 ✔
1421
    return 0;
33 ✔
1422
  } else {
1423
    set_errmsg("Index in materials array is out of bounds.");
×
1424
    return OPENMC_E_OUT_OF_BOUNDS;
×
1425
  }
1426
}
1427

1428
extern "C" int openmc_material_get_fissionable(int32_t index, bool* fissionable)
×
1429
{
UNCOV
1430
  if (index >= 0 && index < model::materials.size()) {
×
UNCOV
1431
    *fissionable = model::materials[index]->fissionable();
×
UNCOV
1432
    return 0;
×
1433
  } else {
UNCOV
1434
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1435
    return OPENMC_E_OUT_OF_BOUNDS;
×
1436
  }
1437
}
1438

1439
extern "C" int openmc_material_get_id(int32_t index, int32_t* id)
10,610 ✔
1440
{
1441
  if (index >= 0 && index < model::materials.size()) {
10,610 ✔
1442
    *id = model::materials[index]->id();
10,610 ✔
1443
    return 0;
10,610 ✔
1444
  } else {
UNCOV
1445
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1446
    return OPENMC_E_OUT_OF_BOUNDS;
×
1447
  }
1448
}
1449

1450
extern "C" int openmc_material_get_temperature(
66 ✔
1451
  int32_t index, double* temperature)
1452
{
1453
  if (index < 0 || index >= model::materials.size()) {
66 ✔
UNCOV
1454
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1455
    return OPENMC_E_OUT_OF_BOUNDS;
×
1456
  }
1457
  *temperature = model::materials[index]->temperature();
66 ✔
1458
  return 0;
66 ✔
1459
}
1460

1461
extern "C" int openmc_material_get_volume(int32_t index, double* volume)
55 ✔
1462
{
1463
  if (index >= 0 && index < model::materials.size()) {
55 ✔
1464
    try {
1465
      *volume = model::materials[index]->volume();
55 ✔
1466
    } catch (const std::exception& e) {
11 ✔
1467
      set_errmsg(e.what());
11 ✔
1468
      return OPENMC_E_UNASSIGNED;
11 ✔
1469
    }
11 ✔
1470
    return 0;
44 ✔
1471
  } else {
UNCOV
1472
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1473
    return OPENMC_E_OUT_OF_BOUNDS;
×
1474
  }
1475
}
1476

1477
extern "C" int openmc_material_set_density(
99 ✔
1478
  int32_t index, double density, const char* units)
1479
{
1480
  if (index >= 0 && index < model::materials.size()) {
99 ✔
1481
    try {
1482
      model::materials[index]->set_density(density, units);
121 ✔
1483
    } catch (const std::exception& e) {
11 ✔
1484
      set_errmsg(e.what());
11 ✔
1485
      return OPENMC_E_UNASSIGNED;
11 ✔
1486
    }
11 ✔
1487
  } else {
UNCOV
1488
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1489
    return OPENMC_E_OUT_OF_BOUNDS;
×
1490
  }
1491
  return 0;
88 ✔
1492
}
1493

1494
extern "C" int openmc_material_set_densities(
7,196 ✔
1495
  int32_t index, int n, const char** name, const double* density)
1496
{
1497
  if (index >= 0 && index < model::materials.size()) {
7,196 ✔
1498
    try {
1499
      model::materials[index]->set_densities(
21,588 ✔
1500
        {name, name + n}, {density, density + n});
14,392 ✔
UNCOV
1501
    } catch (const std::exception& e) {
×
UNCOV
1502
      set_errmsg(e.what());
×
UNCOV
1503
      return OPENMC_E_UNASSIGNED;
×
UNCOV
1504
    }
×
1505
  } else {
UNCOV
1506
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1507
    return OPENMC_E_OUT_OF_BOUNDS;
×
1508
  }
1509
  return 0;
7,196 ✔
1510
}
1511

1512
extern "C" int openmc_material_set_id(int32_t index, int32_t id)
44 ✔
1513
{
1514
  if (index >= 0 && index < model::materials.size()) {
44 ✔
1515
    try {
1516
      model::materials.at(index)->set_id(id);
44 ✔
UNCOV
1517
    } catch (const std::exception& e) {
×
UNCOV
1518
      set_errmsg(e.what());
×
UNCOV
1519
      return OPENMC_E_UNASSIGNED;
×
UNCOV
1520
    }
×
1521
  } else {
UNCOV
1522
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1523
    return OPENMC_E_OUT_OF_BOUNDS;
×
1524
  }
1525
  return 0;
44 ✔
1526
}
1527

1528
extern "C" int openmc_material_get_name(int32_t index, const char** name)
33 ✔
1529
{
1530
  if (index < 0 || index >= model::materials.size()) {
33 ✔
UNCOV
1531
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1532
    return OPENMC_E_OUT_OF_BOUNDS;
×
1533
  }
1534

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

1537
  return 0;
33 ✔
1538
}
1539

1540
extern "C" int openmc_material_set_name(int32_t index, const char* name)
11 ✔
1541
{
1542
  if (index < 0 || index >= model::materials.size()) {
11 ✔
UNCOV
1543
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1544
    return OPENMC_E_OUT_OF_BOUNDS;
×
1545
  }
1546

1547
  model::materials[index]->set_name(name);
11 ✔
1548

1549
  return 0;
11 ✔
1550
}
1551

1552
extern "C" int openmc_material_set_volume(int32_t index, double volume)
47 ✔
1553
{
1554
  if (index >= 0 && index < model::materials.size()) {
47 ✔
1555
    auto& m {model::materials[index]};
47 ✔
1556
    if (volume >= 0.0) {
47 ✔
1557
      m->volume_ = volume;
47 ✔
1558
      return 0;
47 ✔
1559
    } else {
UNCOV
1560
      set_errmsg("Volume must be non-negative");
×
UNCOV
1561
      return OPENMC_E_INVALID_ARGUMENT;
×
1562
    }
1563
  } else {
UNCOV
1564
    set_errmsg("Index in materials array is out of bounds.");
×
1565
    return OPENMC_E_OUT_OF_BOUNDS;
×
1566
  }
1567
}
1568

1569
extern "C" int openmc_material_get_depletable(int32_t index, bool* depletable)
22 ✔
1570
{
1571
  if (index < 0 || index >= model::materials.size()) {
22 ✔
UNCOV
1572
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1573
    return OPENMC_E_OUT_OF_BOUNDS;
×
1574
  }
1575

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

1578
  return 0;
22 ✔
1579
}
1580

1581
extern "C" int openmc_material_set_depletable(int32_t index, bool depletable)
11 ✔
1582
{
1583
  if (index < 0 || index >= model::materials.size()) {
11 ✔
UNCOV
1584
    set_errmsg("Index in materials array is out of bounds.");
×
UNCOV
1585
    return OPENMC_E_OUT_OF_BOUNDS;
×
1586
  }
1587

1588
  model::materials[index]->depletable() = depletable;
11 ✔
1589

1590
  return 0;
11 ✔
1591
}
1592

1593
extern "C" int openmc_extend_materials(
44 ✔
1594
  int32_t n, int32_t* index_start, int32_t* index_end)
1595
{
1596
  if (index_start)
44 ✔
1597
    *index_start = model::materials.size();
44 ✔
1598
  if (index_end)
44 ✔
UNCOV
1599
    *index_end = model::materials.size() + n - 1;
×
1600
  for (int32_t i = 0; i < n; i++) {
88 ✔
1601
    model::materials.push_back(make_unique<Material>());
44 ✔
1602
  }
1603
  return 0;
44 ✔
1604
}
1605

1606
extern "C" size_t n_materials()
66 ✔
1607
{
1608
  return model::materials.size();
66 ✔
1609
}
1610

1611
} // namespace openmc
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc