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

openmc-dev / openmc / 14390624619

10 Apr 2025 09:26PM UTC coverage: 85.179% (+0.1%) from 85.044%
14390624619

Pull #3133

github

web-flow
Merge e7fbf3b55 into cdc254ccf
Pull Request #3133: Kinetics parameters using Iterated Fission Probability

320 of 333 new or added lines in 12 files covered. (96.1%)

106 existing lines in 18 files now uncovered.

51990 of 61036 relevant lines covered (85.18%)

36926450.13 hits per line

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

80.89
/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) {
24,059✔
185
    for (int score : scores_) {
57,277✔
186
      switch (score) {
33,252✔
187
      case SCORE_IFP_TIME_NUM:
34✔
188
      case SCORE_IFP_BETA_NUM:
189
      case SCORE_IFP_DENOM:
190
        if (settings::run_mode == RunMode::EIGENVALUE) {
34✔
191
          if (settings::ifp_n_generation < 0) {
25✔
192
            settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION;
9✔
193
            warning(fmt::format(
9✔
194
              "{} generations will be used for IFP (default value). It can be "
195
              "changed using the 'ifp_n_generation' settings.",
196
              settings::ifp_n_generation));
197
          }
198
          if (settings::ifp_n_generation > settings::n_inactive) {
25✔
199
            fatal_error("'ifp_n_generation' must be lower than or equal to the "
9✔
200
                        "number of inactive cycles.");
201
          }
202
          settings::ifp = true;
16✔
203
        } else {
204
          fatal_error(
9✔
205
            "Iterated Fission Probability can only be used in an eigenvalue "
206
            "calculation.");
207
        }
208
        goto exit_for_loop;
16✔
209
        break;
210
      }
211
    }
212
  }
NEW
213
exit_for_loop:;
×
214

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

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

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

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

303
    deriv_ = it->second;
320✔
304

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

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

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

336
  // =======================================================================
337
  // SET TALLY ESTIMATOR
338

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

355
      // Set estimator to track-length estimator
356
      estimator_ = TallyEstimator::TRACKLENGTH;
11,192✔
357

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

367
      // Set estimator to collision estimator
368
      estimator_ = TallyEstimator::COLLISION;
403✔
369

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

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

391
Tally::~Tally()
25,151✔
392
{
393
  model::tally_map.erase(id_);
25,151✔
394
}
25,151✔
395

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

402
void Tally::set_id(int32_t id)
26,170✔
403
{
404
  assert(id >= 0 || id == C_NONE);
21,278✔
405

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

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

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

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

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

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

446
    filter_indices[f->type()] = i;
7,138✔
447
  }
448
  return filter_indices;
2,394✔
449
}
×
450

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

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

466
  // Copy in the given filter indices.
467
  auto n = filters.size();
26,275✔
468
  filters_.reserve(n);
26,275✔
469

470
  for (auto* filter : filters) {
76,178✔
471
    add_filter(filter);
49,903✔
472
  }
473
}
26,275✔
474

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

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

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

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

515
  auto scores = get_node_array<std::string>(node, "scores");
24,059✔
516
  set_scores(scores);
24,059✔
517
}
24,059✔
518

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

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

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

561
    // Determine integer code for score
562
    int score = reaction_type(score_str);
34,903✔
563

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

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

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

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

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

616
    case HEATING:
310✔
617
      if (settings::photon_transport)
310✔
618
        estimator_ = TallyEstimator::COLLISION;
150✔
619
      break;
310✔
620

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

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

645
    case SCORE_IFP_TIME_NUM:
102✔
646
    case SCORE_IFP_BETA_NUM:
647
    case SCORE_IFP_DENOM:
648
      estimator_ = TallyEstimator::COLLISION;
102✔
649
      break;
102✔
650
    }
651

652
    scores_.push_back(score);
34,903✔
653
  }
34,903✔
654

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

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

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

685
void Tally::set_nuclides(pugi::xml_node node)
24,059✔
686
{
687
  nuclides_.clear();
24,059✔
688

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

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

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

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

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

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

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

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

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

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

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

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

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

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

828
    if (settings::solver_type == SolverType::RANDOM_RAY) {
163,038✔
829
      norm = 1.0;
11,220✔
830
    }
831

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

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

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

861
  // add number of scores and nuclides to tally
862
  shape.push_back(results_.shape()[1]);
×
863
  shape.push_back(results_.shape()[2]);
×
864

865
  xt::xarray<double> reshaped_results = results_;
×
866
  reshaped_results.reshape(shape);
×
867
  return reshaped_results;
×
868
}
869

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

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

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

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

899
//==============================================================================
900
// Non-member functions
901
//==============================================================================
902

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

910
  write_message("Reading tallies XML file...", 5);
461✔
911

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

917
  read_tallies_xml(root);
461✔
918
}
1,315✔
919

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

927
  // Check for user meshes and allocate
928
  read_meshes(root);
3,679✔
929

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

934
  // Read data for tally derivatives
935
  read_tally_derivatives(root);
3,646✔
936

937
  // ==========================================================================
938
  // READ FILTER DATA
939

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

945
  // ==========================================================================
946
  // READ TALLY DATA
947

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

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

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

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

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

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

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

991
  // Note that global tallies are *always* reduced even when no_reduce option is
992
  // on.
993

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

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

1003
  // Reduce contiguous data
1004
  MPI_Reduce(gt_values.data(), gt_values_reduced.data(), N_GLOBAL_TALLIES,
32,680✔
1005
    MPI_DOUBLE, MPI_SUM, 0, mpi::intracomm);
1006

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

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

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

1033
  // Increase number of realizations (only used for global tallies)
1034
  simulation::n_realizations += 1;
109,477✔
1035

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

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

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

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

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

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

1084
    if (tally.active_) {
329,477✔
1085
      model::active_tallies.push_back(i);
200,753✔
1086
      switch (tally.type_) {
200,753✔
1087

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

1101
      case TallyType::MESH_SURFACE:
4,015✔
1102
        model::active_meshsurf_tallies.push_back(i);
4,015✔
1103
        break;
4,015✔
1104

1105
      case TallyType::SURFACE:
2,102✔
1106
        model::active_surface_tallies.push_back(i);
2,102✔
1107
        break;
2,102✔
1108

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

1117
void free_memory_tally()
6,624✔
1118
{
1119
  model::tally_derivs.clear();
6,624✔
1120
  model::tally_deriv_map.clear();
6,624✔
1121

1122
  model::tally_filters.clear();
6,624✔
1123
  model::filter_map.clear();
6,624✔
1124

1125
  model::tallies.clear();
6,624✔
1126

1127
  model::active_tallies.clear();
6,624✔
1128
  model::active_analog_tallies.clear();
6,624✔
1129
  model::active_tracklength_tallies.clear();
6,624✔
1130
  model::active_collision_tallies.clear();
6,624✔
1131
  model::active_meshsurf_tallies.clear();
6,624✔
1132
  model::active_surface_tallies.clear();
6,624✔
1133
  model::active_pulse_height_tallies.clear();
6,624✔
1134

1135
  model::tally_map.clear();
6,624✔
1136
}
6,624✔
1137

1138
//==============================================================================
1139
// C-API functions
1140
//==============================================================================
1141

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

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

1163
  *index = it->second;
20,834✔
1164
  return 0;
20,834✔
1165
}
1166

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

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

1183
  *estimator = static_cast<int>(model::tallies[index]->estimator_);
×
1184
  return 0;
×
1185
}
1186

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

1194
  auto& t {model::tallies[index]};
660✔
1195

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

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

1217
  *id = model::tallies[index]->id_;
26,972✔
1218
  return 0;
26,972✔
1219
}
1220

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

1228
  model::tallies[index]->set_id(id);
1,001✔
1229
  return 0;
1,001✔
1230
}
1231

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

1240
  return 0;
11✔
1241
}
1242

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

1262
  return 0;
660✔
1263
}
1264

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

1273
  return 0;
22✔
1274
}
1275

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

1284
  return 0;
671✔
1285
}
1286

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

1295
  return 0;
22✔
1296
}
1297

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

1306
  return 0;
341✔
1307
}
1308

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

1317
  return 0;
22✔
1318
}
1319

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

1328
  return 0;
198✔
1329
}
1330

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

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

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

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

1359
  return 0;
1,012✔
1360
}
1,012✔
1361

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

1370
  *n = model::tallies[index]->nuclides_.size();
231✔
1371
  *nuclides = model::tallies[index]->nuclides_.data();
231✔
1372

1373
  return 0;
231✔
1374
}
1375

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

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

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

1405
  return 0;
1,188✔
1406
}
1,199✔
1407

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

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

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

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

1444
  return 0;
1,056✔
1445
}
1446

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

1456
  model::tallies[index]->reset();
2,684✔
1457
  return 0;
2,684✔
1458
}
1459

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

1468
  *n = model::tallies[index]->n_realizations_;
3,344✔
1469
  return 0;
3,344✔
1470
}
1471

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

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

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

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

1504
extern "C" size_t tallies_size()
1,045✔
1505
{
1506
  return model::tallies.size();
1,045✔
1507
}
1508

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

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

1522
  return 0;
11✔
1523
}
1524

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