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

openmc-dev / openmc / 23460042813

23 Mar 2026 09:04PM UTC coverage: 81.334% (-0.2%) from 81.556%
23460042813

Pull #3832

github

web-flow
Merge 1144e8851 into 6cd39073b
Pull Request #3832: Allowing multiple cells in surface source banking

17652 of 25502 branches covered (69.22%)

Branch coverage included in aggregate %.

94 of 97 new or added lines in 4 files covered. (96.91%)

868 existing lines in 32 files now uncovered.

58133 of 67676 relevant lines covered (85.9%)

44449215.59 hits per line

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

77.2
/src/settings.cpp
1
#include "openmc/settings.h"
2
#include "openmc/random_ray/flat_source_domain.h"
3

4
#include <cmath>  // for ceil, pow
5
#include <limits> // for numeric_limits
6
#include <string>
7
#include <string_view>
8
#include <unordered_map>
9
#include <unordered_set>
10

11
#include <fmt/core.h>
12
#ifdef _OPENMP
13
#include <omp.h>
14
#endif
15

16
#include "openmc/capi.h"
17
#include "openmc/collision_track.h"
18
#include "openmc/constants.h"
19
#include "openmc/container_util.h"
20
#include "openmc/distribution.h"
21
#include "openmc/distribution_multi.h"
22
#include "openmc/distribution_spatial.h"
23
#include "openmc/eigenvalue.h"
24
#include "openmc/error.h"
25
#include "openmc/file_utils.h"
26
#include "openmc/mcpl_interface.h"
27
#include "openmc/mesh.h"
28
#include "openmc/message_passing.h"
29
#include "openmc/output.h"
30
#include "openmc/plot.h"
31
#include "openmc/random_lcg.h"
32
#include "openmc/random_ray/random_ray.h"
33
#include "openmc/reaction.h"
34
#include "openmc/simulation.h"
35
#include "openmc/source.h"
36
#include "openmc/string_utils.h"
37
#include "openmc/tallies/trigger.h"
38
#include "openmc/volume_calc.h"
39
#include "openmc/weight_windows.h"
40
#include "openmc/xml_interface.h"
41

42
namespace openmc {
43

44
//==============================================================================
45
// Global variables
46
//==============================================================================
47

48
namespace settings {
49

50
// Default values for boolean flags
51
bool assume_separate {false};
52
bool check_overlaps {false};
53
bool collision_track {false};
54
bool cmfd_run {false};
55
bool confidence_intervals {false};
56
bool create_delayed_neutrons {true};
57
bool create_fission_neutrons {true};
58
bool delayed_photon_scaling {true};
59
bool entropy_on {false};
60
bool event_based {false};
61
bool ifp_on {false};
62
bool legendre_to_tabular {true};
63
bool material_cell_offsets {true};
64
bool output_summary {true};
65
bool output_tallies {true};
66
bool particle_restart_run {false};
67
bool photon_transport {false};
68
bool atomic_relaxation {true};
69
bool reduce_tallies {true};
70
bool res_scat_on {false};
71
bool restart_run {false};
72
bool run_CE {true};
73
bool source_latest {false};
74
bool source_separate {false};
75
bool source_write {true};
76
bool source_mcpl_write {false};
77
bool surf_source_write {false};
78
bool surf_mcpl_write {false};
79
bool surf_source_read {false};
80
bool survival_biasing {false};
81
bool survival_normalization {false};
82
bool temperature_multipole {false};
83
bool trigger_on {false};
84
bool trigger_predict {false};
85
bool uniform_source_sampling {false};
86
bool ufs_on {false};
87
bool urr_ptables_on {true};
88
bool use_decay_photons {false};
89
bool weight_windows_on {false};
90
bool weight_window_checkpoint_surface {false};
91
bool weight_window_checkpoint_collision {true};
92
bool write_all_tracks {false};
93
bool write_initial_source {false};
94

95
std::string path_cross_sections;
96
std::string path_input;
97
std::string path_output;
98
std::string path_particle_restart;
99
std::string path_sourcepoint;
100
std::string path_statepoint;
101
const char* path_statepoint_c {path_statepoint.c_str()};
102
std::string weight_windows_file;
103
std::string properties_file;
104

105
int32_t n_inactive {0};
106
int32_t max_lost_particles {10};
107
double rel_max_lost_particles {1.0e-6};
108
int32_t max_write_lost_particles {-1};
109
int32_t gen_per_batch {1};
110
int64_t n_particles {-1};
111

112
int64_t max_particles_in_flight {100000};
113
int max_particle_events {1000000};
114

115
ElectronTreatment electron_treatment {ElectronTreatment::TTB};
116
array<double, 4> energy_cutoff {0.0, 1000.0, 0.0, 0.0};
117
array<double, 4> time_cutoff {INFTY, INFTY, INFTY, INFTY};
118
int ifp_n_generation {-1};
119
IFPParameter ifp_parameter {IFPParameter::None};
120
int legendre_to_tabular_points {C_NONE};
121
int max_order {0};
122
int n_log_bins {8000};
123
int n_batches;
124
int n_max_batches;
125
int max_secondaries {10000};
126
int max_history_splits {10'000'000};
127
int max_tracks {1000};
128
ResScatMethod res_scat_method {ResScatMethod::rvs};
129
double res_scat_energy_min {0.01};
130
double res_scat_energy_max {1000.0};
131
vector<std::string> res_scat_nuclides;
132
RunMode run_mode {RunMode::UNSET};
133
SolverType solver_type {SolverType::MONTE_CARLO};
134
std::unordered_set<int> sourcepoint_batch;
135
std::unordered_set<int> statepoint_batch;
136
double source_rejection_fraction {0.05};
137
double free_gas_threshold {400.0};
138
std::unordered_set<int> source_write_surf_id;
139
CollisionTrackConfig collision_track_config {};
140
int64_t ssw_max_particles;
141
int64_t ssw_max_files;
142
std::unordered_map<int64_t, SSWCellType> ssw_cells;
143
double surface_grazing_cutoff {0.001};
144
double surface_grazing_ratio {0.5};
145
TemperatureMethod temperature_method {TemperatureMethod::NEAREST};
146
double temperature_tolerance {10.0};
147
double temperature_default {293.6};
148
array<double, 2> temperature_range {0.0, 0.0};
149
int trace_batch;
150
int trace_gen;
151
int64_t trace_particle;
152
vector<array<int, 3>> track_identifiers;
153
int trigger_batch_interval {1};
154
int verbosity {-1};
155
double weight_cutoff {0.25};
156
double weight_survive {1.0};
157

158
} // namespace settings
159

160
//==============================================================================
161
// Functions
162
//==============================================================================
163

164
void get_run_parameters(pugi::xml_node node_base)
7,533✔
165
{
166
  using namespace settings;
7,533✔
167
  using namespace pugi;
7,533✔
168

169
  // Check number of particles
170
  if (!check_for_node(node_base, "particles")) {
7,533!
UNCOV
171
    fatal_error("Need to specify number of particles.");
×
172
  }
173

174
  // Get number of particles if it wasn't specified as a command-line argument
175
  if (n_particles == -1) {
7,533!
176
    n_particles = std::stoll(get_node_value(node_base, "particles"));
7,533✔
177
  }
178

179
  // Get maximum number of in flight particles for event-based mode
180
  if (check_for_node(node_base, "max_particles_in_flight")) {
7,533!
UNCOV
181
    max_particles_in_flight =
×
UNCOV
182
      std::stoll(get_node_value(node_base, "max_particles_in_flight"));
×
183
  }
184

185
  // Get maximum number of events allowed per particle
186
  if (check_for_node(node_base, "max_particle_events")) {
7,533!
UNCOV
187
    max_particle_events =
×
UNCOV
188
      std::stoll(get_node_value(node_base, "max_particle_events"));
×
189
  }
190

191
  // Get number of basic batches
192
  if (check_for_node(node_base, "batches")) {
7,533!
193
    n_batches = std::stoi(get_node_value(node_base, "batches"));
7,533✔
194
  }
195
  if (!trigger_on)
7,533✔
196
    n_max_batches = n_batches;
7,392✔
197

198
  // Get max number of lost particles
199
  if (check_for_node(node_base, "max_lost_particles")) {
7,533✔
200
    max_lost_particles =
92✔
201
      std::stoi(get_node_value(node_base, "max_lost_particles"));
46✔
202
  }
203

204
  // Get relative number of lost particles
205
  if (check_for_node(node_base, "rel_max_lost_particles")) {
7,533!
UNCOV
206
    rel_max_lost_particles =
×
UNCOV
207
      std::stod(get_node_value(node_base, "rel_max_lost_particles"));
×
208
  }
209

210
  // Get relative number of lost particles
211
  if (check_for_node(node_base, "max_write_lost_particles")) {
7,533✔
212
    max_write_lost_particles =
30✔
213
      std::stoi(get_node_value(node_base, "max_write_lost_particles"));
15✔
214
  }
215

216
  // Get number of inactive batches
217
  if (run_mode == RunMode::EIGENVALUE ||
7,533✔
218
      solver_type == SolverType::RANDOM_RAY) {
2,963✔
219
    if (check_for_node(node_base, "inactive")) {
4,976✔
220
      n_inactive = std::stoi(get_node_value(node_base, "inactive"));
4,765✔
221
    }
222
    if (check_for_node(node_base, "generations_per_batch")) {
4,976✔
223
      gen_per_batch =
30✔
224
        std::stoi(get_node_value(node_base, "generations_per_batch"));
15✔
225
    }
226

227
    // Preallocate space for keff and entropy by generation
228
    int m = settings::n_max_batches * settings::gen_per_batch;
4,976✔
229
    simulation::k_generation.reserve(m);
4,976✔
230
    simulation::entropy.reserve(m);
4,976✔
231

232
    // Get the trigger information for keff
233
    if (check_for_node(node_base, "keff_trigger")) {
4,976✔
234
      xml_node node_keff_trigger = node_base.child("keff_trigger");
101✔
235

236
      if (check_for_node(node_keff_trigger, "type")) {
101!
237
        auto temp = get_node_value(node_keff_trigger, "type", true, true);
101✔
238
        if (temp == "std_dev") {
101!
239
          keff_trigger.metric = TriggerMetric::standard_deviation;
101✔
240
        } else if (temp == "variance") {
×
241
          keff_trigger.metric = TriggerMetric::variance;
×
UNCOV
242
        } else if (temp == "rel_err") {
×
243
          keff_trigger.metric = TriggerMetric::relative_error;
×
244
        } else {
245
          fatal_error("Unrecognized keff trigger type " + temp);
×
246
        }
UNCOV
247
      } else {
×
UNCOV
248
        fatal_error("Specify keff trigger type in settings XML");
×
249
      }
250

251
      if (check_for_node(node_keff_trigger, "threshold")) {
101!
252
        keff_trigger.threshold =
202✔
253
          std::stod(get_node_value(node_keff_trigger, "threshold"));
202✔
254
        if (keff_trigger.threshold <= 0) {
101!
UNCOV
255
          fatal_error("keff trigger threshold must be positive");
×
256
        }
257
      } else {
UNCOV
258
        fatal_error("Specify keff trigger threshold in settings XML");
×
259
      }
260
    }
261
  }
262

263
  // Random ray variables
264
  if (solver_type == SolverType::RANDOM_RAY) {
7,533✔
265
    xml_node random_ray_node = node_base.child("random_ray");
762✔
266
    if (check_for_node(random_ray_node, "distance_active")) {
762!
267
      RandomRay::distance_active_ =
1,524✔
268
        std::stod(get_node_value(random_ray_node, "distance_active"));
1,524✔
269
      if (RandomRay::distance_active_ <= 0.0) {
762!
UNCOV
270
        fatal_error("Random ray active distance must be greater than 0");
×
271
      }
272
    } else {
UNCOV
273
      fatal_error("Specify random ray active distance in settings XML");
×
274
    }
275
    if (check_for_node(random_ray_node, "distance_inactive")) {
762!
276
      RandomRay::distance_inactive_ =
1,524✔
277
        std::stod(get_node_value(random_ray_node, "distance_inactive"));
1,524✔
278
      if (RandomRay::distance_inactive_ < 0) {
762!
UNCOV
279
        fatal_error(
×
280
          "Random ray inactive distance must be greater than or equal to 0");
281
      }
282
    } else {
UNCOV
283
      fatal_error("Specify random ray inactive distance in settings XML");
×
284
    }
285
    if (check_for_node(random_ray_node, "source")) {
762!
286
      xml_node source_node = random_ray_node.child("source");
762✔
287
      // Get point to list of <source> elements and make sure there is at least
288
      // one
289
      RandomRay::ray_source_ = Source::create(source_node);
1,524✔
290
    } else {
UNCOV
291
      fatal_error("Specify random ray source in settings XML");
×
292
    }
293
    if (check_for_node(random_ray_node, "volume_estimator")) {
762✔
294
      std::string temp_str =
121✔
295
        get_node_value(random_ray_node, "volume_estimator", true, true);
121✔
296
      if (temp_str == "simulation_averaged") {
121✔
297
        FlatSourceDomain::volume_estimator_ =
30✔
298
          RandomRayVolumeEstimator::SIMULATION_AVERAGED;
299
      } else if (temp_str == "naive") {
91✔
300
        FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::NAIVE;
61✔
301
      } else if (temp_str == "hybrid") {
30!
302
        FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID;
30✔
303
      } else {
UNCOV
304
        fatal_error("Unrecognized volume estimator: " + temp_str);
×
305
      }
306
    }
121✔
307
    if (check_for_node(random_ray_node, "source_shape")) {
762✔
308
      std::string temp_str =
450✔
309
        get_node_value(random_ray_node, "source_shape", true, true);
450✔
310
      if (temp_str == "flat") {
450✔
311
        RandomRay::source_shape_ = RandomRaySourceShape::FLAT;
75✔
312
      } else if (temp_str == "linear") {
375✔
313
        RandomRay::source_shape_ = RandomRaySourceShape::LINEAR;
330✔
314
      } else if (temp_str == "linear_xy") {
45!
315
        RandomRay::source_shape_ = RandomRaySourceShape::LINEAR_XY;
45✔
316
      } else {
UNCOV
317
        fatal_error("Unrecognized source shape: " + temp_str);
×
318
      }
319
    }
450✔
320
    if (check_for_node(random_ray_node, "volume_normalized_flux_tallies")) {
762✔
321
      FlatSourceDomain::volume_normalized_flux_tallies_ =
521✔
322
        get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies");
521✔
323
    }
324
    if (check_for_node(random_ray_node, "adjoint")) {
762✔
325
      FlatSourceDomain::adjoint_ =
30✔
326
        get_node_value_bool(random_ray_node, "adjoint");
30✔
327
    }
328
    if (check_for_node(random_ray_node, "sample_method")) {
762✔
329
      std::string temp_str =
30✔
330
        get_node_value(random_ray_node, "sample_method", true, true);
30✔
331
      if (temp_str == "prng") {
30!
UNCOV
332
        RandomRay::sample_method_ = RandomRaySampleMethod::PRNG;
×
333
      } else if (temp_str == "halton") {
30✔
334
        RandomRay::sample_method_ = RandomRaySampleMethod::HALTON;
15✔
335
      } else if (temp_str == "s2") {
15!
336
        RandomRay::sample_method_ = RandomRaySampleMethod::S2;
15✔
337
      } else {
UNCOV
338
        fatal_error("Unrecognized sample method: " + temp_str);
×
339
      }
340
    }
30✔
341
    if (check_for_node(random_ray_node, "source_region_meshes")) {
762✔
342
      pugi::xml_node node_source_region_meshes =
316✔
343
        random_ray_node.child("source_region_meshes");
316✔
344
      for (pugi::xml_node node_mesh :
692✔
345
        node_source_region_meshes.children("mesh")) {
692✔
346
        int mesh_id = std::stoi(node_mesh.attribute("id").value());
752✔
347
        for (pugi::xml_node node_domain : node_mesh.children("domain")) {
752✔
348
          int domain_id = std::stoi(node_domain.attribute("id").value());
752✔
349
          std::string domain_type = node_domain.attribute("type").value();
376✔
350
          Source::DomainType type;
376✔
351
          if (domain_type == "material") {
376✔
352
            type = Source::DomainType::MATERIAL;
30✔
353
          } else if (domain_type == "cell") {
346✔
354
            type = Source::DomainType::CELL;
30✔
355
          } else if (domain_type == "universe") {
316!
356
            type = Source::DomainType::UNIVERSE;
316✔
357
          } else {
UNCOV
358
            throw std::runtime_error("Unknown domain type: " + domain_type);
×
359
          }
360
          FlatSourceDomain::mesh_domain_map_[mesh_id].emplace_back(
376✔
361
            type, domain_id);
362
        }
376✔
363
      }
364
    }
365
    if (check_for_node(random_ray_node, "diagonal_stabilization_rho")) {
762✔
366
      FlatSourceDomain::diagonal_stabilization_rho_ = std::stod(
15✔
367
        get_node_value(random_ray_node, "diagonal_stabilization_rho"));
15✔
368
      if (FlatSourceDomain::diagonal_stabilization_rho_ < 0.0 ||
15!
369
          FlatSourceDomain::diagonal_stabilization_rho_ > 1.0) {
UNCOV
370
        fatal_error("Random ray diagonal stabilization rho factor must be "
×
371
                    "between 0 and 1");
372
      }
373
    }
374
  }
375
}
7,533✔
376

377
void read_settings_xml()
1,349✔
378
{
379
  using namespace settings;
1,349✔
380
  using namespace pugi;
1,349✔
381
  // Check if settings.xml exists
382
  std::string filename = settings::path_input + "settings.xml";
1,349✔
383
  if (!file_exists(filename)) {
1,349✔
384
    if (run_mode != RunMode::PLOTTING) {
22!
UNCOV
385
      fatal_error("Could not find any XML input files! In order to run OpenMC, "
×
386
                  "you first need a set of input files; at a minimum, this "
387
                  "includes settings.xml, geometry.xml, and materials.xml or a "
388
                  "single model XML file. Please consult the user's guide at "
389
                  "https://docs.openmc.org for further information.");
390
    } else {
391
      // The settings.xml file is optional if we just want to make a plot.
392
      return;
22✔
393
    }
394
  }
395

396
  // Parse settings.xml file
397
  xml_document doc;
1,327✔
398
  auto result = doc.load_file(filename.c_str());
1,327✔
399
  if (!result) {
1,327!
UNCOV
400
    fatal_error("Error processing settings.xml file.");
×
401
  }
402

403
  // Get root element
404
  xml_node root = doc.document_element();
1,327✔
405

406
  // Verbosity
407
  if (check_for_node(root, "verbosity") && verbosity == -1) {
1,327!
408
    verbosity = std::stoi(get_node_value(root, "verbosity"));
362✔
409
  } else if (verbosity == -1) {
1,146!
410
    verbosity = 7;
1,146✔
411
  }
412

413
  // To this point, we haven't displayed any output since we didn't know what
414
  // the verbosity is. Now that we checked for it, show the title if necessary
415
  if (mpi::master) {
1,327✔
416
    if (verbosity >= 2)
1,147✔
417
      title();
974✔
418
  }
419

420
  write_message("Reading settings XML file...", 5);
1,327✔
421

422
  read_settings_xml(root);
1,327✔
423
}
1,339✔
424

425
void read_settings_xml(pugi::xml_node root)
8,341✔
426
{
427
  using namespace settings;
8,341✔
428
  using namespace pugi;
8,341✔
429

430
  // Find if a multi-group or continuous-energy simulation is desired
431
  if (check_for_node(root, "energy_mode")) {
8,341✔
432
    std::string temp_str = get_node_value(root, "energy_mode", true, true);
1,286✔
433
    if (temp_str == "mg" || temp_str == "multi-group") {
2,572!
434
      run_CE = false;
1,286✔
UNCOV
435
    } else if (temp_str == "ce" || temp_str == "continuous-energy") {
×
UNCOV
436
      run_CE = true;
×
437
    }
438
  }
1,286✔
439

440
  // Check for user meshes and allocate
441
  read_meshes(root);
8,341✔
442

443
  // Look for deprecated cross_sections.xml file in settings.xml
444
  if (check_for_node(root, "cross_sections")) {
8,341!
UNCOV
445
    warning(
×
446
      "Setting cross_sections in settings.xml has been deprecated."
447
      " The cross_sections are now set in materials.xml and the "
448
      "cross_sections input to materials.xml and the OPENMC_CROSS_SECTIONS"
449
      " environment variable will take precendent over setting "
450
      "cross_sections in settings.xml.");
UNCOV
451
    path_cross_sections = get_node_value(root, "cross_sections");
×
452
  }
453

454
  if (!run_CE) {
8,341✔
455
    // Scattering Treatments
456
    if (check_for_node(root, "max_order")) {
1,286✔
457
      max_order = std::stoi(get_node_value(root, "max_order"));
30✔
458
    } else {
459
      // Set to default of largest int - 1, which means to use whatever is
460
      // contained in library. This is largest int - 1 because for legendre
461
      // scattering, a value of 1 is added to the order; adding 1 to the largest
462
      // int gets you the largest negative integer, which is not what we want.
463
      max_order = std::numeric_limits<int>::max() - 1;
1,271✔
464
    }
465
  }
466

467
  // Check for a trigger node and get trigger information
468
  if (check_for_node(root, "trigger")) {
8,341✔
469
    xml_node node_trigger = root.child("trigger");
156✔
470

471
    // Check if trigger(s) are to be turned on
472
    trigger_on = get_node_value_bool(node_trigger, "active");
156✔
473

474
    if (trigger_on) {
156✔
475
      if (check_for_node(node_trigger, "max_batches")) {
141!
476
        n_max_batches = std::stoi(get_node_value(node_trigger, "max_batches"));
282✔
477
      } else {
UNCOV
478
        fatal_error("<max_batches> must be specified with triggers");
×
479
      }
480

481
      // Get the batch interval to check triggers
482
      if (!check_for_node(node_trigger, "batch_interval")) {
141✔
483
        trigger_predict = true;
15✔
484
      } else {
485
        trigger_batch_interval =
252✔
486
          std::stoi(get_node_value(node_trigger, "batch_interval"));
252✔
487
        if (trigger_batch_interval <= 0) {
126!
UNCOV
488
          fatal_error("Trigger batch interval must be greater than zero");
×
489
        }
490
      }
491
    }
492
  }
493

494
  // Check run mode if it hasn't been set from the command line
495
  xml_node node_mode;
8,341✔
496
  if (run_mode == RunMode::UNSET) {
8,341✔
497
    if (check_for_node(root, "run_mode")) {
7,565✔
498
      std::string temp_str = get_node_value(root, "run_mode", true, true);
7,535✔
499
      if (temp_str == "eigenvalue") {
7,535✔
500
        run_mode = RunMode::EIGENVALUE;
4,540✔
501
      } else if (temp_str == "fixed source") {
2,995✔
502
        run_mode = RunMode::FIXED_SOURCE;
2,963✔
503
      } else if (temp_str == "plot") {
32!
504
        run_mode = RunMode::PLOTTING;
×
505
      } else if (temp_str == "particle restart") {
32!
UNCOV
506
        run_mode = RunMode::PARTICLE;
×
507
      } else if (temp_str == "volume") {
32!
508
        run_mode = RunMode::VOLUME;
32✔
509
      } else {
UNCOV
510
        fatal_error("Unrecognized run mode: " + temp_str);
×
511
      }
512

513
      // Assume XML specifies <particles>, <batches>, etc. directly
514
      node_mode = root;
7,535✔
515
    } else {
7,535✔
516
      warning("<run_mode> should be specified.");
30✔
517

518
      // Make sure that either eigenvalue or fixed source was specified
519
      node_mode = root.child("eigenvalue");
30✔
520
      if (node_mode) {
30!
521
        run_mode = RunMode::EIGENVALUE;
30✔
522
      } else {
523
        node_mode = root.child("fixed_source");
×
UNCOV
524
        if (node_mode) {
×
525
          run_mode = RunMode::FIXED_SOURCE;
×
526
        } else {
UNCOV
527
          fatal_error("<eigenvalue> or <fixed_source> not specified.");
×
528
        }
529
      }
530
    }
531
  }
532

533
  // Check solver type
534
  if (check_for_node(root, "random_ray")) {
8,341✔
535
    solver_type = SolverType::RANDOM_RAY;
762✔
536
    if (run_CE)
762!
UNCOV
537
      fatal_error("multi-group energy mode must be specified in settings XML "
×
538
                  "when using the random ray solver.");
539
  }
540

541
  if (run_mode == RunMode::EIGENVALUE || run_mode == RunMode::FIXED_SOURCE) {
8,341✔
542
    // Read run parameters
543
    get_run_parameters(node_mode);
7,533✔
544

545
    // Check number of active batches, inactive batches, max lost particles and
546
    // particles
547
    if (n_batches <= n_inactive) {
7,533!
548
      fatal_error("Number of active batches must be greater than zero.");
×
549
    } else if (n_inactive < 0) {
7,533!
550
      fatal_error("Number of inactive batches must be non-negative.");
×
551
    } else if (n_particles <= 0) {
7,533!
552
      fatal_error("Number of particles must be greater than zero.");
×
553
    } else if (max_lost_particles <= 0) {
7,533!
554
      fatal_error("Number of max lost particles must be greater than zero.");
×
555
    } else if (rel_max_lost_particles <= 0.0 || rel_max_lost_particles >= 1.0) {
7,533!
UNCOV
556
      fatal_error("Relative max lost particles must be between zero and one.");
×
557
    }
558

559
    // Check for user value for the number of generation of the Iterated Fission
560
    // Probability (IFP) method
561
    if (check_for_node(root, "ifp_n_generation")) {
7,533✔
562
      ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation"));
166✔
563
      if (ifp_n_generation <= 0) {
83!
UNCOV
564
        fatal_error("'ifp_n_generation' must be greater than 0.");
×
565
      }
566
      // Avoid tallying 0 if IFP logs are not complete when active cycles start
567
      if (ifp_n_generation > n_inactive) {
83✔
568
        fatal_error("'ifp_n_generation' must be lower than or equal to the "
9✔
569
                    "number of inactive cycles.");
570
      }
571
    }
572
  }
573

574
  // Copy plotting random number seed if specified
575
  if (check_for_node(root, "plot_seed")) {
8,332!
UNCOV
576
    auto seed = std::stoll(get_node_value(root, "plot_seed"));
×
UNCOV
577
    model::plotter_seed = seed;
×
578
  }
579

580
  // Copy random number seed if specified
581
  if (check_for_node(root, "seed")) {
8,332✔
582
    auto seed = std::stoll(get_node_value(root, "seed"));
1,214✔
583
    openmc_set_seed(seed);
607✔
584
  }
585

586
  // Copy random number stride if specified
587
  if (check_for_node(root, "stride")) {
8,332✔
588
    auto stride = std::stoull(get_node_value(root, "stride"));
30✔
589
    openmc_set_stride(stride);
15✔
590
  }
591

592
  // Check for electron treatment
593
  if (check_for_node(root, "electron_treatment")) {
8,332✔
594
    auto temp_str = get_node_value(root, "electron_treatment", true, true);
82✔
595
    if (temp_str == "led") {
82✔
596
      electron_treatment = ElectronTreatment::LED;
26✔
597
    } else if (temp_str == "ttb") {
56!
598
      electron_treatment = ElectronTreatment::TTB;
56✔
599
    } else {
UNCOV
600
      fatal_error("Unrecognized electron treatment: " + temp_str + ".");
×
601
    }
602
  }
82✔
603

604
  // Check for photon transport
605
  if (check_for_node(root, "photon_transport")) {
8,332✔
606
    photon_transport = get_node_value_bool(root, "photon_transport");
205✔
607

608
    if (!run_CE && photon_transport) {
205!
UNCOV
609
      fatal_error("Photon transport is not currently supported in "
×
610
                  "multigroup mode");
611
    }
612
  }
613

614
  // Check for atomic relaxation
615
  if (check_for_node(root, "atomic_relaxation")) {
8,332✔
616
    atomic_relaxation = get_node_value_bool(root, "atomic_relaxation");
15✔
617
  }
618

619
  // Number of bins for logarithmic grid
620
  if (check_for_node(root, "log_grid_bins")) {
8,332✔
621
    n_log_bins = std::stoi(get_node_value(root, "log_grid_bins"));
30✔
622
    if (n_log_bins < 1) {
15!
623
      fatal_error("Number of bins for logarithmic grid must be greater "
×
624
                  "than zero.");
625
    }
626
  }
627

628
  // Number of OpenMP threads
629
  if (check_for_node(root, "threads")) {
8,332!
UNCOV
630
    if (mpi::master)
×
UNCOV
631
      warning("The <threads> element has been deprecated. Use "
×
632
              "the OMP_NUM_THREADS environment variable to set the number of "
633
              "threads.");
634
  }
635

636
  // ==========================================================================
637
  // EXTERNAL SOURCE
638

639
  // Get point to list of <source> elements and make sure there is at least one
640
  for (pugi::xml_node node : root.children("source")) {
16,203✔
641
    model::external_sources.push_back(Source::create(node));
15,752✔
642
  }
643

644
  // Check if the user has specified to read surface source
645
  if (check_for_node(root, "surf_source_read")) {
8,322✔
646
    surf_source_read = true;
30✔
647
    // Get surface source read node
648
    xml_node node_ssr = root.child("surf_source_read");
30✔
649

650
    std::string path = "surface_source.h5";
30✔
651
    // Check if the user has specified different file for surface source reading
652
    if (check_for_node(node_ssr, "path")) {
30!
653
      path = get_node_value(node_ssr, "path", false, true);
30✔
654
    }
655
    model::external_sources.push_back(make_unique<FileSource>(path));
30✔
656
  }
30✔
657

658
  // If no source specified, default to isotropic point source at origin with
659
  // Watt spectrum. No default source is needed in random ray mode.
660
  if (model::external_sources.empty() &&
8,322✔
661
      settings::solver_type != SolverType::RANDOM_RAY) {
2,261✔
662
    double T[] {0.0};
2,115✔
663
    double p[] {1.0};
2,115✔
664
    model::external_sources.push_back(make_unique<IndependentSource>(
2,115✔
665
      UPtrSpace {new SpatialPoint({0.0, 0.0, 0.0})},
4,230✔
666
      UPtrAngle {new Isotropic()}, UPtrDist {new Watt(0.988e6, 2.249e-6)},
4,230✔
667
      UPtrDist {new Discrete(T, p, 1)}));
4,230✔
668
  }
669

670
  // Build probability mass function for sampling external sources
671
  vector<double> source_strengths;
8,322✔
672
  for (auto& s : model::external_sources) {
18,371✔
673
    source_strengths.push_back(s->strength());
10,049✔
674
  }
675
  model::external_sources_probability.assign(source_strengths);
8,322✔
676

677
  // Check if we want to write out source
678
  if (check_for_node(root, "write_initial_source")) {
8,322!
UNCOV
679
    write_initial_source = get_node_value_bool(root, "write_initial_source");
×
680
  }
681

682
  // Get relative number of lost particles
683
  if (check_for_node(root, "source_rejection_fraction")) {
8,322✔
684
    source_rejection_fraction =
14✔
685
      std::stod(get_node_value(root, "source_rejection_fraction"));
14!
686
  }
687

688
  if (check_for_node(root, "free_gas_threshold")) {
8,322!
UNCOV
689
    free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold"));
×
690
  }
691

692
  // Surface grazing
693
  if (check_for_node(root, "surface_grazing_cutoff"))
8,322!
UNCOV
694
    surface_grazing_cutoff =
×
UNCOV
695
      std::stod(get_node_value(root, "surface_grazing_cutoff"));
×
696
  if (check_for_node(root, "surface_grazing_ratio"))
8,322!
UNCOV
697
    surface_grazing_ratio =
×
UNCOV
698
      std::stod(get_node_value(root, "surface_grazing_ratio"));
×
699

700
  // Survival biasing
701
  if (check_for_node(root, "survival_biasing")) {
8,322✔
702
    survival_biasing = get_node_value_bool(root, "survival_biasing");
190✔
703
  }
704

705
  // Probability tables
706
  if (check_for_node(root, "ptables")) {
8,322✔
707
    urr_ptables_on = get_node_value_bool(root, "ptables");
15✔
708
  }
709

710
  // Cutoffs
711
  if (check_for_node(root, "cutoff")) {
8,322✔
712
    xml_node node_cutoff = root.child("cutoff");
138✔
713
    if (check_for_node(node_cutoff, "weight")) {
138✔
714
      weight_cutoff = std::stod(get_node_value(node_cutoff, "weight"));
30✔
715
    }
716
    if (check_for_node(node_cutoff, "weight_avg")) {
138✔
717
      weight_survive = std::stod(get_node_value(node_cutoff, "weight_avg"));
30✔
718
    }
719
    if (check_for_node(node_cutoff, "survival_normalization")) {
138!
720
      survival_normalization =
×
UNCOV
721
        get_node_value_bool(node_cutoff, "survival_normalization");
×
722
    }
723
    if (check_for_node(node_cutoff, "energy_neutron")) {
138✔
724
      energy_cutoff[0] =
15✔
725
        std::stod(get_node_value(node_cutoff, "energy_neutron"));
30✔
726
    } else if (check_for_node(node_cutoff, "energy")) {
123!
UNCOV
727
      warning("The use of an <energy> cutoff is deprecated and should "
×
728
              "be replaced by <energy_neutron>.");
729
      energy_cutoff[0] = std::stod(get_node_value(node_cutoff, "energy"));
×
730
    }
731
    if (check_for_node(node_cutoff, "energy_photon")) {
138✔
732
      energy_cutoff[1] =
82✔
733
        std::stod(get_node_value(node_cutoff, "energy_photon"));
164✔
734
    }
735
    if (check_for_node(node_cutoff, "energy_electron")) {
138!
UNCOV
736
      energy_cutoff[2] =
×
UNCOV
737
        std::stof(get_node_value(node_cutoff, "energy_electron"));
×
738
    }
739
    if (check_for_node(node_cutoff, "energy_positron")) {
138!
740
      energy_cutoff[3] =
×
UNCOV
741
        std::stod(get_node_value(node_cutoff, "energy_positron"));
×
742
    }
743
    if (check_for_node(node_cutoff, "time_neutron")) {
138✔
744
      time_cutoff[0] = std::stod(get_node_value(node_cutoff, "time_neutron"));
26✔
745
    }
746
    if (check_for_node(node_cutoff, "time_photon")) {
138!
UNCOV
747
      time_cutoff[1] = std::stod(get_node_value(node_cutoff, "time_photon"));
×
748
    }
749
    if (check_for_node(node_cutoff, "time_electron")) {
138!
UNCOV
750
      time_cutoff[2] = std::stod(get_node_value(node_cutoff, "time_electron"));
×
751
    }
752
    if (check_for_node(node_cutoff, "time_positron")) {
138!
UNCOV
753
      time_cutoff[3] = std::stod(get_node_value(node_cutoff, "time_positron"));
×
754
    }
755
  }
756

757
  // read properties from file
758
  if (check_for_node(root, "properties_file")) {
8,322✔
759
    properties_file = get_node_value(root, "properties_file");
11✔
760
    if (!file_exists(properties_file)) {
11!
UNCOV
761
      fatal_error(fmt::format("File '{}' does not exist.", properties_file));
×
762
    }
763
  }
764

765
  // Particle trace
766
  if (check_for_node(root, "trace")) {
8,322✔
767
    auto temp = get_node_array<int64_t>(root, "trace");
15✔
768
    if (temp.size() != 3) {
15!
UNCOV
769
      fatal_error("Must provide 3 integers for <trace> that specify the "
×
770
                  "batch, generation, and particle number.");
771
    }
772
    trace_batch = temp.at(0);
15✔
773
    trace_gen = temp.at(1);
15✔
774
    trace_particle = temp.at(2);
15✔
775
  }
15✔
776

777
  // Particle tracks
778
  if (check_for_node(root, "track")) {
8,322✔
779
    // Get values and make sure there are three per particle
780
    auto temp = get_node_array<int>(root, "track");
45✔
781
    if (temp.size() % 3 != 0) {
45!
UNCOV
782
      fatal_error(
×
783
        "Number of integers specified in 'track' is not "
784
        "divisible by 3.  Please provide 3 integers per particle to be "
785
        "tracked.");
786
    }
787

788
    // Reshape into track_identifiers
789
    int n_tracks = temp.size() / 3;
45✔
790
    for (int i = 0; i < n_tracks; ++i) {
180✔
791
      track_identifiers.push_back(
135✔
792
        {temp[3 * i], temp[3 * i + 1], temp[3 * i + 2]});
135✔
793
    }
794
  }
45✔
795

796
  // Shannon entropy
797
  if (solver_type == SolverType::RANDOM_RAY) {
8,322✔
798
    if (check_for_node(root, "entropy_mesh")) {
762!
799
      fatal_error("Random ray uses FSRs to compute the Shannon entropy. "
×
800
                  "No user-defined entropy mesh is supported.");
801
    }
802
    entropy_on = true;
762✔
803
  } else if (solver_type == SolverType::MONTE_CARLO) {
7,560!
804
    if (check_for_node(root, "entropy_mesh")) {
7,560✔
805
      int temp = std::stoi(get_node_value(root, "entropy_mesh"));
668✔
806
      if (model::mesh_map.find(temp) == model::mesh_map.end()) {
334!
UNCOV
807
        fatal_error(fmt::format(
×
808
          "Mesh {} specified for Shannon entropy does not exist.", temp));
809
      }
810

811
      auto* m = dynamic_cast<RegularMesh*>(
334!
812
        model::meshes[model::mesh_map.at(temp)].get());
334!
813
      if (!m)
334!
UNCOV
814
        fatal_error("Only regular meshes can be used as an entropy mesh");
×
815
      simulation::entropy_mesh = m;
334✔
816

817
      // Turn on Shannon entropy calculation
818
      entropy_on = true;
334✔
819

820
    } else if (check_for_node(root, "entropy")) {
7,226!
UNCOV
821
      fatal_error(
×
822
        "Specifying a Shannon entropy mesh via the <entropy> element "
823
        "is deprecated. Please create a mesh using <mesh> and then reference "
824
        "it by specifying its ID in an <entropy_mesh> element.");
825
    }
826
  }
827
  // Uniform fission source weighting mesh
828
  if (check_for_node(root, "ufs_mesh")) {
8,322✔
829
    auto temp = std::stoi(get_node_value(root, "ufs_mesh"));
30✔
830
    if (model::mesh_map.find(temp) == model::mesh_map.end()) {
15!
831
      fatal_error(fmt::format("Mesh {} specified for uniform fission site "
×
832
                              "method does not exist.",
833
        temp));
834
    }
835

836
    auto* m =
15✔
837
      dynamic_cast<RegularMesh*>(model::meshes[model::mesh_map.at(temp)].get());
15!
838
    if (!m)
15!
UNCOV
839
      fatal_error("Only regular meshes can be used as a UFS mesh");
×
840
    simulation::ufs_mesh = m;
15✔
841

842
    // Turn on uniform fission source weighting
843
    ufs_on = true;
15✔
844

845
  } else if (check_for_node(root, "uniform_fs")) {
8,307!
UNCOV
846
    fatal_error(
×
847
      "Specifying a UFS mesh via the <uniform_fs> element "
848
      "is deprecated. Please create a mesh using <mesh> and then reference "
849
      "it by specifying its ID in a <ufs_mesh> element.");
850
  }
851

852
  // Check if the user has specified to write state points
853
  if (check_for_node(root, "state_point")) {
8,322✔
854

855
    // Get pointer to state_point node
856
    auto node_sp = root.child("state_point");
160✔
857

858
    // Determine number of batches at which to store state points
859
    if (check_for_node(node_sp, "batches")) {
160!
860
      // User gave specific batches to write state points
861
      auto temp = get_node_array<int>(node_sp, "batches");
160✔
862
      for (const auto& b : temp) {
491✔
863
        statepoint_batch.insert(b);
331✔
864
      }
865
    } else {
160✔
866
      // If neither were specified, write state point at last batch
UNCOV
867
      statepoint_batch.insert(n_batches);
×
868
    }
869
  } else {
870
    // If no <state_point> tag was present, by default write state point at
871
    // last batch only
872
    statepoint_batch.insert(n_batches);
8,162✔
873
  }
874

875
  // Check if the user has specified to write source points
876
  if (check_for_node(root, "source_point")) {
8,322✔
877
    // Get source_point node
878
    xml_node node_sp = root.child("source_point");
101✔
879

880
    // Determine batches at which to store source points
881
    if (check_for_node(node_sp, "batches")) {
101✔
882
      // User gave specific batches to write source points
883
      auto temp = get_node_array<int>(node_sp, "batches");
45✔
884
      for (const auto& b : temp) {
120✔
885
        sourcepoint_batch.insert(b);
75✔
886
      }
887
    } else {
45✔
888
      // If neither were specified, write source points with state points
889
      sourcepoint_batch = statepoint_batch;
56!
890
    }
891

892
    // Check if the user has specified to write binary source file
893
    if (check_for_node(node_sp, "separate")) {
101✔
894
      source_separate = get_node_value_bool(node_sp, "separate");
71✔
895
    }
896
    if (check_for_node(node_sp, "write")) {
101!
UNCOV
897
      source_write = get_node_value_bool(node_sp, "write");
×
898
    }
899
    if (check_for_node(node_sp, "mcpl")) {
101✔
900
      source_mcpl_write = get_node_value_bool(node_sp, "mcpl");
26✔
901
    }
902
    if (check_for_node(node_sp, "overwrite_latest")) {
101✔
903
      source_latest = get_node_value_bool(node_sp, "overwrite_latest");
15✔
904
      source_separate = source_latest;
15✔
905
    }
906
  } else {
907
    // If no <source_point> tag was present, by default we keep source bank in
908
    // statepoint file and write it out at statepoints intervals
909
    source_separate = false;
8,221✔
910
    sourcepoint_batch = statepoint_batch;
8,221!
911
  }
912

913
  // Check is the user specified to convert strength to statistical weight
914
  if (check_for_node(root, "uniform_source_sampling")) {
8,322✔
915
    uniform_source_sampling =
55✔
916
      get_node_value_bool(root, "uniform_source_sampling");
55✔
917
  }
918

919
  // Check if the user has specified to write surface source
920
  if (check_for_node(root, "surf_source_write")) {
8,322✔
921
    surf_source_write = true;
531✔
922
    // Get surface source write node
923
    xml_node node_ssw = root.child("surf_source_write");
531✔
924

925
    // Determine surface ids at which crossing particles are to be banked.
926
    // If no surfaces are specified, all surfaces in the model will be used
927
    // to bank source points.
928
    if (check_for_node(node_ssw, "surface_ids")) {
531✔
929
      auto temp = get_node_array<int>(node_ssw, "surface_ids");
276✔
930
      for (const auto& b : temp) {
1,232✔
931
        source_write_surf_id.insert(b);
956✔
932
      }
933
    }
276✔
934

935
    // Get maximum number of particles to be banked per surface
936
    if (check_for_node(node_ssw, "max_particles")) {
531✔
937
      ssw_max_particles = std::stoll(get_node_value(node_ssw, "max_particles"));
1,044✔
938
    } else {
939
      fatal_error("A maximum number of particles needs to be specified "
9✔
940
                  "using the 'max_particles' parameter to store surface "
941
                  "source points.");
942
    }
943

944
    // Get maximum number of surface source files to be created
945
    if (check_for_node(node_ssw, "max_source_files")) {
522✔
946
      ssw_max_files = std::stoll(get_node_value(node_ssw, "max_source_files"));
66✔
947
    } else {
948
      ssw_max_files = 1;
489✔
949
    }
950

951
    // Determine if sites are to be stored in MCPL format
952
    if (check_for_node(node_ssw, "mcpl")) {
522✔
953
      surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl");
11✔
954
    }
955

956
    // Get cells information from 'cells'
957
    if (check_for_node(node_ssw, "cells")) {
522✔
958
      // Error if the new syntax is mixed with the previous syntax
959
      if (check_for_node(node_ssw, "cell") ||
211✔
960
          check_for_node(node_ssw, "cellfrom") ||
202✔
961
          check_for_node(node_ssw, "cellto")) {
92✔
962
        fatal_error("In the <surf_source_write> element, 'cells' cannot be "
27✔
963
                    "used at the same time with 'cell', "
964
                    "'cellfrom' or 'cellto'.");
965
      }
966

967
      // Read 'cells' information
968
      auto ids = get_node_array<int64_t>(node_ssw, "cells");
83✔
969
      // If 'directions' is declared, retrieve directions
970
      if (check_for_node(node_ssw, "directions")) {
83!
971
        auto directions = get_node_array<std::string>(node_ssw, "directions");
83✔
972

973
        // Check that 'cells' and 'directions' have the same length
974
        if (directions.size() != ids.size()) {
83✔
975
          fatal_error("In the <surf_source_write> element, 'directions' must "
9✔
976
                      "have the same length as 'cells'.");
977
        }
978

979
        // Store directions
980
        for (std::size_t i {0}; i < ids.size(); ++i) {
202✔
981
          SSWCellType direction = ssw_cell_type_from_string(directions[i]);
128✔
982
          auto [it, inserted] = ssw_cells.emplace(ids[i], direction);
128✔
983
          // check for duplicate keys with different values
984
          if (!inserted && it->second != direction) {
128✔
985
            // the union of different values will always be 'Both'
986
            it->second = SSWCellType::Both;
33✔
987
          }
988
        }
989
      } else {
74✔
990
        // If 'directions' is not declared, assume 'both' for every cells
NEW
991
        for (std::size_t i {0}; i < ids.size(); ++i) {
×
NEW
992
          ssw_cells.emplace(ids[i], SSWCellType::Both);
×
993
        }
994
      }
995
    } else {
74✔
996
      // If 'cells' is not declared, get cells information from 'cell', 'cellto'
997
      // or 'cellfrom' instead - will be deprecated in the future
998
      int64_t ssw_cell_id {C_NONE};
412✔
999

1000
      // Error if 'directions' is set without 'cells'
1001
      if (check_for_node(node_ssw, "directions")) {
412✔
1002
        fatal_error("In the <surf_source_write> element, 'directions' cannot "
9✔
1003
                    "be used if 'cells' is not defined.");
1004
      }
1005

1006
      // Cell
1007
      if (check_for_node(node_ssw, "cell")) {
403✔
1008
        warning("In the <surf_source_write> element, 'cell' is deprecated and "
104✔
1009
                "will be removed in the future. Please use "
1010
                "'cells' and 'directions' instead.");
1011
        ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell"));
208✔
1012
        ssw_cells.emplace(ssw_cell_id, SSWCellType::Both);
104✔
1013
      }
1014
      // Cellfrom
1015
      if (check_for_node(node_ssw, "cellfrom")) {
403✔
1016
        warning("In the <surf_source_write> element, 'cellfrom' is deprecated "
90✔
1017
                "and will be removed in the future. "
1018
                "Please use 'cells' and 'directions' instead.");
1019
        if (ssw_cell_id != C_NONE) {
90✔
1020
          fatal_error("In the <surf_source_write> element, 'cell', 'cellfrom' "
18✔
1021
                      "and 'cellto' cannot be used at the same time.");
1022
        }
1023
        ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellfrom"));
144✔
1024
        ssw_cells.emplace(ssw_cell_id, SSWCellType::From);
72✔
1025
      }
1026
      // Cellto
1027
      if (check_for_node(node_ssw, "cellto")) {
385✔
1028
        warning("In the <surf_source_write> element, 'cellto' is deprecated "
71✔
1029
                "and will be removed in the future. Please use "
1030
                "'cells' and 'directions' instead.");
1031
        if (ssw_cell_id != C_NONE) {
71✔
1032
          fatal_error("In the <surf_source_write> element, 'cell', 'cellfrom' "
18✔
1033
                      "and 'cellto' cannot be used at the same time.");
1034
        }
1035
        ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellto"));
106✔
1036
        ssw_cells.emplace(ssw_cell_id, SSWCellType::To);
53✔
1037
      }
1038
    }
1039
  }
1040

1041
  // Check if the user has specified to write specific collisions
1042
  if (check_for_node(root, "collision_track")) {
8,232✔
1043
    settings::collision_track = true;
148✔
1044
    // Get collision track node
1045
    xml_node node_ct = root.child("collision_track");
148✔
1046
    collision_track_config = CollisionTrackConfig {};
148✔
1047

1048
    // Determine cell ids at which crossing particles are to be banked
1049
    if (check_for_node(node_ct, "cell_ids")) {
148✔
1050
      auto temp = get_node_array<int>(node_ct, "cell_ids");
78✔
1051
      for (const auto& b : temp) {
204✔
1052
        collision_track_config.cell_ids.insert(b);
126✔
1053
      }
1054
    }
78✔
1055
    if (check_for_node(node_ct, "reactions")) {
148✔
1056
      auto temp = get_node_array<std::string>(node_ct, "reactions");
63✔
1057
      for (const auto& b : temp) {
171✔
1058
        int reaction_int = reaction_mt(b);
108✔
1059
        if (reaction_int > 0) {
108!
1060
          collision_track_config.mt_numbers.insert(reaction_int);
108✔
1061
        }
1062
      }
1063
    }
63✔
1064
    if (check_for_node(node_ct, "universe_ids")) {
148✔
1065
      auto temp = get_node_array<int>(node_ct, "universe_ids");
30✔
1066
      for (const auto& b : temp) {
60✔
1067
        collision_track_config.universe_ids.insert(b);
30✔
1068
      }
1069
    }
30✔
1070
    if (check_for_node(node_ct, "material_ids")) {
148✔
1071
      auto temp = get_node_array<int>(node_ct, "material_ids");
30✔
1072
      for (const auto& b : temp) {
75✔
1073
        collision_track_config.material_ids.insert(b);
45✔
1074
      }
1075
    }
30✔
1076
    if (check_for_node(node_ct, "nuclides")) {
148✔
1077
      auto temp = get_node_array<std::string>(node_ct, "nuclides");
30✔
1078
      for (const auto& b : temp) {
120✔
1079
        collision_track_config.nuclides.insert(b);
90✔
1080
      }
1081
    }
30✔
1082
    if (check_for_node(node_ct, "deposited_E_threshold")) {
148✔
1083
      collision_track_config.deposited_energy_threshold =
60✔
1084
        std::stod(get_node_value(node_ct, "deposited_E_threshold"));
60✔
1085
    }
1086
    // Get maximum number of particles to be banked per collision
1087
    if (check_for_node(node_ct, "max_collisions")) {
148!
1088
      collision_track_config.max_collisions =
296✔
1089
        std::stoll(get_node_value(node_ct, "max_collisions"));
296✔
1090
    } else {
UNCOV
1091
      warning("A maximum number of collisions needs to be specified. "
×
1092
              "By default the code sets 'max_collisions' parameter equals to "
1093
              "1000.");
1094
    }
1095
    // Get maximum number of collision_track files to be created
1096
    if (check_for_node(node_ct, "max_collision_track_files")) {
148!
UNCOV
1097
      collision_track_config.max_files =
×
UNCOV
1098
        std::stoll(get_node_value(node_ct, "max_collision_track_files"));
×
1099
    }
1100
    if (check_for_node(node_ct, "mcpl")) {
148✔
1101
      collision_track_config.mcpl_write = get_node_value_bool(node_ct, "mcpl");
22✔
1102
    }
1103
  }
1104

1105
  // If source is not separate and is to be written out in the statepoint
1106
  // file, make sure that the sourcepoint batch numbers are contained in the
1107
  // statepoint list
1108
  if (!source_separate) {
8,232✔
1109
    for (const auto& b : sourcepoint_batch) {
16,418✔
1110
      if (!contains(statepoint_batch, b)) {
16,544!
UNCOV
1111
        fatal_error(
×
1112
          "Sourcepoint batches are not a subset of statepoint batches.");
1113
      }
1114
    }
1115
  }
1116

1117
  // Check if the user has specified to not reduce tallies at the end of every
1118
  // batch
1119
  if (check_for_node(root, "no_reduce")) {
8,232✔
1120
    reduce_tallies = !get_node_value_bool(root, "no_reduce");
30✔
1121
  }
1122

1123
  // Check if the user has specified to use confidence intervals for
1124
  // uncertainties rather than standard deviations
1125
  if (check_for_node(root, "confidence_intervals")) {
8,232✔
1126
    confidence_intervals = get_node_value_bool(root, "confidence_intervals");
15✔
1127
  }
1128

1129
  // Check for output options
1130
  if (check_for_node(root, "output")) {
8,232✔
1131
    // Get pointer to output node
1132
    pugi::xml_node node_output = root.child("output");
764✔
1133

1134
    // Check for summary option
1135
    if (check_for_node(node_output, "summary")) {
764✔
1136
      output_summary = get_node_value_bool(node_output, "summary");
738✔
1137
    }
1138

1139
    // Check for ASCII tallies output option
1140
    if (check_for_node(node_output, "tallies")) {
764✔
1141
      output_tallies = get_node_value_bool(node_output, "tallies");
349✔
1142
    }
1143

1144
    // Set output directory if a path has been specified
1145
    if (check_for_node(node_output, "path")) {
764!
1146
      path_output = get_node_value(node_output, "path");
×
UNCOV
1147
      if (!ends_with(path_output, "/")) {
×
1148
        path_output += "/";
764!
1149
      }
1150
    }
1151
  }
1152

1153
  // Resonance scattering parameters
1154
  if (check_for_node(root, "resonance_scattering")) {
8,232✔
1155
    xml_node node_res_scat = root.child("resonance_scattering");
15✔
1156

1157
    // See if resonance scattering is enabled
1158
    if (check_for_node(node_res_scat, "enable")) {
15!
1159
      res_scat_on = get_node_value_bool(node_res_scat, "enable");
15✔
1160
    } else {
UNCOV
1161
      res_scat_on = true;
×
1162
    }
1163

1164
    // Determine what method is used
1165
    if (check_for_node(node_res_scat, "method")) {
15!
1166
      auto temp = get_node_value(node_res_scat, "method", true, true);
15✔
1167
      if (temp == "rvs") {
15!
1168
        res_scat_method = ResScatMethod::rvs;
15✔
UNCOV
1169
      } else if (temp == "dbrc") {
×
UNCOV
1170
        res_scat_method = ResScatMethod::dbrc;
×
1171
      } else {
UNCOV
1172
        fatal_error(
×
UNCOV
1173
          "Unrecognized resonance elastic scattering method: " + temp + ".");
×
1174
      }
1175
    }
15✔
1176

1177
    // Minimum energy for resonance scattering
1178
    if (check_for_node(node_res_scat, "energy_min")) {
15!
1179
      res_scat_energy_min =
30✔
1180
        std::stod(get_node_value(node_res_scat, "energy_min"));
30✔
1181
    }
1182
    if (res_scat_energy_min < 0.0) {
15!
UNCOV
1183
      fatal_error("Lower resonance scattering energy bound is negative");
×
1184
    }
1185

1186
    // Maximum energy for resonance scattering
1187
    if (check_for_node(node_res_scat, "energy_max")) {
15!
1188
      res_scat_energy_max =
30✔
1189
        std::stod(get_node_value(node_res_scat, "energy_max"));
30✔
1190
    }
1191
    if (res_scat_energy_max < res_scat_energy_min) {
15!
UNCOV
1192
      fatal_error("Upper resonance scattering energy bound is below the "
×
1193
                  "lower resonance scattering energy bound.");
1194
    }
1195

1196
    // Get resonance scattering nuclides
1197
    if (check_for_node(node_res_scat, "nuclides")) {
15!
1198
      res_scat_nuclides =
15✔
1199
        get_node_array<std::string>(node_res_scat, "nuclides");
30✔
1200
    }
1201
  }
1202

1203
  // Get volume calculations
1204
  for (pugi::xml_node node_vol : root.children("volume_calc")) {
8,546✔
1205
    model::volume_calcs.emplace_back(node_vol);
314✔
1206
  }
1207

1208
  // Get temperature settings
1209
  if (check_for_node(root, "temperature_default")) {
8,232✔
1210
    temperature_default =
342✔
1211
      std::stod(get_node_value(root, "temperature_default"));
342✔
1212
  }
1213
  if (check_for_node(root, "temperature_method")) {
8,232✔
1214
    auto temp = get_node_value(root, "temperature_method", true, true);
485✔
1215
    if (temp == "nearest") {
485✔
1216
      temperature_method = TemperatureMethod::NEAREST;
304✔
1217
    } else if (temp == "interpolation") {
181!
1218
      temperature_method = TemperatureMethod::INTERPOLATION;
181✔
1219
    } else {
UNCOV
1220
      fatal_error("Unknown temperature method: " + temp);
×
1221
    }
1222
  }
485✔
1223
  if (check_for_node(root, "temperature_tolerance")) {
8,232✔
1224
    temperature_tolerance =
680✔
1225
      std::stod(get_node_value(root, "temperature_tolerance"));
680✔
1226
  }
1227
  if (check_for_node(root, "temperature_multipole")) {
8,232✔
1228
    temperature_multipole = get_node_value_bool(root, "temperature_multipole");
185✔
1229

1230
    // Multipole currently doesn't work with photon transport
1231
    if (temperature_multipole && photon_transport) {
185!
UNCOV
1232
      fatal_error("Multipole data cannot currently be used in conjunction with "
×
1233
                  "photon transport.");
1234
    }
1235
  }
1236
  if (check_for_node(root, "temperature_range")) {
8,232✔
1237
    auto range = get_node_array<double>(root, "temperature_range");
170✔
1238
    temperature_range[0] = range.at(0);
170✔
1239
    temperature_range[1] = range.at(1);
170✔
1240
  }
170✔
1241

1242
  // Check for tabular_legendre options
1243
  if (check_for_node(root, "tabular_legendre")) {
8,232✔
1244
    // Get pointer to tabular_legendre node
1245
    xml_node node_tab_leg = root.child("tabular_legendre");
90✔
1246

1247
    // Check for enable option
1248
    if (check_for_node(node_tab_leg, "enable")) {
90!
1249
      legendre_to_tabular = get_node_value_bool(node_tab_leg, "enable");
90✔
1250
    }
1251

1252
    // Check for the number of points
1253
    if (check_for_node(node_tab_leg, "num_points")) {
90!
UNCOV
1254
      legendre_to_tabular_points =
×
UNCOV
1255
        std::stoi(get_node_value(node_tab_leg, "num_points"));
×
UNCOV
1256
      if (legendre_to_tabular_points <= 1 && !run_CE) {
×
UNCOV
1257
        fatal_error(
×
1258
          "The 'num_points' subelement/attribute of the "
1259
          "<tabular_legendre> element must contain a value greater than 1");
1260
      }
1261
    }
1262
  }
1263

1264
  // Check whether create delayed neutrons in fission
1265
  if (check_for_node(root, "create_delayed_neutrons")) {
8,232!
1266
    create_delayed_neutrons =
×
UNCOV
1267
      get_node_value_bool(root, "create_delayed_neutrons");
×
1268
  }
1269

1270
  // Check whether create fission sites
1271
  if (run_mode == RunMode::FIXED_SOURCE) {
8,232✔
1272
    if (check_for_node(root, "create_fission_neutrons")) {
2,872✔
1273
      create_fission_neutrons =
280✔
1274
        get_node_value_bool(root, "create_fission_neutrons");
280✔
1275
    }
1276
  }
1277

1278
  // Check whether to scale fission photon yields
1279
  if (check_for_node(root, "delayed_photon_scaling")) {
8,232!
UNCOV
1280
    delayed_photon_scaling =
×
UNCOV
1281
      get_node_value_bool(root, "delayed_photon_scaling");
×
1282
  }
1283

1284
  // Check whether to use event-based parallelism
1285
  if (check_for_node(root, "event_based")) {
8,232!
UNCOV
1286
    event_based = get_node_value_bool(root, "event_based");
×
1287
  }
1288

1289
  // Check whether material cell offsets should be generated
1290
  if (check_for_node(root, "material_cell_offsets")) {
8,232!
1291
    material_cell_offsets = get_node_value_bool(root, "material_cell_offsets");
×
1292
  }
1293

1294
  // Weight window information
1295
  for (pugi::xml_node node_ww : root.children("weight_windows")) {
8,337✔
1296
    variance_reduction::weight_windows.emplace_back(
105✔
1297
      std::make_unique<WeightWindows>(node_ww));
210✔
1298
  }
1299

1300
  // Enable weight windows by default if one or more are present
1301
  if (variance_reduction::weight_windows.size() > 0)
8,232✔
1302
    settings::weight_windows_on = true;
79✔
1303

1304
  // read weight windows from file
1305
  if (check_for_node(root, "weight_windows_file")) {
8,232!
UNCOV
1306
    weight_windows_file = get_node_value(root, "weight_windows_file");
×
1307
  }
1308

1309
  // read settings for weight windows value, this will override
1310
  // the automatic setting even if weight windows are present
1311
  if (check_for_node(root, "weight_windows_on")) {
8,232✔
1312
    weight_windows_on = get_node_value_bool(root, "weight_windows_on");
38✔
1313
  }
1314

1315
  if (check_for_node(root, "max_secondaries")) {
8,232!
UNCOV
1316
    settings::max_secondaries =
×
UNCOV
1317
      std::stoi(get_node_value(root, "max_secondaries"));
×
1318
  }
1319

1320
  if (check_for_node(root, "max_history_splits")) {
8,232✔
1321
    settings::max_history_splits =
428✔
1322
      std::stoi(get_node_value(root, "max_history_splits"));
428✔
1323
  }
1324

1325
  if (check_for_node(root, "max_tracks")) {
8,232✔
1326
    settings::max_tracks = std::stoi(get_node_value(root, "max_tracks"));
90✔
1327
  }
1328

1329
  // Create weight window generator objects
1330
  if (check_for_node(root, "weight_window_generators")) {
8,232✔
1331
    auto wwgs_node = root.child("weight_window_generators");
79✔
1332
    for (pugi::xml_node node_wwg :
158✔
1333
      wwgs_node.children("weight_windows_generator")) {
158✔
1334
      variance_reduction::weight_windows_generators.emplace_back(
79✔
1335
        std::make_unique<WeightWindowsGenerator>(node_wwg));
158✔
1336
    }
1337
    // if any of the weight windows are intended to be generated otf, make
1338
    // sure they're applied
1339
    for (const auto& wwg : variance_reduction::weight_windows_generators) {
79!
1340
      if (wwg->on_the_fly_) {
79!
1341
        settings::weight_windows_on = true;
79✔
1342
        break;
79✔
1343
      }
1344
    }
1345
  }
1346

1347
  // Set up weight window checkpoints
1348
  if (check_for_node(root, "weight_window_checkpoints")) {
8,232✔
1349
    xml_node ww_checkpoints = root.child("weight_window_checkpoints");
32✔
1350
    if (check_for_node(ww_checkpoints, "collision")) {
32!
1351
      weight_window_checkpoint_collision =
32✔
1352
        get_node_value_bool(ww_checkpoints, "collision");
32✔
1353
    }
1354
    if (check_for_node(ww_checkpoints, "surface")) {
32!
1355
      weight_window_checkpoint_surface =
32✔
1356
        get_node_value_bool(ww_checkpoints, "surface");
32✔
1357
    }
1358
  }
1359

1360
  if (weight_windows_on) {
8,232✔
1361
    if (!weight_window_checkpoint_surface &&
158✔
1362
        !weight_window_checkpoint_collision)
126!
NEW
1363
      fatal_error(
×
1364
        "Weight Windows are enabled but there are no valid checkpoints.");
1365
  }
1366

1367
  if (check_for_node(root, "use_decay_photons")) {
8,232✔
1368
    settings::use_decay_photons =
11✔
1369
      get_node_value_bool(root, "use_decay_photons");
11✔
1370
  }
1371
}
8,232✔
1372

1373
void free_memory_settings()
8,383✔
1374
{
1375
  settings::statepoint_batch.clear();
8,383✔
1376
  settings::sourcepoint_batch.clear();
8,383✔
1377
  settings::source_write_surf_id.clear();
8,383✔
1378
  settings::ssw_cells.clear();
8,383✔
1379
  settings::res_scat_nuclides.clear();
8,383✔
1380
}
8,383✔
1381

1382
SSWCellType ssw_cell_type_from_string(std::string_view s)
128✔
1383
{
1384
  if (s == "from")
128✔
1385
    return SSWCellType::From;
1386
  if (s == "to")
117✔
1387
    return SSWCellType::To;
1388
  if (s == "both")
22!
1389
    return SSWCellType::Both;
UNCOV
1390
  fatal_error("Direction must be 'from', 'to', or 'both'");
×
1391
}
1392

1393
//==============================================================================
1394
// C API functions
1395
//==============================================================================
1396

1397
extern "C" int openmc_set_n_batches(
44✔
1398
  int32_t n_batches, bool set_max_batches, bool add_statepoint_batch)
1399
{
1400
  if (settings::n_inactive >= n_batches) {
44✔
1401
    set_errmsg("Number of active batches must be greater than zero.");
11✔
1402
    return OPENMC_E_INVALID_ARGUMENT;
11✔
1403
  }
1404

1405
  if (!settings::trigger_on) {
33✔
1406
    // Set n_batches and n_max_batches to same value
1407
    settings::n_batches = n_batches;
11✔
1408
    settings::n_max_batches = n_batches;
11✔
1409
  } else {
1410
    // Set n_batches and n_max_batches based on value of set_max_batches
1411
    if (set_max_batches) {
22✔
1412
      settings::n_max_batches = n_batches;
11✔
1413
    } else {
1414
      settings::n_batches = n_batches;
11✔
1415
    }
1416
  }
1417

1418
  // Update size of k_generation and entropy
1419
  int m = settings::n_max_batches * settings::gen_per_batch;
33✔
1420
  simulation::k_generation.reserve(m);
33✔
1421
  simulation::entropy.reserve(m);
33✔
1422

1423
  // Add value of n_batches to statepoint_batch
1424
  if (add_statepoint_batch &&
33✔
1425
      !(contains(settings::statepoint_batch, n_batches)))
22!
1426
    settings::statepoint_batch.insert(n_batches);
22✔
1427

1428
  return 0;
1429
}
1430

1431
extern "C" int openmc_get_n_batches(int* n_batches, bool get_max_batches)
2,530✔
1432
{
1433
  *n_batches = get_max_batches ? settings::n_max_batches : settings::n_batches;
2,530✔
1434

1435
  return 0;
2,530✔
1436
}
1437

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