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

openmc-dev / openmc / 19058781736

04 Nov 2025 05:26AM UTC coverage: 82.008% (-3.1%) from 85.155%
19058781736

Pull #3252

github

web-flow
Merge b8a72730f into bd76fc056
Pull Request #3252: Adding vtkhdf option to write vtk data

16714 of 23236 branches covered (71.93%)

Branch coverage included in aggregate %.

61 of 66 new or added lines in 1 file covered. (92.42%)

3175 existing lines in 103 files now uncovered.

54243 of 63288 relevant lines covered (85.71%)

43393337.77 hits per line

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

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

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

38
#include "xtensor/xadapt.hpp"
39
#include "xtensor/xbuilder.hpp" // for empty_like
40
#include "xtensor/xview.hpp"
41
#include <fmt/core.h>
42

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

49
namespace openmc {
50

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

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

71
namespace simulation {
72
xt::xtensor_fixed<double, xt::xshape<N_GLOBAL_TALLIES, 3>> global_tallies;
73
int32_t n_realizations {0};
74
} // namespace simulation
75

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

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

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

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

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

103
  if (check_for_node(node, "name"))
24,927✔
104
    name_ = get_node_value(node, "name");
2,668✔
105

106
  if (check_for_node(node, "multiply_density")) {
24,927✔
107
    multiply_density_ = get_node_value_bool(node, "multiply_density");
55✔
108
  }
109

110
  // =======================================================================
111
  // READ DATA FOR FILTERS
112

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

123
  // Determine number of filters
124
  vector<int> filter_ids;
24,927✔
125
  if (check_for_node(node, "filters")) {
24,927✔
126
    filter_ids = get_node_array<int>(node, "filters");
23,820✔
127
  }
128

129
  // Allocate and store filter user ids
130
  vector<Filter*> filters;
24,927✔
131
  for (int filter_id : filter_ids) {
73,974✔
132
    // Determine if filter ID is valid
133
    auto it = model::filter_map.find(filter_id);
49,047✔
134
    if (it == model::filter_map.end()) {
49,047!
UNCOV
135
      throw std::runtime_error {fmt::format(
×
UNCOV
136
        "Could not find filter {} specified on tally {}", filter_id, id_)};
×
137
    }
138

139
    // Store the index of the filter
140
    filters.push_back(model::tally_filters[it->second].get());
49,047✔
141
  }
142

143
  // Set the filters
144
  this->set_filters(filters);
24,927✔
145

146
  // Check for the presence of certain filter types
147
  bool has_energyout = energyout_filter_ >= 0;
24,927✔
148
  int particle_filter_index = C_NONE;
24,927✔
149
  for (int64_t j = 0; j < filters_.size(); ++j) {
73,974✔
150
    int i_filter = filters_[j];
49,047✔
151
    const auto& f = model::tally_filters[i_filter].get();
49,047✔
152

153
    auto pf = dynamic_cast<ParticleFilter*>(f);
49,047!
154
    if (pf)
49,047✔
155
      particle_filter_index = i_filter;
438✔
156

157
    // Change the tally estimator if a filter demands it
158
    FilterType filt_type = f->type();
49,047✔
159
    if (filt_type == FilterType::ENERGY_OUT ||
49,047✔
160
        filt_type == FilterType::LEGENDRE) {
161
      estimator_ = TallyEstimator::ANALOG;
6,558✔
162
    } else if (filt_type == FilterType::SPHERICAL_HARMONICS) {
42,489✔
163
      auto sf = dynamic_cast<SphericalHarmonicsFilter*>(f);
70!
164
      if (sf->cosine() == SphericalHarmonicsCosine::scatter) {
70✔
165
        estimator_ = TallyEstimator::ANALOG;
11✔
166
      }
167
    } else if (filt_type == FilterType::SPATIAL_LEGENDRE ||
42,419✔
168
               filt_type == FilterType::ZERNIKE ||
42,352!
169
               filt_type == FilterType::ZERNIKE_RADIAL) {
170
      estimator_ = TallyEstimator::COLLISION;
67✔
171
    }
172
  }
173

174
  // =======================================================================
175
  // READ DATA FOR NUCLIDES
176

177
  this->set_nuclides(node);
24,927✔
178

179
  // =======================================================================
180
  // READ DATA FOR SCORES
181

182
  this->set_scores(node);
24,927✔
183

184
  if (!check_for_node(node, "scores")) {
24,927!
UNCOV
185
    fatal_error(fmt::format("No scores specified on tally {}.", id_));
×
186
  }
187

188
  // Set IFP if needed
189
  if (!settings::ifp_on) {
24,927✔
190
    // Determine if this tally has an IFP score
191
    bool has_ifp_score = false;
24,807✔
192
    for (int score : scores_) {
58,855✔
193
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
34,142!
194
          score == SCORE_IFP_DENOM) {
195
        has_ifp_score = true;
94✔
196
        break;
94✔
197
      }
198
    }
199

200
    // Check for errors
201
    if (has_ifp_score) {
24,807✔
202
      if (settings::run_mode == RunMode::EIGENVALUE) {
94✔
203
        if (settings::ifp_n_generation < 0) {
85✔
204
          settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION;
9✔
205
          warning(fmt::format(
9!
206
            "{} generations will be used for IFP (default value). It can be "
207
            "changed using the 'ifp_n_generation' settings.",
208
            settings::ifp_n_generation));
209
        }
210
        if (settings::ifp_n_generation > settings::n_inactive) {
85✔
211
          fatal_error("'ifp_n_generation' must be lower than or equal to the "
9✔
212
                      "number of inactive cycles.");
213
        }
214
        settings::ifp_on = true;
76✔
215
      } else {
216
        fatal_error(
9✔
217
          "Iterated Fission Probability can only be used in an eigenvalue "
218
          "calculation.");
219
      }
220
    }
221
  }
222

223
  // Set IFP parameters if needed
224
  if (settings::ifp_on) {
24,909✔
225
    for (int score : scores_) {
424✔
226
      switch (score) {
228!
227
      case SCORE_IFP_TIME_NUM:
76✔
228
        if (settings::ifp_parameter == IFPParameter::None) {
76!
229
          settings::ifp_parameter = IFPParameter::GenerationTime;
76✔
UNCOV
230
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
UNCOV
231
          settings::ifp_parameter = IFPParameter::Both;
×
232
        }
233
        break;
76✔
234
      case SCORE_IFP_BETA_NUM:
152✔
235
      case SCORE_IFP_DENOM:
236
        if (settings::ifp_parameter == IFPParameter::None) {
152!
UNCOV
237
          settings::ifp_parameter = IFPParameter::BetaEffective;
×
238
        } else if (settings::ifp_parameter == IFPParameter::GenerationTime) {
152✔
239
          settings::ifp_parameter = IFPParameter::Both;
76✔
240
        }
241
        break;
152✔
242
      }
243
    }
244
  }
245

246
  // Check if tally is compatible with particle type
247
  if (!settings::photon_transport) {
24,909✔
248
    for (int score : scores_) {
58,075✔
249
      switch (score) {
33,593!
UNCOV
250
      case SCORE_PULSE_HEIGHT:
×
UNCOV
251
        fatal_error("For pulse-height tallies, photon transport needs to be "
×
252
                    "activated.");
253
        break;
254
      }
255
    }
256
  }
257
  if (settings::photon_transport) {
24,909✔
258
    if (particle_filter_index == C_NONE) {
427✔
259
      for (int score : scores_) {
218✔
260
        switch (score) {
109!
UNCOV
261
        case SCORE_INVERSE_VELOCITY:
×
UNCOV
262
          fatal_error("Particle filter must be used with photon "
×
263
                      "transport on and inverse velocity score");
264
          break;
265
        case SCORE_FLUX:
44✔
266
        case SCORE_TOTAL:
267
        case SCORE_SCATTER:
268
        case SCORE_NU_SCATTER:
269
        case SCORE_ABSORPTION:
270
        case SCORE_FISSION:
271
        case SCORE_NU_FISSION:
272
        case SCORE_CURRENT:
273
        case SCORE_EVENTS:
274
        case SCORE_DELAYED_NU_FISSION:
275
        case SCORE_PROMPT_NU_FISSION:
276
        case SCORE_DECAY_RATE:
277
          warning("You are tallying the '" + reaction_name(score) +
44✔
278
                  "' score and haven't used a particle filter. This score will "
279
                  "include contributions from all particles.");
280
          break;
44✔
281
        }
282
      }
283
    }
284
  } else {
285
    if (particle_filter_index >= 0) {
24,482✔
286
      const auto& f = model::tally_filters[particle_filter_index].get();
120✔
287
      auto pf = dynamic_cast<ParticleFilter*>(f);
120!
288
      for (auto p : pf->particles()) {
360✔
289
        if (p != ParticleType::neutron) {
240✔
290
          warning(fmt::format(
120✔
291
            "Particle filter other than NEUTRON used with "
292
            "photon transport turned off. All tallies for particle type {}"
293
            " will have no scores",
294
            static_cast<int>(p)));
240✔
295
        }
296
      }
297
    }
298
  }
299

300
  // Check for a tally derivative.
301
  if (check_for_node(node, "derivative")) {
24,909✔
302
    int deriv_id = std::stoi(get_node_value(node, "derivative"));
320✔
303

304
    // Find the derivative with the given id, and store it's index.
305
    auto it = model::tally_deriv_map.find(deriv_id);
320✔
306
    if (it == model::tally_deriv_map.end()) {
320!
UNCOV
307
      fatal_error(fmt::format(
×
UNCOV
308
        "Could not find derivative {} specified on tally {}", deriv_id, id_));
×
309
    }
310

311
    deriv_ = it->second;
320✔
312

313
    // Only analog or collision estimators are supported for differential
314
    // tallies.
315
    if (estimator_ == TallyEstimator::TRACKLENGTH) {
320✔
316
      estimator_ = TallyEstimator::COLLISION;
240✔
317
    }
318

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

340
  // If settings.xml trigger is turned on, create tally triggers
341
  if (settings::trigger_on) {
24,909✔
342
    this->init_triggers(node);
183✔
343
  }
344

345
  // =======================================================================
346
  // SET TALLY ESTIMATOR
347

348
  // Check if user specified estimator
349
  if (check_for_node(node, "estimator")) {
24,909✔
350
    std::string est = get_node_value(node, "estimator");
19,809✔
351
    if (est == "analog") {
19,809✔
352
      estimator_ = TallyEstimator::ANALOG;
8,143✔
353
    } else if (est == "tracklength" || est == "track-length" ||
12,494!
354
               est == "pathlength" || est == "path-length") {
12,494!
355
      // If the estimator was set to an analog estimator, this means the
356
      // tally needs post-collision information
357
      if (estimator_ == TallyEstimator::ANALOG ||
11,252!
358
          estimator_ == TallyEstimator::COLLISION) {
11,252!
UNCOV
359
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
360
                                              "estimator for tally {}",
UNCOV
361
          id_)};
×
362
      }
363

364
      // Set estimator to track-length estimator
365
      estimator_ = TallyEstimator::TRACKLENGTH;
11,252✔
366

367
    } else if (est == "collision") {
414!
368
      // If the estimator was set to an analog estimator, this means the
369
      // tally needs post-collision information
370
      if (estimator_ == TallyEstimator::ANALOG) {
414!
UNCOV
371
        throw std::runtime_error {fmt::format("Cannot use collision estimator "
×
372
                                              "for tally ",
UNCOV
373
          id_)};
×
374
      }
375

376
      // Set estimator to collision estimator
377
      estimator_ = TallyEstimator::COLLISION;
414✔
378

379
    } else {
UNCOV
380
      throw std::runtime_error {
×
UNCOV
381
        fmt::format("Invalid estimator '{}' on tally {}", est, id_)};
×
382
    }
383
  }
19,809✔
384

385
#ifdef OPENMC_LIBMESH_ENABLED
386
  // ensure a tracklength tally isn't used with a libMesh filter
387
  for (auto i : this->filters_) {
13,795✔
388
    auto df = dynamic_cast<MeshFilter*>(model::tally_filters[i].get());
9,154!
389
    if (df) {
9,154✔
390
      auto lm = dynamic_cast<LibMesh*>(model::meshes[df->mesh()].get());
1,096!
391
      if (lm && estimator_ == TallyEstimator::TRACKLENGTH) {
1,096!
392
        fatal_error("A tracklength estimator cannot be used with "
393
                    "an unstructured LibMesh tally.");
394
      }
395
    }
396
  }
397
#endif
398
}
24,909✔
399

400
Tally::~Tally()
26,627✔
401
{
402
  model::tally_map.erase(id_);
26,627✔
403
}
26,627✔
404

405
Tally* Tally::create(int32_t id)
109✔
406
{
407
  model::tallies.push_back(make_unique<Tally>(id));
109✔
408
  return model::tallies.back().get();
109✔
409
}
410

411
void Tally::set_id(int32_t id)
28,253✔
412
{
413
  assert(id >= 0 || id == C_NONE);
23,036!
414

415
  // Clear entry in tally map if an ID was already assigned before
416
  if (id_ != C_NONE) {
28,253✔
417
    model::tally_map.erase(id_);
1,608✔
418
    id_ = C_NONE;
1,608✔
419
  }
420

421
  // Make sure no other tally has the same ID
422
  if (model::tally_map.find(id) != model::tally_map.end()) {
28,253!
UNCOV
423
    throw std::runtime_error {
×
UNCOV
424
      fmt::format("Two tallies have the same ID: {}", id)};
×
425
  }
426

427
  // If no ID specified, auto-assign next ID in sequence
428
  if (id == C_NONE) {
28,253✔
429
    id = 0;
1,718✔
430
    for (const auto& t : model::tallies) {
5,512✔
431
      id = std::max(id, t->id_);
3,794✔
432
    }
433
    ++id;
1,718✔
434
  }
435

436
  // Update ID and entry in tally map
437
  id_ = id;
28,253✔
438
  model::tally_map[id] = index_;
28,253✔
439
}
28,253✔
440

441
std::vector<FilterType> Tally::filter_types() const
263✔
442
{
443
  std::vector<FilterType> filter_types;
263✔
444
  for (auto idx : this->filters())
1,004✔
445
    filter_types.push_back(model::tally_filters[idx]->type());
741✔
446
  return filter_types;
263✔
UNCOV
447
}
×
448

449
std::unordered_map<FilterType, int32_t> Tally::filter_indices() const
263✔
450
{
451
  std::unordered_map<FilterType, int32_t> filter_indices;
263✔
452
  for (int i = 0; i < this->filters().size(); i++) {
1,004✔
453
    const auto& f = model::tally_filters[this->filters(i)];
741✔
454

455
    filter_indices[f->type()] = i;
741✔
456
  }
457
  return filter_indices;
263✔
UNCOV
458
}
×
459

460
bool Tally::has_filter(FilterType filter_type) const
789✔
461
{
462
  for (auto idx : this->filters()) {
1,900✔
463
    if (model::tally_filters[idx]->type() == filter_type)
1,828✔
464
      return true;
717✔
465
  }
466
  return false;
72✔
467
}
468

469
void Tally::set_filters(span<Filter*> filters)
28,510✔
470
{
471
  // Clear old data.
472
  filters_.clear();
28,510✔
473
  strides_.clear();
28,510✔
474

475
  // Copy in the given filter indices.
476
  auto n = filters.size();
28,510✔
477
  filters_.reserve(n);
28,510✔
478

479
  for (auto* filter : filters) {
80,232✔
480
    add_filter(filter);
51,722✔
481
  }
482
}
28,510✔
483

484
void Tally::add_filter(Filter* filter)
52,001✔
485
{
486
  int32_t filter_idx = model::filter_map.at(filter->id());
52,001✔
487
  // if this filter is already present, do nothing and return
488
  if (std::find(filters_.begin(), filters_.end(), filter_idx) != filters_.end())
52,001✔
489
    return;
22✔
490

491
  // Keep track of indices for special filters
492
  if (filter->type() == FilterType::ENERGY_OUT) {
51,979✔
493
    energyout_filter_ = filters_.size();
4,717✔
494
  } else if (filter->type() == FilterType::DELAYED_GROUP) {
47,262✔
495
    delayedgroup_filter_ = filters_.size();
838✔
496
  }
497
  filters_.push_back(filter_idx);
51,979✔
498
}
499

500
void Tally::set_strides()
27,104✔
501
{
502
  // Set the strides.  Filters are traversed in reverse so that the last
503
  // filter has the shortest stride in memory and the first filter has the
504
  // longest stride.
505
  auto n = filters_.size();
27,104✔
506
  strides_.resize(n, 0);
27,104✔
507
  int stride = 1;
27,104✔
508
  for (int i = n - 1; i >= 0; --i) {
79,166✔
509
    strides_[i] = stride;
52,062✔
510
    stride *= model::tally_filters[filters_[i]]->n_bins();
52,062✔
511
  }
512
  n_filter_bins_ = stride;
27,104✔
513
}
27,104✔
514

515
void Tally::set_scores(pugi::xml_node node)
24,927✔
516
{
517
  if (!check_for_node(node, "scores"))
24,927!
UNCOV
518
    fatal_error(fmt::format("No scores specified on tally {}", id_));
×
519

520
  auto scores = get_node_array<std::string>(node, "scores");
24,927✔
521
  set_scores(scores);
24,927✔
522
}
24,927✔
523

524
void Tally::set_scores(const vector<std::string>& scores)
26,649✔
525
{
526
  // Reset state and prepare for the new scores.
527
  scores_.clear();
26,649✔
528
  scores_.reserve(scores.size());
26,649✔
529

530
  // Check for the presence of certain restrictive filters.
531
  bool energyout_present = energyout_filter_ != C_NONE;
26,649✔
532
  bool legendre_present = false;
26,649✔
533
  bool cell_present = false;
26,649✔
534
  bool cellfrom_present = false;
26,649✔
535
  bool surface_present = false;
26,649✔
536
  bool meshsurface_present = false;
26,649✔
537
  bool non_cell_energy_present = false;
26,649✔
538
  for (auto i_filt : filters_) {
77,058✔
539
    const auto* filt {model::tally_filters[i_filt].get()};
50,409✔
540
    // Checking for only cell and energy filters for pulse-height tally
541
    if (!(filt->type() == FilterType::CELL ||
99,697✔
542
          filt->type() == FilterType::ENERGY)) {
49,288✔
543
      non_cell_energy_present = true;
32,168✔
544
    }
545
    if (filt->type() == FilterType::LEGENDRE) {
50,409✔
546
      legendre_present = true;
2,045✔
547
    } else if (filt->type() == FilterType::CELLFROM) {
48,364✔
548
      cellfrom_present = true;
304✔
549
    } else if (filt->type() == FilterType::CELL) {
48,060✔
550
      cell_present = true;
1,121✔
551
    } else if (filt->type() == FilterType::SURFACE) {
46,939✔
552
      surface_present = true;
225✔
553
    } else if (filt->type() == FilterType::MESH_SURFACE) {
46,714✔
554
      meshsurface_present = true;
367✔
555
    }
556
  }
557

558
  // Iterate over the given scores.
559
  for (auto score_str : scores) {
63,291✔
560
    // Make sure a delayed group filter wasn't used with an incompatible
561
    // score.
562
    if (delayedgroup_filter_ != C_NONE) {
36,642✔
563
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate" &&
892!
564
          score_str != "ifp-beta-numerator")
38✔
UNCOV
565
        fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
×
566
    }
567

568
    // Determine integer code for score
569
    int score = reaction_type(score_str);
36,642✔
570

571
    switch (score) {
36,642✔
572
    case SCORE_FLUX:
11,132✔
573
      if (!nuclides_.empty())
11,132!
574
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
11,132!
UNCOV
575
          fatal_error("Cannot tally flux for an individual nuclide.");
×
576
      if (energyout_present)
11,132!
UNCOV
577
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
578
      break;
11,132✔
579

580
    case SCORE_TOTAL:
7,031✔
581
    case SCORE_ABSORPTION:
582
    case SCORE_FISSION:
583
      if (energyout_present)
7,031!
UNCOV
584
        fatal_error("Cannot tally " + score_str +
×
585
                    " reaction rate with an "
586
                    "outgoing energy filter");
587
      break;
7,031✔
588

589
    case SCORE_SCATTER:
3,901✔
590
      if (legendre_present)
3,901✔
591
        estimator_ = TallyEstimator::ANALOG;
1,362✔
592
    case SCORE_NU_FISSION:
593
    case SCORE_DELAYED_NU_FISSION:
594
    case SCORE_PROMPT_NU_FISSION:
595
      if (energyout_present)
8,644✔
596
        estimator_ = TallyEstimator::ANALOG;
3,571✔
597
      break;
8,644✔
598

599
    case SCORE_NU_SCATTER:
2,099✔
600
      if (settings::run_CE) {
2,099✔
601
        estimator_ = TallyEstimator::ANALOG;
1,971✔
602
      } else {
603
        if (energyout_present || legendre_present)
128!
604
          estimator_ = TallyEstimator::ANALOG;
64✔
605
      }
606
      break;
2,099✔
607

608
    case SCORE_CURRENT:
624✔
609
      // Check which type of current is desired: mesh or surface currents.
610
      if (surface_present || cell_present || cellfrom_present) {
624!
611
        if (meshsurface_present)
257!
UNCOV
612
          fatal_error("Cannot tally mesh surface currents in the same tally as "
×
613
                      "normal surface currents");
614
        type_ = TallyType::SURFACE;
257✔
615
        estimator_ = TallyEstimator::ANALOG;
257✔
616
      } else if (meshsurface_present) {
367!
617
        type_ = TallyType::MESH_SURFACE;
367✔
618
      } else {
UNCOV
619
        fatal_error("Cannot tally currents without surface type filters");
×
620
      }
621
      break;
624✔
622

623
    case HEATING:
370✔
624
      if (settings::photon_transport)
370✔
625
        estimator_ = TallyEstimator::COLLISION;
199✔
626
      break;
370✔
627

628
    case SCORE_PULSE_HEIGHT:
16✔
629
      if (non_cell_energy_present) {
16!
UNCOV
630
        fatal_error("Pulse-height tallies are not compatible with filters "
×
631
                    "other than CellFilter and EnergyFilter");
632
      }
633
      type_ = TallyType::PULSE_HEIGHT;
16✔
634

635
      // Collecting indices of all cells covered by the filters in the pulse
636
      // height tally in global variable pulse_height_cells
637
      for (const auto& i_filt : filters_) {
48✔
638
        auto cell_filter =
639
          dynamic_cast<CellFilter*>(model::tally_filters[i_filt].get());
32!
640
        if (cell_filter) {
32✔
641
          const auto& cells = cell_filter->cells();
16✔
642
          for (int i = 0; i < cell_filter->n_bins(); i++) {
32✔
643
            int cell_index = cells[i];
16✔
644
            if (!contains(model::pulse_height_cells, cell_index)) {
16!
645
              model::pulse_height_cells.push_back(cell_index);
16✔
646
            }
647
          }
648
        }
649
      }
650
      break;
16✔
651

652
    case SCORE_IFP_TIME_NUM:
282✔
653
    case SCORE_IFP_BETA_NUM:
654
    case SCORE_IFP_DENOM:
655
      estimator_ = TallyEstimator::COLLISION;
282✔
656
      break;
282✔
657
    }
658

659
    scores_.push_back(score);
36,642✔
660
  }
36,642✔
661

662
  // Make sure that no duplicate scores exist.
663
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
63,291✔
664
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
84,204✔
665
      if (*it1 == *it2)
47,562!
UNCOV
666
        fatal_error(
×
UNCOV
667
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
UNCOV
668
            reaction_name(*it1), id_));
×
669
    }
670
  }
671

672
  // Make sure all scores are compatible with multigroup mode.
673
  if (!settings::run_CE) {
26,649✔
674
    for (auto sc : scores_)
6,254✔
675
      if (sc > 0)
4,599!
UNCOV
676
        fatal_error("Cannot tally " + reaction_name(sc) +
×
677
                    " reaction rate "
678
                    "in multi-group mode");
679
  }
680

681
  // Make sure current scores are not mixed in with volumetric scores.
682
  if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) {
26,649✔
683
    if (scores_.size() != 1)
624!
684
      fatal_error("Cannot tally other scores in the same tally as surface "
×
685
                  "currents.");
686
  }
687
  if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT)
26,649!
UNCOV
688
    fatal_error("Cannot tally score other than 'current' when using a surface "
×
689
                "or mesh-surface filter.");
690
}
26,649✔
691

692
void Tally::set_nuclides(pugi::xml_node node)
24,927✔
693
{
694
  nuclides_.clear();
24,927✔
695

696
  // By default, we tally just the total material rates.
697
  if (!check_for_node(node, "nuclides")) {
24,927✔
698
    nuclides_.push_back(-1);
6,602✔
699
    return;
6,602✔
700
  }
701

702
  // The user provided specifics nuclides.  Parse it as an array with either
703
  // "total" or a nuclide name like "U235" in each position.
704
  auto words = get_node_array<std::string>(node, "nuclides");
18,325✔
705
  this->set_nuclides(words);
18,325✔
706
}
18,325✔
707

708
void Tally::set_nuclides(const vector<std::string>& nuclides)
18,325✔
709
{
710
  nuclides_.clear();
18,325✔
711

712
  for (const auto& nuc : nuclides) {
47,574✔
713
    if (nuc == "total") {
29,249✔
714
      nuclides_.push_back(-1);
14,768✔
715
    } else {
716
      auto search = data::nuclide_map.find(nuc);
14,481✔
717
      if (search == data::nuclide_map.end()) {
14,481✔
718
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
143✔
719
        if (err < 0)
143!
UNCOV
720
          throw std::runtime_error {openmc_err_msg};
×
721
      }
722
      nuclides_.push_back(data::nuclide_map.at(nuc));
14,481✔
723
    }
724
  }
725
}
18,325✔
726

727
void Tally::init_triggers(pugi::xml_node node)
183✔
728
{
729
  for (auto trigger_node : node.children("trigger")) {
254✔
730
    // Read the trigger type.
731
    TriggerMetric metric;
732
    if (check_for_node(trigger_node, "type")) {
71!
733
      auto type_str = get_node_value(trigger_node, "type");
71✔
734
      if (type_str == "std_dev") {
71!
UNCOV
735
        metric = TriggerMetric::standard_deviation;
×
736
      } else if (type_str == "variance") {
71!
737
        metric = TriggerMetric::variance;
×
738
      } else if (type_str == "rel_err") {
71!
739
        metric = TriggerMetric::relative_error;
71✔
740
      } else {
741
        fatal_error(fmt::format(
×
742
          "Unknown trigger type \"{}\" in tally {}", type_str, id_));
×
743
      }
744
    } else {
71✔
UNCOV
745
      fatal_error(fmt::format(
×
UNCOV
746
        "Must specify trigger type for tally {} in tally XML file", id_));
×
747
    }
748

749
    // Read the trigger threshold.
750
    double threshold;
751
    if (check_for_node(trigger_node, "threshold")) {
71!
752
      threshold = std::stod(get_node_value(trigger_node, "threshold"));
71✔
753
      if (threshold <= 0) {
71!
754
        fatal_error("Tally trigger threshold must be positive");
×
755
      }
756
    } else {
UNCOV
757
      fatal_error(fmt::format(
×
UNCOV
758
        "Must specify trigger threshold for tally {} in tally XML file", id_));
×
759
    }
760

761
    // Read whether to allow zero-tally bins to be ignored.
762
    bool ignore_zeros = false;
71✔
763
    if (check_for_node(trigger_node, "ignore_zeros")) {
71✔
764
      ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros");
11✔
765
    }
766

767
    // Read the trigger scores.
768
    vector<std::string> trigger_scores;
71✔
769
    if (check_for_node(trigger_node, "scores")) {
71!
770
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
71✔
771
    } else {
UNCOV
772
      trigger_scores.push_back("all");
×
773
    }
774

775
    // Parse the trigger scores and populate the triggers_ vector.
776
    for (auto score_str : trigger_scores) {
142✔
777
      if (score_str == "all") {
71✔
778
        triggers_.reserve(triggers_.size() + this->scores_.size());
16✔
779
        for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) {
80✔
780
          triggers_.push_back({metric, threshold, ignore_zeros, i_score});
64✔
781
        }
782
      } else {
783
        int i_score = 0;
55✔
784
        for (; i_score < this->scores_.size(); ++i_score) {
77!
785
          if (this->scores_[i_score] == reaction_type(score_str))
77✔
786
            break;
55✔
787
        }
788
        if (i_score == this->scores_.size()) {
55!
UNCOV
789
          fatal_error(
×
UNCOV
790
            fmt::format("Could not find the score \"{}\" in tally "
×
791
                        "{} but it was listed in a trigger on that tally",
UNCOV
792
              score_str, id_));
×
793
        }
794
        triggers_.push_back({metric, threshold, ignore_zeros, i_score});
55✔
795
      }
796
    }
71✔
797
  }
71✔
798
}
183✔
799

800
void Tally::init_results()
27,104✔
801
{
802
  int n_scores = scores_.size() * nuclides_.size();
27,104✔
803
  results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
27,104✔
804
}
27,104✔
805

806
void Tally::reset()
58,844✔
807
{
808
  n_realizations_ = 0;
58,844✔
809
  if (results_.size() != 0) {
58,844✔
810
    xt::view(results_, xt::all()) = 0.0;
57,089✔
811
  }
812
}
58,844✔
813

814
void Tally::accumulate()
218,164✔
815
{
816
  // Increment number of realizations
817
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
218,164✔
818

819
  if (mpi::master || !settings::reduce_tallies) {
218,164✔
820
    // Calculate total source strength for normalization
821
    double total_source = 0.0;
180,204✔
822
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
180,204✔
823
      total_source = model::external_sources_probability.integral();
52,189✔
824
    } else {
825
      total_source = 1.0;
128,015✔
826
    }
827

828
    // Determine number of particles contributing to tally
829
    double contributing_particles = settings::reduce_tallies
180,204✔
830
                                      ? settings::n_particles
831
                                      : simulation::work_per_rank;
832

833
    // Account for number of source particles in normalization
834
    double norm =
180,204✔
835
      total_source / (contributing_particles * settings::gen_per_batch);
180,204✔
836

837
    if (settings::solver_type == SolverType::RANDOM_RAY) {
180,204✔
838
      norm = 1.0;
11,391✔
839
    }
840

841
// Accumulate each result
842
#pragma omp parallel for
100,955✔
843
    for (int i = 0; i < results_.shape()[0]; ++i) {
44,510,975✔
844
      for (int j = 0; j < results_.shape()[1]; ++j) {
107,129,412✔
845
        double val = results_(i, j, TallyResult::VALUE) * norm;
62,697,686✔
846
        results_(i, j, TallyResult::VALUE) = 0.0;
62,697,686✔
847
        results_(i, j, TallyResult::SUM) += val;
62,697,686✔
848
        results_(i, j, TallyResult::SUM_SQ) += val * val;
62,697,686✔
849
      }
850
    }
851
  }
852
}
218,164✔
853

854
int Tally::score_index(const std::string& score) const
263✔
855
{
856
  for (int i = 0; i < scores_.size(); i++) {
263!
857
    if (this->score_name(i) == score)
263!
858
      return i;
263✔
859
  }
UNCOV
860
  return -1;
×
861
}
862

863
xt::xarray<double> Tally::get_reshaped_data() const
×
864
{
865
  std::vector<uint64_t> shape;
×
866
  for (auto f : filters()) {
×
867
    shape.push_back(model::tally_filters[f]->n_bins());
×
868
  }
869

870
  // add number of scores and nuclides to tally
UNCOV
871
  shape.push_back(results_.shape()[1]);
×
UNCOV
872
  shape.push_back(results_.shape()[2]);
×
873

UNCOV
874
  xt::xarray<double> reshaped_results = results_;
×
UNCOV
875
  reshaped_results.reshape(shape);
×
UNCOV
876
  return reshaped_results;
×
UNCOV
877
}
×
878

879
std::string Tally::score_name(int score_idx) const
293✔
880
{
881
  if (score_idx < 0 || score_idx >= scores_.size()) {
293!
882
    fatal_error("Index in scores array is out of bounds.");
×
883
  }
884
  return reaction_name(scores_[score_idx]);
293✔
885
}
886

UNCOV
887
std::vector<std::string> Tally::scores() const
×
888
{
889
  std::vector<std::string> score_names;
×
UNCOV
890
  for (int score : scores_)
×
UNCOV
891
    score_names.push_back(reaction_name(score));
×
UNCOV
892
  return score_names;
×
UNCOV
893
}
×
894

895
std::string Tally::nuclide_name(int nuclide_idx) const
30✔
896
{
897
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
30!
UNCOV
898
    fatal_error("Index in nuclides array is out of bounds");
×
899
  }
900

901
  int nuclide = nuclides_.at(nuclide_idx);
30✔
902
  if (nuclide == -1) {
30!
903
    return "total";
30!
904
  }
UNCOV
905
  return data::nuclides.at(nuclide)->name_;
×
906
}
907

908
//==============================================================================
909
// Non-member functions
910
//==============================================================================
911

912
void read_tallies_xml()
1,753✔
913
{
914
  // Check if tallies.xml exists. If not, just return since it is optional
915
  std::string filename = settings::path_input + "tallies.xml";
1,753✔
916
  if (!file_exists(filename))
1,753✔
917
    return;
1,175✔
918

919
  write_message("Reading tallies XML file...", 5);
578✔
920

921
  // Parse tallies.xml file
922
  pugi::xml_document doc;
578✔
923
  doc.load_file(filename.c_str());
578✔
924
  pugi::xml_node root = doc.document_element();
578✔
925

926
  read_tallies_xml(root);
578✔
927
}
1,753✔
928

929
void read_tallies_xml(pugi::xml_node root)
4,301✔
930
{
931
  // Check for <assume_separate> setting
932
  if (check_for_node(root, "assume_separate")) {
4,301✔
933
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
16✔
934
  }
935

936
  // Check for user meshes and allocate
937
  read_meshes(root);
4,301✔
938

939
  // We only need the mesh info for plotting
940
  if (settings::run_mode == RunMode::PLOTTING)
4,301✔
941
    return;
33✔
942

943
  // Read data for tally derivatives
944
  read_tally_derivatives(root);
4,268✔
945

946
  // ==========================================================================
947
  // READ FILTER DATA
948

949
  // Check for user filters and allocate
950
  for (auto node_filt : root.children("filter")) {
12,518✔
951
    auto f = Filter::create(node_filt);
8,250✔
952
  }
953

954
  // ==========================================================================
955
  // READ TALLY DATA
956

957
  // Check for user tallies
958
  int n = 0;
4,268✔
959
  for (auto node : root.children("tally"))
29,195✔
960
    ++n;
24,927✔
961
  if (n == 0 && mpi::master) {
4,268!
UNCOV
962
    warning("No tallies present in tallies.xml file.");
×
963
  }
964

965
  for (auto node_tal : root.children("tally")) {
29,177✔
966
    model::tallies.push_back(make_unique<Tally>(node_tal));
24,927✔
967
  }
968
}
969

970
#ifdef OPENMC_MPI
971
void reduce_tally_results()
33,190✔
972
{
973
  // Don't reduce tally is no_reduce option is on
974
  if (settings::reduce_tallies) {
33,190✔
975
    for (int i_tally : model::active_tallies) {
98,640✔
976
      // Skip any tallies that are not active
977
      auto& tally {model::tallies[i_tally]};
65,550✔
978

979
      // Get view of accumulated tally values
980
      auto values_view = xt::view(tally->results_, xt::all(), xt::all(),
65,550✔
981
        static_cast<int>(TallyResult::VALUE));
131,100✔
982

983
      // Make copy of tally values in contiguous array
984
      xt::xtensor<double, 2> values = values_view;
65,550✔
985
      xt::xtensor<double, 2> values_reduced = xt::empty_like(values);
65,550✔
986

987
      // Reduce contiguous set of tally results
988
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
65,550✔
989
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
990

991
      // Transfer values on master and reset on other ranks
992
      if (mpi::master) {
65,550✔
993
        values_view = values_reduced;
32,765✔
994
      } else {
995
        values_view = 0.0;
32,785✔
996
      }
997
    }
65,550✔
998
  }
999

1000
  // Note that global tallies are *always* reduced even when no_reduce option
1001
  // is on.
1002

1003
  // Get view of global tally values
1004
  auto& gt = simulation::global_tallies;
33,190✔
1005
  auto gt_values_view =
1006
    xt::view(gt, xt::all(), static_cast<int>(TallyResult::VALUE));
33,190✔
1007

1008
  // Make copy of values in contiguous array
1009
  xt::xtensor<double, 1> gt_values = gt_values_view;
33,190✔
1010
  xt::xtensor<double, 1> gt_values_reduced = xt::empty_like(gt_values);
33,190✔
1011

1012
  // Reduce contiguous data
1013
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
33,190✔
1014
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1015

1016
  // Transfer values on master and reset on other ranks
1017
  if (mpi::master) {
33,190✔
1018
    gt_values_view = gt_values_reduced;
16,590✔
1019
  } else {
1020
    gt_values_view = 0.0;
16,600✔
1021
  }
1022

1023
  // We also need to determine the total starting weight of particles from the
1024
  // last realization
1025
  double weight_reduced;
1026
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
33,190✔
1027
    0, mpi::intracomm);
1028
  if (mpi::master)
33,190✔
1029
    simulation::total_weight = weight_reduced;
16,590✔
1030
}
33,190✔
1031
#endif
1032

1033
void accumulate_tallies()
123,870✔
1034
{
1035
#ifdef OPENMC_MPI
1036
  // Combine tally results onto master process
1037
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
71,565✔
1038
    reduce_tally_results();
33,190✔
1039
  }
1040
#endif
1041

1042
  // Increase number of realizations (only used for global tallies)
1043
  simulation::n_realizations += 1;
123,870✔
1044

1045
  // Accumulate on master only unless run is not reduced then do it on all
1046
  if (mpi::master || !settings::reduce_tallies) {
123,870✔
1047
    auto& gt = simulation::global_tallies;
102,020✔
1048

1049
    if (settings::run_mode == RunMode::EIGENVALUE) {
102,020✔
1050
      if (simulation::current_batch > settings::n_inactive) {
71,010✔
1051
        // Accumulate products of different estimators of k
1052
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
55,082✔
1053
                       simulation::total_weight;
55,082✔
1054
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
55,082✔
1055
                       simulation::total_weight;
55,082✔
1056
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
55,082✔
1057
                       simulation::total_weight;
55,082✔
1058
        simulation::k_col_abs += k_col * k_abs;
55,082✔
1059
        simulation::k_col_tra += k_col * k_tra;
55,082✔
1060
        simulation::k_abs_tra += k_abs * k_tra;
55,082✔
1061
      }
1062
    }
1063

1064
    // Accumulate results for global tallies
1065
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
510,100✔
1066
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
408,080✔
1067
      gt(i, TallyResult::VALUE) = 0.0;
408,080✔
1068
      gt(i, TallyResult::SUM) += val;
408,080✔
1069
      gt(i, TallyResult::SUM_SQ) += val * val;
408,080✔
1070
    }
1071
  }
1072

1073
  // Accumulate results for each tally
1074
  for (int i_tally : model::active_tallies) {
342,034✔
1075
    auto& tally {model::tallies[i_tally]};
218,164✔
1076
    tally->accumulate();
218,164✔
1077
  }
1078
}
123,870✔
1079

1080
double distance_to_time_boundary(double time, double speed)
7,787,043✔
1081
{
1082
  if (model::time_grid.empty()) {
7,787,043!
UNCOV
1083
    return INFTY;
×
1084
  } else if (time >= model::time_grid.back()) {
7,787,043✔
1085
    return INFTY;
26,620✔
1086
  } else {
1087
    double next_time =
1088
      *std::upper_bound(model::time_grid.begin(), model::time_grid.end(), time);
7,760,423✔
1089
    return (next_time - time) * speed;
7,760,423✔
1090
  }
1091
}
1092

1093
//! Add new points to the global time grid
1094
//
1095
//! \param grid Vector of new time points to add
1096
void add_to_time_grid(vector<double> grid)
220✔
1097
{
1098
  if (grid.empty())
220!
UNCOV
1099
    return;
×
1100

1101
  // Create new vector with enough space to hold old and new grid points
1102
  vector<double> merged;
220✔
1103
  merged.reserve(model::time_grid.size() + grid.size());
220✔
1104

1105
  // Merge and remove duplicates
1106
  std::set_union(model::time_grid.begin(), model::time_grid.end(), grid.begin(),
220✔
1107
    grid.end(), std::back_inserter(merged));
1108

1109
  // Swap in the new grid
1110
  model::time_grid.swap(merged);
220✔
1111
}
220✔
1112

1113
void setup_active_tallies()
123,882✔
1114
{
1115
  model::active_tallies.clear();
123,882✔
1116
  model::active_analog_tallies.clear();
123,882✔
1117
  model::active_tracklength_tallies.clear();
123,882✔
1118
  model::active_timed_tracklength_tallies.clear();
123,882✔
1119
  model::active_collision_tallies.clear();
123,882✔
1120
  model::active_meshsurf_tallies.clear();
123,882✔
1121
  model::active_surface_tallies.clear();
123,882✔
1122
  model::active_pulse_height_tallies.clear();
123,882✔
1123
  model::time_grid.clear();
123,882✔
1124

1125
  for (auto i = 0; i < model::tallies.size(); ++i) {
472,922✔
1126
    const auto& tally {*model::tallies[i]};
349,040✔
1127

1128
    if (tally.active_) {
349,040✔
1129
      model::active_tallies.push_back(i);
218,164✔
1130
      bool mesh_present = (tally.get_filter<MeshFilter>() ||
386,064✔
1131
                           tally.get_filter<MeshMaterialFilter>());
167,900✔
1132
      auto time_filter = tally.get_filter<TimeFilter>();
218,164✔
1133
      switch (tally.type_) {
218,164!
1134

1135
      case TallyType::VOLUME:
211,351✔
1136
        switch (tally.estimator_) {
211,351!
1137
        case TallyEstimator::ANALOG:
81,203✔
1138
          model::active_analog_tallies.push_back(i);
81,203✔
1139
          break;
81,203✔
1140
        case TallyEstimator::TRACKLENGTH:
121,986✔
1141
          if (time_filter && mesh_present) {
121,986✔
1142
            model::active_timed_tracklength_tallies.push_back(i);
220✔
1143
            add_to_time_grid(time_filter->bins());
220✔
1144
          } else {
1145
            model::active_tracklength_tallies.push_back(i);
121,766✔
1146
          }
1147
          break;
121,986✔
1148
        case TallyEstimator::COLLISION:
8,162✔
1149
          model::active_collision_tallies.push_back(i);
8,162✔
1150
        }
1151
        break;
211,351✔
1152

1153
      case TallyType::MESH_SURFACE:
4,311✔
1154
        model::active_meshsurf_tallies.push_back(i);
4,311✔
1155
        break;
4,311✔
1156

1157
      case TallyType::SURFACE:
2,422✔
1158
        model::active_surface_tallies.push_back(i);
2,422✔
1159
        break;
2,422✔
1160

1161
      case TallyType::PULSE_HEIGHT:
80✔
1162
        model::active_pulse_height_tallies.push_back(i);
80✔
1163
        break;
80✔
1164
      }
1165
    }
1166
  }
1167
}
123,882✔
1168

1169
void free_memory_tally()
8,194✔
1170
{
1171
  model::tally_derivs.clear();
8,194✔
1172
  model::tally_deriv_map.clear();
8,194✔
1173

1174
  model::tally_filters.clear();
8,194✔
1175
  model::filter_map.clear();
8,194✔
1176

1177
  model::tallies.clear();
8,194✔
1178

1179
  model::active_tallies.clear();
8,194✔
1180
  model::active_analog_tallies.clear();
8,194✔
1181
  model::active_tracklength_tallies.clear();
8,194✔
1182
  model::active_timed_tracklength_tallies.clear();
8,194✔
1183
  model::active_collision_tallies.clear();
8,194✔
1184
  model::active_meshsurf_tallies.clear();
8,194✔
1185
  model::active_surface_tallies.clear();
8,194✔
1186
  model::active_pulse_height_tallies.clear();
8,194✔
1187
  model::time_grid.clear();
8,194✔
1188

1189
  model::tally_map.clear();
8,194✔
1190
}
8,194✔
1191

1192
//==============================================================================
1193
// C-API functions
1194
//==============================================================================
1195

1196
extern "C" int openmc_extend_tallies(
1,608✔
1197
  int32_t n, int32_t* index_start, int32_t* index_end)
1198
{
1199
  if (index_start)
1,608!
1200
    *index_start = model::tallies.size();
1,608✔
1201
  if (index_end)
1,608!
1202
    *index_end = model::tallies.size() + n - 1;
×
1203
  for (int i = 0; i < n; ++i) {
3,216✔
1204
    model::tallies.push_back(make_unique<Tally>(-1));
1,608✔
1205
  }
1206
  return 0;
1,608✔
1207
}
1208

1209
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
22,926✔
1210
{
1211
  auto it = model::tally_map.find(id);
22,926✔
1212
  if (it == model::tally_map.end()) {
22,926✔
1213
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
30✔
1214
    return OPENMC_E_INVALID_ID;
30✔
1215
  }
1216

1217
  *index = it->second;
22,896✔
1218
  return 0;
22,896✔
1219
}
1220

UNCOV
1221
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1222
{
UNCOV
1223
  int32_t largest_tally_id = 0;
×
1224
  for (const auto& t : model::tallies) {
×
1225
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1226
  }
UNCOV
1227
  *id = largest_tally_id + 1;
×
UNCOV
1228
}
×
1229

UNCOV
1230
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1231
{
UNCOV
1232
  if (index < 0 || index >= model::tallies.size()) {
×
UNCOV
1233
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1234
    return OPENMC_E_OUT_OF_BOUNDS;
×
1235
  }
1236

UNCOV
1237
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
UNCOV
1238
  return 0;
×
1239
}
1240

1241
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
720✔
1242
{
1243
  if (index < 0 || index >= model::tallies.size()) {
720!
UNCOV
1244
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1245
    return OPENMC_E_OUT_OF_BOUNDS;
×
1246
  }
1247

1248
  auto& t {model::tallies[index]};
720✔
1249

1250
  std::string est = estimator;
720✔
1251
  if (est == "analog") {
720!
1252
    t->estimator_ = TallyEstimator::ANALOG;
720✔
1253
  } else if (est == "collision") {
×
1254
    t->estimator_ = TallyEstimator::COLLISION;
×
1255
  } else if (est == "tracklength") {
×
1256
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1257
  } else {
1258
    set_errmsg("Unknown tally estimator: " + est);
×
1259
    return OPENMC_E_INVALID_ARGUMENT;
×
1260
  }
1261
  return 0;
720✔
1262
}
720✔
1263

1264
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
32,471✔
1265
{
1266
  if (index < 0 || index >= model::tallies.size()) {
32,471!
UNCOV
1267
    set_errmsg("Index in tallies array is out of bounds.");
×
1268
    return OPENMC_E_OUT_OF_BOUNDS;
×
1269
  }
1270

1271
  *id = model::tallies[index]->id_;
32,471✔
1272
  return 0;
32,471✔
1273
}
1274

1275
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,608✔
1276
{
1277
  if (index < 0 || index >= model::tallies.size()) {
1,608!
UNCOV
1278
    set_errmsg("Index in tallies array is out of bounds.");
×
1279
    return OPENMC_E_OUT_OF_BOUNDS;
×
1280
  }
1281

1282
  model::tallies[index]->set_id(id);
1,608✔
1283
  return 0;
1,608✔
1284
}
1285

1286
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
15✔
1287
{
1288
  if (index < 0 || index >= model::tallies.size()) {
15!
UNCOV
1289
    set_errmsg("Index in tallies array is out of bounds.");
×
1290
    return OPENMC_E_OUT_OF_BOUNDS;
×
1291
  }
1292
  *type = static_cast<int>(model::tallies[index]->type_);
15✔
1293

1294
  return 0;
15✔
1295
}
1296

1297
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
720✔
1298
{
1299
  if (index < 0 || index >= model::tallies.size()) {
720!
UNCOV
1300
    set_errmsg("Index in tallies array is out of bounds.");
×
1301
    return OPENMC_E_OUT_OF_BOUNDS;
×
1302
  }
1303
  if (strcmp(type, "volume") == 0) {
720✔
1304
    model::tallies[index]->type_ = TallyType::VOLUME;
540✔
1305
  } else if (strcmp(type, "mesh-surface") == 0) {
180!
1306
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
180✔
UNCOV
1307
  } else if (strcmp(type, "surface") == 0) {
×
UNCOV
1308
    model::tallies[index]->type_ = TallyType::SURFACE;
×
UNCOV
1309
  } else if (strcmp(type, "pulse-height") == 0) {
×
UNCOV
1310
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1311
  } else {
1312
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
1313
    return OPENMC_E_INVALID_ARGUMENT;
×
1314
  }
1315

1316
  return 0;
720✔
1317
}
1318

1319
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
30✔
1320
{
1321
  if (index < 0 || index >= model::tallies.size()) {
30!
UNCOV
1322
    set_errmsg("Index in tallies array is out of bounds.");
×
1323
    return OPENMC_E_OUT_OF_BOUNDS;
×
1324
  }
1325
  *active = model::tallies[index]->active_;
30✔
1326

1327
  return 0;
30✔
1328
}
1329

1330
extern "C" int openmc_tally_set_active(int32_t index, bool active)
735✔
1331
{
1332
  if (index < 0 || index >= model::tallies.size()) {
735!
UNCOV
1333
    set_errmsg("Index in tallies array is out of bounds.");
×
1334
    return OPENMC_E_OUT_OF_BOUNDS;
×
1335
  }
1336
  model::tallies[index]->active_ = active;
735✔
1337

1338
  return 0;
735✔
1339
}
1340

1341
extern "C" int openmc_tally_get_writable(int32_t index, bool* writable)
30✔
1342
{
1343
  if (index < 0 || index >= model::tallies.size()) {
30!
UNCOV
1344
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1345
    return OPENMC_E_OUT_OF_BOUNDS;
×
1346
  }
1347
  *writable = model::tallies[index]->writable();
30✔
1348

1349
  return 0;
30✔
1350
}
1351

1352
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
888✔
1353
{
1354
  if (index < 0 || index >= model::tallies.size()) {
888!
1355
    set_errmsg("Index in tallies array is out of bounds.");
×
1356
    return OPENMC_E_OUT_OF_BOUNDS;
×
1357
  }
1358
  model::tallies[index]->set_writable(writable);
888✔
1359

1360
  return 0;
888✔
1361
}
1362

1363
extern "C" int openmc_tally_get_multiply_density(int32_t index, bool* value)
30✔
1364
{
1365
  if (index < 0 || index >= model::tallies.size()) {
30!
1366
    set_errmsg("Index in tallies array is out of bounds.");
×
1367
    return OPENMC_E_OUT_OF_BOUNDS;
×
1368
  }
1369
  *value = model::tallies[index]->multiply_density();
30✔
1370

1371
  return 0;
30✔
1372
}
1373

1374
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
400✔
1375
{
1376
  if (index < 0 || index >= model::tallies.size()) {
400!
UNCOV
1377
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1378
    return OPENMC_E_OUT_OF_BOUNDS;
×
1379
  }
1380
  model::tallies[index]->set_multiply_density(value);
400✔
1381

1382
  return 0;
400✔
1383
}
1384

1385
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
585✔
1386
{
1387
  if (index < 0 || index >= model::tallies.size()) {
585!
UNCOV
1388
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1389
    return OPENMC_E_OUT_OF_BOUNDS;
×
1390
  }
1391

1392
  *scores = model::tallies[index]->scores_.data();
585✔
1393
  *n = model::tallies[index]->scores_.size();
585✔
1394
  return 0;
585✔
1395
}
1396

1397
extern "C" int openmc_tally_set_scores(
1,623✔
1398
  int32_t index, int n, const char** scores)
1399
{
1400
  if (index < 0 || index >= model::tallies.size()) {
1,623!
UNCOV
1401
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1402
    return OPENMC_E_OUT_OF_BOUNDS;
×
1403
  }
1404

1405
  vector<std::string> scores_str(scores, scores + n);
1,623✔
1406
  try {
1407
    model::tallies[index]->set_scores(scores_str);
1,623✔
UNCOV
1408
  } catch (const std::invalid_argument& ex) {
×
UNCOV
1409
    set_errmsg(ex.what());
×
UNCOV
1410
    return OPENMC_E_INVALID_ARGUMENT;
×
UNCOV
1411
  }
×
1412

1413
  return 0;
1,623✔
1414
}
1,623✔
1415

1416
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
741✔
1417
{
1418
  // Make sure the index fits in the array bounds.
1419
  if (index < 0 || index >= model::tallies.size()) {
741!
UNCOV
1420
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1421
    return OPENMC_E_OUT_OF_BOUNDS;
×
1422
  }
1423

1424
  *n = model::tallies[index]->nuclides_.size();
741✔
1425
  *nuclides = model::tallies[index]->nuclides_.data();
741✔
1426

1427
  return 0;
741✔
1428
}
1429

1430
extern "C" int openmc_tally_set_nuclides(
1,927✔
1431
  int32_t index, int n, const char** nuclides)
1432
{
1433
  // Make sure the index fits in the array bounds.
1434
  if (index < 0 || index >= model::tallies.size()) {
1,927!
UNCOV
1435
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1436
    return OPENMC_E_OUT_OF_BOUNDS;
×
1437
  }
1438

1439
  vector<std::string> words(nuclides, nuclides + n);
1,927✔
1440
  vector<int> nucs;
1,927✔
1441
  for (auto word : words) {
8,113✔
1442
    if (word == "total") {
6,201✔
1443
      nucs.push_back(-1);
720✔
1444
    } else {
1445
      auto search = data::nuclide_map.find(word);
5,481✔
1446
      if (search == data::nuclide_map.end()) {
5,481✔
1447
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
1,062✔
1448
        if (err < 0) {
1,062✔
1449
          set_errmsg(openmc_err_msg);
15✔
1450
          return OPENMC_E_DATA;
15✔
1451
        }
1452
      }
1453
      nucs.push_back(data::nuclide_map.at(word));
5,466✔
1454
    }
1455
  }
6,201✔
1456

1457
  model::tallies[index]->nuclides_ = nucs;
1,912✔
1458

1459
  return 0;
1,912✔
1460
}
1,927✔
1461

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

1470
  *indices = model::tallies[index]->filters().data();
1,755✔
1471
  *n = model::tallies[index]->filters().size();
1,755✔
1472
  return 0;
1,755✔
1473
}
1474

1475
extern "C" int openmc_tally_set_filters(
1,815✔
1476
  int32_t index, size_t n, const int32_t* indices)
1477
{
1478
  // Make sure the index fits in the array bounds.
1479
  if (index < 0 || index >= model::tallies.size()) {
1,815!
1480
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1481
    return OPENMC_E_OUT_OF_BOUNDS;
×
1482
  }
1483

1484
  // Set the filters.
1485
  try {
1486
    // Convert indices to filter pointers
1487
    vector<Filter*> filters;
1,815✔
1488
    for (int64_t i = 0; i < n; ++i) {
4,418✔
1489
      int32_t i_filt = indices[i];
2,603✔
1490
      filters.push_back(model::tally_filters.at(i_filt).get());
2,603✔
1491
    }
1492
    model::tallies[index]->set_filters(filters);
1,815✔
1493
  } catch (const std::out_of_range& ex) {
1,815!
UNCOV
1494
    set_errmsg("Index in tally filter array out of bounds.");
×
UNCOV
1495
    return OPENMC_E_OUT_OF_BOUNDS;
×
UNCOV
1496
  }
×
1497

1498
  return 0;
1,815✔
1499
}
1500

1501
//! Reset tally results and number of realizations
1502
extern "C" int openmc_tally_reset(int32_t index)
2,928✔
1503
{
1504
  // Make sure the index fits in the array bounds.
1505
  if (index < 0 || index >= model::tallies.size()) {
2,928!
UNCOV
1506
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1507
    return OPENMC_E_OUT_OF_BOUNDS;
×
1508
  }
1509

1510
  model::tallies[index]->reset();
2,928✔
1511
  return 0;
2,928✔
1512
}
1513

1514
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
4,138✔
1515
{
1516
  // Make sure the index fits in the array bounds.
1517
  if (index < 0 || index >= model::tallies.size()) {
4,138!
UNCOV
1518
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1519
    return OPENMC_E_OUT_OF_BOUNDS;
×
1520
  }
1521

1522
  *n = model::tallies[index]->n_realizations_;
4,138✔
1523
  return 0;
4,138✔
1524
}
1525

1526
//! \brief Returns a pointer to a tally results array along with its shape.
1527
//! This allows a user to obtain in-memory tally results from Python directly.
1528
extern "C" int openmc_tally_results(
17,758✔
1529
  int32_t index, double** results, size_t* shape)
1530
{
1531
  // Make sure the index fits in the array bounds.
1532
  if (index < 0 || index >= model::tallies.size()) {
17,758!
UNCOV
1533
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1534
    return OPENMC_E_OUT_OF_BOUNDS;
×
1535
  }
1536

1537
  const auto& t {model::tallies[index]};
17,758✔
1538
  if (t->results_.size() == 0) {
17,758!
UNCOV
1539
    set_errmsg("Tally results have not been allocated yet.");
×
UNCOV
1540
    return OPENMC_E_ALLOCATE;
×
1541
  }
1542

1543
  // Set pointer to results and copy shape
1544
  *results = t->results_.data();
17,758✔
1545
  auto s = t->results_.shape();
17,758✔
1546
  shape[0] = s[0];
17,758✔
1547
  shape[1] = s[1];
17,758✔
1548
  shape[2] = s[2];
17,758✔
1549
  return 0;
17,758✔
1550
}
1551

1552
extern "C" int openmc_global_tallies(double** ptr)
15✔
1553
{
1554
  *ptr = simulation::global_tallies.data();
15✔
1555
  return 0;
15✔
1556
}
1557

1558
extern "C" size_t tallies_size()
1,668✔
1559
{
1560
  return model::tallies.size();
1,668✔
1561
}
1562

1563
// given a tally ID, remove it from the tallies vector
1564
extern "C" int openmc_remove_tally(int32_t index)
15✔
1565
{
1566
  // check that id is in the map
1567
  if (index < 0 || index >= model::tallies.size()) {
15!
UNCOV
1568
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1569
    return OPENMC_E_OUT_OF_BOUNDS;
×
1570
  }
1571

1572
  // delete the tally via iterator pointing to correct position
1573
  // this calls the Tally destructor, removing the tally from the map as well
1574
  model::tallies.erase(model::tallies.begin() + index);
15✔
1575

1576
  return 0;
15✔
1577
}
1578

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