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

openmc-dev / openmc / 23503641174

24 Mar 2026 05:36PM UTC coverage: 81.328% (-0.1%) from 81.447%
23503641174

Pull #3886

github

web-flow
Merge 63fd9c86f into 6cd39073b
Pull Request #3886: Implement python tally types

17611 of 25453 branches covered (69.19%)

Branch coverage included in aggregate %.

244 of 297 new or added lines in 12 files covered. (82.15%)

238 existing lines in 4 files now uncovered.

58250 of 67825 relevant lines covered (85.88%)

44553638.72 hits per line

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

75.98
/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,169✔
85
{
86
  index_ = model::tallies.size(); // Avoids warning about narrowing
1,169✔
87
  this->set_id(id);
1,169✔
88
  this->set_filters({});
1,169✔
89
}
1,169✔
90

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

95
  // Copy and set tally id
96
  if (!check_for_node(node, "id")) {
24,894!
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"));
49,788✔
100
  this->set_id(id);
24,894✔
101

102
  if (check_for_node(node, "name"))
24,894✔
103
    name_ = get_node_value(node, "name");
2,578✔
104

105
  if (check_for_node(node, "multiply_density")) {
24,894✔
106
    multiply_density_ = get_node_value_bool(node, "multiply_density");
66✔
107
  }
108

109
  if (check_for_node(node, "higher_moments")) {
24,894✔
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")) {
24,894!
UNCOV
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 "
UNCOV
122
      "<tally>."};
×
123
  }
124

125
  // Determine number of filters
126
  vector<int> filter_ids;
24,894✔
127
  if (check_for_node(node, "filters")) {
24,894✔
128
    filter_ids = get_node_array<int>(node, "filters");
47,886✔
129
  }
130

131
  // Allocate and store filter user ids
132
  vector<Filter*> filters;
24,894✔
133
  for (int filter_id : filter_ids) {
74,687✔
134
    // Determine if filter ID is valid
135
    auto it = model::filter_map.find(filter_id);
49,793!
136
    if (it == model::filter_map.end()) {
49,793!
UNCOV
137
      throw std::runtime_error {fmt::format(
×
UNCOV
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());
49,793✔
143
  }
144

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

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

155
    auto pf = dynamic_cast<ParticleFilter*>(f);
49,793!
156
    if (pf)
49,793✔
157
      particle_filter_index = i_filter;
457✔
158

159
    // Change the tally estimator if a filter demands it
160
    FilterType filt_type = f->type();
49,793✔
161
    if (filt_type == FilterType::ENERGY_OUT ||
49,793✔
162
        filt_type == FilterType::LEGENDRE) {
163
      estimator_ = TallyEstimator::ANALOG;
7,185✔
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;
30✔
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);
24,894✔
186

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

190
  this->set_scores(node);
24,894✔
191

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

196
  // Set IFP if needed
197
  if (!settings::ifp_on) {
24,894✔
198
    // Determine if this tally has an IFP score
199
    bool has_ifp_score = false;
24,776✔
200
    for (int score : scores_) {
60,247✔
201
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
35,563!
202
          score == SCORE_IFP_DENOM) {
203
        has_ifp_score = true;
204
        break;
205
      }
206
    }
207

208
    // Check for errors
209
    if (has_ifp_score) {
24,776✔
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) {
24,876✔
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✔
UNCOV
238
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
UNCOV
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!
UNCOV
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) {
24,876✔
256
    for (int score : scores_) {
59,406✔
257
      switch (score) {
34,984!
UNCOV
258
      case SCORE_PULSE_HEIGHT:
×
UNCOV
259
        fatal_error("For pulse-height tallies, photon transport needs to be "
×
260
                    "activated.");
261
        break;
34,984✔
262
      }
263
    }
264
  }
265
  if (settings::photon_transport) {
24,876✔
266
    if (particle_filter_index == C_NONE) {
454✔
267
      for (int score : scores_) {
214✔
268
        switch (score) {
107!
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) {
24,422✔
294
      const auto& f = model::tally_filters[particle_filter_index].get();
110!
295
      auto pf = dynamic_cast<ParticleFilter*>(f);
110!
296
      for (auto p : pf->particles()) {
330✔
297
        if (!p.is_neutron()) {
220✔
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")) {
24,876✔
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!
UNCOV
315
      fatal_error(fmt::format(
×
UNCOV
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!
UNCOV
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.",
UNCOV
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) {
24,876✔
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")) {
24,876✔
358
    std::string est = get_node_value(node, "estimator");
20,473✔
359
    if (est == "analog") {
20,473✔
360
      estimator_ = TallyEstimator::ANALOG;
9,162✔
361
    } else if (est == "tracklength" || est == "track-length" ||
11,728!
362
               est == "pathlength" || est == "path-length") {
12,145!
363
      // If the estimator was set to an analog estimator, this means the
364
      // tally needs post-collision information
365
      if (estimator_ == TallyEstimator::ANALOG ||
10,894!
366
          estimator_ == TallyEstimator::COLLISION) {
UNCOV
367
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
368
                                              "estimator for tally {}",
UNCOV
369
          id_)};
×
370
      }
371

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

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

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

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

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

408
Tally::~Tally()
26,045✔
409
{
410
  model::tally_map.erase(id_);
26,045✔
411
}
52,090✔
412

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

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

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

429
  // Make sure no other tally has the same ID
430
  if (model::tally_map.find(id) != model::tally_map.end()) {
27,126!
UNCOV
431
    throw std::runtime_error {
×
UNCOV
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) {
27,126✔
437
    id = 0;
1,169✔
438
    for (const auto& t : model::tallies) {
3,582✔
439
      id = std::max(id, t->id_);
4,736✔
440
    }
441
    ++id;
1,169✔
442
  }
443

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

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

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

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

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

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

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

487
  for (auto* filter : filters) {
78,630✔
488
    add_filter(filter);
51,400✔
489
  }
490
}
27,230✔
491

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

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

508
void Tally::set_strides()
26,449✔
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();
26,449✔
514
  strides_.resize(n, 0);
26,449✔
515
  int stride = 1;
26,449✔
516
  for (int i = n - 1; i >= 0; --i) {
78,509✔
517
    strides_[i] = stride;
52,060✔
518
    stride *= model::tally_filters[filters_[i]]->n_bins();
52,060✔
519
  }
520
  n_filter_bins_ = stride;
26,449✔
521
}
26,449✔
522

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

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

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

538
  // Check for the presence of certain restrictive filters.
539
  bool energyout_present = energyout_filter_ != C_NONE;
26,063✔
540
  bool legendre_present = false;
26,063✔
541
  bool cell_present = false;
26,063✔
542
  bool cellfrom_present = false;
26,063✔
543
  bool material_present = false;
26,063✔
544
  bool materialfrom_present = false;
26,063✔
545
  bool surface_present = false;
26,063✔
546
  bool meshsurface_present = false;
26,063✔
547
  bool non_cell_energy_present = false;
26,063✔
548
  for (auto i_filt : filters_) {
76,906✔
549
    const auto* filt {model::tally_filters[i_filt].get()};
50,843✔
550
    // Checking for only cell and energy filters for pulse-height tally
551
    if (!(filt->type() == FilterType::CELL ||
100,634✔
552
          filt->type() == FilterType::ENERGY)) {
49,791✔
553
      non_cell_energy_present = true;
554
    }
555
    if (filt->type() == FilterType::LEGENDRE) {
50,843✔
556
      legendre_present = true;
557
    } else if (filt->type() == FilterType::CELLFROM) {
48,670✔
558
      cellfrom_present = true;
559
    } else if (filt->type() == FilterType::CELL) {
48,374✔
560
      cell_present = true;
561
    } else if (filt->type() == FilterType::MATERIALFROM) {
47,322✔
562
      materialfrom_present = true;
563
    } else if (filt->type() == FilterType::MATERIAL) {
47,292✔
564
      material_present = true;
565
    } else if (filt->type() == FilterType::SURFACE) {
32,430✔
566
      surface_present = true;
567
    } else if (filt->type() == FilterType::MESH_SURFACE) {
32,173✔
568
      meshsurface_present = true;
341✔
569
    }
570
  }
571
  bool surface_types_present =
52,126✔
572
    (surface_present || cellfrom_present || materialfrom_present);
26,063✔
573
  bool non_meshsurface_types_present =
52,126✔
574
    (surface_present || cell_present || cellfrom_present || material_present ||
26,063!
575
      materialfrom_present);
576

577
  // Iterate over the given scores.
578
  for (auto score_str : scores) {
63,514✔
579
    // Make sure a delayed group filter wasn't used with an incompatible
580
    // score.
581
    if (delayedgroup_filter_ != C_NONE) {
37,451✔
582
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate" &&
959✔
583
          score_str != "ifp-beta-numerator")
37!
UNCOV
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);
37,451✔
589

590
    switch (score) {
37,451✔
591
    case SCORE_FLUX:
10,818✔
592
      if (!nuclides_.empty())
10,818!
593
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
10,818!
NEW
594
          fatal_error("Cannot tally flux for an individual nuclide.");
×
595
      if (energyout_present)
10,818!
NEW
596
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
597
      if (surface_types_present) {
10,818✔
598
        if (meshsurface_present)
48!
NEW
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,234✔
606
    case SCORE_ABSORPTION:
7,234✔
607
    case SCORE_FISSION:
7,234✔
608
      if (energyout_present)
7,234!
UNCOV
609
        fatal_error("Cannot tally " + score_str +
×
610
                    " reaction rate with an "
611
                    "outgoing energy filter");
612
      break;
613

614
    case SCORE_SCATTER:
4,456✔
615
      if (legendre_present)
4,456✔
616
        estimator_ = TallyEstimator::ANALOG;
1,532✔
617
    case SCORE_NU_FISSION:
9,723✔
618
    case SCORE_DELAYED_NU_FISSION:
9,723✔
619
    case SCORE_PROMPT_NU_FISSION:
9,723✔
620
      if (energyout_present)
9,723✔
621
        estimator_ = TallyEstimator::ANALOG;
4,126✔
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!
UNCOV
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:
366✔
647
      if (settings::photon_transport)
366✔
648
        estimator_ = TallyEstimator::COLLISION;
205✔
649
      break;
650

651
    case SCORE_PULSE_HEIGHT: {
15✔
652
      if (non_cell_energy_present) {
15!
UNCOV
653
        fatal_error("Pulse-height tallies are not compatible with filters "
×
654
                    "other than CellFilter and EnergyFilter");
655
      }
656
      type_ = TallyType::PULSE_HEIGHT;
15✔
657
      // Collect all unique cell indices covered by this tally.
658
      // If no CellFilter is present, all cells in the geometry are scored.
659
      const auto* cell_filter_ptr = get_filter<CellFilter>();
15✔
660
      int n = cell_filter_ptr ? cell_filter_ptr->n_bins()
15!
NEW
661
                              : static_cast<int>(model::cells.size());
×
662
      for (int i = 0; i < n; ++i) {
30✔
663
        int32_t cell_index = cell_filter_ptr ? cell_filter_ptr->cells()[i] : i;
15!
664
        if (!contains(model::pulse_height_cells, cell_index))
15!
665
          model::pulse_height_cells.push_back(cell_index);
15✔
666
      }
667
      break;
668
    }
669

670
    case SCORE_IFP_TIME_NUM:
276✔
671
    case SCORE_IFP_BETA_NUM:
276✔
672
    case SCORE_IFP_DENOM:
276✔
673
      estimator_ = TallyEstimator::COLLISION;
276✔
674
      break;
276✔
675
    }
676

677
    scores_.push_back(score);
37,451✔
678
  }
37,451✔
679

680
  // Make sure that no duplicate scores exist.
681
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
63,514✔
682
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
88,351✔
683
      if (*it1 == *it2)
50,900!
UNCOV
684
        fatal_error(
×
UNCOV
685
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
UNCOV
686
            reaction_name(*it1), id_));
×
687
    }
688
  }
689

690
  // Make sure all scores are compatible with multigroup mode.
691
  if (!settings::run_CE) {
26,063✔
692
    for (auto sc : scores_)
6,240✔
693
      if (sc > 0)
4,541!
UNCOV
694
        fatal_error("Cannot tally " + reaction_name(sc) +
×
695
                    " reaction rate "
696
                    "in multi-group mode");
697
  }
698

699
  // Make sure mesh surface tallies contain only current score.
700
  if (meshsurface_present) {
26,063✔
701
    if ((scores_[0] != SCORE_CURRENT) || (scores_.size() > 1))
341!
702
      fatal_error("Cannot tally score other than 'current' when using a "
×
703
                  "mesh-surface filter.");
704
  }
705

706
  // Make sure surface tallies contain only surface type scores score.
707
  if (type_ == TallyType::SURFACE) {
26,063✔
708
    for (auto sc : scores_)
589✔
709
      if ((sc != SCORE_CURRENT) && (sc != SCORE_FLUX))
302!
UNCOV
710
        fatal_error("Cannot tally scores other than 'current' or 'flux' "
×
711
                    "when using surface filters.");
712
  }
713
}
26,063✔
714

715
void Tally::set_nuclides(pugi::xml_node node)
24,894✔
716
{
717
  nuclides_.clear();
24,894!
718

719
  // By default, we tally just the total material rates.
720
  if (!check_for_node(node, "nuclides")) {
24,894✔
721
    nuclides_.push_back(-1);
6,414✔
722
    return;
6,414✔
723
  }
724

725
  // The user provided specifics nuclides.  Parse it as an array with either
726
  // "total" or a nuclide name like "U235" in each position.
727
  auto words = get_node_array<std::string>(node, "nuclides");
18,480✔
728
  this->set_nuclides(words);
18,480✔
729
}
18,480✔
730

731
void Tally::set_nuclides(const vector<std::string>& nuclides)
18,480✔
732
{
733
  nuclides_.clear();
18,480!
734

735
  for (const auto& nuc : nuclides) {
47,212✔
736
    if (nuc == "total") {
28,732✔
737
      nuclides_.push_back(-1);
15,138✔
738
    } else {
739
      auto search = data::nuclide_map.find(nuc);
13,594✔
740
      if (search == data::nuclide_map.end()) {
13,594✔
741
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
143✔
742
        if (err < 0)
143!
UNCOV
743
          throw std::runtime_error {openmc_err_msg};
×
744
      }
745
      nuclides_.push_back(data::nuclide_map.at(nuc));
13,594✔
746
    }
747
  }
748
}
18,480✔
749

750
void Tally::init_triggers(pugi::xml_node node)
175✔
751
{
752
  for (auto trigger_node : node.children("trigger")) {
245✔
753
    // Read the trigger type.
754
    TriggerMetric metric;
70✔
755
    if (check_for_node(trigger_node, "type")) {
70!
756
      auto type_str = get_node_value(trigger_node, "type");
70✔
757
      if (type_str == "std_dev") {
70!
758
        metric = TriggerMetric::standard_deviation;
759
      } else if (type_str == "variance") {
70!
760
        metric = TriggerMetric::variance;
761
      } else if (type_str == "rel_err") {
70!
762
        metric = TriggerMetric::relative_error;
763
      } else {
UNCOV
764
        fatal_error(fmt::format(
×
UNCOV
765
          "Unknown trigger type \"{}\" in tally {}", type_str, id_));
×
766
      }
UNCOV
767
    } else {
×
768
      fatal_error(fmt::format(
×
UNCOV
769
        "Must specify trigger type for tally {} in tally XML file", id_));
×
770
    }
771

772
    // Read the trigger threshold.
773
    double threshold;
70✔
774
    if (check_for_node(trigger_node, "threshold")) {
70!
775
      threshold = std::stod(get_node_value(trigger_node, "threshold"));
140✔
776
      if (threshold <= 0) {
70!
UNCOV
777
        fatal_error("Tally trigger threshold must be positive");
×
778
      }
779
    } else {
UNCOV
780
      fatal_error(fmt::format(
×
UNCOV
781
        "Must specify trigger threshold for tally {} in tally XML file", id_));
×
782
    }
783

784
    // Read whether to allow zero-tally bins to be ignored.
785
    bool ignore_zeros = false;
70✔
786
    if (check_for_node(trigger_node, "ignore_zeros")) {
70✔
787
      ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros");
11✔
788
    }
789

790
    // Read the trigger scores.
791
    vector<std::string> trigger_scores;
70✔
792
    if (check_for_node(trigger_node, "scores")) {
70!
793
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
140✔
794
    } else {
UNCOV
795
      trigger_scores.push_back("all");
×
796
    }
797

798
    // Parse the trigger scores and populate the triggers_ vector.
799
    for (auto score_str : trigger_scores) {
140✔
800
      if (score_str == "all") {
70✔
801
        triggers_.reserve(triggers_.size() + this->scores_.size());
15✔
802
        for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) {
75✔
803
          triggers_.push_back({metric, threshold, ignore_zeros, i_score});
60✔
804
        }
805
      } else {
806
        int i_score = 0;
807
        for (; i_score < this->scores_.size(); ++i_score) {
77!
808
          if (this->scores_[i_score] == reaction_tally_mt(score_str))
77✔
809
            break;
810
        }
811
        if (i_score == this->scores_.size()) {
55!
UNCOV
812
          fatal_error(
×
UNCOV
813
            fmt::format("Could not find the score \"{}\" in tally "
×
814
                        "{} but it was listed in a trigger on that tally",
UNCOV
815
              score_str, id_));
×
816
        }
817
        triggers_.push_back({metric, threshold, ignore_zeros, i_score});
55✔
818
      }
819
    }
70✔
820
  }
70✔
821
}
175✔
822

823
void Tally::init_results()
26,449✔
824
{
825
  int n_scores = scores_.size() * nuclides_.size();
26,449✔
826
  if (higher_moments_) {
26,449✔
827
    results_ = tensor::Tensor<double>({static_cast<size_t>(n_filter_bins_),
11✔
828
      static_cast<size_t>(n_scores), size_t {5}});
11✔
829
  } else {
830
    results_ = tensor::Tensor<double>({static_cast<size_t>(n_filter_bins_),
26,438✔
831
      static_cast<size_t>(n_scores), size_t {3}});
26,438✔
832
  }
833
}
26,449✔
834

835
void Tally::reset()
56,405✔
836
{
837
  n_realizations_ = 0;
56,405✔
838
  if (results_.size() != 0) {
56,405✔
839
    results_.fill(0.0);
56,405✔
840
  }
841
}
56,405✔
842

843
void Tally::accumulate()
363,302✔
844
{
845
  // Increment number of realizations
846
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
363,302✔
847

848
  if (mpi::master || !settings::reduce_tallies) {
363,302✔
849
    // Calculate total source strength for normalization
850
    double total_source = 0.0;
332,500✔
851
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
332,500✔
852
      total_source = model::external_sources_probability.integral();
195,526✔
853
    } else {
854
      total_source = 1.0;
855
    }
856

857
    // Determine number of particles contributing to tally
858
    double contributing_particles = settings::reduce_tallies
332,500✔
859
                                      ? settings::n_particles
860
                                      : simulation::work_per_rank;
861

862
    // Account for number of source particles in normalization
863
    double norm =
332,500✔
864
      total_source / (contributing_particles * settings::gen_per_batch);
332,500✔
865

866
    if (settings::solver_type == SolverType::RANDOM_RAY) {
332,500✔
867
      norm = 1.0;
13,701✔
868
    }
869

870
    // Accumulate each result
871
    if (higher_moments_) {
332,500✔
872
#pragma omp parallel for
60✔
873
      // filter bins (specific cell, energy bins)
874
      for (int i = 0; i < results_.shape(0); ++i) {
2,500!
875
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
876
        for (int j = 0; j < results_.shape(1); ++j) {
12,000!
877
          double val = results_(i, j, TallyResult::VALUE) * norm;
4,800✔
878
          double val2 = val * val;
4,800✔
879
          results_(i, j, TallyResult::VALUE) = 0.0;
4,800✔
880
          results_(i, j, TallyResult::SUM) += val;
4,800✔
881
          results_(i, j, TallyResult::SUM_SQ) += val2;
4,800✔
882
          results_(i, j, TallyResult::SUM_THIRD) += val2 * val;
4,800✔
883
          results_(i, j, TallyResult::SUM_FOURTH) += val2 * val2;
4,800✔
884
        }
885
      }
886
    } else {
887
#pragma omp parallel for
182,370✔
888
      // filter bins (specific cell, energy bins)
889
      for (int i = 0; i < results_.shape(0); ++i) {
92,157,450!
890
        // score bins (flux, total reaction rate, fission reaction rate, etc.)
891
        for (int j = 0; j < results_.shape(1); ++j) {
223,045,520!
892
          double val = results_(i, j, TallyResult::VALUE) * norm;
65,594,055✔
893
          results_(i, j, TallyResult::VALUE) = 0.0;
65,594,055✔
894
          results_(i, j, TallyResult::SUM) += val;
65,594,055✔
895
          results_(i, j, TallyResult::SUM_SQ) += val * val;
65,594,055✔
896
        }
897
      }
898
    }
899
  }
900
}
363,302✔
901

902
int Tally::score_index(const std::string& score) const
246✔
903
{
904
  for (int i = 0; i < scores_.size(); i++) {
246!
905
    if (this->score_name(i) == score)
246!
906
      return i;
246✔
907
  }
908
  return -1;
909
}
910

UNCOV
911
tensor::Tensor<double> Tally::get_reshaped_data() const
×
912
{
UNCOV
913
  vector<size_t> shape;
×
UNCOV
914
  for (auto f : filters()) {
×
UNCOV
915
    shape.push_back(model::tally_filters[f]->n_bins());
×
916
  }
917

918
  // add number of scores and nuclides to tally
UNCOV
919
  shape.push_back(results_.shape(1));
×
UNCOV
920
  shape.push_back(results_.shape(2));
×
921

UNCOV
922
  tensor::Tensor<double> reshaped_results = results_;
×
UNCOV
923
  reshaped_results.reshape(shape);
×
UNCOV
924
  return reshaped_results;
×
UNCOV
925
}
×
926

927
std::string Tally::score_name(int score_idx) const
276✔
928
{
929
  if (score_idx < 0 || score_idx >= scores_.size()) {
276!
UNCOV
930
    fatal_error("Index in scores array is out of bounds.");
×
931
  }
932
  return reaction_name(scores_[score_idx]);
276✔
933
}
934

UNCOV
935
std::vector<std::string> Tally::scores() const
×
936
{
UNCOV
937
  std::vector<std::string> score_names;
×
938
  for (int score : scores_)
×
939
    score_names.push_back(reaction_name(score));
×
940
  return score_names;
×
UNCOV
941
}
×
942

943
std::string Tally::nuclide_name(int nuclide_idx) const
30✔
944
{
945
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
30!
UNCOV
946
    fatal_error("Index in nuclides array is out of bounds");
×
947
  }
948

949
  int nuclide = nuclides_.at(nuclide_idx);
30✔
950
  if (nuclide == -1) {
30!
951
    return "total";
30✔
952
  }
UNCOV
953
  return data::nuclides.at(nuclide)->name_;
×
954
}
955

956
//==============================================================================
957
// Non-member functions
958
//==============================================================================
959

960
void read_tallies_xml()
1,339✔
961
{
962
  // Check if tallies.xml exists. If not, just return since it is optional
963
  std::string filename = settings::path_input + "tallies.xml";
1,339✔
964
  if (!file_exists(filename))
1,339✔
965
    return;
882✔
966

967
  write_message("Reading tallies XML file...", 5);
457✔
968

969
  // Parse tallies.xml file
970
  pugi::xml_document doc;
457✔
971
  doc.load_file(filename.c_str());
457✔
972
  pugi::xml_node root = doc.document_element();
457✔
973

974
  read_tallies_xml(root);
457✔
975
}
1,339✔
976

977
void read_tallies_xml(pugi::xml_node root)
4,428✔
978
{
979
  // Check for <assume_separate> setting
980
  if (check_for_node(root, "assume_separate")) {
4,428✔
981
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
15✔
982
  }
983

984
  // Check for user meshes and allocate
985
  read_meshes(root);
4,428✔
986

987
  // We only need the mesh info for plotting
988
  if (settings::run_mode == RunMode::PLOTTING)
4,428✔
989
    return;
990

991
  // Read data for tally derivatives
992
  read_tally_derivatives(root);
4,395✔
993

994
  // ==========================================================================
995
  // READ FILTER DATA
996

997
  // Check for user filters and allocate
998
  for (auto node_filt : root.children("filter")) {
13,854✔
999
    auto f = Filter::create(node_filt);
9,459✔
1000
  }
1001

1002
  // ==========================================================================
1003
  // READ TALLY DATA
1004

1005
  // Check for user tallies
1006
  int n = 0;
4,395✔
1007
  for (auto node : root.children("tally"))
29,289✔
1008
    ++n;
24,894✔
1009
  if (n == 0 && mpi::master) {
4,395!
UNCOV
1010
    warning("No tallies present in tallies.xml file.");
×
1011
  }
1012

1013
  for (auto node_tal : root.children("tally")) {
29,271✔
1014
    model::tallies.push_back(make_unique<Tally>(node_tal));
49,770✔
1015
  }
1016
}
1017

1018
#ifdef OPENMC_MPI
1019
void reduce_tally_results()
27,268✔
1020
{
1021
  // Don't reduce tally is no_reduce option is on
1022
  if (settings::reduce_tallies) {
27,268✔
1023
    for (int i_tally : model::active_tallies) {
80,012✔
1024
      // Skip any tallies that are not active
1025
      auto& tally {model::tallies[i_tally]};
52,824✔
1026

1027
      // Extract 2D view of the VALUE column from the 3D results tensor,
1028
      // then copy into a contiguous array for MPI reduction
1029
      const int val_idx = static_cast<int>(TallyResult::VALUE);
52,824✔
1030
      tensor::View<double> val_view =
52,824✔
1031
        tally->results_.slice(tensor::all, tensor::all, val_idx);
52,824✔
1032
      tensor::Tensor<double> values(val_view);
52,824✔
1033

1034
      tensor::Tensor<double> values_reduced(values.shape());
52,824✔
1035

1036
      // Reduce contiguous set of tally results
1037
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
52,824✔
1038
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1039

1040
      // Transfer values on master and reset on other ranks
1041
      if (mpi::master) {
52,824✔
1042
        val_view = values_reduced;
26,402✔
1043
      } else {
1044
        val_view = 0.0;
26,422✔
1045
      }
1046
    }
158,472✔
1047
  }
1048

1049
  // Note that global tallies are *always* reduced even when no_reduce option
1050
  // is on.
1051

1052
  // Get reference to global tallies
1053
  auto& gt = simulation::global_tallies;
27,268✔
1054
  const int val_col = static_cast<int>(TallyResult::VALUE);
27,268✔
1055

1056
  // Copy VALUE column into contiguous array for MPI reduction
1057
  tensor::Tensor<double> gt_values(gt.slice(tensor::all, val_col));
27,268✔
1058
  tensor::Tensor<double> gt_values_reduced({size_t {N_GLOBAL_TALLIES}});
27,268✔
1059

1060
  // Reduce contiguous data
1061
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
27,268✔
1062
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1063

1064
  // Transfer values on master and reset on other ranks
1065
  if (mpi::master) {
27,268✔
1066
    gt.slice(tensor::all, val_col) = gt_values_reduced;
27,258✔
1067
  } else {
1068
    gt.slice(tensor::all, val_col) = 0.0;
27,278✔
1069
  }
1070

1071
  // We also need to determine the total starting weight of particles from the
1072
  // last realization
1073
  double weight_reduced;
27,268✔
1074
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
27,268✔
1075
    0, mpi::intracomm);
1076
  if (mpi::master)
27,268✔
1077
    simulation::total_weight = weight_reduced;
13,629✔
1078
}
54,536✔
1079
#endif
1080

1081
void accumulate_tallies()
155,549✔
1082
{
1083
#ifdef OPENMC_MPI
1084
  // Combine tally results onto master process
1085
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
68,674✔
1086
    reduce_tally_results();
27,268✔
1087
  }
1088
#endif
1089

1090
  // Increase number of realizations (only used for global tallies)
1091
  simulation::n_realizations += 1;
155,549✔
1092

1093
  // Accumulate on master only unless run is not reduced then do it on all
1094
  if (mpi::master || !settings::reduce_tallies) {
155,549✔
1095
    auto& gt = simulation::global_tallies;
137,070✔
1096

1097
    if (settings::run_mode == RunMode::EIGENVALUE) {
137,070✔
1098
      if (simulation::current_batch > settings::n_inactive) {
78,354✔
1099
        // Accumulate products of different estimators of k
1100
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
59,065✔
1101
                       simulation::total_weight;
59,065✔
1102
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
59,065✔
1103
                       simulation::total_weight;
59,065✔
1104
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
59,065✔
1105
                       simulation::total_weight;
59,065✔
1106
        simulation::k_col_abs += k_col * k_abs;
59,065✔
1107
        simulation::k_col_tra += k_col * k_tra;
59,065✔
1108
        simulation::k_abs_tra += k_abs * k_tra;
59,065✔
1109
      }
1110
    }
1111

1112
    // Accumulate results for global tallies
1113
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
685,350✔
1114
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
548,280✔
1115
      gt(i, TallyResult::VALUE) = 0.0;
548,280✔
1116
      gt(i, TallyResult::SUM) += val;
548,280✔
1117
      gt(i, TallyResult::SUM_SQ) += val * val;
548,280✔
1118
    }
1119
  }
1120

1121
  // Accumulate results for each tally
1122
  for (int i_tally : model::active_tallies) {
518,851✔
1123
    auto& tally {model::tallies[i_tally]};
363,302✔
1124
    tally->accumulate();
363,302✔
1125
  }
1126
}
155,549✔
1127

1128
double distance_to_time_boundary(double time, double speed)
7,788,187✔
1129
{
1130
  if (model::time_grid.empty()) {
7,788,187!
1131
    return INFTY;
1132
  } else if (time >= model::time_grid.back()) {
7,788,187✔
1133
    return INFTY;
1134
  } else {
1135
    double next_time =
7,760,511✔
1136
      *std::upper_bound(model::time_grid.begin(), model::time_grid.end(), time);
7,760,511✔
1137
    return (next_time - time) * speed;
7,760,511✔
1138
  }
1139
}
1140

1141
//! Add new points to the global time grid
1142
//
1143
//! \param grid Vector of new time points to add
1144
void add_to_time_grid(vector<double> grid)
220✔
1145
{
1146
  if (grid.empty())
220!
UNCOV
1147
    return;
×
1148

1149
  // Create new vector with enough space to hold old and new grid points
1150
  vector<double> merged;
220✔
1151
  merged.reserve(model::time_grid.size() + grid.size());
220✔
1152

1153
  // Merge and remove duplicates
1154
  std::set_union(model::time_grid.begin(), model::time_grid.end(), grid.begin(),
220✔
1155
    grid.end(), std::back_inserter(merged));
1156

1157
  // Swap in the new grid
1158
  model::time_grid.swap(merged);
220✔
1159
}
220✔
1160

1161
void setup_active_tallies()
155,562✔
1162
{
1163
  model::active_tallies.clear();
155,562✔
1164
  model::active_analog_tallies.clear();
155,562✔
1165
  model::active_tracklength_tallies.clear();
155,562✔
1166
  model::active_timed_tracklength_tallies.clear();
155,562✔
1167
  model::active_collision_tallies.clear();
155,562✔
1168
  model::active_meshsurf_tallies.clear();
155,562✔
1169
  model::active_surface_tallies.clear();
155,562✔
1170
  model::active_pulse_height_tallies.clear();
155,562✔
1171
  model::time_grid.clear();
155,562✔
1172

1173
  for (auto i = 0; i < model::tallies.size(); ++i) {
654,652✔
1174
    const auto& tally {*model::tallies[i]};
499,090✔
1175

1176
    if (tally.active_) {
499,090✔
1177
      model::active_tallies.push_back(i);
363,302✔
1178
      bool mesh_present = (tally.get_filter<MeshFilter>() ||
363,302✔
1179
                           tally.get_filter<MeshMaterialFilter>());
315,062✔
1180
      auto time_filter = tally.get_filter<TimeFilter>();
363,302✔
1181
      switch (tally.type_) {
363,302!
1182

1183
      case TallyType::VOLUME:
356,907✔
1184
        switch (tally.estimator_) {
356,907!
1185
        case TallyEstimator::ANALOG:
208,040✔
1186
          model::active_analog_tallies.push_back(i);
208,040✔
1187
          break;
1188
        case TallyEstimator::TRACKLENGTH:
141,102✔
1189
          if (time_filter && mesh_present) {
141,102✔
1190
            model::active_timed_tracklength_tallies.push_back(i);
220✔
1191
            add_to_time_grid(time_filter->bins());
440✔
1192
          } else {
1193
            model::active_tracklength_tallies.push_back(i);
140,882✔
1194
          }
1195
          break;
1196
        case TallyEstimator::COLLISION:
7,765✔
1197
          model::active_collision_tallies.push_back(i);
7,765✔
1198
        }
1199
        break;
1200

1201
      case TallyType::MESH_SURFACE:
3,971✔
1202
        model::active_meshsurf_tallies.push_back(i);
3,971✔
1203
        break;
1204

1205
      case TallyType::SURFACE:
2,349✔
1206
        model::active_surface_tallies.push_back(i);
2,349✔
1207
        break;
1208

1209
      case TallyType::PULSE_HEIGHT:
75✔
1210
        model::active_pulse_height_tallies.push_back(i);
75✔
1211
        break;
1212
      }
1213
    }
1214
  }
1215
}
155,562✔
1216

1217
void free_memory_tally()
8,309✔
1218
{
1219
  model::tally_derivs.clear();
8,309✔
1220
  model::tally_deriv_map.clear();
8,309✔
1221

1222
  model::tally_filters.clear();
8,309✔
1223
  model::filter_map.clear();
8,309✔
1224

1225
  model::tallies.clear();
8,309✔
1226

1227
  model::active_tallies.clear();
8,309✔
1228
  model::active_analog_tallies.clear();
8,309✔
1229
  model::active_tracklength_tallies.clear();
8,309✔
1230
  model::active_timed_tracklength_tallies.clear();
8,309✔
1231
  model::active_collision_tallies.clear();
8,309✔
1232
  model::active_meshsurf_tallies.clear();
8,309✔
1233
  model::active_surface_tallies.clear();
8,309✔
1234
  model::active_pulse_height_tallies.clear();
8,309✔
1235
  model::time_grid.clear();
8,309✔
1236

1237
  model::tally_map.clear();
8,309✔
1238
}
8,309✔
1239

1240
//==============================================================================
1241
// C-API functions
1242
//==============================================================================
1243

1244
extern "C" int openmc_extend_tallies(
1,063✔
1245
  int32_t n, int32_t* index_start, int32_t* index_end)
1246
{
1247
  if (index_start)
1,063!
1248
    *index_start = model::tallies.size();
1,063✔
1249
  if (index_end)
1,063!
UNCOV
1250
    *index_end = model::tallies.size() + n - 1;
×
1251
  for (int i = 0; i < n; ++i) {
2,126✔
1252
    model::tallies.push_back(make_unique<Tally>(-1));
2,126✔
1253
  }
1254
  return 0;
1,063✔
1255
}
1256

1257
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
20,900✔
1258
{
1259
  auto it = model::tally_map.find(id);
20,900✔
1260
  if (it == model::tally_map.end()) {
20,900✔
1261
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
22✔
1262
    return OPENMC_E_INVALID_ID;
22✔
1263
  }
1264

1265
  *index = it->second;
20,878✔
1266
  return 0;
20,878✔
1267
}
1268

UNCOV
1269
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1270
{
UNCOV
1271
  int32_t largest_tally_id = 0;
×
UNCOV
1272
  for (const auto& t : model::tallies) {
×
UNCOV
1273
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1274
  }
1275
  *id = largest_tally_id + 1;
×
UNCOV
1276
}
×
1277

UNCOV
1278
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1279
{
UNCOV
1280
  if (index < 0 || index >= model::tallies.size()) {
×
UNCOV
1281
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1282
    return OPENMC_E_OUT_OF_BOUNDS;
×
1283
  }
1284

UNCOV
1285
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
UNCOV
1286
  return 0;
×
1287
}
1288

1289
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
660✔
1290
{
1291
  if (index < 0 || index >= model::tallies.size()) {
660!
UNCOV
1292
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1293
    return OPENMC_E_OUT_OF_BOUNDS;
×
1294
  }
1295

1296
  auto& t {model::tallies[index]};
660✔
1297

1298
  std::string est = estimator;
660✔
1299
  if (est == "analog") {
660!
1300
    t->estimator_ = TallyEstimator::ANALOG;
660✔
1301
  } else if (est == "collision") {
×
UNCOV
1302
    t->estimator_ = TallyEstimator::COLLISION;
×
1303
  } else if (est == "tracklength") {
×
UNCOV
1304
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1305
  } else {
1306
    set_errmsg("Unknown tally estimator: " + est);
×
1307
    return OPENMC_E_INVALID_ARGUMENT;
×
1308
  }
1309
  return 0;
1310
}
660✔
1311

1312
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
27,078✔
1313
{
1314
  if (index < 0 || index >= model::tallies.size()) {
27,078!
UNCOV
1315
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1316
    return OPENMC_E_OUT_OF_BOUNDS;
×
1317
  }
1318

1319
  *id = model::tallies[index]->id_;
27,078✔
1320
  return 0;
27,078✔
1321
}
1322

1323
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,063✔
1324
{
1325
  if (index < 0 || index >= model::tallies.size()) {
1,063!
1326
    set_errmsg("Index in tallies array is out of bounds.");
×
1327
    return OPENMC_E_OUT_OF_BOUNDS;
×
1328
  }
1329

1330
  model::tallies[index]->set_id(id);
1,063✔
1331
  return 0;
1,063✔
1332
}
1333

1334
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
11✔
1335
{
1336
  if (index < 0 || index >= model::tallies.size()) {
11!
UNCOV
1337
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1338
    return OPENMC_E_OUT_OF_BOUNDS;
×
1339
  }
1340
  *type = static_cast<int>(model::tallies[index]->type_);
11✔
1341

1342
  return 0;
11✔
1343
}
1344

1345
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
660✔
1346
{
1347
  if (index < 0 || index >= model::tallies.size()) {
660!
UNCOV
1348
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1349
    return OPENMC_E_OUT_OF_BOUNDS;
×
1350
  }
1351
  if (strcmp(type, "volume") == 0) {
660✔
1352
    model::tallies[index]->type_ = TallyType::VOLUME;
495✔
1353
  } else if (strcmp(type, "mesh-surface") == 0) {
165!
1354
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
165✔
UNCOV
1355
  } else if (strcmp(type, "surface") == 0) {
×
UNCOV
1356
    model::tallies[index]->type_ = TallyType::SURFACE;
×
UNCOV
1357
  } else if (strcmp(type, "pulse-height") == 0) {
×
UNCOV
1358
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1359
  } else {
UNCOV
1360
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
UNCOV
1361
    return OPENMC_E_INVALID_ARGUMENT;
×
1362
  }
1363

1364
  return 0;
1365
}
1366

1367
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
22✔
1368
{
1369
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1370
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1371
    return OPENMC_E_OUT_OF_BOUNDS;
×
1372
  }
1373
  *active = model::tallies[index]->active_;
22✔
1374

1375
  return 0;
22✔
1376
}
1377

1378
extern "C" int openmc_tally_set_active(int32_t index, bool active)
671✔
1379
{
1380
  if (index < 0 || index >= model::tallies.size()) {
671!
1381
    set_errmsg("Index in tallies array is out of bounds.");
×
1382
    return OPENMC_E_OUT_OF_BOUNDS;
×
1383
  }
1384
  model::tallies[index]->active_ = active;
671✔
1385

1386
  return 0;
671✔
1387
}
1388

1389
extern "C" int openmc_tally_get_writable(int32_t index, bool* writable)
22✔
1390
{
1391
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1392
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1393
    return OPENMC_E_OUT_OF_BOUNDS;
×
1394
  }
1395
  *writable = model::tallies[index]->writable();
22✔
1396

1397
  return 0;
22✔
1398
}
1399

1400
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
403✔
1401
{
1402
  if (index < 0 || index >= model::tallies.size()) {
403!
UNCOV
1403
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1404
    return OPENMC_E_OUT_OF_BOUNDS;
×
1405
  }
1406
  model::tallies[index]->set_writable(writable);
403✔
1407

1408
  return 0;
403✔
1409
}
1410

1411
extern "C" int openmc_tally_get_multiply_density(int32_t index, bool* value)
22✔
1412
{
1413
  if (index < 0 || index >= model::tallies.size()) {
22!
UNCOV
1414
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1415
    return OPENMC_E_OUT_OF_BOUNDS;
×
1416
  }
1417
  *value = model::tallies[index]->multiply_density();
22✔
1418

1419
  return 0;
22✔
1420
}
1421

1422
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
260✔
1423
{
1424
  if (index < 0 || index >= model::tallies.size()) {
260!
UNCOV
1425
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1426
    return OPENMC_E_OUT_OF_BOUNDS;
×
1427
  }
1428
  model::tallies[index]->set_multiply_density(value);
260✔
1429

1430
  return 0;
260✔
1431
}
1432

1433
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
187✔
1434
{
1435
  if (index < 0 || index >= model::tallies.size()) {
187!
UNCOV
1436
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1437
    return OPENMC_E_OUT_OF_BOUNDS;
×
1438
  }
1439

1440
  *scores = model::tallies[index]->scores_.data();
187✔
1441
  *n = model::tallies[index]->scores_.size();
187✔
1442
  return 0;
187✔
1443
}
1444

1445
extern "C" int openmc_tally_set_scores(
1,074✔
1446
  int32_t index, int n, const char** scores)
1447
{
1448
  if (index < 0 || index >= model::tallies.size()) {
1,074!
UNCOV
1449
    set_errmsg("Index in tallies array is out of bounds.");
×
1450
    return OPENMC_E_OUT_OF_BOUNDS;
×
1451
  }
1452

1453
  vector<std::string> scores_str(scores, scores + n);
1,074✔
1454
  try {
1,074✔
1455
    model::tallies[index]->set_scores(scores_str);
1,074✔
UNCOV
1456
  } catch (const std::invalid_argument& ex) {
×
UNCOV
1457
    set_errmsg(ex.what());
×
UNCOV
1458
    return OPENMC_E_INVALID_ARGUMENT;
×
UNCOV
1459
  }
×
1460

1461
  return 0;
1462
}
1,074✔
1463

1464
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
231✔
1465
{
1466
  // Make sure the index fits in the array bounds.
1467
  if (index < 0 || index >= model::tallies.size()) {
231!
UNCOV
1468
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1469
    return OPENMC_E_OUT_OF_BOUNDS;
×
1470
  }
1471

1472
  *n = model::tallies[index]->nuclides_.size();
231✔
1473
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1474

1475
  return 0;
231✔
1476
}
1477

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

1487
  vector<std::string> words(nuclides, nuclides + n);
1,326✔
1488
  vector<int> nucs;
1,326✔
1489
  for (auto word : words) {
6,110✔
1490
    if (word == "total") {
4,795✔
1491
      nucs.push_back(-1);
660✔
1492
    } else {
1493
      auto search = data::nuclide_map.find(word);
4,135✔
1494
      if (search == data::nuclide_map.end()) {
4,135✔
1495
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
993✔
1496
        if (err < 0) {
993✔
1497
          set_errmsg(openmc_err_msg);
11✔
1498
          return OPENMC_E_DATA;
11✔
1499
        }
1500
      }
1501
      nucs.push_back(data::nuclide_map.at(word));
4,124✔
1502
    }
1503
  }
4,795✔
1504

1505
  model::tallies[index]->nuclides_ = nucs;
1,315✔
1506

1507
  return 0;
1508
}
1,326✔
1509

1510
extern "C" int openmc_tally_get_filters(
1,012✔
1511
  int32_t index, const int32_t** indices, size_t* n)
1512
{
1513
  if (index < 0 || index >= model::tallies.size()) {
1,012!
UNCOV
1514
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1515
    return OPENMC_E_OUT_OF_BOUNDS;
×
1516
  }
1517

1518
  *indices = model::tallies[index]->filters().data();
1,012✔
1519
  *n = model::tallies[index]->filters().size();
1,012✔
1520
  return 0;
1,012✔
1521
}
1522

1523
extern "C" int openmc_tally_set_filters(
1,118✔
1524
  int32_t index, size_t n, const int32_t* indices)
1525
{
1526
  // Make sure the index fits in the array bounds.
1527
  if (index < 0 || index >= model::tallies.size()) {
1,118!
UNCOV
1528
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1529
    return OPENMC_E_OUT_OF_BOUNDS;
×
1530
  }
1531

1532
  // Set the filters.
1533
  try {
1,118✔
1534
    // Convert indices to filter pointers
1535
    vector<Filter*> filters;
1,118✔
1536
    for (int64_t i = 0; i < n; ++i) {
2,654✔
1537
      int32_t i_filt = indices[i];
1,536✔
1538
      filters.push_back(model::tally_filters.at(i_filt).get());
1,536✔
1539
    }
1540
    model::tallies[index]->set_filters(filters);
1,118✔
UNCOV
1541
  } catch (const std::out_of_range& ex) {
×
UNCOV
1542
    set_errmsg("Index in tally filter array out of bounds.");
×
UNCOV
1543
    return OPENMC_E_OUT_OF_BOUNDS;
×
UNCOV
1544
  }
×
1545

1546
  return 0;
1,118✔
1547
}
1548

1549
//! Reset tally results and number of realizations
1550
extern "C" int openmc_tally_reset(int32_t index)
2,684✔
1551
{
1552
  // Make sure the index fits in the array bounds.
1553
  if (index < 0 || index >= model::tallies.size()) {
2,684!
1554
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1555
    return OPENMC_E_OUT_OF_BOUNDS;
×
1556
  }
1557

1558
  model::tallies[index]->reset();
2,684✔
1559
  return 0;
2,684✔
1560
}
1561

1562
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
3,416✔
1563
{
1564
  // Make sure the index fits in the array bounds.
1565
  if (index < 0 || index >= model::tallies.size()) {
3,416!
1566
    set_errmsg("Index in tallies array is out of bounds.");
×
1567
    return OPENMC_E_OUT_OF_BOUNDS;
×
1568
  }
1569

1570
  *n = model::tallies[index]->n_realizations_;
3,416✔
1571
  return 0;
3,416✔
1572
}
1573

1574
//! \brief Returns a pointer to a tally results array along with its shape.
1575
//! This allows a user to obtain in-memory tally results from Python directly.
1576
extern "C" int openmc_tally_results(
15,912✔
1577
  int32_t index, double** results, size_t* shape)
1578
{
1579
  // Make sure the index fits in the array bounds.
1580
  if (index < 0 || index >= model::tallies.size()) {
15,912!
UNCOV
1581
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1582
    return OPENMC_E_OUT_OF_BOUNDS;
×
1583
  }
1584

1585
  const auto& t {model::tallies[index]};
15,912!
1586
  if (t->results_.size() == 0) {
15,912!
UNCOV
1587
    set_errmsg("Tally results have not been allocated yet.");
×
UNCOV
1588
    return OPENMC_E_ALLOCATE;
×
1589
  }
1590

1591
  // Set pointer to results and copy shape
1592
  *results = t->results_.data();
15,912✔
1593
  auto s = t->results_.shape();
15,912✔
1594
  shape[0] = s[0];
15,912✔
1595
  shape[1] = s[1];
15,912✔
1596
  shape[2] = s[2];
15,912✔
1597
  return 0;
15,912✔
1598
}
15,912✔
1599

1600
extern "C" int openmc_global_tallies(double** ptr)
11✔
1601
{
1602
  *ptr = simulation::global_tallies.data();
11✔
1603
  return 0;
11✔
1604
}
1605

1606
extern "C" size_t tallies_size()
1,107✔
1607
{
1608
  return model::tallies.size();
1,107✔
1609
}
1610

1611
// given a tally ID, remove it from the tallies vector
1612
extern "C" int openmc_remove_tally(int32_t index)
11✔
1613
{
1614
  // check that id is in the map
1615
  if (index < 0 || index >= model::tallies.size()) {
11!
UNCOV
1616
    set_errmsg("Index in tallies array is out of bounds.");
×
UNCOV
1617
    return OPENMC_E_OUT_OF_BOUNDS;
×
1618
  }
1619

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

1624
  return 0;
11✔
1625
}
1626

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