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

openmc-dev / openmc / 17049809793

18 Aug 2025 06:58PM UTC coverage: 85.212%. First build
17049809793

Pull #3525

github

web-flow
Merge c8f2d25f4 into 68e894c4b
Pull Request #3525: fixed a bug in a combination of time_filter, mesh_filter and tracklength estimator

38 of 39 new or added lines in 2 files covered. (97.44%)

52982 of 62177 relevant lines covered (85.21%)

38220216.48 hits per line

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

81.34
/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
44
#include <cassert>
45
#include <cstddef> // for size_t
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_collision_tallies;
62
vector<int> active_meshsurf_tallies;
63
vector<int> active_surface_tallies;
64
vector<int> active_pulse_height_tallies;
65
vector<int> pulse_height_cells;
66
vector<double> time_grid;
67

68
double distance_to_time_boundary(double time, double speed)
2,147,483,647✔
69
{
70
  if (time_grid.size() == 0) {
2,147,483,647✔
71
    return INFTY;
2,147,483,647✔
72
  } else if (time >= time_grid[time_grid.size() - 1]) {
77,771,661✔
73
    return INFTY;
275,770✔
74
  } else {
75
    return (*std::upper_bound(time_grid.begin(), time_grid.end(), time) -
77,495,891✔
76
             time) *
77
           speed;
77,495,891✔
78
  }
79
}
80

81
void add_to_time_grid(vector<double> grid)
220✔
82
{
83
  auto temp = time_grid;
220✔
84
  time_grid.resize(time_grid.size() + grid.size());
220✔
85
  std::merge(temp.begin(), temp.end(), grid.begin(), grid.end(),
220✔
86
    std::back_inserter(time_grid));
87
  auto last_unique = std::unique(time_grid.begin(), time_grid.end());
220✔
88
  time_grid.erase(last_unique, time_grid.end());
220✔
89
}
220✔
90
} // namespace model
91

92
namespace simulation {
93
xt::xtensor_fixed<double, xt::xshape<N_GLOBAL_TALLIES, 3>> global_tallies;
94
int32_t n_realizations {0};
95
} // namespace simulation
96

97
double global_tally_absorption;
98
double global_tally_collision;
99
double global_tally_tracklength;
100
double global_tally_leakage;
101

102
//==============================================================================
103
// Tally object implementation
104
//==============================================================================
105

106
Tally::Tally(int32_t id)
1,144✔
107
{
108
  index_ = model::tallies.size(); // Avoids warning about narrowing
1,144✔
109
  this->set_id(id);
1,144✔
110
  this->set_filters({});
1,144✔
111
}
1,144✔
112

113
Tally::Tally(pugi::xml_node node)
24,234✔
114
{
115
  index_ = model::tallies.size(); // Avoids warning about narrowing
24,234✔
116

117
  // Copy and set tally id
118
  if (!check_for_node(node, "id")) {
24,234✔
119
    throw std::runtime_error {"Must specify id for tally in tally XML file."};
×
120
  }
121
  int32_t id = std::stoi(get_node_value(node, "id"));
24,234✔
122
  this->set_id(id);
24,234✔
123

124
  if (check_for_node(node, "name"))
24,234✔
125
    name_ = get_node_value(node, "name");
2,287✔
126

127
  if (check_for_node(node, "multiply_density")) {
24,234✔
128
    multiply_density_ = get_node_value_bool(node, "multiply_density");
33✔
129
  }
130

131
  // =======================================================================
132
  // READ DATA FOR FILTERS
133

134
  // Check if user is using old XML format and throw an error if so
135
  if (check_for_node(node, "filter")) {
24,234✔
136
    throw std::runtime_error {
×
137
      "Tally filters must be specified independently of "
138
      "tallies in a <filter> element. The <tally> element itself should "
139
      "have a list of filters that apply, e.g., <filters>1 2</filters> "
140
      "where 1 and 2 are the IDs of filters specified outside of "
141
      "<tally>."};
×
142
  }
143

144
  // Determine number of filters
145
  vector<int> filter_ids;
24,234✔
146
  if (check_for_node(node, "filters")) {
24,234✔
147
    filter_ids = get_node_array<int>(node, "filters");
23,425✔
148
  }
149

150
  // Allocate and store filter user ids
151
  vector<Filter*> filters;
24,234✔
152
  for (int filter_id : filter_ids) {
72,796✔
153
    // Determine if filter ID is valid
154
    auto it = model::filter_map.find(filter_id);
48,562✔
155
    if (it == model::filter_map.end()) {
48,562✔
156
      throw std::runtime_error {fmt::format(
×
157
        "Could not find filter {} specified on tally {}", filter_id, id_)};
×
158
    }
159

160
    // Store the index of the filter
161
    filters.push_back(model::tally_filters[it->second].get());
48,562✔
162
  }
163

164
  // Set the filters
165
  this->set_filters(filters);
24,234✔
166

167
  // Check for the presence of certain filter types
168
  bool has_energyout = energyout_filter_ >= 0;
24,234✔
169
  int particle_filter_index = C_NONE;
24,234✔
170
  for (int64_t j = 0; j < filters_.size(); ++j) {
72,796✔
171
    int i_filter = filters_[j];
48,562✔
172
    const auto& f = model::tally_filters[i_filter].get();
48,562✔
173

174
    auto pf = dynamic_cast<ParticleFilter*>(f);
48,562✔
175
    if (pf)
48,562✔
176
      particle_filter_index = i_filter;
427✔
177

178
    // Change the tally estimator if a filter demands it
179
    FilterType filt_type = f->type();
48,562✔
180
    if (filt_type == FilterType::ENERGY_OUT ||
48,562✔
181
        filt_type == FilterType::LEGENDRE) {
182
      estimator_ = TallyEstimator::ANALOG;
6,558✔
183
    } else if (filt_type == FilterType::SPHERICAL_HARMONICS) {
42,004✔
184
      auto sf = dynamic_cast<SphericalHarmonicsFilter*>(f);
70✔
185
      if (sf->cosine() == SphericalHarmonicsCosine::scatter) {
70✔
186
        estimator_ = TallyEstimator::ANALOG;
11✔
187
      }
188
    } else if (filt_type == FilterType::SPATIAL_LEGENDRE ||
41,934✔
189
               filt_type == FilterType::ZERNIKE ||
41,879✔
190
               filt_type == FilterType::ZERNIKE_RADIAL) {
191
      estimator_ = TallyEstimator::COLLISION;
55✔
192
    }
193
  }
194

195
  // =======================================================================
196
  // READ DATA FOR NUCLIDES
197

198
  this->set_nuclides(node);
24,234✔
199

200
  // =======================================================================
201
  // READ DATA FOR SCORES
202

203
  this->set_scores(node);
24,234✔
204

205
  if (!check_for_node(node, "scores")) {
24,234✔
206
    fatal_error(fmt::format("No scores specified on tally {}.", id_));
×
207
  }
208

209
  // Set IFP if needed
210
  if (!settings::ifp_on) {
24,234✔
211
    // Determine if this tally has an IFP score
212
    bool has_ifp_score = false;
24,234✔
213
    for (int score : scores_) {
57,655✔
214
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
33,455✔
215
          score == SCORE_IFP_DENOM) {
216
        has_ifp_score = true;
34✔
217
        break;
34✔
218
      }
219
    }
220

221
    // Check for errors
222
    if (has_ifp_score) {
24,234✔
223
      if (settings::run_mode == RunMode::EIGENVALUE) {
34✔
224
        if (settings::ifp_n_generation < 0) {
25✔
225
          settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION;
9✔
226
          warning(fmt::format(
9✔
227
            "{} generations will be used for IFP (default value). It can be "
228
            "changed using the 'ifp_n_generation' settings.",
229
            settings::ifp_n_generation));
230
        }
231
        if (settings::ifp_n_generation > settings::n_inactive) {
25✔
232
          fatal_error("'ifp_n_generation' must be lower than or equal to the "
9✔
233
                      "number of inactive cycles.");
234
        }
235
        settings::ifp_on = true;
16✔
236
      } else {
237
        fatal_error(
9✔
238
          "Iterated Fission Probability can only be used in an eigenvalue "
239
          "calculation.");
240
      }
241
    }
242
  }
243

244
  // Set IFP parameters if needed
245
  if (settings::ifp_on) {
24,216✔
246
    for (int score : scores_) {
64✔
247
      switch (score) {
48✔
248
      case SCORE_IFP_TIME_NUM:
16✔
249
        if (settings::ifp_parameter == IFPParameter::None) {
16✔
250
          settings::ifp_parameter = IFPParameter::GenerationTime;
16✔
251
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
252
          settings::ifp_parameter = IFPParameter::Both;
×
253
        }
254
        break;
16✔
255
      case SCORE_IFP_BETA_NUM:
32✔
256
      case SCORE_IFP_DENOM:
257
        if (settings::ifp_parameter == IFPParameter::None) {
32✔
258
          settings::ifp_parameter = IFPParameter::BetaEffective;
×
259
        } else if (settings::ifp_parameter == IFPParameter::GenerationTime) {
32✔
260
          settings::ifp_parameter = IFPParameter::Both;
16✔
261
        }
262
        break;
32✔
263
      }
264
    }
265
  }
266

267
  // Check if tally is compatible with particle type
268
  if (!settings::photon_transport) {
24,216✔
269
    for (int score : scores_) {
56,609✔
270
      switch (score) {
32,803✔
271
      case SCORE_PULSE_HEIGHT:
×
NEW
272
        fatal_error("For pulse-height tallies, photon transport needs to be "
×
273
                    "activated.");
274
        break;
275
      }
276
    }
277
  }
278
  if (settings::photon_transport) {
24,216✔
279
    if (particle_filter_index == C_NONE) {
410✔
280
      for (int score : scores_) {
186✔
281
        switch (score) {
93✔
282
        case SCORE_INVERSE_VELOCITY:
×
283
          fatal_error("Particle filter must be used with photon "
×
284
                      "transport on and inverse velocity score");
285
          break;
286
        case SCORE_FLUX:
44✔
287
        case SCORE_TOTAL:
288
        case SCORE_SCATTER:
289
        case SCORE_NU_SCATTER:
290
        case SCORE_ABSORPTION:
291
        case SCORE_FISSION:
292
        case SCORE_NU_FISSION:
293
        case SCORE_CURRENT:
294
        case SCORE_EVENTS:
295
        case SCORE_DELAYED_NU_FISSION:
296
        case SCORE_PROMPT_NU_FISSION:
297
        case SCORE_DECAY_RATE:
298
          warning("You are tallying the '" + reaction_name(score) +
44✔
299
                  "' score and haven't used a particle filter. This score will "
300
                  "include contributions from all particles.");
301
          break;
44✔
302
        }
303
      }
304
    }
305
  } else {
306
    if (particle_filter_index >= 0) {
23,806✔
307
      const auto& f = model::tally_filters[particle_filter_index].get();
110✔
308
      auto pf = dynamic_cast<ParticleFilter*>(f);
110✔
309
      for (auto p : pf->particles()) {
330✔
310
        if (p != ParticleType::neutron) {
220✔
311
          warning(fmt::format(
110✔
312
            "Particle filter other than NEUTRON used with "
313
            "photon transport turned off. All tallies for particle type {}"
314
            " will have no scores",
315
            static_cast<int>(p)));
220✔
316
        }
317
      }
318
    }
319
  }
320

321
  // Check for a tally derivative.
322
  if (check_for_node(node, "derivative")) {
24,216✔
323
    int deriv_id = std::stoi(get_node_value(node, "derivative"));
320✔
324

325
    // Find the derivative with the given id, and store it's index.
326
    auto it = model::tally_deriv_map.find(deriv_id);
320✔
327
    if (it == model::tally_deriv_map.end()) {
320✔
328
      fatal_error(fmt::format(
×
329
        "Could not find derivative {} specified on tally {}", deriv_id, id_));
×
330
    }
331

332
    deriv_ = it->second;
320✔
333

334
    // Only analog or collision estimators are supported for differential
335
    // tallies.
336
    if (estimator_ == TallyEstimator::TRACKLENGTH) {
320✔
337
      estimator_ = TallyEstimator::COLLISION;
240✔
338
    }
339

340
    const auto& deriv = model::tally_derivs[deriv_];
320✔
341
    if (deriv.variable == DerivativeVariable::NUCLIDE_DENSITY ||
320✔
342
        deriv.variable == DerivativeVariable::TEMPERATURE) {
192✔
343
      for (int i_nuc : nuclides_) {
432✔
344
        if (has_energyout && i_nuc == -1) {
240✔
345
          fatal_error(fmt::format(
×
346
            "Error on tally {}: Cannot use a "
347
            "'nuclide_density' or 'temperature' derivative on a tally with "
348
            "an "
349
            "outgoing energy filter and 'total' nuclide rate. Instead, tally "
350
            "each nuclide in the material individually.",
351
            id_));
×
352
          // Note that diff tallies with these characteristics would work
353
          // correctly if no tally events occur in the perturbed material
354
          // (e.g. pertrubing moderator but only tallying fuel), but this
355
          // case would be hard to check for by only reading inputs.
356
        }
357
      }
358
    }
359
  }
360

361
  // If settings.xml trigger is turned on, create tally triggers
362
  if (settings::trigger_on) {
24,216✔
363
    this->init_triggers(node);
183✔
364
  }
365

366
  // =======================================================================
367
  // SET TALLY ESTIMATOR
368

369
  // Check if user specified estimator
370
  if (check_for_node(node, "estimator")) {
24,216✔
371
    std::string est = get_node_value(node, "estimator");
19,761✔
372
    if (est == "analog") {
19,761✔
373
      estimator_ = TallyEstimator::ANALOG;
8,143✔
374
    } else if (est == "tracklength" || est == "track-length" ||
12,446✔
375
               est == "pathlength" || est == "path-length") {
12,446✔
376
      // If the estimator was set to an analog estimator, this means the
377
      // tally needs post-collision information
378
      if (estimator_ == TallyEstimator::ANALOG ||
11,204✔
379
          estimator_ == TallyEstimator::COLLISION) {
11,204✔
380
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
381
                                              "estimator for tally {}",
382
          id_)};
×
383
      }
384

385
      // Set estimator to track-length estimator
386
      estimator_ = TallyEstimator::TRACKLENGTH;
11,204✔
387

388
    } else if (est == "collision") {
414✔
389
      // If the estimator was set to an analog estimator, this means the
390
      // tally needs post-collision information
391
      if (estimator_ == TallyEstimator::ANALOG) {
414✔
392
        throw std::runtime_error {fmt::format("Cannot use collision estimator "
×
393
                                              "for tally ",
394
          id_)};
×
395
      }
396

397
      // Set estimator to collision estimator
398
      estimator_ = TallyEstimator::COLLISION;
414✔
399

400
    } else {
401
      throw std::runtime_error {
×
402
        fmt::format("Invalid estimator '{}' on tally {}", est, id_)};
×
403
    }
404
  }
19,761✔
405

406
#ifdef OPENMC_LIBMESH_ENABLED
407
  // ensure a tracklength tally isn't used with a libMesh filter
408
  for (auto i : this->filters_) {
13,637✔
409
    auto df = dynamic_cast<MeshFilter*>(model::tally_filters[i].get());
9,098✔
410
    if (df) {
9,098✔
411
      auto lm = dynamic_cast<LibMesh*>(model::meshes[df->mesh()].get());
1,090✔
412
      if (lm && estimator_ == TallyEstimator::TRACKLENGTH) {
1,090✔
413
        fatal_error("A tracklength estimator cannot be used with "
414
                    "an unstructured LibMesh tally.");
415
      }
416
    }
417
  }
418
#endif
419
}
24,216✔
420

421
Tally::~Tally()
25,360✔
422
{
423
  model::tally_map.erase(id_);
25,360✔
424
}
25,360✔
425

426
Tally* Tally::create(int32_t id)
109✔
427
{
428
  model::tallies.push_back(make_unique<Tally>(id));
109✔
429
  return model::tallies.back().get();
109✔
430
}
431

432
void Tally::set_id(int32_t id)
26,412✔
433
{
434
  assert(id >= 0 || id == C_NONE);
21,477✔
435

436
  // Clear entry in tally map if an ID was already assigned before
437
  if (id_ != C_NONE) {
26,412✔
438
    model::tally_map.erase(id_);
1,034✔
439
    id_ = C_NONE;
1,034✔
440
  }
441

442
  // Make sure no other tally has the same ID
443
  if (model::tally_map.find(id) != model::tally_map.end()) {
26,412✔
444
    throw std::runtime_error {
×
445
      fmt::format("Two tallies have the same ID: {}", id)};
×
446
  }
447

448
  // If no ID specified, auto-assign next ID in sequence
449
  if (id == C_NONE) {
26,412✔
450
    id = 0;
1,144✔
451
    for (const auto& t : model::tallies) {
3,566✔
452
      id = std::max(id, t->id_);
2,422✔
453
    }
454
    ++id;
1,144✔
455
  }
456

457
  // Update ID and entry in tally map
458
  id_ = id;
26,412✔
459
  model::tally_map[id] = index_;
26,412✔
460
}
26,412✔
461

462
std::vector<FilterType> Tally::filter_types() const
252✔
463
{
464
  std::vector<FilterType> filter_types;
252✔
465
  for (auto idx : this->filters())
964✔
466
    filter_types.push_back(model::tally_filters[idx]->type());
712✔
467
  return filter_types;
252✔
468
}
×
469

470
std::unordered_map<FilterType, int32_t> Tally::filter_indices() const
252✔
471
{
472
  std::unordered_map<FilterType, int32_t> filter_indices;
252✔
473
  for (int i = 0; i < this->filters().size(); i++) {
964✔
474
    const auto& f = model::tally_filters[this->filters(i)];
712✔
475

476
    filter_indices[f->type()] = i;
712✔
477
  }
478
  return filter_indices;
252✔
479
}
×
480

481
bool Tally::has_filter(FilterType filter_type) const
756✔
482
{
483
  for (auto idx : this->filters()) {
1,829✔
484
    if (model::tally_filters[idx]->type() == filter_type)
1,763✔
485
      return true;
690✔
486
  }
487
  return false;
66✔
488
}
489

490
void Tally::set_filters(span<Filter*> filters)
26,517✔
491
{
492
  // Clear old data.
493
  filters_.clear();
26,517✔
494
  strides_.clear();
26,517✔
495

496
  // Copy in the given filter indices.
497
  auto n = filters.size();
26,517✔
498
  filters_.reserve(n);
26,517✔
499

500
  for (auto* filter : filters) {
76,658✔
501
    add_filter(filter);
50,141✔
502
  }
503
}
26,517✔
504

505
void Tally::add_filter(Filter* filter)
50,420✔
506
{
507
  int32_t filter_idx = model::filter_map.at(filter->id());
50,420✔
508
  // if this filter is already present, do nothing and return
509
  if (std::find(filters_.begin(), filters_.end(), filter_idx) != filters_.end())
50,420✔
510
    return;
22✔
511

512
  // Keep track of indices for special filters
513
  if (filter->type() == FilterType::ENERGY_OUT) {
50,398✔
514
    energyout_filter_ = filters_.size();
4,715✔
515
  } else if (filter->type() == FilterType::DELAYED_GROUP) {
45,683✔
516
    delayedgroup_filter_ = filters_.size();
800✔
517
  }
518
  filters_.push_back(filter_idx);
50,398✔
519
}
520

521
void Tally::set_strides()
25,792✔
522
{
523
  // Set the strides.  Filters are traversed in reverse so that the last
524
  // filter has the shortest stride in memory and the first filter has the
525
  // longest stride.
526
  auto n = filters_.size();
25,792✔
527
  strides_.resize(n, 0);
25,792✔
528
  int stride = 1;
25,792✔
529
  for (int i = n - 1; i >= 0; --i) {
76,637✔
530
    strides_[i] = stride;
50,845✔
531
    stride *= model::tally_filters[filters_[i]]->n_bins();
50,845✔
532
  }
533
  n_filter_bins_ = stride;
25,792✔
534
}
25,792✔
535

536
void Tally::set_scores(pugi::xml_node node)
24,234✔
537
{
538
  if (!check_for_node(node, "scores"))
24,234✔
539
    fatal_error(fmt::format("No scores specified on tally {}", id_));
×
540

541
  auto scores = get_node_array<std::string>(node, "scores");
24,234✔
542
  set_scores(scores);
24,234✔
543
}
24,234✔
544

545
void Tally::set_scores(const vector<std::string>& scores)
25,378✔
546
{
547
  // Reset state and prepare for the new scores.
548
  scores_.clear();
25,378✔
549
  scores_.reserve(scores.size());
25,378✔
550

551
  // Check for the presence of certain restrictive filters.
552
  bool energyout_present = energyout_filter_ != C_NONE;
25,378✔
553
  bool legendre_present = false;
25,378✔
554
  bool cell_present = false;
25,378✔
555
  bool cellfrom_present = false;
25,378✔
556
  bool surface_present = false;
25,378✔
557
  bool meshsurface_present = false;
25,378✔
558
  bool non_cell_energy_present = false;
25,378✔
559
  for (auto i_filt : filters_) {
74,991✔
560
    const auto* filt {model::tally_filters[i_filt].get()};
49,613✔
561
    // Checking for only cell and energy filters for pulse-height tally
562
    if (!(filt->type() == FilterType::CELL ||
98,153✔
563
          filt->type() == FilterType::ENERGY)) {
48,540✔
564
      non_cell_energy_present = true;
31,606✔
565
    }
566
    if (filt->type() == FilterType::LEGENDRE) {
49,613✔
567
      legendre_present = true;
2,030✔
568
    } else if (filt->type() == FilterType::CELLFROM) {
47,583✔
569
      cellfrom_present = true;
304✔
570
    } else if (filt->type() == FilterType::CELL) {
47,279✔
571
      cell_present = true;
1,073✔
572
    } else if (filt->type() == FilterType::SURFACE) {
46,206✔
573
      surface_present = true;
193✔
574
    } else if (filt->type() == FilterType::MESH_SURFACE) {
46,013✔
575
      meshsurface_present = true;
352✔
576
    }
577
  }
578

579
  // Iterate over the given scores.
580
  for (auto score_str : scores) {
60,551✔
581
    // Make sure a delayed group filter wasn't used with an incompatible
582
    // score.
583
    if (delayedgroup_filter_ != C_NONE) {
35,173✔
584
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate")
816✔
585
        fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
×
586
    }
587

588
    // Determine integer code for score
589
    int score = reaction_type(score_str);
35,173✔
590

591
    switch (score) {
35,173✔
592
    case SCORE_FLUX:
10,744✔
593
      if (!nuclides_.empty())
10,744✔
594
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
10,744✔
595
          fatal_error("Cannot tally flux for an individual nuclide.");
×
596
      if (energyout_present)
10,744✔
597
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
598
      break;
10,744✔
599

600
    case SCORE_TOTAL:
6,666✔
601
    case SCORE_ABSORPTION:
602
    case SCORE_FISSION:
603
      if (energyout_present)
6,666✔
604
        fatal_error("Cannot tally " + score_str +
×
605
                    " reaction rate with an "
606
                    "outgoing energy filter");
607
      break;
6,666✔
608

609
    case SCORE_SCATTER:
3,882✔
610
      if (legendre_present)
3,882✔
611
        estimator_ = TallyEstimator::ANALOG;
1,347✔
612
    case SCORE_NU_FISSION:
613
    case SCORE_DELAYED_NU_FISSION:
614
    case SCORE_PROMPT_NU_FISSION:
615
      if (energyout_present)
8,606✔
616
        estimator_ = TallyEstimator::ANALOG;
3,569✔
617
      break;
8,606✔
618

619
    case SCORE_NU_SCATTER:
2,084✔
620
      if (settings::run_CE) {
2,084✔
621
        estimator_ = TallyEstimator::ANALOG;
1,956✔
622
      } else {
623
        if (energyout_present || legendre_present)
128✔
624
          estimator_ = TallyEstimator::ANALOG;
64✔
625
      }
626
      break;
2,084✔
627

628
    case SCORE_CURRENT:
577✔
629
      // Check which type of current is desired: mesh or surface currents.
630
      if (surface_present || cell_present || cellfrom_present) {
577✔
631
        if (meshsurface_present)
225✔
632
          fatal_error("Cannot tally mesh surface currents in the same tally as "
×
633
                      "normal surface currents");
634
        type_ = TallyType::SURFACE;
225✔
635
        estimator_ = TallyEstimator::ANALOG;
225✔
636
      } else if (meshsurface_present) {
352✔
637
        type_ = TallyType::MESH_SURFACE;
352✔
638
      } else {
639
        fatal_error("Cannot tally currents without surface type filters");
×
640
      }
641
      break;
577✔
642

643
    case HEATING:
343✔
644
      if (settings::photon_transport)
343✔
645
        estimator_ = TallyEstimator::COLLISION;
183✔
646
      break;
343✔
647

648
    case SCORE_PULSE_HEIGHT:
16✔
649
      if (non_cell_energy_present) {
16✔
650
        fatal_error("Pulse-height tallies are not compatible with filters "
×
651
                    "other than CellFilter and EnergyFilter");
652
      }
653
      type_ = TallyType::PULSE_HEIGHT;
16✔
654

655
      // Collecting indices of all cells covered by the filters in the pulse
656
      // height tally in global variable pulse_height_cells
657
      for (const auto& i_filt : filters_) {
48✔
658
        auto cell_filter =
659
          dynamic_cast<CellFilter*>(model::tally_filters[i_filt].get());
32✔
660
        if (cell_filter) {
32✔
661
          const auto& cells = cell_filter->cells();
16✔
662
          for (int i = 0; i < cell_filter->n_bins(); i++) {
32✔
663
            int cell_index = cells[i];
16✔
664
            if (!contains(model::pulse_height_cells, cell_index)) {
16✔
665
              model::pulse_height_cells.push_back(cell_index);
16✔
666
            }
667
          }
668
        }
669
      }
670
      break;
16✔
671

672
    case SCORE_IFP_TIME_NUM:
102✔
673
    case SCORE_IFP_BETA_NUM:
674
    case SCORE_IFP_DENOM:
675
      estimator_ = TallyEstimator::COLLISION;
102✔
676
      break;
102✔
677
    }
678

679
    scores_.push_back(score);
35,173✔
680
  }
35,173✔
681

682
  // Make sure that no duplicate scores exist.
683
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
60,551✔
684
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
82,447✔
685
      if (*it1 == *it2)
47,274✔
686
        fatal_error(
×
687
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
688
            reaction_name(*it1), id_));
×
689
    }
690
  }
691

692
  // Make sure all scores are compatible with multigroup mode.
693
  if (!settings::run_CE) {
25,378✔
694
    for (auto sc : scores_)
6,158✔
695
      if (sc > 0)
4,551✔
696
        fatal_error("Cannot tally " + reaction_name(sc) +
×
697
                    " reaction rate "
698
                    "in multi-group mode");
699
  }
700

701
  // Make sure current scores are not mixed in with volumetric scores.
702
  if (type_ == TallyType::SURFACE || type_ == TallyType::MESH_SURFACE) {
25,378✔
703
    if (scores_.size() != 1)
577✔
704
      fatal_error("Cannot tally other scores in the same tally as surface "
×
705
                  "currents.");
706
  }
707
  if ((surface_present || meshsurface_present) && scores_[0] != SCORE_CURRENT)
25,378✔
708
    fatal_error("Cannot tally score other than 'current' when using a surface "
×
709
                "or mesh-surface filter.");
710
}
25,378✔
711

712
void Tally::set_nuclides(pugi::xml_node node)
24,234✔
713
{
714
  nuclides_.clear();
24,234✔
715

716
  // By default, we tally just the total material rates.
717
  if (!check_for_node(node, "nuclides")) {
24,234✔
718
    nuclides_.push_back(-1);
5,943✔
719
    return;
5,943✔
720
  }
721

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

728
void Tally::set_nuclides(const vector<std::string>& nuclides)
18,291✔
729
{
730
  nuclides_.clear();
18,291✔
731

732
  for (const auto& nuc : nuclides) {
47,274✔
733
    if (nuc == "total") {
28,983✔
734
      nuclides_.push_back(-1);
14,768✔
735
    } else {
736
      auto search = data::nuclide_map.find(nuc);
14,215✔
737
      if (search == data::nuclide_map.end()) {
14,215✔
738
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
33✔
739
        if (err < 0)
33✔
740
          throw std::runtime_error {openmc_err_msg};
×
741
      }
742
      nuclides_.push_back(data::nuclide_map.at(nuc));
14,215✔
743
    }
744
  }
745
}
18,291✔
746

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

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

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

787
    // Read the trigger scores.
788
    vector<std::string> trigger_scores;
71✔
789
    if (check_for_node(trigger_node, "scores")) {
71✔
790
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
71✔
791
    } else {
792
      trigger_scores.push_back("all");
×
793
    }
794

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

820
void Tally::init_results()
25,792✔
821
{
822
  int n_scores = scores_.size() * nuclides_.size();
25,792✔
823
  results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
25,792✔
824
}
25,792✔
825

826
void Tally::reset()
55,013✔
827
{
828
  n_realizations_ = 0;
55,013✔
829
  if (results_.size() != 0) {
55,013✔
830
    xt::view(results_, xt::all()) = 0.0;
54,176✔
831
  }
832
}
55,013✔
833

834
void Tally::accumulate()
203,173✔
835
{
836
  // Increment number of realizations
837
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
203,173✔
838

839
  if (mpi::master || !settings::reduce_tallies) {
203,173✔
840
    // Calculate total source strength for normalization
841
    double total_source = 0.0;
165,408✔
842
    if (settings::run_mode == RunMode::FIXED_SOURCE) {
165,408✔
843
      total_source = model::external_sources_probability.integral();
44,367✔
844
    } else {
845
      total_source = 1.0;
121,041✔
846
    }
847

848
    // Account for number of source particles in normalization
849
    double norm =
165,408✔
850
      total_source / (settings::n_particles * settings::gen_per_batch);
165,408✔
851

852
    if (settings::solver_type == SolverType::RANDOM_RAY) {
165,408✔
853
      norm = 1.0;
11,226✔
854
    }
855

856
// Accumulate each result
857
#pragma omp parallel for
90,858✔
858
    for (int i = 0; i < results_.shape()[0]; ++i) {
44,411,560✔
859
      for (int j = 0; j < results_.shape()[1]; ++j) {
106,931,720✔
860
        double val = results_(i, j, TallyResult::VALUE) * norm;
62,594,710✔
861
        results_(i, j, TallyResult::VALUE) = 0.0;
62,594,710✔
862
        results_(i, j, TallyResult::SUM) += val;
62,594,710✔
863
        results_(i, j, TallyResult::SUM_SQ) += val * val;
62,594,710✔
864
      }
865
    }
866
  }
867
}
203,173✔
868

869
int Tally::score_index(const std::string& score) const
252✔
870
{
871
  for (int i = 0; i < scores_.size(); i++) {
252✔
872
    if (this->score_name(i) == score)
252✔
873
      return i;
252✔
874
  }
875
  return -1;
×
876
}
877

878
xt::xarray<double> Tally::get_reshaped_data() const
×
879
{
880
  std::vector<uint64_t> shape;
×
881
  for (auto f : filters()) {
×
882
    shape.push_back(model::tally_filters[f]->n_bins());
×
883
  }
884

885
  // add number of scores and nuclides to tally
886
  shape.push_back(results_.shape()[1]);
×
887
  shape.push_back(results_.shape()[2]);
×
888

889
  xt::xarray<double> reshaped_results = results_;
×
890
  reshaped_results.reshape(shape);
×
891
  return reshaped_results;
×
892
}
893

894
std::string Tally::score_name(int score_idx) const
282✔
895
{
896
  if (score_idx < 0 || score_idx >= scores_.size()) {
282✔
897
    fatal_error("Index in scores array is out of bounds.");
×
898
  }
899
  return reaction_name(scores_[score_idx]);
282✔
900
}
901

902
std::vector<std::string> Tally::scores() const
×
903
{
904
  std::vector<std::string> score_names;
×
905
  for (int score : scores_)
×
906
    score_names.push_back(reaction_name(score));
×
907
  return score_names;
×
908
}
×
909

910
std::string Tally::nuclide_name(int nuclide_idx) const
30✔
911
{
912
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
30✔
913
    fatal_error("Index in nuclides array is out of bounds");
×
914
  }
915

916
  int nuclide = nuclides_.at(nuclide_idx);
30✔
917
  if (nuclide == -1) {
30✔
918
    return "total";
30✔
919
  }
920
  return data::nuclides.at(nuclide)->name_;
×
921
}
922

923
//==============================================================================
924
// Non-member functions
925
//==============================================================================
926

927
void read_tallies_xml()
1,359✔
928
{
929
  // Check if tallies.xml exists. If not, just return since it is optional
930
  std::string filename = settings::path_input + "tallies.xml";
1,359✔
931
  if (!file_exists(filename))
1,359✔
932
    return;
898✔
933

934
  write_message("Reading tallies XML file...", 5);
461✔
935

936
  // Parse tallies.xml file
937
  pugi::xml_document doc;
461✔
938
  doc.load_file(filename.c_str());
461✔
939
  pugi::xml_node root = doc.document_element();
461✔
940

941
  read_tallies_xml(root);
461✔
942
}
1,359✔
943

944
void read_tallies_xml(pugi::xml_node root)
3,817✔
945
{
946
  // Check for <assume_separate> setting
947
  if (check_for_node(root, "assume_separate")) {
3,817✔
948
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
16✔
949
  }
950

951
  // Check for user meshes and allocate
952
  read_meshes(root);
3,817✔
953

954
  // We only need the mesh info for plotting
955
  if (settings::run_mode == RunMode::PLOTTING)
3,817✔
956
    return;
33✔
957

958
  // Read data for tally derivatives
959
  read_tally_derivatives(root);
3,784✔
960

961
  // ==========================================================================
962
  // READ FILTER DATA
963

964
  // Check for user filters and allocate
965
  for (auto node_filt : root.children("filter")) {
11,593✔
966
    auto f = Filter::create(node_filt);
7,809✔
967
  }
968

969
  // ==========================================================================
970
  // READ TALLY DATA
971

972
  // Check for user tallies
973
  int n = 0;
3,784✔
974
  for (auto node : root.children("tally"))
28,018✔
975
    ++n;
24,234✔
976
  if (n == 0 && mpi::master) {
3,784✔
977
    warning("No tallies present in tallies.xml file.");
×
978
  }
979

980
  for (auto node_tal : root.children("tally")) {
28,000✔
981
    model::tallies.push_back(make_unique<Tally>(node_tal));
24,234✔
982
  }
983
}
984

985
#ifdef OPENMC_MPI
986
void reduce_tally_results()
32,980✔
987
{
988
  // Don't reduce tally is no_reduce option is on
989
  if (settings::reduce_tallies) {
32,980✔
990
    for (int i_tally : model::active_tallies) {
98,290✔
991
      // Skip any tallies that are not active
992
      auto& tally {model::tallies[i_tally]};
65,310✔
993

994
      // Get view of accumulated tally values
995
      auto values_view = xt::view(tally->results_, xt::all(), xt::all(),
65,310✔
996
        static_cast<int>(TallyResult::VALUE));
130,620✔
997

998
      // Make copy of tally values in contiguous array
999
      xt::xtensor<double, 2> values = values_view;
65,310✔
1000
      xt::xtensor<double, 2> values_reduced = xt::empty_like(values);
65,310✔
1001

1002
      // Reduce contiguous set of tally results
1003
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
65,310✔
1004
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1005

1006
      // Transfer values on master and reset on other ranks
1007
      if (mpi::master) {
65,310✔
1008
        values_view = values_reduced;
32,645✔
1009
      } else {
1010
        values_view = 0.0;
32,665✔
1011
      }
1012
    }
65,310✔
1013
  }
1014

1015
  // Note that global tallies are *always* reduced even when no_reduce option
1016
  // is on.
1017

1018
  // Get view of global tally values
1019
  auto& gt = simulation::global_tallies;
32,980✔
1020
  auto gt_values_view =
1021
    xt::view(gt, xt::all(), static_cast<int>(TallyResult::VALUE));
32,980✔
1022

1023
  // Make copy of values in contiguous array
1024
  xt::xtensor<double, 1> gt_values = gt_values_view;
32,980✔
1025
  xt::xtensor<double, 1> gt_values_reduced = xt::empty_like(gt_values);
32,980✔
1026

1027
  // Reduce contiguous data
1028
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
32,980✔
1029
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1030

1031
  // Transfer values on master and reset on other ranks
1032
  if (mpi::master) {
32,980✔
1033
    gt_values_view = gt_values_reduced;
16,485✔
1034
  } else {
1035
    gt_values_view = 0.0;
16,495✔
1036
  }
1037

1038
  // We also need to determine the total starting weight of particles from the
1039
  // last realization
1040
  double weight_reduced;
1041
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
32,980✔
1042
    0, mpi::intracomm);
1043
  if (mpi::master)
32,980✔
1044
    simulation::total_weight = weight_reduced;
16,485✔
1045
}
32,980✔
1046
#endif
1047

1048
void accumulate_tallies()
111,325✔
1049
{
1050
#ifdef OPENMC_MPI
1051
  // Combine tally results onto master process
1052
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
62,722✔
1053
    reduce_tally_results();
32,980✔
1054
  }
1055
#endif
1056

1057
  // Increase number of realizations (only used for global tallies)
1058
  simulation::n_realizations += 1;
111,325✔
1059

1060
  // Accumulate on master only unless run is not reduced then do it on all
1061
  if (mpi::master || !settings::reduce_tallies) {
111,325✔
1062
    auto& gt = simulation::global_tallies;
89,580✔
1063

1064
    if (settings::run_mode == RunMode::EIGENVALUE) {
89,580✔
1065
      if (simulation::current_batch > settings::n_inactive) {
62,415✔
1066
        // Accumulate products of different estimators of k
1067
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
47,943✔
1068
                       simulation::total_weight;
47,943✔
1069
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
47,943✔
1070
                       simulation::total_weight;
47,943✔
1071
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
47,943✔
1072
                       simulation::total_weight;
47,943✔
1073
        simulation::k_col_abs += k_col * k_abs;
47,943✔
1074
        simulation::k_col_tra += k_col * k_tra;
47,943✔
1075
        simulation::k_abs_tra += k_abs * k_tra;
47,943✔
1076
      }
1077
    }
1078

1079
    // Accumulate results for global tallies
1080
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
447,900✔
1081
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
358,320✔
1082
      gt(i, TallyResult::VALUE) = 0.0;
358,320✔
1083
      gt(i, TallyResult::SUM) += val;
358,320✔
1084
      gt(i, TallyResult::SUM_SQ) += val * val;
358,320✔
1085
    }
1086
  }
1087

1088
  // Accumulate results for each tally
1089
  for (int i_tally : model::active_tallies) {
314,498✔
1090
    auto& tally {model::tallies[i_tally]};
203,173✔
1091
    tally->accumulate();
203,173✔
1092
  }
1093
}
111,325✔
1094

1095
void setup_active_tallies()
111,337✔
1096
{
1097
  model::active_tallies.clear();
111,337✔
1098
  model::active_analog_tallies.clear();
111,337✔
1099
  model::active_tracklength_tallies.clear();
111,337✔
1100
  model::active_collision_tallies.clear();
111,337✔
1101
  model::active_meshsurf_tallies.clear();
111,337✔
1102
  model::active_surface_tallies.clear();
111,337✔
1103
  model::active_pulse_height_tallies.clear();
111,337✔
1104
  model::time_grid.clear();
111,337✔
1105

1106
  for (auto i = 0; i < model::tallies.size(); ++i) {
443,350✔
1107
    const auto& tally {*model::tallies[i]};
332,013✔
1108

1109
    if (tally.active_) {
332,013✔
1110
      model::active_tallies.push_back(i);
203,173✔
1111
      switch (tally.type_) {
203,173✔
1112

1113
      case TallyType::VOLUME:
196,965✔
1114
        switch (tally.estimator_) {
196,965✔
1115
        case TallyEstimator::ANALOG:
80,348✔
1116
          model::active_analog_tallies.push_back(i);
80,348✔
1117
          break;
80,348✔
1118
        case TallyEstimator::TRACKLENGTH:
111,507✔
1119
          model::active_tracklength_tallies.push_back(i);
111,507✔
1120
          if (auto time_filter = tally.get_filter<TimeFilter>()) {
111,507✔
1121
            if ((tally.get_filter<MeshFilter>() != nullptr) ||
1,640✔
1122
                (tally.get_filter<MeshMaterialFilter>() != nullptr)) {
710✔
1123
              model::add_to_time_grid(time_filter->bins());
220✔
1124
            }
1125
          }
1126
          break;
111,507✔
1127
        case TallyEstimator::COLLISION:
5,110✔
1128
          model::active_collision_tallies.push_back(i);
5,110✔
1129
        }
1130
        break;
196,965✔
1131

1132
      case TallyType::MESH_SURFACE:
4,026✔
1133
        model::active_meshsurf_tallies.push_back(i);
4,026✔
1134
        break;
4,026✔
1135

1136
      case TallyType::SURFACE:
2,102✔
1137
        model::active_surface_tallies.push_back(i);
2,102✔
1138
        break;
2,102✔
1139

1140
      case TallyType::PULSE_HEIGHT:
80✔
1141
        model::active_pulse_height_tallies.push_back(i);
80✔
1142
        break;
80✔
1143
      }
1144
    }
1145
  }
1146
}
111,337✔
1147

1148
void free_memory_tally()
6,994✔
1149
{
1150
  model::tally_derivs.clear();
6,994✔
1151
  model::tally_deriv_map.clear();
6,994✔
1152

1153
  model::tally_filters.clear();
6,994✔
1154
  model::filter_map.clear();
6,994✔
1155

1156
  model::tallies.clear();
6,994✔
1157

1158
  model::active_tallies.clear();
6,994✔
1159
  model::active_analog_tallies.clear();
6,994✔
1160
  model::active_tracklength_tallies.clear();
6,994✔
1161
  model::active_collision_tallies.clear();
6,994✔
1162
  model::active_meshsurf_tallies.clear();
6,994✔
1163
  model::active_surface_tallies.clear();
6,994✔
1164
  model::active_pulse_height_tallies.clear();
6,994✔
1165
  model::time_grid.clear();
6,994✔
1166

1167
  model::tally_map.clear();
6,994✔
1168
}
6,994✔
1169

1170
//==============================================================================
1171
// C-API functions
1172
//==============================================================================
1173

1174
extern "C" int openmc_extend_tallies(
1,034✔
1175
  int32_t n, int32_t* index_start, int32_t* index_end)
1176
{
1177
  if (index_start)
1,034✔
1178
    *index_start = model::tallies.size();
1,034✔
1179
  if (index_end)
1,034✔
1180
    *index_end = model::tallies.size() + n - 1;
×
1181
  for (int i = 0; i < n; ++i) {
2,068✔
1182
    model::tallies.push_back(make_unique<Tally>(-1));
1,034✔
1183
  }
1184
  return 0;
1,034✔
1185
}
1186

1187
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
20,867✔
1188
{
1189
  auto it = model::tally_map.find(id);
20,867✔
1190
  if (it == model::tally_map.end()) {
20,867✔
1191
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
22✔
1192
    return OPENMC_E_INVALID_ID;
22✔
1193
  }
1194

1195
  *index = it->second;
20,845✔
1196
  return 0;
20,845✔
1197
}
1198

1199
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1200
{
1201
  int32_t largest_tally_id = 0;
×
1202
  for (const auto& t : model::tallies) {
×
1203
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1204
  }
1205
  *id = largest_tally_id + 1;
×
1206
}
1207

1208
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1209
{
1210
  if (index < 0 || index >= model::tallies.size()) {
×
1211
    set_errmsg("Index in tallies array is out of bounds.");
×
1212
    return OPENMC_E_OUT_OF_BOUNDS;
×
1213
  }
1214

1215
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1216
  return 0;
×
1217
}
1218

1219
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
660✔
1220
{
1221
  if (index < 0 || index >= model::tallies.size()) {
660✔
1222
    set_errmsg("Index in tallies array is out of bounds.");
×
1223
    return OPENMC_E_OUT_OF_BOUNDS;
×
1224
  }
1225

1226
  auto& t {model::tallies[index]};
660✔
1227

1228
  std::string est = estimator;
660✔
1229
  if (est == "analog") {
660✔
1230
    t->estimator_ = TallyEstimator::ANALOG;
660✔
1231
  } else if (est == "collision") {
×
1232
    t->estimator_ = TallyEstimator::COLLISION;
×
1233
  } else if (est == "tracklength") {
×
1234
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1235
  } else {
1236
    set_errmsg("Unknown tally estimator: " + est);
×
1237
    return OPENMC_E_INVALID_ARGUMENT;
×
1238
  }
1239
  return 0;
660✔
1240
}
660✔
1241

1242
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
27,016✔
1243
{
1244
  if (index < 0 || index >= model::tallies.size()) {
27,016✔
1245
    set_errmsg("Index in tallies array is out of bounds.");
×
1246
    return OPENMC_E_OUT_OF_BOUNDS;
×
1247
  }
1248

1249
  *id = model::tallies[index]->id_;
27,016✔
1250
  return 0;
27,016✔
1251
}
1252

1253
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,034✔
1254
{
1255
  if (index < 0 || index >= model::tallies.size()) {
1,034✔
1256
    set_errmsg("Index in tallies array is out of bounds.");
×
1257
    return OPENMC_E_OUT_OF_BOUNDS;
×
1258
  }
1259

1260
  model::tallies[index]->set_id(id);
1,034✔
1261
  return 0;
1,034✔
1262
}
1263

1264
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
11✔
1265
{
1266
  if (index < 0 || index >= model::tallies.size()) {
11✔
1267
    set_errmsg("Index in tallies array is out of bounds.");
×
1268
    return OPENMC_E_OUT_OF_BOUNDS;
×
1269
  }
1270
  *type = static_cast<int>(model::tallies[index]->type_);
11✔
1271

1272
  return 0;
11✔
1273
}
1274

1275
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
660✔
1276
{
1277
  if (index < 0 || index >= model::tallies.size()) {
660✔
1278
    set_errmsg("Index in tallies array is out of bounds.");
×
1279
    return OPENMC_E_OUT_OF_BOUNDS;
×
1280
  }
1281
  if (strcmp(type, "volume") == 0) {
660✔
1282
    model::tallies[index]->type_ = TallyType::VOLUME;
495✔
1283
  } else if (strcmp(type, "mesh-surface") == 0) {
165✔
1284
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
165✔
1285
  } else if (strcmp(type, "surface") == 0) {
×
1286
    model::tallies[index]->type_ = TallyType::SURFACE;
×
1287
  } else if (strcmp(type, "pulse-height") == 0) {
×
1288
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1289
  } else {
1290
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
1291
    return OPENMC_E_INVALID_ARGUMENT;
×
1292
  }
1293

1294
  return 0;
660✔
1295
}
1296

1297
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
22✔
1298
{
1299
  if (index < 0 || index >= model::tallies.size()) {
22✔
1300
    set_errmsg("Index in tallies array is out of bounds.");
×
1301
    return OPENMC_E_OUT_OF_BOUNDS;
×
1302
  }
1303
  *active = model::tallies[index]->active_;
22✔
1304

1305
  return 0;
22✔
1306
}
1307

1308
extern "C" int openmc_tally_set_active(int32_t index, bool active)
671✔
1309
{
1310
  if (index < 0 || index >= model::tallies.size()) {
671✔
1311
    set_errmsg("Index in tallies array is out of bounds.");
×
1312
    return OPENMC_E_OUT_OF_BOUNDS;
×
1313
  }
1314
  model::tallies[index]->active_ = active;
671✔
1315

1316
  return 0;
671✔
1317
}
1318

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

1327
  return 0;
22✔
1328
}
1329

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

1338
  return 0;
374✔
1339
}
1340

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

1349
  return 0;
22✔
1350
}
1351

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

1360
  return 0;
231✔
1361
}
1362

1363
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
187✔
1364
{
1365
  if (index < 0 || index >= model::tallies.size()) {
187✔
1366
    set_errmsg("Index in tallies array is out of bounds.");
×
1367
    return OPENMC_E_OUT_OF_BOUNDS;
×
1368
  }
1369

1370
  *scores = model::tallies[index]->scores_.data();
187✔
1371
  *n = model::tallies[index]->scores_.size();
187✔
1372
  return 0;
187✔
1373
}
1374

1375
extern "C" int openmc_tally_set_scores(
1,045✔
1376
  int32_t index, int n, const char** scores)
1377
{
1378
  if (index < 0 || index >= model::tallies.size()) {
1,045✔
1379
    set_errmsg("Index in tallies array is out of bounds.");
×
1380
    return OPENMC_E_OUT_OF_BOUNDS;
×
1381
  }
1382

1383
  vector<std::string> scores_str(scores, scores + n);
1,045✔
1384
  try {
1385
    model::tallies[index]->set_scores(scores_str);
1,045✔
1386
  } catch (const std::invalid_argument& ex) {
×
1387
    set_errmsg(ex.what());
×
1388
    return OPENMC_E_INVALID_ARGUMENT;
×
1389
  }
×
1390

1391
  return 0;
1,045✔
1392
}
1,045✔
1393

1394
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
231✔
1395
{
1396
  // Make sure the index fits in the array bounds.
1397
  if (index < 0 || index >= model::tallies.size()) {
231✔
1398
    set_errmsg("Index in tallies array is out of bounds.");
×
1399
    return OPENMC_E_OUT_OF_BOUNDS;
×
1400
  }
1401

1402
  *n = model::tallies[index]->nuclides_.size();
231✔
1403
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1404

1405
  return 0;
231✔
1406
}
1407

1408
extern "C" int openmc_tally_set_nuclides(
1,276✔
1409
  int32_t index, int n, const char** nuclides)
1410
{
1411
  // Make sure the index fits in the array bounds.
1412
  if (index < 0 || index >= model::tallies.size()) {
1,276✔
1413
    set_errmsg("Index in tallies array is out of bounds.");
×
1414
    return OPENMC_E_OUT_OF_BOUNDS;
×
1415
  }
1416

1417
  vector<std::string> words(nuclides, nuclides + n);
1,276✔
1418
  vector<int> nucs;
1,276✔
1419
  for (auto word : words) {
5,610✔
1420
    if (word == "total") {
4,345✔
1421
      nucs.push_back(-1);
660✔
1422
    } else {
1423
      auto search = data::nuclide_map.find(word);
3,685✔
1424
      if (search == data::nuclide_map.end()) {
3,685✔
1425
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
803✔
1426
        if (err < 0) {
803✔
1427
          set_errmsg(openmc_err_msg);
11✔
1428
          return OPENMC_E_DATA;
11✔
1429
        }
1430
      }
1431
      nucs.push_back(data::nuclide_map.at(word));
3,674✔
1432
    }
1433
  }
4,345✔
1434

1435
  model::tallies[index]->nuclides_ = nucs;
1,265✔
1436

1437
  return 0;
1,265✔
1438
}
1,276✔
1439

1440
extern "C" int openmc_tally_get_filters(
979✔
1441
  int32_t index, const int32_t** indices, size_t* n)
1442
{
1443
  if (index < 0 || index >= model::tallies.size()) {
979✔
1444
    set_errmsg("Index in tallies array is out of bounds.");
×
1445
    return OPENMC_E_OUT_OF_BOUNDS;
×
1446
  }
1447

1448
  *indices = model::tallies[index]->filters().data();
979✔
1449
  *n = model::tallies[index]->filters().size();
979✔
1450
  return 0;
979✔
1451
}
1452

1453
extern "C" int openmc_tally_set_filters(
1,089✔
1454
  int32_t index, size_t n, const int32_t* indices)
1455
{
1456
  // Make sure the index fits in the array bounds.
1457
  if (index < 0 || index >= model::tallies.size()) {
1,089✔
1458
    set_errmsg("Index in tallies array is out of bounds.");
×
1459
    return OPENMC_E_OUT_OF_BOUNDS;
×
1460
  }
1461

1462
  // Set the filters.
1463
  try {
1464
    // Convert indices to filter pointers
1465
    vector<Filter*> filters;
1,089✔
1466
    for (int64_t i = 0; i < n; ++i) {
2,596✔
1467
      int32_t i_filt = indices[i];
1,507✔
1468
      filters.push_back(model::tally_filters.at(i_filt).get());
1,507✔
1469
    }
1470
    model::tallies[index]->set_filters(filters);
1,089✔
1471
  } catch (const std::out_of_range& ex) {
1,089✔
1472
    set_errmsg("Index in tally filter array out of bounds.");
×
1473
    return OPENMC_E_OUT_OF_BOUNDS;
×
1474
  }
×
1475

1476
  return 0;
1,089✔
1477
}
1478

1479
//! Reset tally results and number of realizations
1480
extern "C" int openmc_tally_reset(int32_t index)
2,684✔
1481
{
1482
  // Make sure the index fits in the array bounds.
1483
  if (index < 0 || index >= model::tallies.size()) {
2,684✔
1484
    set_errmsg("Index in tallies array is out of bounds.");
×
1485
    return OPENMC_E_OUT_OF_BOUNDS;
×
1486
  }
1487

1488
  model::tallies[index]->reset();
2,684✔
1489
  return 0;
2,684✔
1490
}
1491

1492
extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n)
3,366✔
1493
{
1494
  // Make sure the index fits in the array bounds.
1495
  if (index < 0 || index >= model::tallies.size()) {
3,366✔
1496
    set_errmsg("Index in tallies array is out of bounds.");
×
1497
    return OPENMC_E_OUT_OF_BOUNDS;
×
1498
  }
1499

1500
  *n = model::tallies[index]->n_realizations_;
3,366✔
1501
  return 0;
3,366✔
1502
}
1503

1504
//! \brief Returns a pointer to a tally results array along with its shape.
1505
//! This allows a user to obtain in-memory tally results from Python directly.
1506
extern "C" int openmc_tally_results(
15,862✔
1507
  int32_t index, double** results, size_t* shape)
1508
{
1509
  // Make sure the index fits in the array bounds.
1510
  if (index < 0 || index >= model::tallies.size()) {
15,862✔
1511
    set_errmsg("Index in tallies array is out of bounds.");
×
1512
    return OPENMC_E_OUT_OF_BOUNDS;
×
1513
  }
1514

1515
  const auto& t {model::tallies[index]};
15,862✔
1516
  if (t->results_.size() == 0) {
15,862✔
1517
    set_errmsg("Tally results have not been allocated yet.");
×
1518
    return OPENMC_E_ALLOCATE;
×
1519
  }
1520

1521
  // Set pointer to results and copy shape
1522
  *results = t->results_.data();
15,862✔
1523
  auto s = t->results_.shape();
15,862✔
1524
  shape[0] = s[0];
15,862✔
1525
  shape[1] = s[1];
15,862✔
1526
  shape[2] = s[2];
15,862✔
1527
  return 0;
15,862✔
1528
}
1529

1530
extern "C" int openmc_global_tallies(double** ptr)
11✔
1531
{
1532
  *ptr = simulation::global_tallies.data();
11✔
1533
  return 0;
11✔
1534
}
1535

1536
extern "C" size_t tallies_size()
1,078✔
1537
{
1538
  return model::tallies.size();
1,078✔
1539
}
1540

1541
// given a tally ID, remove it from the tallies vector
1542
extern "C" int openmc_remove_tally(int32_t index)
11✔
1543
{
1544
  // check that id is in the map
1545
  if (index < 0 || index >= model::tallies.size()) {
11✔
1546
    set_errmsg("Index in tallies array is out of bounds.");
×
1547
    return OPENMC_E_OUT_OF_BOUNDS;
×
1548
  }
1549

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

1554
  return 0;
11✔
1555
}
1556

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