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

openmc-dev / openmc / 14445157240

14 Apr 2025 12:05PM UTC coverage: 85.173% (+0.1%) from 85.044%
14445157240

Pull #3133

github

web-flow
Merge 85f9fc8a6 into c1a4d43da
Pull Request #3133: Kinetics parameters using Iterated Fission Probability

288 of 305 new or added lines in 12 files covered. (94.43%)

151 existing lines in 19 files now uncovered.

52020 of 61076 relevant lines covered (85.17%)

36899999.07 hits per line

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

81.04
/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_meshsurface.h"
31
#include "openmc/tallies/filter_particle.h"
32
#include "openmc/tallies/filter_sph_harm.h"
33
#include "openmc/tallies/filter_surface.h"
34
#include "openmc/xml_interface.h"
35

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

41
#include <algorithm> // for max
42
#include <cassert>
43
#include <cstddef> // for size_t
44
#include <string>
45

46
namespace openmc {
47

48
//==============================================================================
49
// Global variable definitions
50
//==============================================================================
51

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

66
namespace simulation {
67
xt::xtensor_fixed<double, xt::xshape<N_GLOBAL_TALLIES, 3>> global_tallies;
68
int32_t n_realizations {0};
69
} // namespace simulation
70

71
double global_tally_absorption;
72
double global_tally_collision;
73
double global_tally_tracklength;
74
double global_tally_leakage;
75

76
//==============================================================================
77
// Tally object implementation
78
//==============================================================================
79

80
Tally::Tally(int32_t id)
1,110✔
81
{
82
  index_ = model::tallies.size(); // Avoids warning about narrowing
1,110✔
83
  this->set_id(id);
1,110✔
84
  this->set_filters({});
1,110✔
85
}
1,110✔
86

87
Tally::Tally(pugi::xml_node node)
24,059✔
88
{
89
  index_ = model::tallies.size(); // Avoids warning about narrowing
24,059✔
90

91
  // Copy and set tally id
92
  if (!check_for_node(node, "id")) {
24,059✔
93
    throw std::runtime_error {"Must specify id for tally in tally XML file."};
×
94
  }
95
  int32_t id = std::stoi(get_node_value(node, "id"));
24,059✔
96
  this->set_id(id);
24,059✔
97

98
  if (check_for_node(node, "name"))
24,059✔
99
    name_ = get_node_value(node, "name");
2,243✔
100

101
  if (check_for_node(node, "multiply_density")) {
24,059✔
102
    multiply_density_ = get_node_value_bool(node, "multiply_density");
33✔
103
  }
104

105
  // =======================================================================
106
  // READ DATA FOR FILTERS
107

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

118
  // Determine number of filters
119
  vector<int> filter_ids;
24,059✔
120
  if (check_for_node(node, "filters")) {
24,059✔
121
    filter_ids = get_node_array<int>(node, "filters");
23,283✔
122
  }
123

124
  // Allocate and store filter user ids
125
  vector<Filter*> filters;
24,059✔
126
  for (int filter_id : filter_ids) {
72,416✔
127
    // Determine if filter ID is valid
128
    auto it = model::filter_map.find(filter_id);
48,357✔
129
    if (it == model::filter_map.end()) {
48,357✔
130
      throw std::runtime_error {fmt::format(
×
131
        "Could not find filter {} specified on tally {}", filter_id, id_)};
×
132
    }
133

134
    // Store the index of the filter
135
    filters.push_back(model::tally_filters[it->second].get());
48,357✔
136
  }
137

138
  // Set the filters
139
  this->set_filters(filters);
24,059✔
140

141
  // Check for the presence of certain filter types
142
  bool has_energyout = energyout_filter_ >= 0;
24,059✔
143
  int particle_filter_index = C_NONE;
24,059✔
144
  for (int64_t j = 0; j < filters_.size(); ++j) {
72,416✔
145
    int i_filter = filters_[j];
48,357✔
146
    const auto& f = model::tally_filters[i_filter].get();
48,357✔
147

148
    auto pf = dynamic_cast<ParticleFilter*>(f);
48,357✔
149
    if (pf)
48,357✔
150
      particle_filter_index = i_filter;
416✔
151

152
    // Change the tally estimator if a filter demands it
153
    FilterType filt_type = f->type();
48,357✔
154
    if (filt_type == FilterType::ENERGY_OUT ||
48,357✔
155
        filt_type == FilterType::LEGENDRE) {
156
      estimator_ = TallyEstimator::ANALOG;
6,554✔
157
    } else if (filt_type == FilterType::SPHERICAL_HARMONICS) {
41,803✔
158
      auto sf = dynamic_cast<SphericalHarmonicsFilter*>(f);
70✔
159
      if (sf->cosine() == SphericalHarmonicsCosine::scatter) {
70✔
160
        estimator_ = TallyEstimator::ANALOG;
11✔
161
      }
162
    } else if (filt_type == FilterType::SPATIAL_LEGENDRE ||
41,733✔
163
               filt_type == FilterType::ZERNIKE ||
41,678✔
164
               filt_type == FilterType::ZERNIKE_RADIAL) {
165
      estimator_ = TallyEstimator::COLLISION;
55✔
166
    }
167
  }
168

169
  // =======================================================================
170
  // READ DATA FOR NUCLIDES
171

172
  this->set_nuclides(node);
24,059✔
173

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

177
  this->set_scores(node);
24,059✔
178

179
  if (!check_for_node(node, "scores")) {
24,059✔
180
    fatal_error(fmt::format("No scores specified on tally {}.", id_));
×
181
  }
182

183
  // Set IFP if needed
184
  if (!settings::ifp_on) {
24,059✔
185
    // Determine if this tally has an IFP score
186
    bool has_ifp_score = false;
24,059✔
187
    for (int score : scores_) {
57,277✔
188
      if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM ||
33,252✔
189
          score == SCORE_IFP_DENOM) {
190
        has_ifp_score = true;
34✔
191
        break;
34✔
192
      }
193
    }
194

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

218
  // Set IFP parameters if needed
219
  if (settings::ifp_on) {
24,041✔
220
    for (int score : scores_) {
64✔
221
      switch (score) {
48✔
222
      case SCORE_IFP_TIME_NUM:
16✔
223
        if (settings::ifp_parameter == IFPParameter::None) {
16✔
224
          settings::ifp_parameter = IFPParameter::GenerationTime;
16✔
NEW
225
        } else if (settings::ifp_parameter == IFPParameter::BetaEffective) {
×
NEW
226
          settings::ifp_parameter = IFPParameter::Both;
×
227
        }
228
        break;
16✔
229
      case SCORE_IFP_BETA_NUM:
32✔
230
      case SCORE_IFP_DENOM:
231
        if (settings::ifp_parameter == IFPParameter::None) {
32✔
NEW
232
          settings::ifp_parameter = IFPParameter::BetaEffective;
×
233
        } else if (settings::ifp_parameter == IFPParameter::GenerationTime) {
32✔
234
          settings::ifp_parameter = IFPParameter::Both;
16✔
235
        }
236
        break;
32✔
237
      }
238
    }
239
  }
240

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

295
  // Check for a tally derivative.
296
  if (check_for_node(node, "derivative")) {
24,041✔
297
    int deriv_id = std::stoi(get_node_value(node, "derivative"));
320✔
298

299
    // Find the derivative with the given id, and store it's index.
300
    auto it = model::tally_deriv_map.find(deriv_id);
320✔
301
    if (it == model::tally_deriv_map.end()) {
320✔
302
      fatal_error(fmt::format(
×
303
        "Could not find derivative {} specified on tally {}", deriv_id, id_));
×
304
    }
305

306
    deriv_ = it->second;
320✔
307

308
    // Only analog or collision estimators are supported for differential
309
    // tallies.
310
    if (estimator_ == TallyEstimator::TRACKLENGTH) {
320✔
311
      estimator_ = TallyEstimator::COLLISION;
240✔
312
    }
313

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

334
  // If settings.xml trigger is turned on, create tally triggers
335
  if (settings::trigger_on) {
24,041✔
336
    this->init_triggers(node);
183✔
337
  }
338

339
  // =======================================================================
340
  // SET TALLY ESTIMATOR
341

342
  // Check if user specified estimator
343
  if (check_for_node(node, "estimator")) {
24,041✔
344
    std::string est = get_node_value(node, "estimator");
19,723✔
345
    if (est == "analog") {
19,723✔
346
      estimator_ = TallyEstimator::ANALOG;
8,128✔
347
    } else if (est == "tracklength" || est == "track-length" ||
12,401✔
348
               est == "pathlength" || est == "path-length") {
12,401✔
349
      // If the estimator was set to an analog estimator, this means the
350
      // tally needs post-collision information
351
      if (estimator_ == TallyEstimator::ANALOG ||
11,192✔
352
          estimator_ == TallyEstimator::COLLISION) {
11,192✔
353
        throw std::runtime_error {fmt::format("Cannot use track-length "
×
354
                                              "estimator for tally {}",
355
          id_)};
×
356
      }
357

358
      // Set estimator to track-length estimator
359
      estimator_ = TallyEstimator::TRACKLENGTH;
11,192✔
360

361
    } else if (est == "collision") {
403✔
362
      // If the estimator was set to an analog estimator, this means the
363
      // tally needs post-collision information
364
      if (estimator_ == TallyEstimator::ANALOG) {
403✔
365
        throw std::runtime_error {fmt::format("Cannot use collision estimator "
×
366
                                              "for tally ",
367
          id_)};
×
368
      }
369

370
      // Set estimator to collision estimator
371
      estimator_ = TallyEstimator::COLLISION;
403✔
372

373
    } else {
374
      throw std::runtime_error {
×
375
        fmt::format("Invalid estimator '{}' on tally {}", est, id_)};
×
376
    }
377
  }
19,723✔
378

379
#ifdef LIBMESH
380
  // ensure a tracklength tally isn't used with a libMesh filter
381
  for (auto i : this->filters_) {
13,571✔
382
    auto df = dynamic_cast<MeshFilter*>(model::tally_filters[i].get());
9,063✔
383
    if (df) {
9,063✔
384
      auto lm = dynamic_cast<LibMesh*>(model::meshes[df->mesh()].get());
1,076✔
385
      if (lm && estimator_ == TallyEstimator::TRACKLENGTH) {
1,076✔
386
        fatal_error("A tracklength estimator cannot be used with "
387
                    "an unstructured LibMesh tally.");
388
      }
389
    }
390
  }
391
#endif
392
}
24,041✔
393

394
Tally::~Tally()
25,151✔
395
{
396
  model::tally_map.erase(id_);
25,151✔
397
}
25,151✔
398

399
Tally* Tally::create(int32_t id)
108✔
400
{
401
  model::tallies.push_back(make_unique<Tally>(id));
108✔
402
  return model::tallies.back().get();
108✔
403
}
404

405
void Tally::set_id(int32_t id)
26,170✔
406
{
407
  assert(id >= 0 || id == C_NONE);
21,278✔
408

409
  // Clear entry in tally map if an ID was already assigned before
410
  if (id_ != C_NONE) {
26,170✔
411
    model::tally_map.erase(id_);
1,001✔
412
    id_ = C_NONE;
1,001✔
413
  }
414

415
  // Make sure no other tally has the same ID
416
  if (model::tally_map.find(id) != model::tally_map.end()) {
26,170✔
417
    throw std::runtime_error {
×
418
      fmt::format("Two tallies have the same ID: {}", id)};
×
419
  }
420

421
  // If no ID specified, auto-assign next ID in sequence
422
  if (id == C_NONE) {
26,170✔
423
    id = 0;
1,110✔
424
    for (const auto& t : model::tallies) {
3,532✔
425
      id = std::max(id, t->id_);
2,422✔
426
    }
427
    ++id;
1,110✔
428
  }
429

430
  // Update ID and entry in tally map
431
  id_ = id;
26,170✔
432
  model::tally_map[id] = index_;
26,170✔
433
}
26,170✔
434

435
std::vector<FilterType> Tally::filter_types() const
2,394✔
436
{
437
  std::vector<FilterType> filter_types;
2,394✔
438
  for (auto idx : this->filters())
9,532✔
439
    filter_types.push_back(model::tally_filters[idx]->type());
7,138✔
440
  return filter_types;
2,394✔
441
}
×
442

443
std::unordered_map<FilterType, int32_t> Tally::filter_indices() const
2,394✔
444
{
445
  std::unordered_map<FilterType, int32_t> filter_indices;
2,394✔
446
  for (int i = 0; i < this->filters().size(); i++) {
9,532✔
447
    const auto& f = model::tally_filters[this->filters(i)];
7,138✔
448

449
    filter_indices[f->type()] = i;
7,138✔
450
  }
451
  return filter_indices;
2,394✔
452
}
×
453

454
bool Tally::has_filter(FilterType filter_type) const
7,182✔
455
{
456
  for (auto idx : this->filters()) {
18,965✔
457
    if (model::tally_filters[idx]->type() == filter_type)
18,899✔
458
      return true;
7,116✔
459
  }
460
  return false;
66✔
461
}
462

463
void Tally::set_filters(span<Filter*> filters)
26,275✔
464
{
465
  // Clear old data.
466
  filters_.clear();
26,275✔
467
  strides_.clear();
26,275✔
468

469
  // Copy in the given filter indices.
470
  auto n = filters.size();
26,275✔
471
  filters_.reserve(n);
26,275✔
472

473
  for (auto* filter : filters) {
76,178✔
474
    add_filter(filter);
49,903✔
475
  }
476
}
26,275✔
477

478
void Tally::add_filter(Filter* filter)
50,179✔
479
{
480
  int32_t filter_idx = model::filter_map.at(filter->id());
50,179✔
481
  // if this filter is already present, do nothing and return
482
  if (std::find(filters_.begin(), filters_.end(), filter_idx) != filters_.end())
50,179✔
483
    return;
22✔
484

485
  // Keep track of indices for special filters
486
  if (filter->type() == FilterType::ENERGY_OUT) {
50,157✔
487
    energyout_filter_ = filters_.size();
4,712✔
488
  } else if (filter->type() == FilterType::DELAYED_GROUP) {
45,445✔
489
    delayedgroup_filter_ = filters_.size();
800✔
490
  } else if (filter->type() == FilterType::CELL) {
44,645✔
491
    cell_filter_ = filters_.size();
1,090✔
492
  } else if (filter->type() == FilterType::ENERGY) {
43,555✔
493
    energy_filter_ = filters_.size();
17,055✔
494
  }
495
  filters_.push_back(filter_idx);
50,157✔
496
}
497

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

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

518
  auto scores = get_node_array<std::string>(node, "scores");
24,059✔
519
  set_scores(scores);
24,059✔
520
}
24,059✔
521

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

528
  // Check for the presence of certain restrictive filters.
529
  bool energyout_present = energyout_filter_ != C_NONE;
25,169✔
530
  bool legendre_present = false;
25,169✔
531
  bool cell_present = false;
25,169✔
532
  bool cellfrom_present = false;
25,169✔
533
  bool surface_present = false;
25,169✔
534
  bool meshsurface_present = false;
25,169✔
535
  bool non_cell_energy_present = false;
25,169✔
536
  for (auto i_filt : filters_) {
74,577✔
537
    const auto* filt {model::tally_filters[i_filt].get()};
49,408✔
538
    // Checking for only cell and energy filters for pulse-height tally
539
    if (!(filt->type() == FilterType::CELL ||
97,770✔
540
          filt->type() == FilterType::ENERGY)) {
48,362✔
541
      non_cell_energy_present = true;
31,454✔
542
    }
543
    if (filt->type() == FilterType::LEGENDRE) {
49,408✔
544
      legendre_present = true;
2,029✔
545
    } else if (filt->type() == FilterType::CELLFROM) {
47,379✔
546
      cellfrom_present = true;
304✔
547
    } else if (filt->type() == FilterType::CELL) {
47,075✔
548
      cell_present = true;
1,046✔
549
    } else if (filt->type() == FilterType::SURFACE) {
46,029✔
550
      surface_present = true;
193✔
551
    } else if (filt->type() == FilterType::MESH_SURFACE) {
45,836✔
552
      meshsurface_present = true;
341✔
553
    }
554
  }
555

556
  // Iterate over the given scores.
557
  for (auto score_str : scores) {
60,072✔
558
    // Make sure a delayed group filter wasn't used with an incompatible score.
559
    if (delayedgroup_filter_ != C_NONE) {
34,903✔
560
      if (score_str != "delayed-nu-fission" && score_str != "decay-rate")
816✔
561
        fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
×
562
    }
563

564
    // Determine integer code for score
565
    int score = reaction_type(score_str);
34,903✔
566

567
    switch (score) {
34,903✔
568
    case SCORE_FLUX:
10,627✔
569
      if (!nuclides_.empty())
10,627✔
570
        if (!(nuclides_.size() == 1 && nuclides_[0] == -1))
10,627✔
571
          fatal_error("Cannot tally flux for an individual nuclide.");
×
572
      if (energyout_present)
10,627✔
573
        fatal_error("Cannot tally flux with an outgoing energy filter.");
×
574
      break;
10,627✔
575

576
    case SCORE_TOTAL:
6,608✔
577
    case SCORE_ABSORPTION:
578
    case SCORE_FISSION:
579
      if (energyout_present)
6,608✔
580
        fatal_error("Cannot tally " + score_str +
×
581
                    " reaction rate with an "
582
                    "outgoing energy filter");
583
      break;
6,608✔
584

585
    case SCORE_SCATTER:
3,868✔
586
      if (legendre_present)
3,868✔
587
        estimator_ = TallyEstimator::ANALOG;
1,346✔
588
    case SCORE_NU_FISSION:
589
    case SCORE_DELAYED_NU_FISSION:
590
    case SCORE_PROMPT_NU_FISSION:
591
      if (energyout_present)
8,589✔
592
        estimator_ = TallyEstimator::ANALOG;
3,566✔
593
      break;
8,589✔
594

595
    case SCORE_NU_SCATTER:
2,083✔
596
      if (settings::run_CE) {
2,083✔
597
        estimator_ = TallyEstimator::ANALOG;
1,955✔
598
      } else {
599
        if (energyout_present || legendre_present)
128✔
600
          estimator_ = TallyEstimator::ANALOG;
64✔
601
      }
602
      break;
2,083✔
603

604
    case SCORE_CURRENT:
566✔
605
      // Check which type of current is desired: mesh or surface currents.
606
      if (surface_present || cell_present || cellfrom_present) {
566✔
607
        if (meshsurface_present)
225✔
608
          fatal_error("Cannot tally mesh surface currents in the same tally as "
×
609
                      "normal surface currents");
610
        type_ = TallyType::SURFACE;
225✔
611
        estimator_ = TallyEstimator::ANALOG;
225✔
612
      } else if (meshsurface_present) {
341✔
613
        type_ = TallyType::MESH_SURFACE;
341✔
614
      } else {
615
        fatal_error("Cannot tally currents without surface type filters");
×
616
      }
617
      break;
566✔
618

619
    case HEATING:
310✔
620
      if (settings::photon_transport)
310✔
621
        estimator_ = TallyEstimator::COLLISION;
150✔
622
      break;
310✔
623

624
    case SCORE_PULSE_HEIGHT:
16✔
625
      if (non_cell_energy_present) {
16✔
626
        fatal_error("Pulse-height tallies are not compatible with filters "
×
627
                    "other than CellFilter and EnergyFilter");
628
      }
629
      type_ = TallyType::PULSE_HEIGHT;
16✔
630

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

648
    case SCORE_IFP_TIME_NUM:
102✔
649
    case SCORE_IFP_BETA_NUM:
650
    case SCORE_IFP_DENOM:
651
      estimator_ = TallyEstimator::COLLISION;
102✔
652
      break;
102✔
653
    }
654

655
    scores_.push_back(score);
34,903✔
656
  }
34,903✔
657

658
  // Make sure that no duplicate scores exist.
659
  for (auto it1 = scores_.begin(); it1 != scores_.end(); ++it1) {
60,072✔
660
    for (auto it2 = it1 + 1; it2 != scores_.end(); ++it2) {
82,095✔
661
      if (*it1 == *it2)
47,192✔
662
        fatal_error(
×
663
          fmt::format("Duplicate score of type \"{}\" found in tally {}",
×
664
            reaction_name(*it1), id_));
×
665
    }
666
  }
667

668
  // Make sure all scores are compatible with multigroup mode.
669
  if (!settings::run_CE) {
25,169✔
670
    for (auto sc : scores_)
6,156✔
671
      if (sc > 0)
4,550✔
672
        fatal_error("Cannot tally " + reaction_name(sc) +
×
673
                    " reaction rate "
674
                    "in multi-group mode");
675
  }
676

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

688
void Tally::set_nuclides(pugi::xml_node node)
24,059✔
689
{
690
  nuclides_.clear();
24,059✔
691

692
  // By default, we tally just the total material rates.
693
  if (!check_for_node(node, "nuclides")) {
24,059✔
694
    nuclides_.push_back(-1);
5,784✔
695
    return;
5,784✔
696
  }
697

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

704
void Tally::set_nuclides(const vector<std::string>& nuclides)
18,275✔
705
{
706
  nuclides_.clear();
18,275✔
707

708
  for (const auto& nuc : nuclides) {
47,429✔
709
    if (nuc == "total") {
29,154✔
710
      nuclides_.push_back(-1);
14,763✔
711
    } else {
712
      auto search = data::nuclide_map.find(nuc);
14,391✔
713
      if (search == data::nuclide_map.end()) {
14,391✔
714
        int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0);
143✔
715
        if (err < 0)
143✔
716
          throw std::runtime_error {openmc_err_msg};
×
717
      }
718
      nuclides_.push_back(data::nuclide_map.at(nuc));
14,391✔
719
    }
720
  }
721
}
18,275✔
722

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

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

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

763
    // Read the trigger scores.
764
    vector<std::string> trigger_scores;
71✔
765
    if (check_for_node(trigger_node, "scores")) {
71✔
766
      trigger_scores = get_node_array<std::string>(trigger_node, "scores");
71✔
767
    } else {
768
      trigger_scores.push_back("all");
×
769
    }
770

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

796
void Tally::init_results()
25,604✔
797
{
798
  int n_scores = scores_.size() * nuclides_.size();
25,604✔
799
  results_ = xt::empty<double>({n_filter_bins_, n_scores, 3});
25,604✔
800
}
25,604✔
801

802
void Tally::reset()
54,539✔
803
{
804
  n_realizations_ = 0;
54,539✔
805
  if (results_.size() != 0) {
54,539✔
806
    xt::view(results_, xt::all()) = 0.0;
53,801✔
807
  }
808
}
54,539✔
809

810
void Tally::accumulate()
200,753✔
811
{
812
  // Increment number of realizations
813
  n_realizations_ += settings::reduce_tallies ? 1 : mpi::n_procs;
200,753✔
814

815
  if (mpi::master || !settings::reduce_tallies) {
200,753✔
816
    // Calculate total source strength for normalization
817
    double total_source = 0.0;
163,038✔
818
    if (settings::run_mode == RunMode::FIXED_SOURCE &&
163,038✔
819
        !settings::uniform_source_sampling) {
42,327✔
820
      for (const auto& s : model::external_sources) {
87,206✔
821
        total_source += s->strength();
44,901✔
822
      }
823
    } else {
42,305✔
824
      total_source = 1.0;
120,733✔
825
    }
826

827
    // Account for number of source particles in normalization
828
    double norm =
163,038✔
829
      total_source / (settings::n_particles * settings::gen_per_batch);
163,038✔
830

831
    if (settings::solver_type == SolverType::RANDOM_RAY) {
163,038✔
832
      norm = 1.0;
11,220✔
833
    }
834

835
// Accumulate each result
836
#pragma omp parallel for
89,108✔
837
    for (int i = 0; i < results_.shape()[0]; ++i) {
44,305,380✔
838
      for (int j = 0; j < results_.shape()[1]; ++j) {
106,722,595✔
839
        double val = results_(i, j, TallyResult::VALUE) * norm;
62,491,145✔
840
        results_(i, j, TallyResult::VALUE) = 0.0;
62,491,145✔
841
        results_(i, j, TallyResult::SUM) += val;
62,491,145✔
842
        results_(i, j, TallyResult::SUM_SQ) += val * val;
62,491,145✔
843
      }
844
    }
845
  }
846
}
200,753✔
847

848
int Tally::score_index(const std::string& score) const
2,394✔
849
{
850
  for (int i = 0; i < scores_.size(); i++) {
2,394✔
851
    if (this->score_name(i) == score)
2,394✔
852
      return i;
2,394✔
853
  }
854
  return -1;
×
855
}
856

857
xt::xarray<double> Tally::get_reshaped_data() const
×
858
{
859
  std::vector<uint64_t> shape;
×
860
  for (auto f : filters()) {
×
861
    shape.push_back(model::tally_filters[f]->n_bins());
×
862
  }
863

864
  // add number of scores and nuclides to tally
865
  shape.push_back(results_.shape()[1]);
×
866
  shape.push_back(results_.shape()[2]);
×
867

868
  xt::xarray<double> reshaped_results = results_;
×
869
  reshaped_results.reshape(shape);
×
870
  return reshaped_results;
×
871
}
872

873
std::string Tally::score_name(int score_idx) const
2,424✔
874
{
875
  if (score_idx < 0 || score_idx >= scores_.size()) {
2,424✔
876
    fatal_error("Index in scores array is out of bounds.");
×
877
  }
878
  return reaction_name(scores_[score_idx]);
2,424✔
879
}
880

881
std::vector<std::string> Tally::scores() const
×
882
{
883
  std::vector<std::string> score_names;
×
884
  for (int score : scores_)
×
885
    score_names.push_back(reaction_name(score));
×
886
  return score_names;
×
887
}
×
888

889
std::string Tally::nuclide_name(int nuclide_idx) const
30✔
890
{
891
  if (nuclide_idx < 0 || nuclide_idx >= nuclides_.size()) {
30✔
892
    fatal_error("Index in nuclides array is out of bounds");
×
893
  }
894

895
  int nuclide = nuclides_.at(nuclide_idx);
30✔
896
  if (nuclide == -1) {
30✔
897
    return "total";
30✔
898
  }
899
  return data::nuclides.at(nuclide)->name_;
×
900
}
901

902
//==============================================================================
903
// Non-member functions
904
//==============================================================================
905

906
void read_tallies_xml()
1,315✔
907
{
908
  // Check if tallies.xml exists. If not, just return since it is optional
909
  std::string filename = settings::path_input + "tallies.xml";
1,315✔
910
  if (!file_exists(filename))
1,315✔
911
    return;
854✔
912

913
  write_message("Reading tallies XML file...", 5);
461✔
914

915
  // Parse tallies.xml file
916
  pugi::xml_document doc;
461✔
917
  doc.load_file(filename.c_str());
461✔
918
  pugi::xml_node root = doc.document_element();
461✔
919

920
  read_tallies_xml(root);
461✔
921
}
1,315✔
922

923
void read_tallies_xml(pugi::xml_node root)
3,679✔
924
{
925
  // Check for <assume_separate> setting
926
  if (check_for_node(root, "assume_separate")) {
3,679✔
927
    settings::assume_separate = get_node_value_bool(root, "assume_separate");
16✔
928
  }
929

930
  // Check for user meshes and allocate
931
  read_meshes(root);
3,679✔
932

933
  // We only need the mesh info for plotting
934
  if (settings::run_mode == RunMode::PLOTTING)
3,679✔
935
    return;
33✔
936

937
  // Read data for tally derivatives
938
  read_tally_derivatives(root);
3,646✔
939

940
  // ==========================================================================
941
  // READ FILTER DATA
942

943
  // Check for user filters and allocate
944
  for (auto node_filt : root.children("filter")) {
11,280✔
945
    auto f = Filter::create(node_filt);
7,634✔
946
  }
947

948
  // ==========================================================================
949
  // READ TALLY DATA
950

951
  // Check for user tallies
952
  int n = 0;
3,646✔
953
  for (auto node : root.children("tally"))
27,705✔
954
    ++n;
24,059✔
955
  if (n == 0 && mpi::master) {
3,646✔
956
    warning("No tallies present in tallies.xml file.");
×
957
  }
958

959
  for (auto node_tal : root.children("tally")) {
27,687✔
960
    model::tallies.push_back(make_unique<Tally>(node_tal));
24,059✔
961
  }
962
}
963

964
#ifdef OPENMC_MPI
965
void reduce_tally_results()
32,680✔
966
{
967
  // Don't reduce tally is no_reduce option is on
968
  if (settings::reduce_tallies) {
32,680✔
969
    for (int i_tally : model::active_tallies) {
97,890✔
970
      // Skip any tallies that are not active
971
      auto& tally {model::tallies[i_tally]};
65,210✔
972

973
      // Get view of accumulated tally values
974
      auto values_view = xt::view(tally->results_, xt::all(), xt::all(),
65,210✔
975
        static_cast<int>(TallyResult::VALUE));
130,420✔
976

977
      // Make copy of tally values in contiguous array
978
      xt::xtensor<double, 2> values = values_view;
65,210✔
979
      xt::xtensor<double, 2> values_reduced = xt::empty_like(values);
65,210✔
980

981
      // Reduce contiguous set of tally results
982
      MPI_Reduce(values.data(), values_reduced.data(), values.size(),
65,210✔
983
        MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
984

985
      // Transfer values on master and reset on other ranks
986
      if (mpi::master) {
65,210✔
987
        values_view = values_reduced;
32,595✔
988
      } else {
989
        values_view = 0.0;
32,615✔
990
      }
991
    }
65,210✔
992
  }
993

994
  // Note that global tallies are *always* reduced even when no_reduce option is
995
  // on.
996

997
  // Get view of global tally values
998
  auto& gt = simulation::global_tallies;
32,680✔
999
  auto gt_values_view =
1000
    xt::view(gt, xt::all(), static_cast<int>(TallyResult::VALUE));
32,680✔
1001

1002
  // Make copy of values in contiguous array
1003
  xt::xtensor<double, 1> gt_values = gt_values_view;
32,680✔
1004
  xt::xtensor<double, 1> gt_values_reduced = xt::empty_like(gt_values);
32,680✔
1005

1006
  // Reduce contiguous data
1007
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
32,680✔
1008
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1009

1010
  // Transfer values on master and reset on other ranks
1011
  if (mpi::master) {
32,680✔
1012
    gt_values_view = gt_values_reduced;
16,335✔
1013
  } else {
1014
    gt_values_view = 0.0;
16,345✔
1015
  }
1016

1017
  // We also need to determine the total starting weight of particles from the
1018
  // last realization
1019
  double weight_reduced;
1020
  MPI_Reduce(&simulation::total_weight, &weight_reduced, 1, MPI_DOUBLE, MPI_SUM,
32,680✔
1021
    0, mpi::intracomm);
1022
  if (mpi::master)
32,680✔
1023
    simulation::total_weight = weight_reduced;
16,335✔
1024
}
32,680✔
1025
#endif
1026

1027
void accumulate_tallies()
109,477✔
1028
{
1029
#ifdef OPENMC_MPI
1030
  // Combine tally results onto master process
1031
  if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) {
61,684✔
1032
    reduce_tally_results();
32,680✔
1033
  }
1034
#endif
1035

1036
  // Increase number of realizations (only used for global tallies)
1037
  simulation::n_realizations += 1;
109,477✔
1038

1039
  // Accumulate on master only unless run is not reduced then do it on all
1040
  if (mpi::master || !settings::reduce_tallies) {
109,477✔
1041
    auto& gt = simulation::global_tallies;
87,882✔
1042

1043
    if (settings::run_mode == RunMode::EIGENVALUE) {
87,882✔
1044
      if (simulation::current_batch > settings::n_inactive) {
61,700✔
1045
        // Accumulate products of different estimators of k
1046
        double k_col = gt(GlobalTally::K_COLLISION, TallyResult::VALUE) /
47,470✔
1047
                       simulation::total_weight;
47,470✔
1048
        double k_abs = gt(GlobalTally::K_ABSORPTION, TallyResult::VALUE) /
47,470✔
1049
                       simulation::total_weight;
47,470✔
1050
        double k_tra = gt(GlobalTally::K_TRACKLENGTH, TallyResult::VALUE) /
47,470✔
1051
                       simulation::total_weight;
47,470✔
1052
        simulation::k_col_abs += k_col * k_abs;
47,470✔
1053
        simulation::k_col_tra += k_col * k_tra;
47,470✔
1054
        simulation::k_abs_tra += k_abs * k_tra;
47,470✔
1055
      }
1056
    }
1057

1058
    // Accumulate results for global tallies
1059
    for (int i = 0; i < N_GLOBAL_TALLIES; ++i) {
439,410✔
1060
      double val = gt(i, TallyResult::VALUE) / simulation::total_weight;
351,528✔
1061
      gt(i, TallyResult::VALUE) = 0.0;
351,528✔
1062
      gt(i, TallyResult::SUM) += val;
351,528✔
1063
      gt(i, TallyResult::SUM_SQ) += val * val;
351,528✔
1064
    }
1065
  }
1066

1067
  // Accumulate results for each tally
1068
  for (int i_tally : model::active_tallies) {
310,230✔
1069
    auto& tally {model::tallies[i_tally]};
200,753✔
1070
    tally->accumulate();
200,753✔
1071
  }
1072
}
109,477✔
1073

1074
void setup_active_tallies()
109,490✔
1075
{
1076
  model::active_tallies.clear();
109,490✔
1077
  model::active_analog_tallies.clear();
109,490✔
1078
  model::active_tracklength_tallies.clear();
109,490✔
1079
  model::active_collision_tallies.clear();
109,490✔
1080
  model::active_meshsurf_tallies.clear();
109,490✔
1081
  model::active_surface_tallies.clear();
109,490✔
1082
  model::active_pulse_height_tallies.clear();
109,490✔
1083

1084
  for (auto i = 0; i < model::tallies.size(); ++i) {
438,967✔
1085
    const auto& tally {*model::tallies[i]};
329,477✔
1086

1087
    if (tally.active_) {
329,477✔
1088
      model::active_tallies.push_back(i);
200,753✔
1089
      switch (tally.type_) {
200,753✔
1090

1091
      case TallyType::VOLUME:
194,556✔
1092
        switch (tally.estimator_) {
194,556✔
1093
        case TallyEstimator::ANALOG:
79,438✔
1094
          model::active_analog_tallies.push_back(i);
79,438✔
1095
          break;
79,438✔
1096
        case TallyEstimator::TRACKLENGTH:
110,261✔
1097
          model::active_tracklength_tallies.push_back(i);
110,261✔
1098
          break;
110,261✔
1099
        case TallyEstimator::COLLISION:
4,857✔
1100
          model::active_collision_tallies.push_back(i);
4,857✔
1101
        }
1102
        break;
194,556✔
1103

1104
      case TallyType::MESH_SURFACE:
4,015✔
1105
        model::active_meshsurf_tallies.push_back(i);
4,015✔
1106
        break;
4,015✔
1107

1108
      case TallyType::SURFACE:
2,102✔
1109
        model::active_surface_tallies.push_back(i);
2,102✔
1110
        break;
2,102✔
1111

1112
      case TallyType::PULSE_HEIGHT:
80✔
1113
        model::active_pulse_height_tallies.push_back(i);
80✔
1114
        break;
80✔
1115
      }
1116
    }
1117
  }
1118
}
109,490✔
1119

1120
void free_memory_tally()
6,624✔
1121
{
1122
  model::tally_derivs.clear();
6,624✔
1123
  model::tally_deriv_map.clear();
6,624✔
1124

1125
  model::tally_filters.clear();
6,624✔
1126
  model::filter_map.clear();
6,624✔
1127

1128
  model::tallies.clear();
6,624✔
1129

1130
  model::active_tallies.clear();
6,624✔
1131
  model::active_analog_tallies.clear();
6,624✔
1132
  model::active_tracklength_tallies.clear();
6,624✔
1133
  model::active_collision_tallies.clear();
6,624✔
1134
  model::active_meshsurf_tallies.clear();
6,624✔
1135
  model::active_surface_tallies.clear();
6,624✔
1136
  model::active_pulse_height_tallies.clear();
6,624✔
1137

1138
  model::tally_map.clear();
6,624✔
1139
}
6,624✔
1140

1141
//==============================================================================
1142
// C-API functions
1143
//==============================================================================
1144

1145
extern "C" int openmc_extend_tallies(
1,001✔
1146
  int32_t n, int32_t* index_start, int32_t* index_end)
1147
{
1148
  if (index_start)
1,001✔
1149
    *index_start = model::tallies.size();
1,001✔
1150
  if (index_end)
1,001✔
1151
    *index_end = model::tallies.size() + n - 1;
×
1152
  for (int i = 0; i < n; ++i) {
2,002✔
1153
    model::tallies.push_back(make_unique<Tally>(-1));
1,001✔
1154
  }
1155
  return 0;
1,001✔
1156
}
1157

1158
extern "C" int openmc_get_tally_index(int32_t id, int32_t* index)
20,856✔
1159
{
1160
  auto it = model::tally_map.find(id);
20,856✔
1161
  if (it == model::tally_map.end()) {
20,856✔
1162
    set_errmsg(fmt::format("No tally exists with ID={}.", id));
22✔
1163
    return OPENMC_E_INVALID_ID;
22✔
1164
  }
1165

1166
  *index = it->second;
20,834✔
1167
  return 0;
20,834✔
1168
}
1169

1170
extern "C" void openmc_get_tally_next_id(int32_t* id)
×
1171
{
1172
  int32_t largest_tally_id = 0;
×
1173
  for (const auto& t : model::tallies) {
×
1174
    largest_tally_id = std::max(largest_tally_id, t->id_);
×
1175
  }
1176
  *id = largest_tally_id + 1;
×
1177
}
1178

1179
extern "C" int openmc_tally_get_estimator(int32_t index, int* estimator)
×
1180
{
1181
  if (index < 0 || index >= model::tallies.size()) {
×
1182
    set_errmsg("Index in tallies array is out of bounds.");
×
1183
    return OPENMC_E_OUT_OF_BOUNDS;
×
1184
  }
1185

1186
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1187
  return 0;
×
1188
}
1189

1190
extern "C" int openmc_tally_set_estimator(int32_t index, const char* estimator)
660✔
1191
{
1192
  if (index < 0 || index >= model::tallies.size()) {
660✔
1193
    set_errmsg("Index in tallies array is out of bounds.");
×
1194
    return OPENMC_E_OUT_OF_BOUNDS;
×
1195
  }
1196

1197
  auto& t {model::tallies[index]};
660✔
1198

1199
  std::string est = estimator;
660✔
1200
  if (est == "analog") {
660✔
1201
    t->estimator_ = TallyEstimator::ANALOG;
660✔
1202
  } else if (est == "collision") {
×
1203
    t->estimator_ = TallyEstimator::COLLISION;
×
1204
  } else if (est == "tracklength") {
×
1205
    t->estimator_ = TallyEstimator::TRACKLENGTH;
×
1206
  } else {
1207
    set_errmsg("Unknown tally estimator: " + est);
×
1208
    return OPENMC_E_INVALID_ARGUMENT;
×
1209
  }
1210
  return 0;
660✔
1211
}
660✔
1212

1213
extern "C" int openmc_tally_get_id(int32_t index, int32_t* id)
26,972✔
1214
{
1215
  if (index < 0 || index >= model::tallies.size()) {
26,972✔
1216
    set_errmsg("Index in tallies array is out of bounds.");
×
1217
    return OPENMC_E_OUT_OF_BOUNDS;
×
1218
  }
1219

1220
  *id = model::tallies[index]->id_;
26,972✔
1221
  return 0;
26,972✔
1222
}
1223

1224
extern "C" int openmc_tally_set_id(int32_t index, int32_t id)
1,001✔
1225
{
1226
  if (index < 0 || index >= model::tallies.size()) {
1,001✔
1227
    set_errmsg("Index in tallies array is out of bounds.");
×
1228
    return OPENMC_E_OUT_OF_BOUNDS;
×
1229
  }
1230

1231
  model::tallies[index]->set_id(id);
1,001✔
1232
  return 0;
1,001✔
1233
}
1234

1235
extern "C" int openmc_tally_get_type(int32_t index, int32_t* type)
11✔
1236
{
1237
  if (index < 0 || index >= model::tallies.size()) {
11✔
1238
    set_errmsg("Index in tallies array is out of bounds.");
×
1239
    return OPENMC_E_OUT_OF_BOUNDS;
×
1240
  }
1241
  *type = static_cast<int>(model::tallies[index]->type_);
11✔
1242

1243
  return 0;
11✔
1244
}
1245

1246
extern "C" int openmc_tally_set_type(int32_t index, const char* type)
660✔
1247
{
1248
  if (index < 0 || index >= model::tallies.size()) {
660✔
1249
    set_errmsg("Index in tallies array is out of bounds.");
×
1250
    return OPENMC_E_OUT_OF_BOUNDS;
×
1251
  }
1252
  if (strcmp(type, "volume") == 0) {
660✔
1253
    model::tallies[index]->type_ = TallyType::VOLUME;
495✔
1254
  } else if (strcmp(type, "mesh-surface") == 0) {
165✔
1255
    model::tallies[index]->type_ = TallyType::MESH_SURFACE;
165✔
1256
  } else if (strcmp(type, "surface") == 0) {
×
1257
    model::tallies[index]->type_ = TallyType::SURFACE;
×
1258
  } else if (strcmp(type, "pulse-height") == 0) {
×
1259
    model::tallies[index]->type_ = TallyType::PULSE_HEIGHT;
×
1260
  } else {
1261
    set_errmsg(fmt::format("Unknown tally type: {}", type));
×
1262
    return OPENMC_E_INVALID_ARGUMENT;
×
1263
  }
1264

1265
  return 0;
660✔
1266
}
1267

1268
extern "C" int openmc_tally_get_active(int32_t index, bool* active)
22✔
1269
{
1270
  if (index < 0 || index >= model::tallies.size()) {
22✔
1271
    set_errmsg("Index in tallies array is out of bounds.");
×
1272
    return OPENMC_E_OUT_OF_BOUNDS;
×
1273
  }
1274
  *active = model::tallies[index]->active_;
22✔
1275

1276
  return 0;
22✔
1277
}
1278

1279
extern "C" int openmc_tally_set_active(int32_t index, bool active)
671✔
1280
{
1281
  if (index < 0 || index >= model::tallies.size()) {
671✔
1282
    set_errmsg("Index in tallies array is out of bounds.");
×
1283
    return OPENMC_E_OUT_OF_BOUNDS;
×
1284
  }
1285
  model::tallies[index]->active_ = active;
671✔
1286

1287
  return 0;
671✔
1288
}
1289

1290
extern "C" int openmc_tally_get_writable(int32_t index, bool* writable)
22✔
1291
{
1292
  if (index < 0 || index >= model::tallies.size()) {
22✔
1293
    set_errmsg("Index in tallies array is out of bounds.");
×
1294
    return OPENMC_E_OUT_OF_BOUNDS;
×
1295
  }
1296
  *writable = model::tallies[index]->writable();
22✔
1297

1298
  return 0;
22✔
1299
}
1300

1301
extern "C" int openmc_tally_set_writable(int32_t index, bool writable)
341✔
1302
{
1303
  if (index < 0 || index >= model::tallies.size()) {
341✔
1304
    set_errmsg("Index in tallies array is out of bounds.");
×
1305
    return OPENMC_E_OUT_OF_BOUNDS;
×
1306
  }
1307
  model::tallies[index]->set_writable(writable);
341✔
1308

1309
  return 0;
341✔
1310
}
1311

1312
extern "C" int openmc_tally_get_multiply_density(int32_t index, bool* value)
22✔
1313
{
1314
  if (index < 0 || index >= model::tallies.size()) {
22✔
1315
    set_errmsg("Index in tallies array is out of bounds.");
×
1316
    return OPENMC_E_OUT_OF_BOUNDS;
×
1317
  }
1318
  *value = model::tallies[index]->multiply_density();
22✔
1319

1320
  return 0;
22✔
1321
}
1322

1323
extern "C" int openmc_tally_set_multiply_density(int32_t index, bool value)
198✔
1324
{
1325
  if (index < 0 || index >= model::tallies.size()) {
198✔
1326
    set_errmsg("Index in tallies array is out of bounds.");
×
1327
    return OPENMC_E_OUT_OF_BOUNDS;
×
1328
  }
1329
  model::tallies[index]->set_multiply_density(value);
198✔
1330

1331
  return 0;
198✔
1332
}
1333

1334
extern "C" int openmc_tally_get_scores(int32_t index, int** scores, int* n)
187✔
1335
{
1336
  if (index < 0 || index >= model::tallies.size()) {
187✔
1337
    set_errmsg("Index in tallies array is out of bounds.");
×
1338
    return OPENMC_E_OUT_OF_BOUNDS;
×
1339
  }
1340

1341
  *scores = model::tallies[index]->scores_.data();
187✔
1342
  *n = model::tallies[index]->scores_.size();
187✔
1343
  return 0;
187✔
1344
}
1345

1346
extern "C" int openmc_tally_set_scores(
1,012✔
1347
  int32_t index, int n, const char** scores)
1348
{
1349
  if (index < 0 || index >= model::tallies.size()) {
1,012✔
1350
    set_errmsg("Index in tallies array is out of bounds.");
×
1351
    return OPENMC_E_OUT_OF_BOUNDS;
×
1352
  }
1353

1354
  vector<std::string> scores_str(scores, scores + n);
1,012✔
1355
  try {
1356
    model::tallies[index]->set_scores(scores_str);
1,012✔
1357
  } catch (const std::invalid_argument& ex) {
×
1358
    set_errmsg(ex.what());
×
1359
    return OPENMC_E_INVALID_ARGUMENT;
×
1360
  }
×
1361

1362
  return 0;
1,012✔
1363
}
1,012✔
1364

1365
extern "C" int openmc_tally_get_nuclides(int32_t index, int** nuclides, int* n)
231✔
1366
{
1367
  // Make sure the index fits in the array bounds.
1368
  if (index < 0 || index >= model::tallies.size()) {
231✔
1369
    set_errmsg("Index in tallies array is out of bounds.");
×
1370
    return OPENMC_E_OUT_OF_BOUNDS;
×
1371
  }
1372

1373
  *n = model::tallies[index]->nuclides_.size();
231✔
1374
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1375

1376
  return 0;
231✔
1377
}
1378

1379
extern "C" int openmc_tally_set_nuclides(
1,199✔
1380
  int32_t index, int n, const char** nuclides)
1381
{
1382
  // Make sure the index fits in the array bounds.
1383
  if (index < 0 || index >= model::tallies.size()) {
1,199✔
1384
    set_errmsg("Index in tallies array is out of bounds.");
×
1385
    return OPENMC_E_OUT_OF_BOUNDS;
×
1386
  }
1387

1388
  vector<std::string> words(nuclides, nuclides + n);
1,199✔
1389
  vector<int> nucs;
1,199✔
1390
  for (auto word : words) {
4,840✔
1391
    if (word == "total") {
3,652✔
1392
      nucs.push_back(-1);
660✔
1393
    } else {
1394
      auto search = data::nuclide_map.find(word);
2,992✔
1395
      if (search == data::nuclide_map.end()) {
2,992✔
1396
        int err = openmc_load_nuclide(word.c_str(), nullptr, 0);
605✔
1397
        if (err < 0) {
605✔
1398
          set_errmsg(openmc_err_msg);
11✔
1399
          return OPENMC_E_DATA;
11✔
1400
        }
1401
      }
1402
      nucs.push_back(data::nuclide_map.at(word));
2,981✔
1403
    }
1404
  }
3,652✔
1405

1406
  model::tallies[index]->nuclides_ = nucs;
1,188✔
1407

1408
  return 0;
1,188✔
1409
}
1,199✔
1410

1411
extern "C" int openmc_tally_get_filters(
968✔
1412
  int32_t index, const int32_t** indices, size_t* n)
1413
{
1414
  if (index < 0 || index >= model::tallies.size()) {
968✔
1415
    set_errmsg("Index in tallies array is out of bounds.");
×
1416
    return OPENMC_E_OUT_OF_BOUNDS;
×
1417
  }
1418

1419
  *indices = model::tallies[index]->filters().data();
968✔
1420
  *n = model::tallies[index]->filters().size();
968✔
1421
  return 0;
968✔
1422
}
1423

1424
extern "C" int openmc_tally_set_filters(
1,056✔
1425
  int32_t index, size_t n, const int32_t* indices)
1426
{
1427
  // Make sure the index fits in the array bounds.
1428
  if (index < 0 || index >= model::tallies.size()) {
1,056✔
1429
    set_errmsg("Index in tallies array is out of bounds.");
×
1430
    return OPENMC_E_OUT_OF_BOUNDS;
×
1431
  }
1432

1433
  // Set the filters.
1434
  try {
1435
    // Convert indices to filter pointers
1436
    vector<Filter*> filters;
1,056✔
1437
    for (int64_t i = 0; i < n; ++i) {
2,530✔
1438
      int32_t i_filt = indices[i];
1,474✔
1439
      filters.push_back(model::tally_filters.at(i_filt).get());
1,474✔
1440
    }
1441
    model::tallies[index]->set_filters(filters);
1,056✔
1442
  } catch (const std::out_of_range& ex) {
1,056✔
1443
    set_errmsg("Index in tally filter array out of bounds.");
×
1444
    return OPENMC_E_OUT_OF_BOUNDS;
×
1445
  }
×
1446

1447
  return 0;
1,056✔
1448
}
1449

1450
//! Reset tally results and number of realizations
1451
extern "C" int openmc_tally_reset(int32_t index)
2,684✔
1452
{
1453
  // Make sure the index fits in the array bounds.
1454
  if (index < 0 || index >= model::tallies.size()) {
2,684✔
1455
    set_errmsg("Index in tallies array is out of bounds.");
×
1456
    return OPENMC_E_OUT_OF_BOUNDS;
×
1457
  }
1458

1459
  model::tallies[index]->reset();
2,684✔
1460
  return 0;
2,684✔
1461
}
1462

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

1471
  *n = model::tallies[index]->n_realizations_;
3,344✔
1472
  return 0;
3,344✔
1473
}
1474

1475
//! \brief Returns a pointer to a tally results array along with its shape. This
1476
//! allows a user to obtain in-memory tally results from Python directly.
1477
extern "C" int openmc_tally_results(
15,840✔
1478
  int32_t index, double** results, size_t* shape)
1479
{
1480
  // Make sure the index fits in the array bounds.
1481
  if (index < 0 || index >= model::tallies.size()) {
15,840✔
1482
    set_errmsg("Index in tallies array is out of bounds.");
×
1483
    return OPENMC_E_OUT_OF_BOUNDS;
×
1484
  }
1485

1486
  const auto& t {model::tallies[index]};
15,840✔
1487
  if (t->results_.size() == 0) {
15,840✔
1488
    set_errmsg("Tally results have not been allocated yet.");
×
1489
    return OPENMC_E_ALLOCATE;
×
1490
  }
1491

1492
  // Set pointer to results and copy shape
1493
  *results = t->results_.data();
15,840✔
1494
  auto s = t->results_.shape();
15,840✔
1495
  shape[0] = s[0];
15,840✔
1496
  shape[1] = s[1];
15,840✔
1497
  shape[2] = s[2];
15,840✔
1498
  return 0;
15,840✔
1499
}
1500

1501
extern "C" int openmc_global_tallies(double** ptr)
11✔
1502
{
1503
  *ptr = simulation::global_tallies.data();
11✔
1504
  return 0;
11✔
1505
}
1506

1507
extern "C" size_t tallies_size()
1,045✔
1508
{
1509
  return model::tallies.size();
1,045✔
1510
}
1511

1512
// given a tally ID, remove it from the tallies vector
1513
extern "C" int openmc_remove_tally(int32_t index)
11✔
1514
{
1515
  // check that id is in the map
1516
  if (index < 0 || index >= model::tallies.size()) {
11✔
1517
    set_errmsg("Index in tallies array is out of bounds.");
×
1518
    return OPENMC_E_OUT_OF_BOUNDS;
×
1519
  }
1520

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

1525
  return 0;
11✔
1526
}
1527

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