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

openmc-dev / openmc / 12385490469

18 Dec 2024 02:58AM UTC coverage: 82.673% (-2.2%) from 84.823%
12385490469

Pull #3087

github

web-flow
Merge 3317476f5 into 775c41512
Pull Request #3087: wheel building with scikit build core

106679 of 129038 relevant lines covered (82.67%)

12084526.77 hits per line

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

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

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

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

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

32
namespace openmc {
33

34
//==============================================================================
35
// Global variables
36
//==============================================================================
37

38
namespace model {
39

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

43
} // namespace model
44

45
//==============================================================================
46
// Material implementation
47
//==============================================================================
48

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

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

59
  if (check_for_node(node, "name")) {
9,340✔
60
    name_ = get_node_value(node, "name");
5,065✔
61
  }
62

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

70
  if (check_for_node(node, "depletable")) {
9,340✔
71
    depletable_ = get_node_value_bool(node, "depletable");
2,577✔
72
  }
73

74
  bool sum_density {false};
9,340✔
75
  pugi::xml_node density_node = node.child("density");
9,340✔
76
  std::string units;
9,340✔
77
  if (density_node) {
9,340✔
78
    units = get_node_value(density_node, "units");
9,340✔
79
    if (units == "sum") {
9,340✔
80
      sum_density = true;
655✔
81
    } else if (units == "macro") {
8,685✔
82
      if (check_for_node(density_node, "value")) {
1,530✔
83
        density_ = std::stod(get_node_value(density_node, "value"));
1,530✔
84
      } else {
85
        density_ = 1.0;
×
86
      }
87
    } else {
88
      double val = std::stod(get_node_value(density_node, "value"));
7,155✔
89
      if (val <= 0.0) {
7,155✔
90
        fatal_error("Need to specify a positive density on material " +
×
91
                    std::to_string(id_) + ".");
×
92
      }
93

94
      if (units == "g/cc" || units == "g/cm3") {
7,155✔
95
        density_ = -val;
6,783✔
96
      } else if (units == "kg/m3") {
372✔
97
        density_ = -1.0e-3 * val;
17✔
98
      } else if (units == "atom/b-cm") {
355✔
99
        density_ = val;
338✔
100
      } else if (units == "atom/cc" || units == "atom/cm3") {
17✔
101
        density_ = 1.0e-24 * val;
17✔
102
      } else {
103
        fatal_error("Unknown units '" + units + "' specified on material " +
×
104
                    std::to_string(id_) + ".");
×
105
      }
106
    }
107
  } else {
108
    fatal_error("Must specify <density> element in material " +
×
109
                std::to_string(id_) + ".");
×
110
  }
111

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

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

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

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

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

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

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

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

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

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

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

194
        // Copy atom/weight percents
195
        if (has_ao) {
27,797✔
196
          densities.push_back(std::stod(get_node_value(node_nuc, "ao")));
19,892✔
197
        } else {
198
          densities.push_back(-std::stod(get_node_value(node_nuc, "wo")));
7,905✔
199
        }
200
      }
201
    }
27,797✔
202
  }
203

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

207
  vector<std::string> iso_lab;
9,340✔
208
  if (check_for_node(node, "isotropic")) {
9,340✔
209
    iso_lab = get_node_array<std::string>(node, "isotropic");
204✔
210
  }
211

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

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

222
  for (int i = 0; i < n; ++i) {
38,667✔
223
    const auto& name {names[i]};
29,327✔
224

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

236
    // If this nuclide hasn't been encountered yet, we need to add its name
237
    // and alias to the nuclide_dict
238
    if (data::nuclide_map.find(name) == data::nuclide_map.end()) {
29,327✔
239
      int index = data::nuclide_map.size();
20,274✔
240
      data::nuclide_map[name] = index;
20,274✔
241
      nuclide_.push_back(index);
20,274✔
242
    } else {
243
      nuclide_.push_back(data::nuclide_map[name]);
9,053✔
244
    }
245

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

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

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

269
    // Copy atom/weight percent
270
    atom_density_(i) = densities[i];
29,327✔
271
  }
272

273
  if (settings::run_CE) {
9,340✔
274
    // By default, isotropic-in-lab is not used
275
    if (iso_lab.size() > 0) {
7,589✔
276
      p0_.resize(n);
204✔
277

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

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

297
  // Determine density if it is a sum value
298
  if (sum_density)
9,340✔
299
    density_ = xt::sum(atom_density_)();
655✔
300

301
  if (check_for_node(node, "temperature")) {
9,340✔
302
    temperature_ = std::stod(get_node_value(node, "temperature"));
59✔
303
  }
304

305
  if (check_for_node(node, "volume")) {
9,340✔
306
    volume_ = std::stod(get_node_value(node, "volume"));
120✔
307
  }
308

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

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

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

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

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

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

357
Material::~Material()
9,340✔
358
{
359
  model::material_map.erase(id_);
9,340✔
360
}
9,340✔
361

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

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

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

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

391
void Material::finalize()
8,868✔
392
{
393
  // Set fissionable if any nuclide is fissionable
394
  if (settings::run_CE) {
8,868✔
395
    for (const auto& i_nuc : nuclide_) {
26,236✔
396
      if (data::nuclides[i_nuc]->fissionable_) {
22,113✔
397
        fissionable_ = true;
2,994✔
398
        break;
2,994✔
399
      }
400
    }
401

402
    // Generate material bremsstrahlung data for electrons and positrons
403
    if (settings::photon_transport &&
7,117✔
404
        settings::electron_treatment == ElectronTreatment::TTB) {
242✔
405
      this->init_bremsstrahlung();
242✔
406
    }
407

408
    // Assign thermal scattering tables
409
    this->init_thermal();
7,117✔
410
  }
411

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

416
void Material::normalize_density()
8,868✔
417
{
418
  bool percent_in_atom = (atom_density_(0) >= 0.0);
8,868✔
419
  bool density_in_atom = (density_ >= 0.0);
8,868✔
420

421
  for (int i = 0; i < nuclide_.size(); ++i) {
36,736✔
422
    // determine atomic weight ratio
423
    int i_nuc = nuclide_[i];
27,868✔
424
    double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_
30,027✔
425
                                  : data::mg.nuclides_[i_nuc].awr;
2,159✔
426

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

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

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

453
  // Calculate nuclide atom densities
454
  atom_density_ *= density_;
8,868✔
455

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

465
void Material::init_thermal()
7,117✔
466
{
467
  vector<ThermalTable> tables;
7,117✔
468

469
  std::unordered_set<int> already_checked;
7,117✔
470
  for (const auto& table : thermal_tables_) {
8,691✔
471
    // Make sure each S(a,b) table only gets checked once
472
    if (already_checked.find(table.index_table) != already_checked.end()) {
1,574✔
473
      continue;
×
474
    }
475
    already_checked.insert(table.index_table);
1,574✔
476

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

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

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

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

520
  // Update the list of thermal tables
521
  thermal_tables_ = tables;
7,117✔
522
}
7,117✔
523

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

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

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

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

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

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

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

553
    for (int j = 0; j < elm.n_electrons_.size(); ++j) {
17,528✔
554
      if (elm.n_electrons_[j] < 0) {
15,374✔
555
        n_conduction -= elm.n_electrons_[j] * atom_density;
1,574✔
556
        continue;
1,574✔
557
      }
558
      e_b_sq.push_back(elm.ionization_energy_[j] * elm.ionization_energy_[j]);
13,800✔
559
      f.push_back(elm.n_electrons_[j] * atom_density);
13,800✔
560
    }
561
  }
562
  log_I /= electron_density;
484✔
563
  n_conduction /= electron_density;
484✔
564
  for (auto& f_i : f)
14,284✔
565
    f_i /= electron_density;
13,800✔
566

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

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

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

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

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

589
  // Loop over incident charged particle energies
590
  for (int i = 0; i < data::ttb_e_grid.size(); ++i) {
96,972✔
591
    double E = data::ttb_e_grid(i);
96,488✔
592

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

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

602
    double tau = E / MASS_ELECTRON_EV;
96,488✔
603

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

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

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

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

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

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

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

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

649
    double Z_eq_sq = 0.0;
484✔
650
    double sum_density = 0.0;
484✔
651

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

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

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

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

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

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

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

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

706
    // Loop over photon energies
707
    xt::xtensor<double, 1> f({n_e}, 0.0);
484✔
708
    xt::xtensor<double, 1> z({n_e}, 0.0);
484✔
709
    for (int i = 0; i < n_e - 1; ++i) {
96,488✔
710
      double w = data::ttb_e_grid(i);
96,004✔
711

712
      // Loop over incident particle energies
713
      for (int j = i; j < n_e; ++j) {
9,763,392✔
714
        double e = data::ttb_e_grid(j);
9,667,388✔
715

716
        // Reduced photon energy
717
        double k = w / e;
9,667,388✔
718

719
        // Find the lower bounding index of the reduced photon energy
720
        int i_k = lower_bound_index(
9,667,388✔
721
          data::ttb_k_grid.cbegin(), data::ttb_k_grid.cend(), k);
9,667,388✔
722

723
        // Get the interpolation bounds
724
        double k_l = data::ttb_k_grid(i_k);
9,667,388✔
725
        double k_r = data::ttb_k_grid(i_k + 1);
9,667,388✔
726
        double x_l = dcs(j, i_k);
9,667,388✔
727
        double x_r = dcs(j, i_k + 1);
9,667,388✔
728

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

733
        // Square of the ratio of the speed of light to the velocity of the
734
        // charged particle
735
        double beta_sq = e * (e + 2.0 * MASS_ELECTRON_EV) /
9,667,388✔
736
                         ((e + MASS_ELECTRON_EV) * (e + MASS_ELECTRON_EV));
9,667,388✔
737

738
        // Compute the integrand of the PDF
739
        f(j) = x / (beta_sq * stopping_power(j) * w);
9,667,388✔
740
      }
741

742
      // Number of points to integrate
743
      int n = n_e - i;
96,004✔
744

745
      // Integrate the PDF using cubic spline integration over the incident
746
      // particle energy
747
      if (n > 2) {
96,004✔
748
        spline(n, &data::ttb_e_grid(i), &f(i), &z(i));
95,520✔
749

750
        double c = 0.0;
95,520✔
751
        for (int j = i; j < n_e - 1; ++j) {
9,666,420✔
752
          c += spline_integrate(n, &data::ttb_e_grid(i), &f(i), &z(i),
9,570,900✔
753
            data::ttb_e_grid(j), data::ttb_e_grid(j + 1));
9,570,900✔
754

755
          ttb->pdf(j + 1, i) = c;
9,570,900✔
756
        }
757

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

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

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

776
      // Loop over photon energies
777
      double c = 0.0;
96,004✔
778
      for (int i = 0; i < j; ++i) {
9,667,388✔
779
        // Integrate the CDF from the PDF using the trapezoidal rule in log-log
780
        // space
781
        double w_l = std::log(data::ttb_e_grid(i));
9,571,384✔
782
        double w_r = std::log(data::ttb_e_grid(i + 1));
9,571,384✔
783
        double x_l = std::log(ttb->pdf(j, i));
9,571,384✔
784
        double x_r = std::log(ttb->pdf(j, i + 1));
9,571,384✔
785

786
        c += 0.5 * (w_r - w_l) * (std::exp(w_l + x_l) + std::exp(w_r + x_r));
9,571,384✔
787
        ttb->cdf(j, i + 1) = c;
9,571,384✔
788
      }
789

790
      // Set photon number yield
791
      ttb->yield(j) = c;
96,004✔
792
    }
793

794
    // Use logarithm of number yield since it is log-log interpolated
795
    ttb->yield = xt::where(ttb->yield > 0.0, xt::log(ttb->yield), -500.0);
484✔
796
  }
484✔
797
}
242✔
798

799
void Material::init_nuclide_index()
8,697✔
800
{
801
  int n = settings::run_CE ? data::nuclides.size() : data::mg.nuclides_.size();
8,697✔
802
  mat_nuclide_index_.resize(n);
8,697✔
803
  std::fill(mat_nuclide_index_.begin(), mat_nuclide_index_.end(), C_NONE);
8,697✔
804
  for (int i = 0; i < nuclide_.size(); ++i) {
35,966✔
805
    mat_nuclide_index_[nuclide_[i]] = i;
27,269✔
806
  }
807
}
8,697✔
808

809
void Material::calculate_xs(Particle& p) const
745,145,840✔
810
{
811
  // Set all material macroscopic cross sections to zero
812
  p.macro_xs().total = 0.0;
745,145,840✔
813
  p.macro_xs().absorption = 0.0;
745,145,840✔
814
  p.macro_xs().fission = 0.0;
745,145,840✔
815
  p.macro_xs().nu_fission = 0.0;
745,145,840✔
816

817
  if (p.type() == ParticleType::neutron) {
745,145,840✔
818
    this->calculate_neutron_xs(p);
684,694,844✔
819
  } else if (p.type() == ParticleType::photon) {
60,450,996✔
820
    this->calculate_photon_xs(p);
11,638,152✔
821
  }
822
}
745,145,840✔
823

824
void Material::calculate_neutron_xs(Particle& p) const
684,694,844✔
825
{
826
  // Find energy index on energy grid
827
  int neutron = static_cast<int>(ParticleType::neutron);
684,694,844✔
828
  int i_grid =
829
    std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing;
684,694,844✔
830

831
  // Determine if this material has S(a,b) tables
832
  bool check_sab = (thermal_tables_.size() > 0);
684,694,844✔
833

834
  // Initialize position in i_sab_nuclides
835
  int j = 0;
684,694,844✔
836

837
  // Calculate NCrystal cross section
838
  double ncrystal_xs = -1.0;
684,694,844✔
839
  if (ncrystal_mat_ && p.E() < NCRYSTAL_MAX_ENERGY) {
684,694,844✔
840
    ncrystal_xs = ncrystal_mat_.xs(p);
1,014,438✔
841
  }
842

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

848
    int i_sab = C_NONE;
2,147,483,647✔
849
    double sab_frac = 0.0;
2,147,483,647✔
850

851
    // Check if this nuclide matches one of the S(a,b) tables specified.
852
    // This relies on thermal_tables_ being sorted by .index_nuclide
853
    if (check_sab) {
2,147,483,647✔
854
      const auto& sab {thermal_tables_[j]};
246,803,314✔
855
      if (i == sab.index_nuclide) {
246,803,314✔
856
        // Get index in sab_tables
857
        i_sab = sab.index_table;
125,295,358✔
858
        sab_frac = sab.fraction;
125,295,358✔
859

860
        // If particle energy is greater than the highest energy for the
861
        // S(a,b) table, then don't use the S(a,b) table
862
        if (p.E() > data::thermal_scatt[i_sab]->energy_max_)
125,295,358✔
863
          i_sab = C_NONE;
80,045,211✔
864

865
        // Increment position in thermal_tables_
866
        ++j;
125,295,358✔
867

868
        // Don't check for S(a,b) tables if there are no more left
869
        if (j == thermal_tables_.size())
125,295,358✔
870
          check_sab = false;
125,039,410✔
871
      }
872
    }
873

874
    // ======================================================================
875
    // CALCULATE MICROSCOPIC CROSS SECTION
876

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

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

884
    // ======================================================================
885
    // ADD TO MACROSCOPIC CROSS SECTION
886

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

890
    // Add contributions to cross sections
891
    p.macro_xs().total += atom_density * micro.total;
2,147,483,647✔
892
    p.macro_xs().absorption += atom_density * micro.absorption;
2,147,483,647✔
893
    p.macro_xs().fission += atom_density * micro.fission;
2,147,483,647✔
894
    p.macro_xs().nu_fission += atom_density * micro.nu_fission;
2,147,483,647✔
895
  }
896
}
684,694,844✔
897

898
void Material::calculate_photon_xs(Particle& p) const
11,638,152✔
899
{
900
  p.macro_xs().coherent = 0.0;
11,638,152✔
901
  p.macro_xs().incoherent = 0.0;
11,638,152✔
902
  p.macro_xs().photoelectric = 0.0;
11,638,152✔
903
  p.macro_xs().pair_production = 0.0;
11,638,152✔
904

905
  // Add contribution from each nuclide in material
906
  for (int i = 0; i < nuclide_.size(); ++i) {
86,980,800✔
907
    // ========================================================================
908
    // CALCULATE MICROSCOPIC CROSS SECTION
909

910
    // Determine microscopic cross sections for this nuclide
911
    int i_element = element_[i];
75,342,648✔
912

913
    // Calculate microscopic cross section for this nuclide
914
    const auto& micro {p.photon_xs(i_element)};
75,342,648✔
915
    if (p.E() != micro.last_E) {
75,342,648✔
916
      data::elements[i_element]->calculate_xs(p);
31,751,192✔
917
    }
918

919
    // ========================================================================
920
    // ADD TO MACROSCOPIC CROSS SECTION
921

922
    // Copy atom density of nuclide in material
923
    double atom_density = atom_density_(i);
75,342,648✔
924

925
    // Add contributions to material macroscopic cross sections
926
    p.macro_xs().total += atom_density * micro.total;
75,342,648✔
927
    p.macro_xs().coherent += atom_density * micro.coherent;
75,342,648✔
928
    p.macro_xs().incoherent += atom_density * micro.incoherent;
75,342,648✔
929
    p.macro_xs().photoelectric += atom_density * micro.photoelectric;
75,342,648✔
930
    p.macro_xs().pair_production += atom_density * micro.pair_production;
75,342,648✔
931
  }
932
}
11,638,152✔
933

934
void Material::set_id(int32_t id)
9,340✔
935
{
936
  Expects(id >= 0 || id == C_NONE);
9,340✔
937

938
  // Clear entry in material map if an ID was already assigned before
939
  if (id_ != C_NONE) {
9,340✔
940
    model::material_map.erase(id_);
×
941
    id_ = C_NONE;
×
942
  }
943

944
  // Make sure no other material has same ID
945
  if (model::material_map.find(id) != model::material_map.end()) {
9,340✔
946
    throw std::runtime_error {
×
947
      "Two materials have the same ID: " + std::to_string(id)};
×
948
  }
949

950
  // If no ID specified, auto-assign next ID in sequence
951
  if (id == C_NONE) {
9,340✔
952
    id = 0;
×
953
    for (const auto& m : model::materials) {
×
954
      id = std::max(id, m->id_);
×
955
    }
956
    ++id;
×
957
  }
958

959
  // Update ID and entry in material map
960
  id_ = id;
9,340✔
961
  model::material_map[id] = index_;
9,340✔
962
}
9,340✔
963

964
void Material::set_density(double density, gsl::cstring_span units)
×
965
{
966
  Expects(density >= 0.0);
×
967

968
  if (nuclide_.empty()) {
×
969
    throw std::runtime_error {"No nuclides exist in material yet."};
×
970
  }
971

972
  if (units == "atom/b-cm") {
×
973
    // Set total density based on value provided
974
    density_ = density;
×
975

976
    // Determine normalized atom percents
977
    double sum_percent = xt::sum(atom_density_)();
×
978
    atom_density_ /= sum_percent;
×
979

980
    // Recalculate nuclide atom densities based on given density
981
    atom_density_ *= density;
×
982

983
    // Calculate density in g/cm^3.
984
    density_gpcc_ = 0.0;
×
985
    for (int i = 0; i < nuclide_.size(); ++i) {
×
986
      int i_nuc = nuclide_[i];
×
987
      double awr = data::nuclides[i_nuc]->awr_;
×
988
      density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO;
×
989
    }
990
  } else if (units == "g/cm3" || units == "g/cc") {
×
991
    // Determine factor by which to change densities
992
    double previous_density_gpcc = density_gpcc_;
×
993
    double f = density / previous_density_gpcc;
×
994

995
    // Update densities
996
    density_gpcc_ = density;
×
997
    density_ *= f;
×
998
    atom_density_ *= f;
×
999
  } else {
1000
    throw std::invalid_argument {
×
1001
      "Invalid units '" + std::string(units.data()) + "' specified."};
×
1002
  }
1003
}
1004

1005
void Material::set_densities(
×
1006
  const vector<std::string>& name, const vector<double>& density)
1007
{
1008
  auto n = name.size();
×
1009
  Expects(n > 0);
×
1010
  Expects(n == density.size());
×
1011

1012
  if (n != nuclide_.size()) {
×
1013
    nuclide_.resize(n);
×
1014
    atom_density_ = xt::zeros<double>({n});
×
1015
    if (settings::photon_transport)
×
1016
      element_.resize(n);
×
1017
  }
1018

1019
  double sum_density = 0.0;
×
1020
  for (gsl::index i = 0; i < n; ++i) {
×
1021
    const auto& nuc {name[i]};
×
1022
    if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) {
×
1023
      int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
×
1024
      if (err < 0)
×
1025
        throw std::runtime_error {openmc_err_msg};
×
1026
    }
1027

1028
    nuclide_[i] = data::nuclide_map.at(nuc);
×
1029
    Expects(density[i] > 0.0);
×
1030
    atom_density_(i) = density[i];
×
1031
    sum_density += density[i];
×
1032

1033
    if (settings::photon_transport) {
×
1034
      auto element_name = to_element(nuc);
×
1035
      element_[i] = data::element_map.at(element_name);
×
1036
    }
1037
  }
1038

1039
  // Set total density to the sum of the vector
1040
  this->set_density(sum_density, "atom/b-cm");
×
1041

1042
  // Generate material bremsstrahlung data for electrons and positrons
1043
  if (settings::photon_transport &&
×
1044
      settings::electron_treatment == ElectronTreatment::TTB) {
×
1045
    this->init_bremsstrahlung();
×
1046
  }
1047

1048
  // Assign S(a,b) tables
1049
  this->init_thermal();
×
1050
}
1051

1052
double Material::volume() const
×
1053
{
1054
  if (volume_ < 0.0) {
×
1055
    throw std::runtime_error {
×
1056
      "Volume for material with ID=" + std::to_string(id_) + " not set."};
×
1057
  }
1058
  return volume_;
×
1059
}
1060

1061
double Material::temperature() const
12,916✔
1062
{
1063
  // If material doesn't have an assigned temperature, use global default
1064
  return temperature_ >= 0 ? temperature_ : settings::temperature_default;
12,916✔
1065
}
1066

1067
void Material::to_hdf5(hid_t group) const
6,594✔
1068
{
1069
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
6,594✔
1070

1071
  write_attribute(material_group, "depletable", static_cast<int>(depletable()));
6,594✔
1072
  if (volume_ > 0.0) {
6,594✔
1073
    write_attribute(material_group, "volume", volume_);
108✔
1074
  }
1075
  if (temperature_ > 0.0) {
6,594✔
1076
    write_attribute(material_group, "temperature", temperature_);
49✔
1077
  }
1078
  write_dataset(material_group, "name", name_);
6,594✔
1079
  write_dataset(material_group, "atom_density", density_);
6,594✔
1080

1081
  // Copy nuclide/macro name for each nuclide to vector
1082
  vector<std::string> nuc_names;
6,594✔
1083
  vector<std::string> macro_names;
6,594✔
1084
  vector<double> nuc_densities;
6,594✔
1085
  if (settings::run_CE) {
6,594✔
1086
    for (int i = 0; i < nuclide_.size(); ++i) {
25,715✔
1087
      int i_nuc = nuclide_[i];
20,045✔
1088
      nuc_names.push_back(data::nuclides[i_nuc]->name_);
20,045✔
1089
      nuc_densities.push_back(atom_density_(i));
20,045✔
1090
    }
1091
  } else {
1092
    for (int i = 0; i < nuclide_.size(); ++i) {
1,884✔
1093
      int i_nuc = nuclide_[i];
960✔
1094
      if (data::mg.nuclides_[i_nuc].awr != MACROSCOPIC_AWR) {
960✔
1095
        nuc_names.push_back(data::mg.nuclides_[i_nuc].name);
96✔
1096
        nuc_densities.push_back(atom_density_(i));
96✔
1097
      } else {
1098
        macro_names.push_back(data::mg.nuclides_[i_nuc].name);
864✔
1099
      }
1100
    }
1101
  }
1102

1103
  // Write vector to 'nuclides'
1104
  if (!nuc_names.empty()) {
6,594✔
1105
    write_dataset(material_group, "nuclides", nuc_names);
5,730✔
1106
    write_dataset(material_group, "nuclide_densities", nuc_densities);
5,730✔
1107
  }
1108

1109
  // Write vector to 'macroscopics'
1110
  if (!macro_names.empty()) {
6,594✔
1111
    write_dataset(material_group, "macroscopics", macro_names);
864✔
1112
  }
1113

1114
  if (!thermal_tables_.empty()) {
6,594✔
1115
    vector<std::string> sab_names;
1,141✔
1116
    for (const auto& table : thermal_tables_) {
2,354✔
1117
      sab_names.push_back(data::thermal_scatt[table.index_table]->name_);
1,213✔
1118
    }
1119
    write_dataset(material_group, "sab_names", sab_names);
1,141✔
1120
  }
1,141✔
1121

1122
  close_group(material_group);
6,594✔
1123
}
6,594✔
1124

1125
void Material::export_properties_hdf5(hid_t group) const
×
1126
{
1127
  hid_t material_group = create_group(group, "material " + std::to_string(id_));
×
1128
  write_attribute(material_group, "atom_density", density_);
×
1129
  write_attribute(material_group, "mass_density", density_gpcc_);
×
1130
  close_group(material_group);
×
1131
}
1132

1133
void Material::import_properties_hdf5(hid_t group)
×
1134
{
1135
  hid_t material_group = open_group(group, "material " + std::to_string(id_));
×
1136
  double density;
1137
  read_attribute(material_group, "atom_density", density);
×
1138
  this->set_density(density, "atom/b-cm");
×
1139
  close_group(material_group);
×
1140
}
1141

1142
void Material::add_nuclide(const std::string& name, double density)
×
1143
{
1144
  // Check if nuclide is already in material
1145
  for (int i = 0; i < nuclide_.size(); ++i) {
×
1146
    int i_nuc = nuclide_[i];
×
1147
    if (data::nuclides[i_nuc]->name_ == name) {
×
1148
      double awr = data::nuclides[i_nuc]->awr_;
×
1149
      density_ += density - atom_density_(i);
×
1150
      density_gpcc_ +=
×
1151
        (density - atom_density_(i)) * awr * MASS_NEUTRON / N_AVOGADRO;
×
1152
      atom_density_(i) = density;
×
1153
      return;
×
1154
    }
1155
  }
1156

1157
  // If nuclide wasn't found, extend nuclide/density arrays
1158
  int err = openmc_load_nuclide(name.c_str(), nullptr, 0);
×
1159
  if (err < 0)
×
1160
    throw std::runtime_error {openmc_err_msg};
×
1161

1162
  // Append new nuclide/density
1163
  int i_nuc = data::nuclide_map[name];
×
1164
  nuclide_.push_back(i_nuc);
×
1165

1166
  // Append new element if photon transport is on
1167
  if (settings::photon_transport) {
×
1168
    int i_elem = data::element_map[to_element(name)];
×
1169
    element_.push_back(i_elem);
×
1170
  }
1171

1172
  auto n = nuclide_.size();
×
1173

1174
  // Create copy of atom_density_ array with one extra entry
1175
  xt::xtensor<double, 1> atom_density = xt::zeros<double>({n});
×
1176
  xt::view(atom_density, xt::range(0, n - 1)) = atom_density_;
×
1177
  atom_density(n - 1) = density;
×
1178
  atom_density_ = atom_density;
×
1179

1180
  density_ += density;
×
1181
  density_gpcc_ +=
×
1182
    density * data::nuclides[i_nuc]->awr_ * MASS_NEUTRON / N_AVOGADRO;
×
1183
}
1184

1185
//==============================================================================
1186
// Non-method functions
1187
//==============================================================================
1188

1189
double sternheimer_adjustment(const vector<double>& f,
484✔
1190
  const vector<double>& e_b_sq, double e_p_sq, double n_conduction,
1191
  double log_I, double tol, int max_iter)
1192
{
1193
  // Get the total number of oscillators
1194
  int n = f.size();
484✔
1195

1196
  // Calculate the Sternheimer adjustment factor using Newton's method
1197
  double rho = 2.0;
484✔
1198
  int iter;
1199
  for (iter = 0; iter < max_iter; ++iter) {
1,946✔
1200
    double rho_0 = rho;
1,946✔
1201

1202
    // Function to find the root of and its derivative
1203
    double g = 0.0;
1,946✔
1204
    double gp = 0.0;
1,946✔
1205

1206
    for (int i = 0; i < n; ++i) {
57,852✔
1207
      // Square of resonance energy of a bound-shell oscillator
1208
      double e_r_sq = e_b_sq[i] * rho * rho + 2.0 / 3.0 * f[i] * e_p_sq;
55,906✔
1209
      g += f[i] * std::log(e_r_sq);
55,906✔
1210
      gp += e_b_sq[i] * f[i] * rho / e_r_sq;
55,906✔
1211
    }
1212
    // Include conduction electrons
1213
    if (n_conduction > 0.0) {
1,946✔
1214
      g += n_conduction * std::log(n_conduction * e_p_sq);
1,738✔
1215
    }
1216

1217
    // Set the next guess: rho_n+1 = rho_n - g(rho_n)/g'(rho_n)
1218
    rho -= (g - 2.0 * log_I) / (2.0 * gp);
1,946✔
1219

1220
    // If the initial guess is too large, rho can be negative
1221
    if (rho < 0.0)
1,946✔
1222
      rho = rho_0 / 2.0;
×
1223

1224
    // Check for convergence
1225
    if (std::abs(rho - rho_0) / rho_0 < tol)
1,946✔
1226
      break;
484✔
1227
  }
1228
  // Did not converge
1229
  if (iter >= max_iter) {
484✔
1230
    warning("Maximum Newton-Raphson iterations exceeded.");
×
1231
    rho = 1.0e-6;
×
1232
  }
1233
  return rho;
484✔
1234
}
1235

1236
double density_effect(const vector<double>& f, const vector<double>& e_b_sq,
96,488✔
1237
  double e_p_sq, double n_conduction, double rho, double E, double tol,
1238
  int max_iter)
1239
{
1240
  // Get the total number of oscillators
1241
  int n = f.size();
96,488✔
1242

1243
  // Square of the ratio of the speed of light to the velocity of the charged
1244
  // particle
1245
  double beta_sq = E * (E + 2.0 * MASS_ELECTRON_EV) /
96,488✔
1246
                   ((E + MASS_ELECTRON_EV) * (E + MASS_ELECTRON_EV));
96,488✔
1247

1248
  // For nonmetals, delta = 0 for beta < beta_0, where beta_0 is obtained by
1249
  // setting the frequency w = 0.
1250
  double beta_0_sq = 0.0;
96,488✔
1251
  if (n_conduction == 0.0) {
96,488✔
1252
    for (int i = 0; i < n; ++i) {
94,800✔
1253
      beta_0_sq += f[i] * e_p_sq / (e_b_sq[i] * rho * rho);
83,200✔
1254
    }
1255
    beta_0_sq = 1.0 / (1.0 + beta_0_sq);
11,600✔
1256
  }
1257
  double delta = 0.0;
96,488✔
1258
  if (beta_sq < beta_0_sq)
96,488✔
1259
    return delta;
6,738✔
1260

1261
  // Compute the square of the frequency w^2 using Newton's method, with the
1262
  // initial guess of w^2 equal to beta^2 * gamma^2
1263
  double w_sq = E / MASS_ELECTRON_EV * (E / MASS_ELECTRON_EV + 2);
89,750✔
1264
  int iter;
1265
  for (iter = 0; iter < max_iter; ++iter) {
631,186✔
1266
    double w_sq_0 = w_sq;
631,186✔
1267

1268
    // Function to find the root of and its derivative
1269
    double g = 0.0;
631,186✔
1270
    double gp = 0.0;
631,186✔
1271

1272
    for (int i = 0; i < n; ++i) {
21,053,856✔
1273
      double c = e_b_sq[i] * rho * rho / e_p_sq + w_sq;
20,422,670✔
1274
      g += f[i] / c;
20,422,670✔
1275
      gp -= f[i] / (c * c);
20,422,670✔
1276
    }
1277
    // Include conduction electrons
1278
    g += n_conduction / w_sq;
631,186✔
1279
    gp -= n_conduction / (w_sq * w_sq);
631,186✔
1280

1281
    // Set the next guess: w_n+1 = w_n - g(w_n)/g'(w_n)
1282
    w_sq -= (g + 1.0 - 1.0 / beta_sq) / gp;
631,186✔
1283

1284
    // If the initial guess is too large, w can be negative
1285
    if (w_sq < 0.0)
631,186✔
1286
      w_sq = w_sq_0 / 2.0;
145,028✔
1287

1288
    // Check for convergence
1289
    if (std::abs(w_sq - w_sq_0) / w_sq_0 < tol)
631,186✔
1290
      break;
89,750✔
1291
  }
1292
  // Did not converge
1293
  if (iter >= max_iter) {
89,750✔
1294
    warning("Maximum Newton-Raphson iterations exceeded: setting density "
×
1295
            "effect correction to zero.");
1296
    return delta;
×
1297
  }
1298

1299
  // Solve for the density effect correction
1300
  for (int i = 0; i < n; ++i) {
2,799,254✔
1301
    double l_sq = e_b_sq[i] * rho * rho / e_p_sq + 2.0 / 3.0 * f[i];
2,709,504✔
1302
    delta += f[i] * std::log((l_sq + w_sq) / l_sq);
2,709,504✔
1303
  }
1304
  // Include conduction electrons
1305
  if (n_conduction > 0.0) {
89,750✔
1306
    delta += n_conduction * std::log((n_conduction + w_sq) / n_conduction);
84,888✔
1307
  }
1308

1309
  return delta - w_sq * (1.0 - beta_sq);
89,750✔
1310
}
1311

1312
void read_materials_xml()
926✔
1313
{
1314
  write_message("Reading materials XML file...", 5);
926✔
1315

1316
  pugi::xml_document doc;
926✔
1317

1318
  // Check if materials.xml exists
1319
  std::string filename = settings::path_input + "materials.xml";
926✔
1320
  if (!file_exists(filename)) {
926✔
1321
    fatal_error("Material XML file '" + filename + "' does not exist!");
×
1322
  }
1323

1324
  // Parse materials.xml file and get root element
1325
  doc.load_file(filename.c_str());
926✔
1326

1327
  // Loop over XML material elements and populate the array.
1328
  pugi::xml_node root = doc.document_element();
926✔
1329

1330
  read_materials_xml(root);
926✔
1331
}
926✔
1332

1333
void read_materials_xml(pugi::xml_node root)
5,413✔
1334
{
1335
  for (pugi::xml_node material_node : root.children("material")) {
14,753✔
1336
    model::materials.push_back(make_unique<Material>(material_node));
9,340✔
1337
  }
1338
  model::materials.shrink_to_fit();
5,413✔
1339
}
5,413✔
1340

1341
void free_memory_material()
5,357✔
1342
{
1343
  model::materials.clear();
5,357✔
1344
  model::material_map.clear();
5,357✔
1345
}
5,357✔
1346

1347
//==============================================================================
1348
// C API
1349
//==============================================================================
1350

1351
extern "C" int openmc_get_material_index(int32_t id, int32_t* index)
×
1352
{
1353
  auto it = model::material_map.find(id);
×
1354
  if (it == model::material_map.end()) {
×
1355
    set_errmsg("No material exists with ID=" + std::to_string(id) + ".");
×
1356
    return OPENMC_E_INVALID_ID;
×
1357
  } else {
1358
    *index = it->second;
×
1359
    return 0;
×
1360
  }
1361
}
1362

1363
extern "C" int openmc_material_add_nuclide(
×
1364
  int32_t index, const char* name, double density)
1365
{
1366
  int err = 0;
×
1367
  if (index >= 0 && index < model::materials.size()) {
×
1368
    try {
1369
      model::materials[index]->add_nuclide(name, density);
×
1370
    } catch (const std::runtime_error& e) {
×
1371
      return OPENMC_E_DATA;
×
1372
    }
×
1373
  } else {
1374
    set_errmsg("Index in materials array is out of bounds.");
×
1375
    return OPENMC_E_OUT_OF_BOUNDS;
×
1376
  }
1377
  return err;
×
1378
}
1379

1380
extern "C" int openmc_material_get_densities(
×
1381
  int32_t index, const int** nuclides, const double** densities, int* n)
1382
{
1383
  if (index >= 0 && index < model::materials.size()) {
×
1384
    auto& mat = model::materials[index];
×
1385
    if (!mat->nuclides().empty()) {
×
1386
      *nuclides = mat->nuclides().data();
×
1387
      *densities = mat->densities().data();
×
1388
      *n = mat->nuclides().size();
×
1389
      return 0;
×
1390
    } else {
1391
      set_errmsg("Material atom density array has not been allocated.");
×
1392
      return OPENMC_E_ALLOCATE;
×
1393
    }
1394
  } else {
1395
    set_errmsg("Index in materials array is out of bounds.");
×
1396
    return OPENMC_E_OUT_OF_BOUNDS;
×
1397
  }
1398
}
1399

1400
extern "C" int openmc_material_get_density(int32_t index, double* density)
×
1401
{
1402
  if (index >= 0 && index < model::materials.size()) {
×
1403
    auto& mat = model::materials[index];
×
1404
    *density = mat->density_gpcc();
×
1405
    return 0;
×
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_fissionable(int32_t index, bool* fissionable)
×
1413
{
1414
  if (index >= 0 && index < model::materials.size()) {
×
1415
    *fissionable = model::materials[index]->fissionable();
×
1416
    return 0;
×
1417
  } else {
1418
    set_errmsg("Index in materials array is out of bounds.");
×
1419
    return OPENMC_E_OUT_OF_BOUNDS;
×
1420
  }
1421
}
1422

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

1434
extern "C" int openmc_material_get_temperature(
×
1435
  int32_t index, double* temperature)
1436
{
1437
  if (index < 0 || index >= model::materials.size()) {
×
1438
    set_errmsg("Index in materials array is out of bounds.");
×
1439
    return OPENMC_E_OUT_OF_BOUNDS;
×
1440
  }
1441
  *temperature = model::materials[index]->temperature();
×
1442
  return 0;
×
1443
}
1444

1445
extern "C" int openmc_material_get_volume(int32_t index, double* volume)
×
1446
{
1447
  if (index >= 0 && index < model::materials.size()) {
×
1448
    try {
1449
      *volume = model::materials[index]->volume();
×
1450
    } catch (const std::exception& e) {
×
1451
      set_errmsg(e.what());
×
1452
      return OPENMC_E_UNASSIGNED;
×
1453
    }
×
1454
    return 0;
×
1455
  } else {
1456
    set_errmsg("Index in materials array is out of bounds.");
×
1457
    return OPENMC_E_OUT_OF_BOUNDS;
×
1458
  }
1459
}
1460

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

1478
extern "C" int openmc_material_set_densities(
×
1479
  int32_t index, int n, const char** name, const double* density)
1480
{
1481
  if (index >= 0 && index < model::materials.size()) {
×
1482
    try {
1483
      model::materials[index]->set_densities(
×
1484
        {name, name + n}, {density, density + n});
×
1485
    } catch (const std::exception& e) {
×
1486
      set_errmsg(e.what());
×
1487
      return OPENMC_E_UNASSIGNED;
×
1488
    }
×
1489
  } else {
1490
    set_errmsg("Index in materials array is out of bounds.");
×
1491
    return OPENMC_E_OUT_OF_BOUNDS;
×
1492
  }
1493
  return 0;
×
1494
}
1495

1496
extern "C" int openmc_material_set_id(int32_t index, int32_t id)
×
1497
{
1498
  if (index >= 0 && index < model::materials.size()) {
×
1499
    try {
1500
      model::materials.at(index)->set_id(id);
×
1501
    } catch (const std::exception& e) {
×
1502
      set_errmsg(e.what());
×
1503
      return OPENMC_E_UNASSIGNED;
×
1504
    }
×
1505
  } else {
1506
    set_errmsg("Index in materials array is out of bounds.");
×
1507
    return OPENMC_E_OUT_OF_BOUNDS;
×
1508
  }
1509
  return 0;
×
1510
}
1511

1512
extern "C" int openmc_material_get_name(int32_t index, const char** name)
×
1513
{
1514
  if (index < 0 || index >= model::materials.size()) {
×
1515
    set_errmsg("Index in materials array is out of bounds.");
×
1516
    return OPENMC_E_OUT_OF_BOUNDS;
×
1517
  }
1518

1519
  *name = model::materials[index]->name().data();
×
1520

1521
  return 0;
×
1522
}
1523

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

1531
  model::materials[index]->set_name(name);
×
1532

1533
  return 0;
×
1534
}
1535

1536
extern "C" int openmc_material_set_volume(int32_t index, double volume)
×
1537
{
1538
  if (index >= 0 && index < model::materials.size()) {
×
1539
    auto& m {model::materials[index]};
×
1540
    if (volume >= 0.0) {
×
1541
      m->volume_ = volume;
×
1542
      return 0;
×
1543
    } else {
1544
      set_errmsg("Volume must be non-negative");
×
1545
      return OPENMC_E_INVALID_ARGUMENT;
×
1546
    }
1547
  } else {
1548
    set_errmsg("Index in materials array is out of bounds.");
×
1549
    return OPENMC_E_OUT_OF_BOUNDS;
×
1550
  }
1551
}
1552

1553
extern "C" int openmc_material_get_depletable(int32_t index, bool* depletable)
×
1554
{
1555
  if (index < 0 || index >= model::materials.size()) {
×
1556
    set_errmsg("Index in materials array is out of bounds.");
×
1557
    return OPENMC_E_OUT_OF_BOUNDS;
×
1558
  }
1559

1560
  *depletable = model::materials[index]->depletable();
×
1561

1562
  return 0;
×
1563
}
1564

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

1572
  model::materials[index]->depletable() = depletable;
×
1573

1574
  return 0;
×
1575
}
1576

1577
extern "C" int openmc_extend_materials(
×
1578
  int32_t n, int32_t* index_start, int32_t* index_end)
1579
{
1580
  if (index_start)
×
1581
    *index_start = model::materials.size();
×
1582
  if (index_end)
×
1583
    *index_end = model::materials.size() + n - 1;
×
1584
  for (int32_t i = 0; i < n; i++) {
×
1585
    model::materials.push_back(make_unique<Material>());
×
1586
  }
1587
  return 0;
×
1588
}
1589

1590
extern "C" size_t n_materials()
×
1591
{
1592
  return model::materials.size();
×
1593
}
1594

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

© 2025 Coveralls, Inc