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

openmc-dev / openmc / 26783873914

01 Jun 2026 09:43PM UTC coverage: 81.362% (+0.03%) from 81.333%
26783873914

Pull #3948

github

web-flow
Merge 6314ea576 into 111eb7706
Pull Request #3948: Fix get_index_in_direction for regular meshes

18027 of 26121 branches covered (69.01%)

Branch coverage included in aggregate %.

43 of 45 new or added lines in 9 files covered. (95.56%)

443 existing lines in 12 files now uncovered.

59175 of 68766 relevant lines covered (86.05%)

48551677.44 hits per line

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

76.22
/src/tallies/tally.cpp
1
#include "openmc/tallies/tally.h"
2

3
#include "openmc/array.h"
4
#include "openmc/capi.h"
5
#include "openmc/cell.h"
6
#include "openmc/constants.h"
7
#include "openmc/container_util.h"
8
#include "openmc/error.h"
9
#include "openmc/file_utils.h"
10
#include "openmc/mesh.h"
11
#include "openmc/message_passing.h"
12
#include "openmc/mgxs_interface.h"
13
#include "openmc/nuclide.h"
14
#include "openmc/particle.h"
15
#include "openmc/reaction.h"
16
#include "openmc/reaction_product.h"
17
#include "openmc/settings.h"
18
#include "openmc/simulation.h"
19
#include "openmc/source.h"
20
#include "openmc/tallies/derivative.h"
21
#include "openmc/tallies/filter.h"
22
#include "openmc/tallies/filter_cell.h"
23
#include "openmc/tallies/filter_cellborn.h"
24
#include "openmc/tallies/filter_cellfrom.h"
25
#include "openmc/tallies/filter_collision.h"
26
#include "openmc/tallies/filter_delayedgroup.h"
27
#include "openmc/tallies/filter_energy.h"
28
#include "openmc/tallies/filter_legendre.h"
29
#include "openmc/tallies/filter_mesh.h"
30
#include "openmc/tallies/filter_meshborn.h"
31
#include "openmc/tallies/filter_meshmaterial.h"
32
#include "openmc/tallies/filter_meshsurface.h"
33
#include "openmc/tallies/filter_particle.h"
34
#include "openmc/tallies/filter_sph_harm.h"
35
#include "openmc/tallies/filter_surface.h"
36
#include "openmc/tallies/filter_time.h"
37
#include "openmc/xml_interface.h"
38

39
#include "openmc/tensor.h"
40
#include <fmt/core.h>
41

42
#include <algorithm> // for max, set_union
43
#include <cassert>
44
#include <cstddef>  // for size_t
45
#include <iterator> // for back_inserter
46
#include <string>
47

48
namespace openmc {
49

50
//==============================================================================
51
// Global variable definitions
52
//==============================================================================
53

54
namespace model {
55
//! a mapping of tally ID to index in the tallies vector
56
std::unordered_map<int, int> tally_map;
57
vector<unique_ptr<Tally>> tallies;
58
vector<int> active_tallies;
59
vector<int> active_analog_tallies;
60
vector<int> active_tracklength_tallies;
61
vector<int> active_timed_tracklength_tallies;
62
vector<int> active_collision_tallies;
63
vector<int> active_meshsurf_tallies;
64
vector<int> active_surface_tallies;
65
vector<int> active_pulse_height_tallies;
66
vector<int32_t> pulse_height_cells;
67
vector<double> time_grid;
68
} // namespace model
69

70
namespace simulation {
71
tensor::StaticTensor2D<double, N_GLOBAL_TALLIES, 3> global_tallies;
72
int32_t n_realizations {0};
73
} // namespace simulation
74

75
double global_tally_absorption;
76
double global_tally_collision;
77
double global_tally_tracklength;
78
double global_tally_leakage;
79

80
//==============================================================================
81
// Tally object implementation
82
//==============================================================================
83

84
Tally::Tally(int32_t id)
1,228✔
85
{
86
  index_ = model::tallies.size(); // Avoids warning about narrowing
1,228✔
87
  this->set_id(id);
1,228✔
88
  this->set_filters({});
1,228✔
89
}
1,228✔
90

91
Tally::Tally(pugi::xml_node node)
25,676✔
92
{
93
  index_ = model::tallies.size(); // Avoids warning about narrowing
25,676✔
94

95
  // Copy and set tally id
96
  if (!check_for_node(node, "id")) {
25,676!
97
    throw std::runtime_error {"Must specify id for tally in tally XML file."};
×
98
  }
99
  int32_t id = std::stoi(get_node_value(node, "id"));
51,352✔
100
  this->set_id(id);
25,676✔
101

102
  if (check_for_node(node, "name"))
25,676✔
103
    name_ = get_node_value(node, "name");
2,841✔
104

105
  if (check_for_node(node, "multiply_density")) {
25,676✔
106
    multiply_density_ = get_node_value_bool(node, "multiply_density");
110✔
107
  }
108

109
  if (check_for_node(node, "higher_moments")) {
25,676✔
110
    higher_moments_ = get_node_value_bool(node, "higher_moments");
11✔
111
  }
112
  // =======================================================================
113
  // READ DATA FOR FILTERS
114

115
  // Check if user is using old XML format and throw an error if so
116
  if (check_for_node(node, "filter")) {
25,676!
117
    throw std::runtime_error {
×
118
      "Tally filters must be specified independently of "
119
      "tallies in a <filter> element. The <tally> element itself should "
120
      "have a list of filters that apply, e.g., <filters>1 2</filters> "
121
      "where 1 and 2 are the IDs of filters specified outside of "
122
      "<tally>."};
×
123
  }
124

125
  // Determine number of filters
126
  vector<int> filter_ids;
25,676✔
127
  if (check_for_node(node, "filters")) {
25,676✔
128
    filter_ids = get_node_array<int>(node, "filters");
49,362✔
129
  }
130

131
  // Allocate and store filter user ids
132
  vector<Filter*> filters;
25,676✔
133
  for (int filter_id : filter_ids) {
76,969✔
134
    // Determine if filter ID is valid
135
    auto it = model::filter_map.find(filter_id);
51,293!
136
    if (it == model::filter_map.end()) {
51,293!
137
      throw std::runtime_error {fmt::format(
×
138
        "Could not find filter {} specified on tally {}", filter_id, id_)};
×
139
    }
140

141
    // Store the index of the filter
142
    filters.push_back(model::tally_filters[it->second].get());
51,293✔
143
  }
144

145
  // Set the filters
146
  this->set_filters(filters);
25,676✔
147

148
  // Check for the presence of certain filter types
149
  bool has_energyout = energyout_filter_ >= 0;
25,676✔
150
  int particle_filter_index = C_NONE;
25,676✔
151
  for (int64_t j = 0; j < filters_.size(); ++j) {
76,969✔
152
    int i_filter = filters_[j];
51,293!
153
    const auto& f = model::tally_filters[i_filter].get();
51,293!
154

155
    auto pf = dynamic_cast<ParticleFilter*>(f);
51,293!
156
    if (pf)
51,293✔
157
      particle_filter_index = i_filter;
557✔
158

159
    // Change the tally estimator if a filter demands it
160
    FilterType filt_type = f->type();
51,293✔
161
    if (filt_type == FilterType::ENERGY_OUT ||
51,293✔
162
        filt_type == FilterType::LEGENDRE) {
163
      estimator_ = TallyEstimator::ANALOG;
7,320✔
164
    } else if (filt_type == FilterType::SPHERICAL_HARMONICS) {
165
      auto sf = dynamic_cast<SphericalHarmonicsFilter*>(f);
67✔
166
      if (sf->cosine() == SphericalHarmonicsCosine::scatter) {
67✔
167
        estimator_ = TallyEstimator::ANALOG;
11✔
168
      }
169
    } else if (filt_type == FilterType::SPATIAL_LEGENDRE ||
170
               filt_type == FilterType::ZERNIKE ||
171
               filt_type == FilterType::ZERNIKE_RADIAL) {
172
      estimator_ = TallyEstimator::COLLISION;
55✔
173
    } else if (filt_type == FilterType::PARTICLE_PRODUCTION) {
174
      estimator_ = TallyEstimator::ANALOG;
60✔
175
    } else if (filt_type == FilterType::REACTION) {
176
      if (estimator_ == TallyEstimator::TRACKLENGTH) {
15!
177
        estimator_ = TallyEstimator::COLLISION;
15✔
178
      }
179
    }
180
  }
181

182
  // =======================================================================
183
  // READ DATA FOR NUCLIDES
184

185
  this->set_nuclides(node);
25,676✔
186

187
  // =======================================================================
188
  // READ DATA FOR SCORES
189

190
  this->set_scores(node);
25,676✔
191

192
  if (!check_for_node(node, "scores")) {
25,676!
193
    fatal_error(fmt::format("No scores specified on tally {}.", id_));
×
194
  }
195

196
  // Set IFP if needed
197
  if (!settings::ifp_on) {
25,676✔
198
    // Determine if this tally has an IFP score
199
    bool has_ifp_score = false;
25,558✔
200
    for (int score : scores_) {
61,910✔
201
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
36,444!
202
          score == SCORE_IFP_DENOM) {
203
        has_ifp_score = true;
204
        break;
205
      }
206
    }
207

208
    // Check for errors
209
    if (has_ifp_score) {
25,558✔
210
      if (settings::run_mode == RunMode::EIGENVALUE) {
92✔
211
        if (settings::ifp_n_generation < 0) {
83✔
212
          settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION;
9✔
213
          warning(fmt::format(
18!
214
            "{} generations will be used for IFP (default value). It can be "
215
            "changed using the 'ifp_n_generation' settings.",
216
            settings::ifp_n_generation));
217
        }
218
        if (settings::ifp_n_generation > settings::n_inactive) {
83✔
219
          fatal_error("'ifp_n_generation' must be lower than or equal to the "
9✔
220
                      "number of inactive cycles.");
221
        }
222
        settings::ifp_on = true;
74✔
223
      } else if (settings::run_mode == RunMode::FIXED_SOURCE) {
9!
224
        fatal_error(
9✔
225
          "Iterated Fission Probability can only be used in an eigenvalue "
226
          "calculation.");
227
      }
228
    }
229
  }
230

231
  // Set IFP parameters if needed
232
  if (settings::ifp_on) {
25,658✔
233
    for (int score : scores_) {
414✔
234
      switch (score) {
222!
235
      case SCORE_IFP_TIME_NUM:
74✔
236
        if (settings::ifp_parameter == IFPParameter::None) {
74!
237
          settings::ifp_parameter = IFPParameter::GenerationTime;
74✔
238
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
239
          settings::ifp_parameter = IFPParameter::Both;
×
240
        }
241
        break;
242
      case SCORE_IFP_BETA_NUM:
148✔
243
      case SCORE_IFP_DENOM:
148✔
244
        if (settings::ifp_parameter == IFPParameter::None) {
148!
245
          settings::ifp_parameter = IFPParameter::BetaEffective;
×
246
        } else if (settings::ifp_parameter == IFPParameter::GenerationTime) {
148✔
247
          settings::ifp_parameter = IFPParameter::Both;
74✔
248
        }
249
        break;
250
      }
251
    }
252
  }
253

254
  // Check if tally is compatible with particle type
255
  if (!settings::photon_transport) {
25,658✔
256
    for (int score : scores_) {
60,757✔
257
      switch (score) {
35,709!
258
      case SCORE_PULSE_HEIGHT:
×
259
        fatal_error("For pulse-height tallies, photon transport needs to be "
×
260
                    "activated.");
261
        break;
35,709✔
262
      }
263
    }
264
  }
265
  if (settings::photon_transport) {
25,658✔
266
    if (particle_filter_index == C_NONE) {
610✔
267
      for (int score : scores_) {
386✔
268
        switch (score) {
193!
269
        case SCORE_INVERSE_VELOCITY:
×
270
          fatal_error("Particle filter must be used with photon "
×
271
                      "transport on and inverse velocity score");
272
          break;
44✔
273
        case SCORE_FLUX:
44✔
274
        case SCORE_TOTAL:
44✔
275
        case SCORE_SCATTER:
44✔
276
        case SCORE_NU_SCATTER:
44✔
277
        case SCORE_ABSORPTION:
44✔
278
        case SCORE_FISSION:
44✔
279
        case SCORE_NU_FISSION:
44✔
280
        case SCORE_CURRENT:
44✔
281
        case SCORE_EVENTS:
44✔
282
        case SCORE_DELAYED_NU_FISSION:
44✔
283
        case SCORE_PROMPT_NU_FISSION:
44✔
284
        case SCORE_DECAY_RATE:
44✔
285
          warning("You are tallying the '" + reaction_name(score) +
88✔
286
                  "' score and haven't used a particle filter. This score will "
287
                  "include contributions from all particles.");
288
          break;
44✔
289
        }
290
      }
291
    }
292
  } else {
293
    if (particle_filter_index >= 0) {
25,048✔
294
      const auto& f = model::tally_filters[particle_filter_index].get();
140!
295
      auto pf = dynamic_cast<ParticleFilter*>(f);
140!
296
      for (auto p : pf->particles()) {
390✔
297
        if (!p.is_neutron()) {
250✔
298
          warning(fmt::format(
240✔
299
            "Particle filter other than NEUTRON used with "
300
            "photon transport turned off. All tallies for particle type {}"
301
            " will have no scores",
302
            p.str()));
220✔
303
        }
304
      }
305
    }
306
  }
307

308
  // Check for a tally derivative.
309
  if (check_for_node(node, "derivative")) {
25,658✔
310
    int deriv_id = std::stoi(get_node_value(node, "derivative"));
600✔
311

312
    // Find the derivative with the given id, and store it's index.
313
    auto it = model::tally_deriv_map.find(deriv_id);
300!
314
    if (it == model::tally_deriv_map.end()) {
300!
315
      fatal_error(fmt::format(
×
316
        "Could not find derivative {} specified on tally {}", deriv_id, id_));
×
317
    }
318

319
    deriv_ = it->second;
300✔
320

321
    // Only analog or collision estimators are supported for differential
322
    // tallies.
323
    if (estimator_ == TallyEstimator::TRACKLENGTH) {
300✔
324
      estimator_ = TallyEstimator::COLLISION;
225✔
325
    }
326

327
    const auto& deriv = model::tally_derivs[deriv_];
300✔
328
    if (deriv.variable == DerivativeVariable::NUCLIDE_DENSITY ||
300✔
329
        deriv.variable == DerivativeVariable::TEMPERATURE) {
330
      for (int i_nuc : nuclides_) {
405✔
331
        if (has_energyout && i_nuc == -1) {
225!
332
          fatal_error(fmt::format(
×
333
            "Error on tally {}: Cannot use a "
334
            "'nuclide_density' or 'temperature' derivative on a tally with "
335
            "an "
336
            "outgoing energy filter and 'total' nuclide rate. Instead, tally "
337
            "each nuclide in the material individually.",
338
            id_));
×
339
          // Note that diff tallies with these characteristics would work
340
          // correctly if no tally events occur in the perturbed material
341
          // (e.g. pertrubing moderator but only tallying fuel), but this
342
          // case would be hard to check for by only reading inputs.
343
        }
344
      }
345
    }
346
  }
347

348
  // If settings.xml trigger is turned on, create tally triggers
349
  if (settings::trigger_on) {
25,658✔
350
    this->init_triggers(node);
175✔
351
  }
352

353
  // =======================================================================
354
  // SET TALLY ESTIMATOR
355

356
  // Check if user specified estimator
357
  if (check_for_node(node, "estimator")) {
25,658✔
358
    std::string est = get_node_value(node, "estimator");
20,537✔
359
    if (est == "analog") {
20,537✔
360
      estimator_ = TallyEstimator::ANALOG;
8,954✔
361
    } else if (est == "tracklength" || est == "track-length" ||
11,987!
362
               est == "pathlength" || est == "path-length") {
12,391!
363
      // If the estimator was set to an analog estimator, this means the
364
      // tally needs post-collision information
365
      if (estimator_ == TallyEstimator::ANALOG ||
11,179!
366
          estimator_ == TallyEstimator::COLLISION) {
367
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
368
                                              "estimator for tally {}",
369
          id_)};
×
370
      }
371

372
      // Set estimator to track-length estimator
373
      estimator_ = TallyEstimator::TRACKLENGTH;
11,179✔
374

375
    } else if (est == "collision") {
404!
376
      // If the estimator was set to an analog estimator, this means the
377
      // tally needs post-collision information
378
      if (estimator_ == TallyEstimator::ANALOG) {
404!
379
        throw std::runtime_error {fmt::format("Cannot use collision estimator "
×
380
                                              "for tally ",
381
          id_)};
×
382
      }
383

384
      // Set estimator to collision estimator
385
      estimator_ = TallyEstimator::COLLISION;
404✔
386

387
    } else {
388
      throw std::runtime_error {
×
389
        fmt::format("Invalid estimator '{}' on tally {}", est, id_)};
×
390
    }
391
  }
20,537✔
392

393
#ifdef OPENMC_LIBMESH_ENABLED
394
  // ensure a tracklength tally isn't used with a libMesh filter
395
  for (auto i : this->filters_) {
15,196✔
396
    auto df = dynamic_cast<MeshFilter*>(model::tally_filters[i].get());
10,134!
397
    if (df) {
10,134✔
398
      auto lm = dynamic_cast<LibMesh*>(model::meshes[df->mesh()].get());
11,276!
399
      if (lm && estimator_ == TallyEstimator::TRACKLENGTH) {
1,142!
400
        fatal_error("A tracklength estimator cannot be used with "
401
                    "an unstructured LibMesh tally.");
402
      }
403
    }
404
  }
405
#endif
406
}
25,658✔
407

408
Tally::~Tally()
26,886✔
409
{
410
  model::tally_map.erase(id_);
26,886✔
411
}
53,772✔
412

413
Tally* Tally::create(int32_t id)
131✔
414
{
415
  model::tallies.push_back(make_unique<Tally>(id));
131✔
416
  return model::tallies.back().get();
131✔
417
}
418

419
void Tally::set_id(int32_t id)
28,000✔
420
{
421
  assert(id >= 0 || id == C_NONE);
28,000!
422

423
  // Clear entry in tally map if an ID was already assigned before
424
  if (id_ != C_NONE) {
28,000✔
425
    model::tally_map.erase(id_);
1,096✔
426
    id_ = C_NONE;
1,096✔
427
  }
428

429
  // Make sure no other tally has the same ID
430
  if (model::tally_map.find(id) != model::tally_map.end()) {
28,000!
431
    throw std::runtime_error {
×
432
      fmt::format("Two tallies have the same ID: {}", id)};
×
433
  }
434

435
  // If no ID specified, auto-assign next ID in sequence
436
  if (id == C_NONE) {
28,000✔
437
    id = 0;
1,228✔
438
    for (const auto& t : model::tallies) {
3,682✔
439
      id = std::max(id, t->id_);
4,818✔
440
    }
441
    ++id;
1,228✔
442
  }
443

444
  // Update ID and entry in tally map
445
  id_ = id;
28,000✔
446
  model::tally_map[id] = index_;
28,000✔
447
}
28,000✔
448

449
std::vector<FilterType> Tally::filter_types() const
287✔
450
{
451
  std::vector<FilterType> filter_types;
287✔
452
  for (auto idx : this->filters())
1,104✔
453
    filter_types.push_back(model::tally_filters[idx]->type());
817✔
454
  return filter_types;
287✔
455
}
×
456

457
std::unordered_map<FilterType, int32_t> Tally::filter_indices() const
287✔
458
{
459
  std::unordered_map<FilterType, int32_t> filter_indices;
287✔
460
  for (int i = 0; i < this->filters().size(); i++) {
1,104✔
461
    const auto& f = model::tally_filters[this->filters(i)];
817✔
462

463
    filter_indices[f->type()] = i;
817✔
464
  }
465
  return filter_indices;
287✔
466
}
×
467

468
bool Tally::has_filter(FilterType filter_type) const
861✔
469
{
470
  for (auto idx : this->filters()) {
2,109✔
471
    if (model::tally_filters[idx]->type() == filter_type)
2,043✔
472
      return true;
861✔
473
  }
474
  return false;
475
}
476

477
void Tally::set_filters(span<Filter*> filters)
28,104✔
478
{
479
  // Clear old data.
480
  filters_.clear();
28,104✔
481
  strides_.clear();
28,104!
482

483
  // Copy in the given filter indices.
484
  auto n = filters.size();
28,104✔
485
  filters_.reserve(n);
28,104✔
486

487
  for (auto* filter : filters) {
81,037✔
488
    add_filter(filter);
52,933✔
489
  }
490
}
28,104✔
491

492
void Tally::add_filter(Filter* filter)
53,281✔
493
{
494
  int32_t filter_idx = model::filter_map.at(filter->id());
53,281✔
495
  // if this filter is already present, do nothing and return
496
  if (std::find(filters_.begin(), filters_.end(), filter_idx) != filters_.end())
53,281✔
497
    return;
22✔
498

499
  // Keep track of indices for special filters
500
  if (filter->type() == FilterType::ENERGY_OUT) {
53,259✔
501
    energyout_filter_ = filters_.size();
5,289✔
502
  } else if (filter->type() == FilterType::DELAYED_GROUP) {
47,970✔
503
    delayedgroup_filter_ = filters_.size();
787✔
504
  }
505
  filters_.push_back(filter_idx);
53,259✔
506
}
507

508
void Tally::set_strides()
27,457✔
509
{
510
  // Set the strides.  Filters are traversed in reverse so that the last
511
  // filter has the shortest stride in memory and the first filter has the
512
  // longest stride.
513
  auto n = filters_.size();
27,457✔
514
  strides_.resize(n, 0);
27,457✔
515
  int stride = 1;
27,457✔
516
  for (int i = n - 1; i >= 0; --i) {
81,325✔
517
    strides_[i] = stride;
53,868✔
518
    stride *= model::tally_filters[filters_[i]]->n_bins();
53,868✔
519
  }
520
  n_filter_bins_ = stride;
27,457✔
521
}
27,457✔
522

523
void Tally::set_scores(pugi::xml_node node)
25,676✔
524
{
525
  if (!check_for_node(node, "scores"))
25,676!
526
    fatal_error(fmt::format("No scores specified on tally {}", id_));
×
527

528
  auto scores = get_node_array<std::string>(node, "scores");
25,676✔
529
  set_scores(scores);
25,676✔
530
}
25,676✔
531

532
void Tally::set_scores(const vector<std::string>& scores)
26,904✔
533
{
534
  // Reset state and prepare for the new scores.
535
  scores_.clear();
26,904✔
536
  scores_.reserve(scores.size());
26,904✔
537

538
  // Check for the presence of certain restrictive filters.
539
  bool energyout_present = energyout_filter_ != C_NONE;
26,904✔
540
  bool legendre_present = false;
26,904✔
541
  bool cell_present = false;
26,904✔
542
  bool cellfrom_present = false;
26,904✔
543
  bool material_present = false;
26,904✔
544
  bool materialfrom_present = false;
26,904✔
545
  bool surface_present = false;
26,904✔
546
  bool meshsurface_present = false;
26,904✔
547
  bool non_cell_energy_present = false;
26,904✔
548
  for (auto i_filt : filters_) {
79,247✔
549
    const auto* filt {model::tally_filters[i_filt].get()};
52,343✔
550
    // Checking for only cell and energy filters for pulse-height tally
551
    if (!(filt->type() == FilterType::CELL ||
103,488✔
552
          filt->type() == FilterType::ENERGY)) {
51,145✔
553
      non_cell_energy_present = true;
554
    }
555
    if (filt->type() == FilterType::LEGENDRE) {
52,343✔
556
      legendre_present = true;
557
    } else if (filt->type() == FilterType::CELLFROM) {
50,125✔
558
      cellfrom_present = true;
559
    } else if (filt->type() == FilterType::CELL) {
49,829✔
560
      cell_present = true;
561
    } else if (filt->type() == FilterType::MATERIALFROM) {
48,631✔
562
      materialfrom_present = true;
563
    } else if (filt->type() == FilterType::MATERIAL) {
48,601✔
564
      material_present = true;
565
    } else if (filt->type() == FilterType::SURFACE) {
33,334✔
566
      surface_present = true;
567
    } else if (filt->type() == FilterType::MESH_SURFACE) {
33,077✔
568
      meshsurface_present = true;
341✔
569
    }
570
  }
571
  bool surface_types_present =
53,808✔
572
    (surface_present || cellfrom_present || materialfrom_present);
26,904✔
573
  bool non_meshsurface_types_present =
53,808✔
574
    (surface_present || cell_present || cellfrom_present || material_present ||
26,904!
575
      materialfrom_present);
576

577
  // Iterate over the given scores.
578
  for (auto score_str : scores) {
65,328✔
579
    // Make sure a delayed group filter wasn't used with an incompatible
580
    // score.
581
    if (delayedgroup_filter_ != C_NONE) {
38,424✔
582
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate" &&
959✔
583
          score_str != "ifp-beta-numerator")
37!
584
        fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
×
585
    }
586

587
    // Determine integer code for score
588
    int score = reaction_tally_mt(score_str);
38,424✔
589

590
    switch (score) {
38,424✔
591
    case SCORE_FLUX:
11,264✔
592
      if (!nuclides_.empty())
11,264!
593
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
11,264!
594
          fatal_error("Cannot tally flux for an individual nuclide.");
×
595
      if (energyout_present)
11,264!
596
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
597
      if (surface_types_present) {
11,264✔
598
        if (meshsurface_present)
48!
599
          fatal_error("OpenMC does not support mesh surface fluxes yet");
×
600
        type_ = TallyType::SURFACE;
48✔
601
        estimator_ = TallyEstimator::ANALOG;
48✔
602
      }
603
      break;
604

605
    case SCORE_TOTAL:
7,379✔
606
    case SCORE_ABSORPTION:
7,379✔
607
    case SCORE_FISSION:
7,379✔
608
      if (energyout_present)
7,379!
609
        fatal_error("Cannot tally " + score_str +
×
610
                    " reaction rate with an "
611
                    "outgoing energy filter");
612
      break;
613

614
    case SCORE_SCATTER:
4,501✔
615
      if (legendre_present)
4,501✔
616
        estimator_ = TallyEstimator::ANALOG;
1,577✔
617
    case SCORE_NU_FISSION:
9,813✔
618
    case SCORE_DELAYED_NU_FISSION:
9,813✔
619
    case SCORE_PROMPT_NU_FISSION:
9,813✔
620
      if (energyout_present)
9,813✔
621
        estimator_ = TallyEstimator::ANALOG;
4,216✔
622
      break;
623

624
    case SCORE_NU_SCATTER:
2,225✔
625
      if (settings::run_CE) {
2,225✔
626
        estimator_ = TallyEstimator::ANALOG;
2,105✔
627
      } else {
628
        if (energyout_present || legendre_present)
120✔
629
          estimator_ = TallyEstimator::ANALOG;
60✔
630
      }
631
      break;
632

633
    case SCORE_CURRENT:
595✔
634
      // Check which type of current is desired: mesh or surface currents.
635
      if (meshsurface_present) {
595✔
636
        if (non_meshsurface_types_present)
341!
637
          fatal_error("Cannot tally mesh surface currents in the same tally as "
×
638
                      "normal surface currents");
639
        type_ = TallyType::MESH_SURFACE;
341✔
640
      } else {
641
        type_ = TallyType::SURFACE;
254✔
642
        estimator_ = TallyEstimator::ANALOG;
254✔
643
      }
644
      break;
645

646
    case HEATING:
399✔
647
      if (settings::photon_transport) {
399✔
648
        // Photon heating requires a collision estimator (analog energy
649
        // balance). However, if the tally only scores neutrons, we can keep the
650
        // tracklength estimator since neutron heating uses kerma coefficients
651
        // that support tracklength scoring.
652
        bool neutron_only = false;
238✔
653
        for (auto i_filt : filters_) {
428✔
654
          auto pf =
201✔
655
            dynamic_cast<ParticleFilter*>(model::tally_filters[i_filt].get());
391!
656
          if (pf && pf->particles().size() == 1 &&
201✔
657
              pf->particles()[0].is_neutron()) {
33✔
658
            neutron_only = true;
659
            break;
660
          }
661
        }
662
        if (!neutron_only)
238✔
663
          estimator_ = TallyEstimator::COLLISION;
227✔
664
      }
665
      break;
666

667
    case SCORE_PULSE_HEIGHT: {
90✔
668
      if (non_cell_energy_present) {
90!
UNCOV
669
        fatal_error("Pulse-height tallies are not compatible with filters "
×
670
                    "other than CellFilter and EnergyFilter");
671
      }
672
      type_ = TallyType::PULSE_HEIGHT;
90✔
673
      // Collect all unique cell indices covered by this tally.
674
      // If no CellFilter is present, all cells in the geometry are scored.
675
      const auto* cell_filter_ptr = get_filter<CellFilter>();
90✔
676
      int n = cell_filter_ptr ? cell_filter_ptr->n_bins()
90!
UNCOV
677
                              : static_cast<int>(model::cells.size());
×
678
      for (int i = 0; i < n; ++i) {
180✔
679
        int32_t cell_index = cell_filter_ptr ? cell_filter_ptr->cells()[i] : i;
90!
680
        if (!contains(model::pulse_height_cells, cell_index))
90!
681
          model::pulse_height_cells.push_back(cell_index);
90✔
682
      }
683
      break;
684
    }
685

686
    case SCORE_IFP_TIME_NUM:
276✔
687
    case SCORE_IFP_BETA_NUM:
276✔
688
    case SCORE_IFP_DENOM:
276✔
689
      estimator_ = TallyEstimator::COLLISION;
276✔
690
      break;
276✔
691
    }
692

693
    scores_.push_back(score);
38,424✔
694
  }
38,424✔
695

696
  // Make sure that no duplicate scores exist.
697
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
65,328✔
698
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
89,522✔
699
      if (*it1 == *it2)
51,098!
UNCOV
700
        fatal_error(
×
UNCOV
701
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
702
            reaction_name(*it1), id_));
×
703
    }
704
  }
705

706
  // Make sure all scores are compatible with multigroup mode.
707
  if (!settings::run_CE) {
26,904✔
708
    for (auto sc : scores_)
6,543✔
709
      if (sc > 0)
4,698!
710
        fatal_error("Cannot tally " + reaction_name(sc) +
×
711
                    " reaction rate "
712
                    "in multi-group mode");
713
  }
714

715
  // Make sure mesh surface tallies contain only current score.
716
  if (meshsurface_present) {
26,904✔
717
    if ((scores_[0] != SCORE_CURRENT) || (scores_.size() > 1))
341!
UNCOV
718
      fatal_error("Cannot tally score other than 'current' when using a "
×
719
                  "mesh-surface filter.");
720
  }
721

722
  // Make sure surface tallies contain only surface type scores score.
723
  if (type_ == TallyType::SURFACE) {
26,904✔
724
    for (auto sc : scores_)
589✔
725
      if ((sc != SCORE_CURRENT) && (sc != SCORE_FLUX))
302!
UNCOV
726
        fatal_error("Cannot tally scores other than 'current' or 'flux' "
×
727
                    "when using surface filters.");
728
  }
729
}
26,904✔
730

731
void Tally::set_nuclides(pugi::xml_node node)
25,676✔
732
{
733
  nuclides_.clear();
25,676!
734

735
  // By default, we tally just the total material rates.
736
  if (!check_for_node(node, "nuclides")) {
25,676✔
737
    nuclides_.push_back(-1);
6,792✔
738
    return;
6,792✔
739
  }
740

741
  // The user provided specifics nuclides.  Parse it as an array with either
742
  // "total" or a nuclide name like "U235" in each position.
743
  auto words = get_node_array<std::string>(node, "nuclides");
18,884✔
744
  this->set_nuclides(words);
18,884✔
745
}
18,884✔
746

747
void Tally::set_nuclides(const vector<std::string>& nuclides)
18,884✔
748
{
749
  nuclides_.clear();
18,884!
750

751
  for (const auto& nuc : nuclides) {
48,240✔
752
    if (nuc == "total") {
29,356✔
753
      nuclides_.push_back(-1);
15,498✔
754
    } else {
755
      auto search = data::nuclide_map.find(nuc);
13,858✔
756
      if (search == data::nuclide_map.end()) {
13,858✔
757
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
220✔
758
        if (err < 0)
220!
UNCOV
759
          throw std::runtime_error {openmc_err_msg};
×
760
      }
761
      nuclides_.push_back(data::nuclide_map.at(nuc));
13,858✔
762
    }
763
  }
764
}
18,884✔
765

766
void Tally::init_triggers(pugi::xml_node node)
175✔
767
{
768
  for (auto trigger_node : node.children("trigger")) {
245✔
769
    // Read the trigger type.
770
    TriggerMetric metric;
70✔
771
    if (check_for_node(trigger_node, "type")) {
70!
772
      auto type_str = get_node_value(trigger_node, "type");
70✔
773
      if (type_str == "std_dev") {
70!
774
        metric = TriggerMetric::standard_deviation;
775
      } else if (type_str == "variance") {
70!
776
        metric = TriggerMetric::variance;
777
      } else if (type_str == "rel_err") {
70!
778
        metric = TriggerMetric::relative_error;
779
      } else {
780
        fatal_error(fmt::format(
×
781
          "Unknown trigger type \"{}\" in tally {}", type_str, id_));
×
782
      }
UNCOV
783
    } else {
×
UNCOV
784
      fatal_error(fmt::format(
×
UNCOV
785
        "Must specify trigger type for tally {} in tally XML file", id_));
×
786
    }
787

788
    // Read the trigger threshold.
789
    double threshold;
70✔
790
    if (check_for_node(trigger_node, "threshold")) {
70!
791
      threshold = std::stod(get_node_value(trigger_node, "threshold"));
140✔
792
      if (threshold <= 0) {
70!
UNCOV
793
        fatal_error("Tally trigger threshold must be positive");
×
794
      }
795
    } else {
UNCOV
796
      fatal_error(fmt::format(
×
UNCOV
797
        "Must specify trigger threshold for tally {} in tally XML file", id_));
×
798
    }
799

800
    // Read whether to allow zero-tally bins to be ignored.
801
    bool ignore_zeros = false;
70✔
802
    if (check_for_node(trigger_node, "ignore_zeros")) {
70✔
803
      ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros");
11✔
804
    }
805

806
    // Read the trigger scores.
807
    vector<std::string> trigger_scores;
70✔
808
    if (check_for_node(trigger_node, "scores")) {
70!
809
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
140✔
810
    } else {
UNCOV
811
      trigger_scores.push_back("all");
×
812
    }
813

814
    // Parse the trigger scores and populate the triggers_ vector.
815
    for (auto score_str : trigger_scores) {
140✔
816
      if (score_str == "all") {
70✔
817
        triggers_.reserve(triggers_.size() + this->scores_.size());
15✔
818
        for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) {
75✔
819
          triggers_.push_back({metric, threshold, ignore_zeros, i_score});
60✔
820
        }
821
      } else {
822
        int i_score = 0;
823
        for (; i_score < this->scores_.size(); ++i_score) {
77!
824
          if (this->scores_[i_score] == reaction_tally_mt(score_str))
77✔
825
            break;
826
        }
827
        if (i_score == this->scores_.size()) {
55!
UNCOV
828
          fatal_error(
×
UNCOV
829
            fmt::format("Could not find the score \"{}\" in tally "
×
830
                        "{} but it was listed in a trigger on that tally",
UNCOV
831
              score_str, id_));
×
832
        }
833
        triggers_.push_back({metric, threshold, ignore_zeros, i_score});
55✔
834
      }
835
    }
70✔
836
  }
70✔
837
}
175✔
838

839
void Tally::init_results()
27,457✔
840
{
841
  int n_scores = scores_.size() * nuclides_.size();
27,457✔
842
  if (higher_moments_) {
27,457✔
843
    results_ = tensor::Tensor<double>({static_cast<size_t>(n_filter_bins_),
11✔
844
      static_cast<size_t>(n_scores), size_t {5}});
11✔
845
  } else {
846
    results_ = tensor::Tensor<double>({static_cast<size_t>(n_filter_bins_),
27,446✔
847
      static_cast<size_t>(n_scores), size_t {3}});
27,446✔
848
  }
849
}
27,457✔
850

851
void Tally::reset()
58,431✔
852
{
853
  n_realizations_ = 0;
58,431✔
854
  if (results_.size() != 0) {
58,431✔
855
    results_.fill(0.0);
58,431✔
856
  }
857
}
58,431✔
858

859
void Tally::accumulate()
373,891✔
860
{
861
  // Increment number of realizations
862
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
373,891✔
863

864
  if (mpi::master || !settings::reduce_tallies) {
373,891✔
865
    // Calculate total source strength for normalization
866
    double total_source = 0.0;
342,237✔
867
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
342,237✔
868
      total_source = model::external_sources_probability.integral();
197,323✔
869
    } else {
870
      total_source = 1.0;
871
    }
872

873
    // Determine number of particles contributing to tally
874
    double contributing_particles = settings::reduce_tallies
342,237✔
875
                                      ? settings::n_particles
876
                                      : simulation::work_per_rank;
877

878
    // Account for number of source particles in normalization
879
    double norm =
342,237✔
880
      total_source / (contributing_particles * settings::gen_per_batch);
342,237✔
881

882
    if (settings::solver_type == SolverType::RANDOM_RAY) {
342,237✔
883
      norm = 1.0;
14,306✔
884
    }
885

886
    // Accumulate each result
887
    if (higher_moments_) {
342,237✔
888
#pragma omp parallel for
60✔
889
      // filter bins (specific cell, energy bins)
890
      for (int i = 0; i < results_.shape(0); ++i) {
2,500!
891
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
892
        for (int j = 0; j < results_.shape(1); ++j) {
12,000!
893
          double val = results_(i, j, TallyResult::VALUE) * norm;
4,800✔
894
          double val2 = val * val;
4,800✔
895
          results_(i, j, TallyResult::VALUE) = 0.0;
4,800✔
896
          results_(i, j, TallyResult::SUM) += val;
4,800✔
897
          results_(i, j, TallyResult::SUM_SQ) += val2;
4,800✔
898
          results_(i, j, TallyResult::SUM_THIRD) += val2 * val;
4,800✔
899
          results_(i, j, TallyResult::SUM_FOURTH) += val2 * val2;
4,800✔
900
        }
901
      }
902
    } else {
903
#pragma omp parallel for
187,692✔
904
      // filter bins (specific cell, energy bins)
905
      for (int i = 0; i < results_.shape(0); ++i) {
92,347,380!
906
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
907
        for (int j = 0; j < results_.shape(1); ++j) {
223,498,210!
908
          double val = results_(i, j, TallyResult::VALUE) * norm;
65,729,850✔
909
          results_(i, j, TallyResult::VALUE) = 0.0;
65,729,850✔
910
          results_(i, j, TallyResult::SUM) += val;
65,729,850✔
911
          results_(i, j, TallyResult::SUM_SQ) += val * val;
65,729,850✔
912
        }
913
      }
914
    }
915
  }
916
}
373,891✔
917

918
int Tally::score_index(const std::string& score) const
287✔
919
{
920
  for (int i = 0; i < scores_.size(); i++) {
287!
921
    if (this->score_name(i) == score)
287!
922
      return i;
287✔
923
  }
924
  return -1;
925
}
926

UNCOV
927
tensor::Tensor<double> Tally::get_reshaped_data() const
×
928
{
UNCOV
929
  vector<size_t> shape;
×
930
  for (auto f : filters()) {
×
UNCOV
931
    shape.push_back(model::tally_filters[f]->n_bins());
×
932
  }
933

934
  // add number of scores and nuclides to tally
935
  shape.push_back(results_.shape(1));
×
UNCOV
936
  shape.push_back(results_.shape(2));
×
937

938
  tensor::Tensor<double> reshaped_results = results_;
×
939
  reshaped_results.reshape(shape);
×
940
  return reshaped_results;
×
941
}
×
942

943
std::string Tally::score_name(int score_idx) const
321✔
944
{
945
  if (score_idx < 0 || score_idx >= scores_.size()) {
321!
946
    fatal_error("Index in scores array is out of bounds.");
×
947
  }
948
  return reaction_name(scores_[score_idx]);
321✔
949
}
950

UNCOV
951
std::vector<std::string> Tally::scores() const
×
952
{
953
  std::vector<std::string> score_names;
×
UNCOV
954
  for (int score : scores_)
×
UNCOV
955
    score_names.push_back(reaction_name(score));
×
UNCOV
956
  return score_names;
×
UNCOV
957
}
×
958

959
std::string Tally::nuclide_name(int nuclide_idx) const
34✔
960
{
961
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
34!
UNCOV
962
    fatal_error("Index in nuclides array is out of bounds");
×
963
  }
964

965
  int nuclide = nuclides_.at(nuclide_idx);
34✔
966
  if (nuclide == -1) {
34!
967
    return "total";
34✔
968
  }
UNCOV
969
  return data::nuclides.at(nuclide)->name_;
×
970
}
971

972
//==============================================================================
973
// Non-member functions
974
//==============================================================================
975

976
void read_tallies_xml()
1,399✔
977
{
978
  // Check if tallies.xml exists. If not, just return since it is optional
979
  std::string filename = settings::path_input + "tallies.xml";
1,399✔
980
  if (!file_exists(filename))
1,399✔
981
    return;
942✔
982

983
  write_message("Reading tallies XML file...", 5);
457✔
984

985
  // Parse tallies.xml file
986
  pugi::xml_document doc;
457✔
987
  doc.load_file(filename.c_str());
457✔
988
  pugi::xml_node root = doc.document_element();
457✔
989

990
  read_tallies_xml(root);
457✔
991
}
1,399✔
992

993
void read_tallies_xml(pugi::xml_node root)
4,746✔
994
{
995
  // Check for <assume_separate> setting
996
  if (check_for_node(root, "assume_separate")) {
4,746✔
997
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
15✔
998
  }
999

1000
  // Check for user meshes and allocate
1001
  read_meshes(root);
4,746✔
1002

1003
  // We only need the mesh info for plotting
1004
  if (settings::run_mode == RunMode::PLOTTING)
4,746✔
1005
    return;
1006

1007
  // Read data for tally derivatives
1008
  read_tally_derivatives(root);
4,713✔
1009

1010
  // ==========================================================================
1011
  // READ FILTER DATA
1012

1013
  // Check for user filters and allocate
1014
  for (auto node_filt : root.children("filter")) {
14,852✔
1015
    auto f = Filter::create(node_filt);
10,139✔
1016
  }
1017

1018
  // ==========================================================================
1019
  // READ TALLY DATA
1020

1021
  // Check for user tallies
1022
  int n = 0;
4,713✔
1023
  for (auto node : root.children("tally"))
30,389✔
1024
    ++n;
25,676✔
1025
  if (n == 0 && mpi::master) {
4,713!
UNCOV
1026
    warning("No tallies present in tallies.xml file.");
×
1027
  }
1028

1029
  for (auto node_tal : root.children("tally")) {
30,371✔
1030
    model::tallies.push_back(make_unique<Tally>(node_tal));
51,334✔
1031
  }
1032
}
1033

1034
#ifdef OPENMC_MPI
1035
void reduce_tally_results()
27,828✔
1036
{
1037
  // Don't reduce tally is no_reduce option is on
1038
  if (settings::reduce_tallies) {
27,828✔
1039
    for (int i_tally : model::active_tallies) {
81,836✔
1040
      // Skip any tallies that are not active
1041
      auto& tally {model::tallies[i_tally]};
54,088✔
1042

1043
      // Extract 2D view of the VALUE column from the 3D results tensor,
1044
      // then copy into a contiguous array for MPI reduction
1045
      const int val_idx = static_cast<int>(TallyResult::VALUE);
54,088✔
1046
      tensor::View<double> val_view =
54,088✔
1047
        tally->results_.slice(tensor::all, tensor::all, val_idx);
54,088✔
1048
      tensor::Tensor<double> values(val_view);
54,088✔
1049

1050
      tensor::Tensor<double> values_reduced(values.shape());
54,088✔
1051

1052
      // Reduce contiguous set of tally results
1053
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
54,088✔
1054
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1055

1056
      // Transfer values on master and reset on other ranks
1057
      if (mpi::master) {
54,088✔
1058
        val_view = values_reduced;
27,034✔
1059
      } else {
1060
        val_view = 0.0;
27,054✔
1061
      }
1062
    }
162,264✔
1063
  }
1064

1065
  // Note that global tallies are *always* reduced even when no_reduce option
1066
  // is on.
1067

1068
  // Get reference to global tallies
1069
  auto& gt = simulation::global_tallies;
27,828✔
1070
  const int val_col = static_cast<int>(TallyResult::VALUE);
27,828✔
1071

1072
  // Copy VALUE column into contiguous array for MPI reduction
1073
  tensor::Tensor<double> gt_values(gt.slice(tensor::all, val_col));
27,828✔
1074
  tensor::Tensor<double> gt_values_reduced({size_t {N_GLOBAL_TALLIES}});
27,828✔
1075

1076
  // Reduce contiguous data
1077
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
27,828✔
1078
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1079

1080
  // Transfer values on master and reset on other ranks
1081
  if (mpi::master) {
27,828✔
1082
    gt.slice(tensor::all, val_col) = gt_values_reduced;
27,818✔
1083
  } else {
1084
    gt.slice(tensor::all, val_col) = 0.0;
27,838✔
1085
  }
1086

1087
  // We also need to determine the total starting weight of particles from the
1088
  // last realization
1089
  double weight_reduced;
27,828✔
1090
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
27,828✔
1091
    0, mpi::intracomm);
1092
  if (mpi::master)
27,828✔
1093
    simulation::total_weight = weight_reduced;
13,909✔
1094
}
55,656✔
1095
#endif
1096

1097
void accumulate_tallies()
165,927✔
1098
{
1099
#ifdef OPENMC_MPI
1100
  // Combine tally results onto master process
1101
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
72,692✔
1102
    reduce_tally_results();
27,828✔
1103
  }
1104
#endif
1105

1106
  // Increase number of realizations (only used for global tallies)
1107
  simulation::n_realizations += 1;
165,927✔
1108

1109
  // Accumulate on master only unless run is not reduced then do it on all
1110
  if (mpi::master || !settings::reduce_tallies) {
165,927✔
1111
    auto& gt = simulation::global_tallies;
147,048✔
1112

1113
    if (settings::run_mode == RunMode::EIGENVALUE) {
147,048✔
1114
      if (simulation::current_batch > settings::n_inactive) {
86,964✔
1115
        // Accumulate products of different estimators of k
1116
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
65,795✔
1117
                       simulation::total_weight;
65,795✔
1118
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
65,795✔
1119
                       simulation::total_weight;
65,795✔
1120
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
65,795✔
1121
                       simulation::total_weight;
65,795✔
1122
        simulation::k_col_abs += k_col * k_abs;
65,795✔
1123
        simulation::k_col_tra += k_col * k_tra;
65,795✔
1124
        simulation::k_abs_tra += k_abs * k_tra;
65,795✔
1125
      }
1126
    }
1127

1128
    // Accumulate results for global tallies
1129
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
735,240✔
1130
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
588,192✔
1131
      gt(i, TallyResult::VALUE) = 0.0;
588,192✔
1132
      gt(i, TallyResult::SUM) += val;
588,192✔
1133
      gt(i, TallyResult::SUM_SQ) += val * val;
588,192✔
1134
    }
1135
  }
1136

1137
  // Accumulate results for each tally
1138
  for (int i_tally : model::active_tallies) {
539,818✔
1139
    auto& tally {model::tallies[i_tally]};
373,891✔
1140
    tally->accumulate();
373,891✔
1141
  }
1142
}
165,927✔
1143

1144
double distance_to_time_boundary(double time, double speed)
7,787,043✔
1145
{
1146
  if (model::time_grid.empty()) {
7,787,043!
1147
    return INFTY;
1148
  } else if (time >= model::time_grid.back()) {
7,787,043✔
1149
    return INFTY;
1150
  } else {
1151
    double next_time =
7,760,423✔
1152
      *std::upper_bound(model::time_grid.begin(), model::time_grid.end(), time);
7,760,423✔
1153
    return (next_time - time) * speed;
7,760,423✔
1154
  }
1155
}
1156

1157
//! Add new points to the global time grid
1158
//
1159
//! \param grid Vector of new time points to add
1160
void add_to_time_grid(vector<double> grid)
220✔
1161
{
1162
  if (grid.empty())
220!
UNCOV
1163
    return;
×
1164

1165
  // Create new vector with enough space to hold old and new grid points
1166
  vector<double> merged;
220✔
1167
  merged.reserve(model::time_grid.size() + grid.size());
220✔
1168

1169
  // Merge and remove duplicates
1170
  std::set_union(model::time_grid.begin(), model::time_grid.end(), grid.begin(),
220✔
1171
    grid.end(), std::back_inserter(merged));
1172

1173
  // Swap in the new grid
1174
  model::time_grid.swap(merged);
220✔
1175
}
220✔
1176

1177
void setup_active_tallies()
165,940✔
1178
{
1179
  model::active_tallies.clear();
165,940✔
1180
  model::active_analog_tallies.clear();
165,940✔
1181
  model::active_tracklength_tallies.clear();
165,940✔
1182
  model::active_timed_tracklength_tallies.clear();
165,940✔
1183
  model::active_collision_tallies.clear();
165,940✔
1184
  model::active_meshsurf_tallies.clear();
165,940✔
1185
  model::active_surface_tallies.clear();
165,940✔
1186
  model::active_pulse_height_tallies.clear();
165,940✔
1187
  model::time_grid.clear();
165,940✔
1188

1189
  for (auto i = 0; i < model::tallies.size(); ++i) {
680,014✔
1190
    const auto& tally {*model::tallies[i]};
514,074✔
1191

1192
    if (tally.active_) {
514,074✔
1193
      model::active_tallies.push_back(i);
373,891✔
1194
      bool mesh_present = (tally.get_filter<MeshFilter>() ||
373,891✔
1195
                           tally.get_filter<MeshMaterialFilter>());
325,208✔
1196
      auto time_filter = tally.get_filter<TimeFilter>();
373,891✔
1197
      switch (tally.type_) {
373,891!
1198

1199
      case TallyType::VOLUME:
367,121✔
1200
        switch (tally.estimator_) {
367,121!
1201
        case TallyEstimator::ANALOG:
209,000✔
1202
          model::active_analog_tallies.push_back(i);
209,000✔
1203
          break;
1204
        case TallyEstimator::TRACKLENGTH:
150,286✔
1205
          if (time_filter && mesh_present) {
150,286✔
1206
            model::active_timed_tracklength_tallies.push_back(i);
220✔
1207
            add_to_time_grid(time_filter->bins());
440✔
1208
          } else {
1209
            model::active_tracklength_tallies.push_back(i);
150,066✔
1210
          }
1211
          break;
1212
        case TallyEstimator::COLLISION:
7,835✔
1213
          model::active_collision_tallies.push_back(i);
7,835✔
1214
        }
1215
        break;
1216

1217
      case TallyType::MESH_SURFACE:
3,971✔
1218
        model::active_meshsurf_tallies.push_back(i);
3,971✔
1219
        break;
1220

1221
      case TallyType::SURFACE:
2,349✔
1222
        model::active_surface_tallies.push_back(i);
2,349✔
1223
        break;
1224

1225
      case TallyType::PULSE_HEIGHT:
450✔
1226
        model::active_pulse_height_tallies.push_back(i);
450✔
1227
        break;
1228
      }
1229
    }
1230
  }
1231
}
165,940✔
1232

1233
void free_memory_tally()
8,721✔
1234
{
1235
  model::tally_derivs.clear();
8,721✔
1236
  model::tally_deriv_map.clear();
8,721✔
1237

1238
  model::tally_filters.clear();
8,721✔
1239
  model::filter_map.clear();
8,721✔
1240

1241
  model::tallies.clear();
8,721✔
1242

1243
  model::active_tallies.clear();
8,721✔
1244
  model::active_analog_tallies.clear();
8,721✔
1245
  model::active_tracklength_tallies.clear();
8,721✔
1246
  model::active_timed_tracklength_tallies.clear();
8,721✔
1247
  model::active_collision_tallies.clear();
8,721✔
1248
  model::active_meshsurf_tallies.clear();
8,721✔
1249
  model::active_surface_tallies.clear();
8,721✔
1250
  model::active_pulse_height_tallies.clear();
8,721✔
1251
  model::time_grid.clear();
8,721✔
1252

1253
  model::tally_map.clear();
8,721✔
1254
}
8,721✔
1255

1256
//==============================================================================
1257
// C-API functions
1258
//==============================================================================
1259

1260
extern "C" int openmc_extend_tallies(
1,096✔
1261
  int32_t n, int32_t* index_start, int32_t* index_end)
1262
{
1263
  if (index_start)
1,096!
1264
    *index_start = model::tallies.size();
1,096✔
1265
  if (index_end)
1,096!
UNCOV
1266
    *index_end = model::tallies.size() + n - 1;
×
1267
  for (int i = 0; i < n; ++i) {
2,192✔
1268
    model::tallies.push_back(make_unique<Tally>(-1));
2,192✔
1269
  }
1270
  return 0;
1,096✔
1271
}
1272

1273
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
20,922✔
1274
{
1275
  auto it = model::tally_map.find(id);
20,922✔
1276
  if (it == model::tally_map.end()) {
20,922✔
1277
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
22✔
1278
    return OPENMC_E_INVALID_ID;
22✔
1279
  }
1280

1281
  *index = it->second;
20,900✔
1282
  return 0;
20,900✔
1283
}
1284

1285
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1286
{
UNCOV
1287
  int32_t largest_tally_id = 0;
×
UNCOV
1288
  for (const auto& t : model::tallies) {
×
UNCOV
1289
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1290
  }
UNCOV
1291
  *id = largest_tally_id + 1;
×
1292
}
×
1293

UNCOV
1294
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1295
{
UNCOV
1296
  if (index < 0 || index >= model::tallies.size()) {
×
UNCOV
1297
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1298
    return OPENMC_E_OUT_OF_BOUNDS;
×
1299
  }
1300

1301
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1302
  return 0;
×
1303
}
1304

1305
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
660✔
1306
{
1307
  if (index < 0 || index >= model::tallies.size()) {
660!
UNCOV
1308
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1309
    return OPENMC_E_OUT_OF_BOUNDS;
×
1310
  }
1311

1312
  auto& t {model::tallies[index]};
660✔
1313

1314
  std::string est = estimator;
660✔
1315
  if (est == "analog") {
660!
1316
    t->estimator_ = TallyEstimator::ANALOG;
660✔
UNCOV
1317
  } else if (est == "collision") {
×
UNCOV
1318
    t->estimator_ = TallyEstimator::COLLISION;
×
UNCOV
1319
  } else if (est == "tracklength") {
×
UNCOV
1320
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1321
  } else {
UNCOV
1322
    set_errmsg("Unknown tally estimator: " + est);
×
UNCOV
1323
    return OPENMC_E_INVALID_ARGUMENT;
×
1324
  }
1325
  return 0;
1326
}
660✔
1327

1328
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
27,133✔
1329
{
1330
  if (index < 0 || index >= model::tallies.size()) {
27,133!
UNCOV
1331
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1332
    return OPENMC_E_OUT_OF_BOUNDS;
×
1333
  }
1334

1335
  *id = model::tallies[index]->id_;
27,133✔
1336
  return 0;
27,133✔
1337
}
1338

1339
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,096✔
1340
{
1341
  if (index < 0 || index >= model::tallies.size()) {
1,096!
UNCOV
1342
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1343
    return OPENMC_E_OUT_OF_BOUNDS;
×
1344
  }
1345

1346
  model::tallies[index]->set_id(id);
1,096✔
1347
  return 0;
1,096✔
1348
}
1349

1350
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
11✔
1351
{
1352
  if (index < 0 || index >= model::tallies.size()) {
11!
UNCOV
1353
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1354
    return OPENMC_E_OUT_OF_BOUNDS;
×
1355
  }
1356
  *type = static_cast<int>(model::tallies[index]->type_);
11✔
1357

1358
  return 0;
11✔
1359
}
1360

1361
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
660✔
1362
{
1363
  if (index < 0 || index >= model::tallies.size()) {
660!
UNCOV
1364
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1365
    return OPENMC_E_OUT_OF_BOUNDS;
×
1366
  }
1367
  if (strcmp(type, "volume") == 0) {
660✔
1368
    model::tallies[index]->type_ = TallyType::VOLUME;
495✔
1369
  } else if (strcmp(type, "mesh-surface") == 0) {
165!
1370
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
165✔
1371
  } else if (strcmp(type, "surface") == 0) {
×
UNCOV
1372
    model::tallies[index]->type_ = TallyType::SURFACE;
×
UNCOV
1373
  } else if (strcmp(type, "pulse-height") == 0) {
×
UNCOV
1374
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1375
  } else {
UNCOV
1376
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
UNCOV
1377
    return OPENMC_E_INVALID_ARGUMENT;
×
1378
  }
1379

1380
  return 0;
1381
}
1382

1383
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
22✔
1384
{
1385
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1386
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1387
    return OPENMC_E_OUT_OF_BOUNDS;
×
1388
  }
1389
  *active = model::tallies[index]->active_;
22✔
1390

1391
  return 0;
22✔
1392
}
1393

1394
extern "C" int openmc_tally_set_active(int32_t index, bool active)
671✔
1395
{
1396
  if (index < 0 || index >= model::tallies.size()) {
671!
UNCOV
1397
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1398
    return OPENMC_E_OUT_OF_BOUNDS;
×
1399
  }
1400
  model::tallies[index]->active_ = active;
671✔
1401

1402
  return 0;
671✔
1403
}
1404

1405
extern "C" int openmc_tally_get_writable(int32_t index, bool* writable)
22✔
1406
{
1407
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1408
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1409
    return OPENMC_E_OUT_OF_BOUNDS;
×
1410
  }
1411
  *writable = model::tallies[index]->writable();
22✔
1412

1413
  return 0;
22✔
1414
}
1415

1416
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
436✔
1417
{
1418
  if (index < 0 || index >= model::tallies.size()) {
436!
UNCOV
1419
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1420
    return OPENMC_E_OUT_OF_BOUNDS;
×
1421
  }
1422
  model::tallies[index]->set_writable(writable);
436✔
1423

1424
  return 0;
436✔
1425
}
1426

1427
extern "C" int openmc_tally_get_multiply_density(int32_t index, bool* value)
22✔
1428
{
1429
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1430
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1431
    return OPENMC_E_OUT_OF_BOUNDS;
×
1432
  }
1433
  *value = model::tallies[index]->multiply_density();
22✔
1434

1435
  return 0;
22✔
1436
}
1437

1438
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
293✔
1439
{
1440
  if (index < 0 || index >= model::tallies.size()) {
293!
UNCOV
1441
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1442
    return OPENMC_E_OUT_OF_BOUNDS;
×
1443
  }
1444
  model::tallies[index]->set_multiply_density(value);
293✔
1445

1446
  return 0;
293✔
1447
}
1448

1449
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
187✔
1450
{
1451
  if (index < 0 || index >= model::tallies.size()) {
187!
UNCOV
1452
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1453
    return OPENMC_E_OUT_OF_BOUNDS;
×
1454
  }
1455

1456
  *scores = model::tallies[index]->scores_.data();
187✔
1457
  *n = model::tallies[index]->scores_.size();
187✔
1458
  return 0;
187✔
1459
}
1460

1461
extern "C" int openmc_tally_set_scores(
1,107✔
1462
  int32_t index, int n, const char** scores)
1463
{
1464
  if (index < 0 || index >= model::tallies.size()) {
1,107!
UNCOV
1465
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1466
    return OPENMC_E_OUT_OF_BOUNDS;
×
1467
  }
1468

1469
  vector<std::string> scores_str(scores, scores + n);
1,107✔
1470
  try {
1,107✔
1471
    model::tallies[index]->set_scores(scores_str);
1,107✔
UNCOV
1472
  } catch (const std::invalid_argument& ex) {
×
UNCOV
1473
    set_errmsg(ex.what());
×
UNCOV
1474
    return OPENMC_E_INVALID_ARGUMENT;
×
UNCOV
1475
  }
×
1476

1477
  return 0;
1478
}
1,107✔
1479

1480
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
231✔
1481
{
1482
  // Make sure the index fits in the array bounds.
1483
  if (index < 0 || index >= model::tallies.size()) {
231!
1484
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1485
    return OPENMC_E_OUT_OF_BOUNDS;
×
1486
  }
1487

1488
  *n = model::tallies[index]->nuclides_.size();
231✔
1489
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1490

1491
  return 0;
231✔
1492
}
1493

1494
extern "C" int openmc_tally_set_nuclides(
1,392✔
1495
  int32_t index, int n, const char** nuclides)
1496
{
1497
  // Make sure the index fits in the array bounds.
1498
  if (index < 0 || index >= model::tallies.size()) {
1,392!
UNCOV
1499
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1500
    return OPENMC_E_OUT_OF_BOUNDS;
×
1501
  }
1502

1503
  vector<std::string> words(nuclides, nuclides + n);
1,392✔
1504
  vector<int> nucs;
1,392✔
1505
  for (auto word : words) {
6,770✔
1506
    if (word == "total") {
5,389✔
1507
      nucs.push_back(-1);
660✔
1508
    } else {
1509
      auto search = data::nuclide_map.find(word);
4,729✔
1510
      if (search == data::nuclide_map.end()) {
4,729✔
1511
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
1,191✔
1512
        if (err < 0) {
1,191✔
1513
          set_errmsg(openmc_err_msg);
11✔
1514
          return OPENMC_E_DATA;
11✔
1515
        }
1516
      }
1517
      nucs.push_back(data::nuclide_map.at(word));
4,718✔
1518
    }
1519
  }
5,389✔
1520

1521
  model::tallies[index]->nuclides_ = nucs;
1,381✔
1522

1523
  return 0;
1524
}
1,392✔
1525

1526
extern "C" int openmc_tally_get_filters(
1,034✔
1527
  int32_t index, const int32_t** indices, size_t* n)
1528
{
1529
  if (index < 0 || index >= model::tallies.size()) {
1,034!
UNCOV
1530
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1531
    return OPENMC_E_OUT_OF_BOUNDS;
×
1532
  }
1533

1534
  *indices = model::tallies[index]->filters().data();
1,034✔
1535
  *n = model::tallies[index]->filters().size();
1,034✔
1536
  return 0;
1,034✔
1537
}
1538

1539
extern "C" int openmc_tally_set_filters(
1,151✔
1540
  int32_t index, size_t n, const int32_t* indices)
1541
{
1542
  // Make sure the index fits in the array bounds.
1543
  if (index < 0 || index >= model::tallies.size()) {
1,151!
1544
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1545
    return OPENMC_E_OUT_OF_BOUNDS;
×
1546
  }
1547

1548
  // Set the filters.
1549
  try {
1,151✔
1550
    // Convert indices to filter pointers
1551
    vector<Filter*> filters;
1,151✔
1552
    for (int64_t i = 0; i < n; ++i) {
2,720✔
1553
      int32_t i_filt = indices[i];
1,569✔
1554
      filters.push_back(model::tally_filters.at(i_filt).get());
1,569✔
1555
    }
1556
    model::tallies[index]->set_filters(filters);
1,151✔
UNCOV
1557
  } catch (const std::out_of_range& ex) {
×
UNCOV
1558
    set_errmsg("Index in tally filter array out of bounds.");
×
UNCOV
1559
    return OPENMC_E_OUT_OF_BOUNDS;
×
UNCOV
1560
  }
×
1561

1562
  return 0;
1,151✔
1563
}
1564

1565
//! Reset tally results and number of realizations
1566
extern "C" int openmc_tally_reset(int32_t index)
2,684✔
1567
{
1568
  // Make sure the index fits in the array bounds.
1569
  if (index < 0 || index >= model::tallies.size()) {
2,684!
UNCOV
1570
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1571
    return OPENMC_E_OUT_OF_BOUNDS;
×
1572
  }
1573

1574
  model::tallies[index]->reset();
2,684✔
1575
  return 0;
2,684✔
1576
}
1577

1578
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
3,482✔
1579
{
1580
  // Make sure the index fits in the array bounds.
1581
  if (index < 0 || index >= model::tallies.size()) {
3,482!
1582
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1583
    return OPENMC_E_OUT_OF_BOUNDS;
×
1584
  }
1585

1586
  *n = model::tallies[index]->n_realizations_;
3,482✔
1587
  return 0;
3,482✔
1588
}
1589

1590
//! \brief Returns a pointer to a tally results array along with its shape.
1591
//! This allows a user to obtain in-memory tally results from Python directly.
1592
extern "C" int openmc_tally_results(
15,978✔
1593
  int32_t index, double** results, size_t* shape)
1594
{
1595
  // Make sure the index fits in the array bounds.
1596
  if (index < 0 || index >= model::tallies.size()) {
15,978!
UNCOV
1597
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1598
    return OPENMC_E_OUT_OF_BOUNDS;
×
1599
  }
1600

1601
  const auto& t {model::tallies[index]};
15,978!
1602
  if (t->results_.size() == 0) {
15,978!
UNCOV
1603
    set_errmsg("Tally results have not been allocated yet.");
×
UNCOV
1604
    return OPENMC_E_ALLOCATE;
×
1605
  }
1606

1607
  // Set pointer to results and copy shape
1608
  *results = t->results_.data();
15,978✔
1609
  auto s = t->results_.shape();
15,978✔
1610
  shape[0] = s[0];
15,978✔
1611
  shape[1] = s[1];
15,978✔
1612
  shape[2] = s[2];
15,978✔
1613
  return 0;
15,978✔
1614
}
15,978✔
1615

1616
extern "C" int openmc_global_tallies(double** ptr)
11✔
1617
{
1618
  *ptr = simulation::global_tallies.data();
11✔
1619
  return 0;
11✔
1620
}
1621

1622
extern "C" size_t tallies_size()
1,140✔
1623
{
1624
  return model::tallies.size();
1,140✔
1625
}
1626

1627
// given a tally ID, remove it from the tallies vector
1628
extern "C" int openmc_remove_tally(int32_t index)
11✔
1629
{
1630
  // check that id is in the map
1631
  if (index < 0 || index >= model::tallies.size()) {
11!
UNCOV
1632
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1633
    return OPENMC_E_OUT_OF_BOUNDS;
×
1634
  }
1635

1636
  // delete the tally via iterator pointing to correct position
1637
  // this calls the Tally destructor, removing the tally from the map as well
1638
  model::tallies.erase(model::tallies.begin() + index);
11✔
1639

1640
  return 0;
11✔
1641
}
1642

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