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

openmc-dev / openmc / 27444237628

12 Jun 2026 09:31PM UTC coverage: 81.304% (+0.02%) from 81.281%
27444237628

Pull #3971

github

web-flow
Merge 9487507b3 into 02eb999af
Pull Request #3971: Delta tracking

18480 of 26797 branches covered (68.96%)

Branch coverage included in aggregate %.

592 of 644 new or added lines in 20 files covered. (91.93%)

31 existing lines in 3 files now uncovered.

59819 of 69507 relevant lines covered (86.06%)

49514105.74 hits per line

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

74.37
/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 "openmc/tensor.h"
12

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

31
namespace openmc {
32

33
//==============================================================================
34
// Global variables
35
//==============================================================================
36

37
namespace model {
38

39
std::unordered_map<int32_t, int32_t> material_map;
40
vector<unique_ptr<Material>> materials;
41

42
} // namespace model
43

44
//==============================================================================
45
// Material implementation
46
//==============================================================================
47

48
Material::Material(pugi::xml_node node)
16,841✔
49
{
50
  index_ = model::materials.size(); // Avoids warning about narrowing
16,841✔
51

52
  if (check_for_node(node, "id")) {
16,841!
53
    this->set_id(std::stoi(get_node_value(node, "id")));
33,682✔
54
  } else {
55
    fatal_error("Must specify id of material in materials XML file.");
×
56
  }
57

58
  if (check_for_node(node, "name")) {
16,841✔
59
    name_ = get_node_value(node, "name");
8,892✔
60
  }
61

62
  if (check_for_node(node, "cfg")) {
16,841✔
63
    if (settings::delta_tracking) {
15!
NEW
64
      fatal_error("NCrystal materials are presently not supported when running "
×
65
                  "with delta tracking!");
66
    }
67
    auto cfg = get_node_value(node, "cfg");
15✔
68
    write_message(
15✔
69
      5, "NCrystal config string for material #{}: '{}'", this->id(), cfg);
15✔
70
    ncrystal_mat_ = NCrystalMat(cfg);
30✔
71
  }
15✔
72

73
  if (check_for_node(node, "depletable")) {
16,841✔
74
    depletable_ = get_node_value_bool(node, "depletable");
5,890✔
75
  }
76

77
  bool sum_density {false};
16,841✔
78
  pugi::xml_node density_node = node.child("density");
16,841✔
79
  std::string units;
16,841✔
80
  if (density_node) {
16,841!
81
    units = get_node_value(density_node, "units");
16,841✔
82
    if (units == "sum") {
16,841✔
83
      sum_density = true;
84
    } else if (units == "macro") {
14,421✔
85
      if (check_for_node(density_node, "value")) {
2,837!
86
        density_ = std::stod(get_node_value(density_node, "value"));
5,674✔
87
      } else {
88
        density_ = 1.0;
×
89
      }
90
    } else {
91
      double val = std::stod(get_node_value(density_node, "value"));
23,168✔
92
      if (val <= 0.0) {
11,584!
93
        fatal_error("Need to specify a positive density on material " +
×
94
                    std::to_string(id_) + ".");
×
95
      }
96

97
      if (units == "g/cc" || units == "g/cm3") {
19,689✔
98
        density_ = -val;
11,224✔
99
      } else if (units == "kg/m3") {
360✔
100
        density_ = -1.0e-3 * val;
15✔
101
      } else if (units == "atom/b-cm") {
345✔
102
        density_ = val;
330✔
103
      } else if (units == "atom/cc" || units == "atom/cm3") {
30!
104
        density_ = 1.0e-24 * val;
15✔
105
      } else {
106
        fatal_error("Unknown units '" + units + "' specified on material " +
×
107
                    std::to_string(id_) + ".");
×
108
      }
109
    }
110
  } else {
111
    fatal_error("Must specify <density> element in material " +
×
112
                std::to_string(id_) + ".");
×
113
  }
114

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

123
  // =======================================================================
124
  // READ AND PARSE <nuclide> TAGS
125

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

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

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

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

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

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

176
      // store nuclide name
177
      std::string name = get_node_value(node_nuc, "name", false, true);
53,615✔
178
      names.push_back(name);
53,615✔
179

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

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

197
        // Copy atom/weight percents
198
        if (has_ao) {
53,615✔
199
          densities.push_back(std::stod(get_node_value(node_nuc, "ao")));
91,234✔
200
        } else {
201
          densities.push_back(-std::stod(get_node_value(node_nuc, "wo")));
15,996✔
202
        }
203
      }
204
    }
53,615✔
205
  }
206

207
  // =======================================================================
208
  // READ AND PARSE <isotropic> element
209

210
  vector<std::string> iso_lab;
16,841✔
211
  if (check_for_node(node, "isotropic")) {
16,841✔
212
    iso_lab = get_node_array<std::string>(node, "isotropic");
360✔
213
  }
214

215
  // ========================================================================
216
  // COPY NUCLIDES TO ARRAYS IN MATERIAL
217

218
  // allocate arrays in Material object
219
  auto n = names.size();
16,841✔
220
  nuclide_.reserve(n);
16,841✔
221
  atom_density_ = tensor::Tensor<double>({n});
16,841✔
222
  if (settings::photon_transport)
16,841✔
223
    element_.reserve(n);
613✔
224

225
  for (int i = 0; i < n; ++i) {
73,293✔
226
    const auto& name {names[i]};
56,452✔
227

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

239
    // If this nuclide hasn't been encountered yet, we need to add its name
240
    // and alias to the nuclide_dict
241
    if (data::nuclide_map.find(name) == data::nuclide_map.end()) {
56,452✔
242
      int index = data::nuclide_map.size();
35,364✔
243
      data::nuclide_map[name] = index;
35,364✔
244
      nuclide_.push_back(index);
35,364✔
245
    } else {
246
      nuclide_.push_back(data::nuclide_map[name]);
21,088✔
247
    }
248

249
    // If the corresponding element hasn't been encountered yet and photon
250
    // transport will be used, we need to add its symbol to the element_dict
251
    if (settings::photon_transport) {
56,452✔
252
      std::string element = to_element(name);
2,493✔
253

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

263
      if (data::element_map.find(element) == data::element_map.end()) {
2,493✔
264
        int index = data::element_map.size();
1,358✔
265
        data::element_map[element] = index;
1,358✔
266
        element_.push_back(index);
1,358✔
267
      } else {
268
        element_.push_back(data::element_map[element]);
1,135✔
269
      }
270
    }
2,493✔
271

272
    // Copy atom/weight percent
273
    atom_density_(i) = densities[i];
56,452✔
274
  }
275

276
  if (settings::run_CE) {
16,841✔
277
    // By default, isotropic-in-lab is not used
278
    if (iso_lab.size() > 0) {
13,809✔
279
      p0_.resize(n);
180✔
280

281
      // Apply isotropic-in-lab treatment to specified nuclides
282
      for (int j = 0; j < n; ++j) {
1,695✔
283
        for (const auto& nuc : iso_lab) {
7,875!
284
          if (names[j] == nuc) {
7,875✔
285
            p0_[j] = true;
1,515✔
286
            break;
1,515✔
287
          }
288
        }
289
      }
290
    }
291
  }
292

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

300
  // Determine density if it is a sum value
301
  if (sum_density)
16,841✔
302
    density_ = atom_density_.sum();
2,420✔
303

304
  if (check_for_node(node, "temperature")) {
16,841✔
305
    temperature_ = std::stod(get_node_value(node, "temperature"));
3,714✔
306
  }
307

308
  if (check_for_node(node, "volume")) {
16,841✔
309
    volume_ = std::stod(get_node_value(node, "volume"));
3,940✔
310
  }
311

312
  // =======================================================================
313
  // READ AND PARSE <sab> TAG FOR THERMAL SCATTERING DATA
314
  if (settings::run_CE) {
16,841✔
315
    // Loop over <sab> elements
316

317
    vector<std::string> sab_names;
13,809✔
318
    for (auto node_sab : node.children("sab")) {
16,161✔
319
      // Determine name of thermal scattering table
320
      if (!check_for_node(node_sab, "name")) {
2,352!
321
        fatal_error("Need to specify <name> for thermal scattering table.");
×
322
      }
323
      std::string name = get_node_value(node_sab, "name");
2,352✔
324
      sab_names.push_back(name);
2,352✔
325

326
      // Read the fraction of nuclei affected by this thermal scattering table
327
      double fraction = 1.0;
2,352✔
328
      if (check_for_node(node_sab, "fraction")) {
2,352✔
329
        fraction = std::stod(get_node_value(node_sab, "fraction"));
30✔
330
      }
331

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

342
      // Determine index of thermal scattering data in global
343
      // data::thermal_scatt array
344
      int index_table;
2,352✔
345
      if (data::thermal_scatt_map.find(name) == data::thermal_scatt_map.end()) {
2,352✔
346
        index_table = data::thermal_scatt_map.size();
1,641✔
347
        data::thermal_scatt_map[name] = index_table;
1,641✔
348
      } else {
349
        index_table = data::thermal_scatt_map[name];
711✔
350
      }
351

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

360
Material::~Material()
16,885✔
361
{
362
  model::material_map.erase(id_);
16,885✔
363
}
50,655✔
364

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

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

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

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

394
void Material::finalize()
16,552✔
395
{
396
  // Set fissionable if any nuclide is fissionable
397
  if (settings::run_CE) {
16,552✔
398
    for (const auto& i_nuc : nuclide_) {
47,849✔
399
      if (data::nuclides[i_nuc]->fissionable_) {
40,246✔
400
        fissionable_ = true;
5,917✔
401
        break;
5,917✔
402
      }
403
    }
404

405
    // Generate material bremsstrahlung data for electrons and positrons
406
    if (settings::photon_transport &&
13,520✔
407
        settings::electron_treatment == ElectronTreatment::TTB) {
613✔
408
      this->init_bremsstrahlung();
510✔
409
    }
410

411
    // Assign thermal scattering tables
412
    this->init_thermal();
13,520✔
413
  }
414

415
  // Normalize density
416
  this->normalize_density();
16,552✔
417
}
16,552✔
418

419
void Material::normalize_density()
16,552✔
420
{
421
  bool percent_in_atom = (atom_density_(0) >= 0.0);
16,552✔
422
  bool density_in_atom = (density_ >= 0.0);
16,552✔
423

424
  for (int i = 0; i < nuclide_.size(); ++i) {
72,142✔
425
    // determine atomic weight ratio
426
    int i_nuc = nuclide_[i];
55,590✔
427
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
55,590✔
428
                                  : data::mg.nuclides_[i_nuc].awr;
3,392✔
429

430
    // if given weight percent, convert all values so that they are divided
431
    // by awr. thus, when a sum is done over the values, it's actually
432
    // sum(w/awr)
433
    if (!percent_in_atom)
55,590✔
434
      atom_density_(i) = -atom_density_(i) / awr;
7,998✔
435
  }
436

437
  // determine normalized atom percents. if given atom percents, this is
438
  // straightforward. if given weight percents, the value is w/awr and is
439
  // divided by sum(w/awr)
440
  atom_density_ /= atom_density_.sum();
16,552✔
441

442
  // Change density in g/cm^3 to atom/b-cm. Since all values are now in
443
  // atom percent, the sum needs to be re-evaluated as 1/sum(x*awr)
444
  if (!density_in_atom) {
16,552✔
445
    double sum_percent = 0.0;
446
    for (int i = 0; i < nuclide_.size(); ++i) {
48,213✔
447
      int i_nuc = nuclide_[i];
37,267✔
448
      double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
37,267✔
449
                                    : data::mg.nuclides_[i_nuc].awr;
75✔
450
      sum_percent += atom_density_(i) * awr;
37,267✔
451
    }
452
    sum_percent = 1.0 / sum_percent;
10,946✔
453
    density_ = -density_ * N_AVOGADRO / MASS_NEUTRON * sum_percent;
10,946✔
454
  }
455

456
  // Calculate nuclide atom densities
457
  atom_density_ *= density_;
16,552✔
458

459
  // Calculate density in [g/cm^3] and charge density in [e/b-cm]
460
  density_gpcc_ = 0.0;
16,552✔
461
  charge_density_ = 0.0;
16,552✔
462
  for (int i = 0; i < nuclide_.size(); ++i) {
72,142✔
463
    int i_nuc = nuclide_[i];
55,590✔
464
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_ : 1.0;
55,590✔
465
    int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0;
55,590✔
466
    density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
55,590✔
467
    charge_density_ += atom_density_(i) * z;
55,590✔
468
  }
469
}
16,552✔
470

471
void Material::init_thermal()
19,716✔
472
{
473
  vector<ThermalTable> tables;
19,716✔
474

475
  std::unordered_set<int> already_checked;
19,716✔
476
  for (const auto& table : thermal_tables_) {
22,027✔
477
    // Make sure each S(a,b) table only gets checked once
478
    if (already_checked.find(table.index_table) != already_checked.end()) {
2,311!
479
      continue;
×
480
    }
481
    already_checked.insert(table.index_table);
2,311✔
482

483
    // In order to know which nuclide the S(a,b) table applies to, we need
484
    // to search through the list of nuclides for one which has a matching
485
    // name
486
    bool found = false;
487
    for (int j = 0; j < nuclide_.size(); ++j) {
13,939✔
488
      const auto& name {data::nuclides[nuclide_[j]]->name_};
11,628✔
489
      if (contains(data::thermal_scatt[table.index_table]->nuclides_, name)) {
11,628✔
490
        tables.push_back({table.index_table, j, table.fraction});
2,371✔
491
        found = true;
2,371✔
492
      }
493
    }
494

495
    // Check to make sure thermal scattering table matched a nuclide
496
    if (!found) {
2,311!
497
      fatal_error("Thermal scattering table " +
×
498
                  data::thermal_scatt[table.index_table]->name_ +
×
499
                  " did not match any nuclide on material " +
×
500
                  std::to_string(id_));
×
501
    }
502
  }
503

504
  // Make sure each nuclide only appears in one table.
505
  for (int j = 0; j < tables.size(); ++j) {
22,087✔
506
    for (int k = j + 1; k < tables.size(); ++k) {
2,611✔
507
      if (tables[j].index_nuclide == tables[k].index_nuclide) {
240!
508
        int index = nuclide_[tables[j].index_nuclide];
×
509
        auto name = data::nuclides[index]->name_;
×
510
        fatal_error(
×
511
          name + " in material " + std::to_string(id_) +
×
512
          " was found "
513
          "in multiple thermal scattering tables. Each nuclide can appear in "
514
          "only one table per material.");
515
      }
×
516
    }
517
  }
518

519
  // If there are multiple S(a,b) tables, we need to make sure that the
520
  // entries in i_sab_nuclides are sorted or else they won't be applied
521
  // correctly in the cross_section module.
522
  std::sort(tables.begin(), tables.end(), [](ThermalTable a, ThermalTable b) {
19,716✔
523
    return a.index_nuclide < b.index_nuclide;
165!
524
  });
525

526
  // Update the list of thermal tables
527
  thermal_tables_ = tables;
19,716✔
528
}
19,716✔
529

530
void Material::collision_stopping_power(double* s_col, bool positron)
1,020✔
531
{
532
  // Average electron number and average atomic weight
533
  double electron_density = 0.0;
1,020✔
534
  double mass_density = 0.0;
1,020✔
535

536
  // Log of the mean excitation energy of the material
537
  double log_I = 0.0;
1,020✔
538

539
  // Effective number of conduction electrons in the material
540
  double n_conduction = 0.0;
1,020✔
541

542
  // Oscillator strength and square of the binding energy for each oscillator
543
  // in material
544
  vector<double> f;
1,020✔
545
  vector<double> e_b_sq;
1,020✔
546

547
  for (int i = 0; i < element_.size(); ++i) {
5,052✔
548
    const auto& elm = *data::elements[element_[i]];
4,032!
549
    double awr = data::nuclides[nuclide_[i]]->awr_;
4,032!
550

551
    // Get atomic density of nuclide given atom/weight percent
552
    double atom_density =
4,032✔
553
      (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
4,032!
554

555
    electron_density += atom_density * elm.Z_;
4,032✔
556
    mass_density += atom_density * awr * MASS_NEUTRON;
4,032✔
557
    log_I += atom_density * elm.Z_ * std::log(elm.I_);
4,032✔
558

559
    for (int j = 0; j < elm.n_electrons_.size(); ++j) {
31,934✔
560
      if (elm.n_electrons_[j] < 0) {
27,902✔
561
        n_conduction -= elm.n_electrons_[j] * atom_density;
2,642✔
562
        continue;
2,642✔
563
      }
564
      e_b_sq.push_back(elm.ionization_energy_[j] * elm.ionization_energy_[j]);
25,260✔
565
      f.push_back(elm.n_electrons_[j] * atom_density);
25,260✔
566
    }
567
  }
568
  log_I /= electron_density;
1,020✔
569
  n_conduction /= electron_density;
1,020✔
570
  for (auto& f_i : f)
26,280✔
571
    f_i /= electron_density;
25,260✔
572

573
  // Get density in g/cm^3 if it is given in atom/b-cm
574
  double density = (density_ < 0.0) ? -density_ : mass_density / N_AVOGADRO;
1,020✔
575

576
  // Calculate the square of the plasma energy
577
  double e_p_sq =
1,020✔
578
    PLANCK_C * PLANCK_C * PLANCK_C * N_AVOGADRO * electron_density * density /
1,020✔
579
    (2.0 * PI * PI * FINE_STRUCTURE * MASS_ELECTRON_EV * mass_density);
1,020✔
580

581
  // Get the Sternheimer adjustment factor
582
  double rho =
1,020✔
583
    sternheimer_adjustment(f, e_b_sq, e_p_sq, n_conduction, log_I, 1.0e-6, 100);
1,020✔
584

585
  // Classical electron radius in cm
586
  constexpr double CM_PER_ANGSTROM {1.0e-8};
1,020✔
587
  constexpr double r_e =
1,020✔
588
    CM_PER_ANGSTROM * PLANCK_C / (2.0 * PI * FINE_STRUCTURE * MASS_ELECTRON_EV);
589

590
  // Constant in expression for collision stopping power
591
  constexpr double BARN_PER_CM_SQ {1.0e24};
1,020✔
592
  double c =
1,020✔
593
    BARN_PER_CM_SQ * 2.0 * PI * r_e * r_e * MASS_ELECTRON_EV * electron_density;
594

595
  // Loop over incident charged particle energies
596
  for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
204,604✔
597
    double E = data::ttb_e_grid(i);
203,584✔
598

599
    // Get the density effect correction
600
    double delta =
203,584✔
601
      density_effect(f, e_b_sq, e_p_sq, n_conduction, rho, E, 1.0e-6, 100);
203,584✔
602

603
    // Square of the ratio of the speed of light to the velocity of the charged
604
    // particle
605
    double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
203,584✔
606
                     ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
203,584✔
607

608
    double tau = E / MASS_ELECTRON_EV;
203,584✔
609

610
    double F;
203,584✔
611
    if (positron) {
203,584✔
612
      double t = tau + 2.0;
101,792✔
613
      F = std::log(4.0) - (beta_sq / 12.0) * (23.0 + 14.0 / t + 10.0 / (t * t) +
101,792✔
614
                                               4.0 / (t * t * t));
101,792✔
615
    } else {
616
      F = (1.0 - beta_sq) *
203,584✔
617
          (1.0 + tau * tau / 8.0 - (2.0 * tau + 1.0) * std::log(2.0));
101,792✔
618
    }
619

620
    // Calculate the collision stopping power for this energy
621
    s_col[i] =
407,168✔
622
      c / beta_sq *
407,168✔
623
      (2.0 * (std::log(E) - log_I) + std::log(1.0 + tau / 2.0) + F - delta);
203,584✔
624
  }
625
}
1,020✔
626

627
void Material::init_bremsstrahlung()
510✔
628
{
629
  // Create new object
630
  ttb_ = make_unique<Bremsstrahlung>();
510✔
631

632
  // Get the size of the energy grids
633
  auto n_k = data::ttb_k_grid.size();
510✔
634
  auto n_e = data::ttb_e_grid.size();
510✔
635

636
  // Determine number of elements
637
  int n = element_.size();
510✔
638

639
  for (int particle = 0; particle < 2; ++particle) {
1,530✔
640
    // Loop over logic twice, once for electron, once for positron
641
    BremsstrahlungData* ttb =
1,020✔
642
      (particle == 0) ? &ttb_->electron : &ttb_->positron;
1,020✔
643
    bool positron = (particle == 1);
1,020✔
644

645
    // Allocate arrays for TTB data
646
    ttb->pdf = tensor::zeros<double>({n_e, n_e});
1,020✔
647
    ttb->cdf = tensor::zeros<double>({n_e, n_e});
1,020✔
648
    ttb->yield = tensor::zeros<double>({n_e});
1,020✔
649

650
    // Allocate temporary arrays
651
    auto stopping_power_collision = tensor::zeros<double>({n_e});
1,020✔
652
    auto stopping_power_radiative = tensor::zeros<double>({n_e});
1,020✔
653
    auto dcs = tensor::zeros<double>({n_e, n_k});
1,020✔
654

655
    double Z_eq_sq = 0.0;
1,020✔
656
    double sum_density = 0.0;
1,020✔
657

658
    // Get the collision stopping power of the material
659
    this->collision_stopping_power(stopping_power_collision.data(), positron);
1,020✔
660

661
    // Calculate the molecular DCS and the molecular radiative stopping power
662
    // using Bragg's additivity rule.
663
    for (int i = 0; i < n; ++i) {
5,052✔
664
      // Get pointer to current element
665
      const auto& elm = *data::elements[element_[i]];
4,032!
666
      double awr = data::nuclides[nuclide_[i]]->awr_;
4,032!
667

668
      // Get atomic density and mass density of nuclide given atom/weight
669
      // percent
670
      double atom_density =
4,032✔
671
        (atom_density_[0] > 0.0) ? atom_density_[i] : -atom_density_[i] / awr;
4,032!
672

673
      // Calculate the "equivalent" atomic number Zeq of the material
674
      Z_eq_sq += atom_density * elm.Z_ * elm.Z_;
4,032✔
675
      sum_density += atom_density;
4,032✔
676

677
      // Accumulate material DCS
678
      dcs += (atom_density * elm.Z_ * elm.Z_) * elm.dcs_;
4,032✔
679

680
      // Accumulate material radiative stopping power
681
      stopping_power_radiative += atom_density * elm.stopping_power_radiative_;
8,064✔
682
    }
683
    Z_eq_sq /= sum_density;
1,020✔
684

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

708
    // Total material stopping power
709
    tensor::Tensor<double> stopping_power =
1,020✔
710
      stopping_power_collision + stopping_power_radiative;
1,020✔
711

712
    // Loop over photon energies
713
    auto f = tensor::zeros<double>({n_e});
1,020✔
714
    auto z = tensor::zeros<double>({n_e});
1,020✔
715
    for (int i = 0; i < n_e - 1; ++i) {
203,584✔
716
      double w = data::ttb_e_grid(i);
202,564✔
717

718
      // Loop over incident particle energies
719
      for (int j = i; j < n_e; ++j) {
20,624,144✔
720
        double e = data::ttb_e_grid(j);
20,421,580✔
721

722
        // Reduced photon energy
723
        double k = w / e;
20,421,580✔
724

725
        // Find the lower bounding index of the reduced photon energy
726
        int i_k = lower_bound_index(
20,421,580✔
727
          data::ttb_k_grid.cbegin(), data::ttb_k_grid.cend(), k);
20,421,580✔
728

729
        // Get the interpolation bounds
730
        double k_l = data::ttb_k_grid(i_k);
20,421,580✔
731
        double k_r = data::ttb_k_grid(i_k + 1);
20,421,580✔
732
        double x_l = dcs(j, i_k);
20,421,580✔
733
        double x_r = dcs(j, i_k + 1);
20,421,580✔
734

735
        // Find the value of the DCS using linear interpolation in reduced
736
        // photon energy k
737
        double x = x_l + (k - k_l) * (x_r - x_l) / (k_r - k_l);
20,421,580✔
738

739
        // Square of the ratio of the speed of light to the velocity of the
740
        // charged particle
741
        double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
20,421,580✔
742
                         ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
20,421,580✔
743

744
        // Compute the integrand of the PDF
745
        f(j) = x / (beta_sq * stopping_power(j) * w);
20,421,580✔
746
      }
747

748
      // Number of points to integrate
749
      int n = n_e - i;
202,564✔
750

751
      // Integrate the PDF using cubic spline integration over the incident
752
      // particle energy
753
      if (n > 2) {
202,564✔
754
        spline(n, &data::ttb_e_grid(i), &f(i), &z(i));
201,544✔
755

756
        double c = 0.0;
757
        for (int j = i; j < n_e - 1; ++j) {
20,419,540✔
758
          c += spline_integrate(n, &data::ttb_e_grid(i), &f(i), &z(i),
40,435,992✔
759
            data::ttb_e_grid(j), data::ttb_e_grid(j + 1));
20,217,996✔
760

761
          ttb->pdf(j + 1, i) = c;
20,217,996✔
762
        }
763

764
        // Integrate the last two points using trapezoidal rule in log-log space
765
      } else {
766
        double e_l = std::log(data::ttb_e_grid(i));
1,020✔
767
        double e_r = std::log(data::ttb_e_grid(i + 1));
1,020✔
768
        double x_l = std::log(f(i));
1,020✔
769
        double x_r = std::log(f(i + 1));
1,020✔
770

771
        ttb->pdf(i + 1, i) =
2,040✔
772
          0.5 * (e_r - e_l) * (std::exp(e_l + x_l) + std::exp(e_r + x_r));
1,020✔
773
      }
774
    }
775

776
    // Loop over incident particle energies
777
    for (int j = 1; j < n_e; ++j) {
203,584✔
778
      // Set last element of PDF to small non-zero value to enable log-log
779
      // interpolation
780
      ttb->pdf(j, j) = std::exp(-500.0);
202,564✔
781

782
      // Loop over photon energies
783
      double c = 0.0;
202,564✔
784
      for (int i = 0; i < j; ++i) {
20,421,580✔
785
        // Integrate the CDF from the PDF using the fact that the PDF is linear
786
        // in log-log space
787
        double w_l = std::log(data::ttb_e_grid(i));
20,219,016✔
788
        double w_r = std::log(data::ttb_e_grid(i + 1));
20,219,016✔
789
        double x_l = std::log(ttb->pdf(j, i));
20,219,016✔
790
        double x_r = std::log(ttb->pdf(j, i + 1));
20,219,016✔
791
        double beta = (x_r - x_l) / (w_r - w_l);
20,219,016✔
792
        double a = beta + 1.0;
20,219,016✔
793
        c += std::exp(w_l + x_l) / a * std::expm1(a * (w_r - w_l));
20,219,016✔
794
        ttb->cdf(j, i + 1) = c;
20,219,016✔
795
      }
796

797
      // Set photon number yield
798
      ttb->yield(j) = c;
202,564✔
799
    }
800

801
    // Use logarithm of number yield since it is log-log interpolated
802
    ttb->yield =
1,020✔
803
      tensor::where(ttb->yield > 0.0, tensor::log(ttb->yield), -500.0);
4,080✔
804
  }
6,120✔
805
}
510✔
806

807
void Material::init_nuclide_index()
19,947✔
808
{
809
  int n = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
19,947✔
810
  mat_nuclide_index_.resize(n);
19,947✔
811
  std::fill(mat_nuclide_index_.begin(), mat_nuclide_index_.end(), C_NONE);
19,947✔
812
  for (int i = 0; i < nuclide_.size(); ++i) {
117,662✔
813
    mat_nuclide_index_[nuclide_[i]] = i;
97,715✔
814
  }
815
}
19,947✔
816

817
void Material::calculate_xs(Particle& p) const
2,147,483,647✔
818
{
819
  // Set all material macroscopic cross sections to zero
820
  p.macro_xs().total = 0.0;
2,147,483,647✔
821
  p.macro_xs().absorption = 0.0;
2,147,483,647✔
822
  p.macro_xs().fission = 0.0;
2,147,483,647✔
823
  p.macro_xs().nu_fission = 0.0;
2,147,483,647✔
824

825
  if (p.type().is_neutron()) {
2,147,483,647✔
826
    this->calculate_neutron_xs(p);
2,147,483,647✔
827
  } else if (p.type().is_photon()) {
293,607,999✔
828
    this->calculate_photon_xs(p);
112,814,474✔
829
  }
830
}
2,147,483,647✔
831

832
void Material::calculate_neutron_xs(Particle& p) const
2,147,483,647✔
833
{
834
  // Find energy index on energy grid
835
  int neutron = ParticleType::neutron().transport_index();
2,147,483,647✔
836
  int i_grid =
2,147,483,647✔
837
    std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing;
2,147,483,647✔
838

839
  // Determine if this material has S(a,b) tables
840
  bool check_sab = (thermal_tables_.size() > 0);
2,147,483,647✔
841

842
  // Initialize position in i_sab_nuclides
843
  int j = 0;
2,147,483,647✔
844

845
  // Calculate NCrystal cross section
846
  double ncrystal_xs = -1.0;
2,147,483,647✔
847
  if (ncrystal_mat_ && p.E() < NCRYSTAL_MAX_ENERGY) {
2,147,483,647!
848
    ncrystal_xs = ncrystal_mat_.xs(p);
11,158,829✔
849
  }
850

851
  // Add contribution from each nuclide in material
852
  for (int i = 0; i < nuclide_.size(); ++i) {
2,147,483,647✔
853
    // ======================================================================
854
    // CHECK FOR S(A,B) TABLE
855

856
    int i_sab = C_NONE;
2,147,483,647✔
857
    double sab_frac = 0.0;
2,147,483,647✔
858

859
    // Check if this nuclide matches one of the S(a,b) tables specified.
860
    // This relies on thermal_tables_ being sorted by .index_nuclide
861
    if (check_sab) {
2,147,483,647✔
862
      const auto& sab {thermal_tables_[j]};
845,386,234✔
863
      if (i == sab.index_nuclide) {
845,386,234✔
864
        // Get index in sab_tables
865
        i_sab = sab.index_table;
434,613,674✔
866
        sab_frac = sab.fraction;
434,613,674✔
867

868
        // If particle energy is greater than the highest energy for the
869
        // S(a,b) table, then don't use the S(a,b) table
870
        if (p.E() > data::thermal_scatt[i_sab]->energy_max_)
434,613,674✔
871
          i_sab = C_NONE;
261,484,291✔
872

873
        // Increment position in thermal_tables_
874
        ++j;
434,613,674✔
875

876
        // Don't check for S(a,b) tables if there are no more left
877
        if (j == thermal_tables_.size())
434,613,674✔
878
          check_sab = false;
434,380,133✔
879
      }
880
    }
881

882
    // ======================================================================
883
    // CALCULATE MICROSCOPIC CROSS SECTION
884

885
    // Get nuclide index
886
    int i_nuclide = nuclide_[i];
2,147,483,647✔
887

888
    // Update microscopic cross section for this nuclide
889
    p.update_neutron_xs(i_nuclide, i_grid, i_sab, sab_frac, ncrystal_xs);
2,147,483,647✔
890
    auto& micro = p.neutron_xs(i_nuclide);
2,147,483,647✔
891

892
    // ======================================================================
893
    // ADD TO MACROSCOPIC CROSS SECTION
894

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

898
    // Add contributions to cross sections
899
    p.macro_xs().total += atom_density * micro.total;
2,147,483,647✔
900
    p.macro_xs().absorption += atom_density * micro.absorption;
2,147,483,647✔
901
    p.macro_xs().fission += atom_density * micro.fission;
2,147,483,647✔
902
    p.macro_xs().nu_fission += atom_density * micro.nu_fission;
2,147,483,647✔
903
  }
904
}
2,147,483,647✔
905

906
void Material::calculate_photon_xs(Particle& p) const
112,814,474✔
907
{
908
  p.macro_xs().coherent = 0.0;
112,814,474✔
909
  p.macro_xs().incoherent = 0.0;
112,814,474✔
910
  p.macro_xs().photoelectric = 0.0;
112,814,474✔
911
  p.macro_xs().pair_production = 0.0;
112,814,474✔
912

913
  // Add contribution from each nuclide in material
914
  for (int i = 0; i < nuclide_.size(); ++i) {
661,179,192✔
915
    // ========================================================================
916
    // CALCULATE MICROSCOPIC CROSS SECTION
917

918
    // Determine microscopic cross sections for this nuclide
919
    int i_element = element_[i];
548,364,718✔
920

921
    // Calculate microscopic cross section for this nuclide
922
    const auto& micro {p.photon_xs(i_element)};
548,364,718✔
923
    if (p.E() != micro.last_E) {
548,364,718✔
924
      data::elements[i_element]->calculate_xs(p);
198,994,746✔
925
    }
926

927
    // ========================================================================
928
    // ADD TO MACROSCOPIC CROSS SECTION
929

930
    // Copy atom density of nuclide in material
931
    double atom_density = this->atom_density(i, p.density_mult());
548,364,718✔
932

933
    // Add contributions to material macroscopic cross sections
934
    p.macro_xs().total += atom_density * micro.total;
548,364,718✔
935
    p.macro_xs().coherent += atom_density * micro.coherent;
548,364,718✔
936
    p.macro_xs().incoherent += atom_density * micro.incoherent;
548,364,718✔
937
    p.macro_xs().photoelectric += atom_density * micro.photoelectric;
548,364,718✔
938
    p.macro_xs().pair_production += atom_density * micro.pair_production;
548,364,718✔
939
  }
940
}
112,814,474✔
941

942
void Material::set_id(int32_t id)
16,885✔
943
{
944
  assert(id >= 0 || id == C_NONE);
16,885!
945

946
  // Clear entry in material map if an ID was already assigned before
947
  if (id_ != C_NONE) {
16,885!
948
    model::material_map.erase(id_);
×
949
    id_ = C_NONE;
×
950
  }
951

952
  // Make sure no other material has same ID
953
  if (model::material_map.find(id) != model::material_map.end()) {
16,885!
954
    throw std::runtime_error {
×
955
      "Two materials have the same ID: " + std::to_string(id)};
×
956
  }
957

958
  // If no ID specified, auto-assign next ID in sequence
959
  if (id == C_NONE) {
16,885!
960
    id = 0;
×
961
    for (const auto& m : model::materials) {
×
962
      id = std::max(id, m->id_);
×
963
    }
964
    ++id;
×
965
  }
966

967
  // Update ID and entry in material map
968
  id_ = id;
16,885✔
969
  model::material_map[id] = index_;
16,885✔
970
}
16,885✔
971

972
void Material::set_density(double density, const std::string& units)
6,427✔
973
{
974
  assert(density >= 0.0);
6,427!
975

976
  if (nuclide_.empty()) {
6,427!
977
    throw std::runtime_error {"No nuclides exist in material yet."};
×
978
  }
979

980
  if (units == "atom/b-cm") {
6,427✔
981
    // Set total density based on value provided
982
    density_ = density;
6,383✔
983

984
    // Determine normalized atom percents
985
    double sum_percent = atom_density_.sum();
6,383✔
986
    atom_density_ /= sum_percent;
71,764✔
987

988
    // Recalculate nuclide atom densities based on given density
989
    atom_density_ *= density;
6,383✔
990

991
    // Calculate density in g/cm^3 and charge density in [e/b-cm]
992
    density_gpcc_ = 0.0;
6,383✔
993
    charge_density_ = 0.0;
6,383✔
994
    for (int i = 0; i < nuclide_.size(); ++i) {
65,381✔
995
      int i_nuc = nuclide_[i];
58,998!
996
      double awr = data::nuclides[i_nuc]->awr_;
58,998!
997
      int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0;
58,998!
998
      density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
58,998✔
999
      charge_density_ += atom_density_(i) * z;
58,998✔
1000
    }
1001
  } else if (units == "g/cm3" || units == "g/cc") {
55!
1002
    // Determine factor by which to change densities
1003
    double previous_density_gpcc = density_gpcc_;
33✔
1004
    double f = density / previous_density_gpcc;
33✔
1005

1006
    // Update densities
1007
    density_gpcc_ = density;
33✔
1008
    density_ *= f;
33✔
1009
    atom_density_ *= f;
33✔
1010
    charge_density_ *= f;
33✔
1011
  } else {
1012
    throw std::invalid_argument {
22✔
1013
      "Invalid units '" + std::string(units.data()) + "' specified."};
22✔
1014
  }
1015
}
6,416✔
1016

1017
void Material::set_densities(
6,196✔
1018
  const vector<std::string>& name, const vector<double>& density)
1019
{
1020
  auto n = name.size();
6,196✔
1021
  assert(n > 0);
6,196!
1022
  assert(n == density.size());
6,196!
1023

1024
  if (n != nuclide_.size()) {
6,196✔
1025
    nuclide_.resize(n);
1,428✔
1026
    atom_density_ = tensor::zeros<double>({n});
1,428✔
1027
    if (settings::photon_transport)
1,428!
1028
      element_.resize(n);
×
1029
  }
1030

1031
  double sum_density = 0.0;
1032
  for (int64_t i = 0; i < n; ++i) {
64,369✔
1033
    const auto& nuc {name[i]};
58,173✔
1034
    if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) {
58,173!
1035
      int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
×
1036
      if (err < 0)
×
1037
        throw std::runtime_error {openmc_err_msg};
×
1038
    }
1039

1040
    nuclide_[i] = data::nuclide_map.at(nuc);
58,173!
1041
    assert(density[i] > 0.0);
58,173!
1042
    atom_density_(i) = density[i];
58,173!
1043
    sum_density += density[i];
58,173✔
1044

1045
    if (settings::photon_transport) {
58,173!
1046
      auto element_name = to_element(nuc);
×
1047
      element_[i] = data::element_map.at(element_name);
×
1048
    }
×
1049
  }
1050

1051
  // Set total density to the sum of the vector
1052
  this->set_density(sum_density, "atom/b-cm");
6,196✔
1053

1054
  // Generate material bremsstrahlung data for electrons and positrons
1055
  if (settings::photon_transport &&
6,196!
1056
      settings::electron_treatment == ElectronTreatment::TTB) {
×
1057
    this->init_bremsstrahlung();
×
1058
  }
1059

1060
  // Assign S(a,b) tables
1061
  this->init_thermal();
6,196✔
1062
}
6,196✔
1063

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

1073
double Material::temperature() const
20,975✔
1074
{
1075
  // If material doesn't have an assigned temperature, use global default
1076
  return temperature_ >= 0 ? temperature_ : settings::temperature_default;
20,975✔
1077
}
1078

1079
void Material::to_hdf5(hid_t group) const
13,967✔
1080
{
1081
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
13,967✔
1082

1083
  write_attribute(material_group, "depletable", static_cast<int>(depletable()));
13,967✔
1084
  if (volume_ > 0.0) {
13,967✔
1085
    write_attribute(material_group, "volume", volume_);
1,959✔
1086
  }
1087
  if (temperature_ > 0.0) {
13,967✔
1088
    write_attribute(material_group, "temperature", temperature_);
1,809✔
1089
  }
1090
  write_dataset(material_group, "name", name_);
13,967✔
1091
  write_dataset(material_group, "atom_density", density_);
13,967✔
1092

1093
  // Copy nuclide/macro name for each nuclide to vector
1094
  vector<std::string> nuc_names;
13,967✔
1095
  vector<std::string> macro_names;
13,967✔
1096
  vector<double> nuc_densities;
13,967✔
1097
  if (settings::run_CE) {
13,967✔
1098
    for (int i = 0; i < nuclide_.size(); ++i) {
58,881✔
1099
      int i_nuc = nuclide_[i];
46,872✔
1100
      nuc_names.push_back(data::nuclides[i_nuc]->name_);
46,872✔
1101
      nuc_densities.push_back(atom_density_(i));
46,872✔
1102
    }
1103
  } else {
1104
    for (int i = 0; i < nuclide_.size(); ++i) {
3,949✔
1105
      int i_nuc = nuclide_[i];
1,991✔
1106
      if (data::mg.nuclides_[i_nuc].awr != MACROSCOPIC_AWR) {
1,991✔
1107
        nuc_names.push_back(data::mg.nuclides_[i_nuc].name);
88✔
1108
        nuc_densities.push_back(atom_density_(i));
88✔
1109
      } else {
1110
        macro_names.push_back(data::mg.nuclides_[i_nuc].name);
1,903✔
1111
      }
1112
    }
1113
  }
1114

1115
  // Write vector to 'nuclides'
1116
  if (!nuc_names.empty()) {
13,967✔
1117
    write_dataset(material_group, "nuclides", nuc_names);
12,064✔
1118
    write_dataset(material_group, "nuclide_densities", nuc_densities);
12,064✔
1119
  }
1120

1121
  // Write vector to 'macroscopics'
1122
  if (!macro_names.empty()) {
13,967✔
1123
    write_dataset(material_group, "macroscopics", macro_names);
1,903✔
1124
  }
1125

1126
  if (!thermal_tables_.empty()) {
13,967✔
1127
    vector<std::string> sab_names;
1,866✔
1128
    for (const auto& table : thermal_tables_) {
3,798✔
1129
      sab_names.push_back(data::thermal_scatt[table.index_table]->name_);
1,932✔
1130
    }
1131
    write_dataset(material_group, "sab_names", sab_names);
1,866✔
1132
  }
1,866✔
1133

1134
  close_group(material_group);
13,967✔
1135
}
13,967✔
1136

1137
void Material::export_properties_hdf5(hid_t group) const
165✔
1138
{
1139
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
165✔
1140
  write_attribute(material_group, "atom_density", density_);
165✔
1141
  write_attribute(material_group, "mass_density", density_gpcc_);
165✔
1142
  close_group(material_group);
165✔
1143
}
165✔
1144

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

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

1169
  // If nuclide wasn't found, extend nuclide/density arrays
1170
  int err = openmc_load_nuclide(name.c_str(), nullptr, 0);
11✔
1171
  if (err < 0)
11!
1172
    throw std::runtime_error {openmc_err_msg};
×
1173

1174
  // Append new nuclide/density
1175
  int i_nuc = data::nuclide_map[name];
11✔
1176
  nuclide_.push_back(i_nuc);
11✔
1177

1178
  // Append new element if photon transport is on
1179
  if (settings::photon_transport) {
11!
1180
    int i_elem = data::element_map[to_element(name)];
×
1181
    element_.push_back(i_elem);
×
1182
  }
1183

1184
  auto n = nuclide_.size();
11✔
1185

1186
  // Create copy of atom_density_ array with one extra entry
1187
  tensor::Tensor<double> atom_density = tensor::zeros<double>({n});
11✔
1188
  atom_density.slice(tensor::range(0, n - 1)) = atom_density_;
11✔
1189
  atom_density(n - 1) = density;
11✔
1190
  atom_density_ = atom_density;
11✔
1191

1192
  density_ += density;
11✔
1193
  density_gpcc_ +=
11✔
1194
    density * data::nuclides[i_nuc]->awr_ * MASS_NEUTRON / N_AVOGADRO;
11✔
1195
}
11✔
1196

1197
//==============================================================================
1198
// Non-method functions
1199
//==============================================================================
1200

1201
double sternheimer_adjustment(const vector<double>& f,
1,020✔
1202
  const vector<double>& e_b_sq, double e_p_sq, double n_conduction,
1203
  double log_I, double tol, int max_iter)
1204
{
1205
  // Get the total number of oscillators
1206
  int n = f.size();
1,020✔
1207

1208
  // Calculate the Sternheimer adjustment factor using Newton's method
1209
  double rho = 2.0;
1,020✔
1210
  int iter;
1,020✔
1211
  for (iter = 0; iter < max_iter; ++iter) {
8,236✔
1212
    double rho_0 = rho;
1213

1214
    // Function to find the root of and its derivative
1215
    double g = 0.0;
1216
    double gp = 0.0;
1217

1218
    for (int i = 0; i < n; ++i) {
115,640✔
1219
      // Square of resonance energy of a bound-shell oscillator
1220
      double e_r_sq = e_b_sq[i] * rho * rho + 2.0 / 3.0 * f[i] * e_p_sq;
107,448✔
1221
      g += f[i] * std::log(e_r_sq);
107,448✔
1222
      gp += e_b_sq[i] * f[i] * rho / e_r_sq;
107,448✔
1223
    }
1224
    // Include conduction electrons
1225
    if (n_conduction > 0.0) {
8,192✔
1226
      g += n_conduction * std::log(n_conduction * e_p_sq);
3,070✔
1227
    }
1228

1229
    // Set the next guess: rho_n+1 = rho_n - g(rho_n)/g'(rho_n)
1230
    rho -= (g - 2.0 * log_I) / (2.0 * gp);
8,192✔
1231

1232
    // If the initial guess is too large, rho can be negative
1233
    if (rho < 0.0)
8,192✔
1234
      rho = rho_0 / 2.0;
4,400✔
1235

1236
    // Check for convergence
1237
    if (std::abs(rho - rho_0) / rho_0 < tol)
8,192✔
1238
      break;
1239
  }
1240
  // Did not converge
1241
  if (iter >= max_iter) {
1,020✔
1242
    warning("Maximum Newton-Raphson iterations exceeded.");
44✔
1243
    rho = 1.0e-6;
44✔
1244
  }
1245
  return rho;
1,020✔
1246
}
1247

1248
double density_effect(const vector<double>& f, const vector<double>& e_b_sq,
203,584✔
1249
  double e_p_sq, double n_conduction, double rho, double E, double tol,
1250
  int max_iter)
1251
{
1252
  // Get the total number of oscillators
1253
  int n = f.size();
203,584✔
1254

1255
  // Square of the ratio of the speed of light to the velocity of the charged
1256
  // particle
1257
  double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
203,584✔
1258
                   ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
203,584✔
1259

1260
  // For nonmetals, delta = 0 for beta < beta_0, where beta_0 is obtained by
1261
  // setting the frequency w = 0.
1262
  double beta_0_sq = 0.0;
203,584✔
1263
  if (n_conduction == 0.0) {
203,584✔
1264
    for (int i = 0; i < n; ++i) {
270,800✔
1265
      beta_0_sq += f[i] * e_p_sq / (e_b_sq[i] * rho * rho);
218,800✔
1266
    }
1267
    beta_0_sq = 1.0 / (1.0 + beta_0_sq);
52,000✔
1268
  }
1269
  double delta = 0.0;
203,584✔
1270
  if (beta_sq < beta_0_sq)
203,584✔
1271
    return delta;
1272

1273
  // Compute the square of the frequency w^2 using Newton's method, with the
1274
  // initial guess of w^2 equal to beta^2 * gamma^2
1275
  double w_sq = E / MASS_ELECTRON_EV * (E / MASS_ELECTRON_EV + 2);
182,908✔
1276
  int iter;
182,908✔
1277
  for (iter = 0; iter < max_iter; ++iter) {
1,243,380!
1278
    double w_sq_0 = w_sq;
1279

1280
    // Function to find the root of and its derivative
1281
    double g = 0.0;
1282
    double gp = 0.0;
1283

1284
    for (int i = 0; i < n; ++i) {
38,692,278✔
1285
      double c = e_b_sq[i] * rho * rho / e_p_sq + w_sq;
37,448,898✔
1286
      g += f[i] / c;
37,448,898✔
1287
      gp -= f[i] / (c * c);
37,448,898✔
1288
    }
1289
    // Include conduction electrons
1290
    g += n_conduction / w_sq;
1,243,380✔
1291
    gp -= n_conduction / (w_sq * w_sq);
1,243,380✔
1292

1293
    // Set the next guess: w_n+1 = w_n - g(w_n)/g'(w_n)
1294
    w_sq -= (g + 1.0 - 1.0 / beta_sq) / gp;
1,243,380✔
1295

1296
    // If the initial guess is too large, w can be negative
1297
    if (w_sq < 0.0)
1,243,380✔
1298
      w_sq = w_sq_0 / 2.0;
273,494✔
1299

1300
    // Check for convergence
1301
    if (std::abs(w_sq - w_sq_0) / w_sq_0 < tol)
1,243,380✔
1302
      break;
1303
  }
1304
  // Did not converge
1305
  if (iter >= max_iter) {
182,908!
1306
    warning("Maximum Newton-Raphson iterations exceeded: setting density "
×
1307
            "effect correction to zero.");
1308
    return delta;
×
1309
  }
1310

1311
  // Solve for the density effect correction
1312
  for (int i = 0; i < n; ++i) {
5,129,496✔
1313
    double l_sq = e_b_sq[i] * rho * rho / e_p_sq + 2.0 / 3.0 * f[i];
4,946,588✔
1314
    delta += f[i] * std::log((l_sq + w_sq) / l_sq);
4,946,588✔
1315
  }
1316
  // Include conduction electrons
1317
  if (n_conduction > 0.0) {
182,908✔
1318
    delta += n_conduction * std::log((n_conduction + w_sq) / n_conduction);
151,584✔
1319
  }
1320

1321
  return delta - w_sq * (1.0 - beta_sq);
182,908✔
1322
}
1323

1324
void read_materials_xml()
1,399✔
1325
{
1326
  write_message("Reading materials XML file...", 5);
1,399✔
1327

1328
  pugi::xml_document doc;
1,399✔
1329

1330
  // Check if materials.xml exists
1331
  std::string filename = settings::path_input + "materials.xml";
1,399✔
1332
  if (!file_exists(filename)) {
1,399!
1333
    fatal_error("Material XML file '" + filename + "' does not exist!");
×
1334
  }
1335

1336
  // Parse materials.xml file and get root element
1337
  doc.load_file(filename.c_str());
1,399✔
1338

1339
  // Loop over XML material elements and populate the array.
1340
  pugi::xml_node root = doc.document_element();
1,399✔
1341

1342
  read_materials_xml(root);
1,399✔
1343
}
1,399✔
1344

1345
void read_materials_xml(pugi::xml_node root)
8,878✔
1346
{
1347
  for (pugi::xml_node material_node : root.children("material")) {
25,711✔
1348
    model::materials.push_back(make_unique<Material>(material_node));
33,666✔
1349
  }
1350
  model::materials.shrink_to_fit();
8,878✔
1351
}
8,878✔
1352

1353
void free_memory_material()
9,004✔
1354
{
1355
  model::materials.clear();
9,004✔
1356
  model::material_map.clear();
9,004✔
1357
}
9,004✔
1358

1359
//==============================================================================
1360
// C API
1361
//==============================================================================
1362

1363
extern "C" int openmc_get_material_index(int32_t id, int32_t* index)
8,716✔
1364
{
1365
  auto it = model::material_map.find(id);
8,716✔
1366
  if (it == model::material_map.end()) {
8,716✔
1367
    set_errmsg("No material exists with ID=" + std::to_string(id) + ".");
11✔
1368
    return OPENMC_E_INVALID_ID;
11✔
1369
  } else {
1370
    *index = it->second;
8,705✔
1371
    return 0;
8,705✔
1372
  }
1373
}
1374

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

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

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

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

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

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

1457
extern "C" int openmc_material_get_volume(int32_t index, double* volume)
55✔
1458
{
1459
  if (index >= 0 && index < model::materials.size()) {
55!
1460
    try {
55✔
1461
      *volume = model::materials[index]->volume();
55✔
1462
    } catch (const std::exception& e) {
11!
1463
      set_errmsg(e.what());
11✔
1464
      return OPENMC_E_UNASSIGNED;
11✔
1465
    }
11✔
1466
    return 0;
44✔
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_set_density(
99✔
1474
  int32_t index, double density, const char* units)
1475
{
1476
  if (index >= 0 && index < model::materials.size()) {
99!
1477
    try {
99✔
1478
      model::materials[index]->set_density(density, units);
110✔
1479
    } catch (const std::exception& e) {
11!
1480
      set_errmsg(e.what());
11✔
1481
      return OPENMC_E_UNASSIGNED;
11✔
1482
    }
11✔
1483
  } else {
1484
    set_errmsg("Index in materials array is out of bounds.");
×
1485
    return OPENMC_E_OUT_OF_BOUNDS;
×
1486
  }
1487
  return 0;
88✔
1488
}
1489

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

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

1524
extern "C" int openmc_material_get_name(int32_t index, const char** name)
231✔
1525
{
1526
  if (index < 0 || index >= model::materials.size()) {
231!
1527
    set_errmsg("Index in materials array is out of bounds.");
×
1528
    return OPENMC_E_OUT_OF_BOUNDS;
×
1529
  }
1530

1531
  *name = model::materials[index]->name().data();
231✔
1532

1533
  return 0;
231✔
1534
}
1535

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

1543
  model::materials[index]->set_name(name);
22✔
1544

1545
  return 0;
11✔
1546
}
1547

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

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

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

1574
  return 0;
22✔
1575
}
1576

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

1584
  model::materials[index]->depletable() = depletable;
11✔
1585

1586
  return 0;
11✔
1587
}
1588

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

1602
extern "C" size_t n_materials()
143✔
1603
{
1604
  return model::materials.size();
143✔
1605
}
1606

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